diff --git a/.depcheckrc b/.depcheckrc new file mode 100644 index 0000000..e9383a7 --- /dev/null +++ b/.depcheckrc @@ -0,0 +1,18 @@ +ignores: [ + # Dependencies that depcheck thinks are unused but are actually used + "@graphql-codegen/*", + "@commitlint/*", + "i18next", + # Dependencies that depcheck thinks are missing but are actually present or never used + "@yarnpkg/core", + "@yarnpkg/cli", + "clipanion", + "@yarnpkg/fslib", + "bufferutil", + "utf-8-validate", + "@yarnpkg/parsers", + "@yarnpkg/plugin-git", + "semver", + "typanion", + "turbo-ignore", + ] diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..78dd44b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +# Windows files +[*.bat] +end_of_line = crlf diff --git a/.env.defaults b/.env.defaults new file mode 100644 index 0000000..65c65f1 --- /dev/null +++ b/.env.defaults @@ -0,0 +1,26 @@ +ALCHEMY_API_KEY='key' +AMPLITUDE_EXPERIMENTS_DEPLOYMENT_KEY='key' +APPSFLYER_API_KEY='key' +APPSFLYER_APP_ID="123" +FIAT_ON_RAMP_API_URL='https://api.uniswap.org' +MOONPAY_API_KEY='key' +MOONPAY_API_URL='https://api.moonpay.com' +MOONPAY_WIDGET_API_URL='https://api.moonpay.com' +INFURA_PROJECT_ID="123" +ONESIGNAL_APP_ID="123" +QUICKNODE_BNB_RPC_URL='https://api.uniswap.org' +SENTRY_DSN='http://sentry.com' +SHAKE_CLIENT_ID="123" +SHAKE_CLIENT_SECRET="123" +SIMPLEHASH_API_KEY='key' +SIMPLEHASH_API_URL='https://api.simplehash.com' +STATSIG_PROXY_URL='https://api.statsig.com' +TEMP_SCANTASTIC_URL='https://api.uniswap.org' +TRADING_API_KEY='key' +TRADING_API_URL='https://api.uniswap.org' +UNISWAP_API_KEY='key' +UNISWAP_API_BASE_URL='https://api.uniswap.org' +UNISWAP_APP_URL='https://app.uniswap.org' +WALLETCONNECT_PROJECT_ID="123" +UNITAGS_API_URL='https://api.uniswap.org/unitags' +FIREBASE_APP_CHECK_DEBUG_TOKEN='token' diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..355334e --- /dev/null +++ b/.eslintignore @@ -0,0 +1,8 @@ +**/dist/** +**/build/** +**/node_modules/** +**/types/** +**/*.test.{js,jsx,ts,tsx} +**/*.spec.{js,jsx,ts,tsx} +**/config-overrides.js +**/.eslintrc.js diff --git a/.firebaserc b/.firebaserc new file mode 100644 index 0000000..fe974f9 --- /dev/null +++ b/.firebaserc @@ -0,0 +1,5 @@ +{ + "projects": { + "default": "uniswap-mobile" + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..45a3dcb --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Windows files should use crlf line endings +# https://help.github.com/articles/dealing-with-line-endings/ +*.bat text eol=crlf diff --git a/.github/workflows/tag_and_release.yml b/.github/workflows/tag_and_release.yml new file mode 100644 index 0000000..f32ddce --- /dev/null +++ b/.github/workflows/tag_and_release.yml @@ -0,0 +1,40 @@ +name: "Tag and release" +on: + push: + branches: + - 'main' + +jobs: + deploy-to-prod: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: 🧷️ Get version + uses: juliangruber/read-file-action@02bbba9876a8f870efd4ad64e3b9088d3fb94d4b + id: version + with: + path: VERSION + + - name: 🧾️ Get release notes + uses: juliangruber/read-file-action@02bbba9876a8f870efd4ad64e3b9088d3fb94d4b + id: release-notes + with: + path: RELEASE + + - name: 🏷️ Tag + id: github-tag-action + uses: uniswap/github-tag-action@7bddacd4864a0f5671e836721db60174d8a9c399 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + custom_tag: ${{ steps.version.outputs.content }} + tag_prefix: "" + + - name: 🪽 Release + uses: actions/create-release@v1.1.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ steps.github-tag-action.outputs.new_tag }} + release_name: Release ${{ steps.github-tag-action.outputs.new_tag }} + body: ${{ steps.release-notes.outputs.content }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c766ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# utility script output +scripts/dist + +# next.js +.next/ +out/ +build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env +.env.defaults.local + +# turbo +.turbo + +# .yarn files +.yarn/cache +.yarn/versions +.yarn/install-state.gz + +dist/out-tsc/* + +# Locale backup/generated files +packages/wallet/src/i18n/locales/source/*_old.json +packages/wallet/src/i18n/locales/@types/resources.d.ts + +# ci +.ci-cache/ + +# JetBrains +.idea/ + +# Vercel +.vercel diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..0b80bce --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/usr/bin/env sh +. "$(dirname -- "$0")/_/husky.sh" + +yarn g:run-fast-checks \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..2b390f5 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +# used in tandem with package.json engines section to only use yarn +engine-strict=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..3f430af --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..acf9bf0 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.2.2 \ No newline at end of file diff --git a/.yarn/patches/@vercel-og-npm-0.5.8-83a79f2744.patch b/.yarn/patches/@vercel-og-npm-0.5.8-83a79f2744.patch new file mode 100644 index 0000000..dfa07be --- /dev/null +++ b/.yarn/patches/@vercel-og-npm-0.5.8-83a79f2744.patch @@ -0,0 +1,49 @@ +diff --git a/dist/index.edge.js b/dist/index.edge.js +index 5187f88ab3375622199e7628cf15c00ffef65584..c9bd7c209ea70b8db3b57d277bf87535ccd07f6f 100644 +--- a/dist/index.edge.js ++++ b/dist/index.edge.js +@@ -18673,8 +18673,8 @@ var Resvg2 = class extends Resvg { + }; + + // src/index.edge.ts +-import resvg_wasm from "./resvg.wasm?module"; +-import yoga_wasm from "./yoga.wasm?module"; ++import resvg_wasm from "./resvg.wasm"; ++import yoga_wasm from "./yoga.wasm"; + + // src/emoji/index.ts + var U200D = String.fromCharCode(8205); +@@ -18809,18 +18809,15 @@ async function render(satori, resvg, opts, defaultFonts, element) { + // src/index.edge.ts + var initializedResvg = initWasm(resvg_wasm); + var initializedYoga = initYoga(yoga_wasm).then((yoga2) => Ll(yoga2)); +-var fallbackFont = fetch(new URL("./noto-sans-v27-latin-regular.ttf", import.meta.url)).then((res) => res.arrayBuffer()); + var ImageResponse = class { + constructor(element, options = {}) { + const result = new ReadableStream({ + async start(controller) { + await initializedYoga; + await initializedResvg; +- const fontData = await fallbackFont; + const fonts = [ + { + name: "sans serif", +- data: fontData, + weight: 700, + style: "normal" + } +diff --git a/dist/types.d.ts b/dist/types.d.ts +index dde26cc6bbee29507d79bd0a9ec6c27719420038..d075e99d4ac094f35379c8a9014e6ca0b3c8be0f 100644 +--- a/dist/types.d.ts ++++ b/dist/types.d.ts +@@ -28,9 +28,8 @@ declare type ImageOptions = { + * A list of fonts to use. + * + * @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]} +- * @default Noto Sans Latin Regular. + */ +- fonts?: SatoriOptions['fonts']; ++ fonts: SatoriOptions['fonts']; + /** + * Using a specific Emoji style. Defaults to `twemoji`. + * diff --git a/.yarn/patches/@web3-react-coinbase-wallet-npm-8.2.3-9c7073f079.patch b/.yarn/patches/@web3-react-coinbase-wallet-npm-8.2.3-9c7073f079.patch new file mode 100644 index 0000000..2054baa --- /dev/null +++ b/.yarn/patches/@web3-react-coinbase-wallet-npm-8.2.3-9c7073f079.patch @@ -0,0 +1,13 @@ +diff --git a/dist/index.js b/dist/index.js +index f38d06e2de52b5035560d63d6889dc54d5ca021b..4f8fa194b60c73a901dbce4a742f598b1c95c026 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -62,7 +62,7 @@ class CoinbaseWallet extends types_1.Connector { + return __awaiter(this, void 0, void 0, function* () { + if (this.eagerConnection) + return; +- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@coinbase/wallet-sdk'))).then((m) => { ++ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@coinbase/wallet-sdk'))).then((m) => { + const _a = this.options, { url } = _a, options = __rest(_a, ["url"]); + this.coinbaseWallet = new m.default(options); + this.provider = this.coinbaseWallet.makeWeb3Provider(url); diff --git a/.yarn/patches/@web3-react-gnosis-safe-npm-8.2.4-a7e2850335.patch b/.yarn/patches/@web3-react-gnosis-safe-npm-8.2.4-a7e2850335.patch new file mode 100644 index 0000000..f4a15fe --- /dev/null +++ b/.yarn/patches/@web3-react-gnosis-safe-npm-8.2.4-a7e2850335.patch @@ -0,0 +1,15 @@ +diff --git a/dist/index.js b/dist/index.js +index 015a33c37fe87f13f31559d462351acd7ae9bac7..4cd7cdeb4437f30c1c063c0ffb8fd5692a399dbf 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -68,8 +68,8 @@ class GnosisSafe extends types_1.Connector { + if (this.eagerConnection) + return; + // kick off import early to minimize waterfalls +- const SafeAppProviderPromise = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider); +- yield (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () { ++ const SafeAppProviderPromise = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-provider'))).then(({ SafeAppProvider }) => SafeAppProvider); ++ yield (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@safe-global/safe-apps-sdk'))).then((m) => __awaiter(this, void 0, void 0, function* () { + this.sdk = new m.default(this.options); + const safe = yield Promise.race([ + this.sdk.safe.getInfo(), diff --git a/.yarn/patches/@web3-react-metamask-npm-8.2.4-84b10de2d2.patch b/.yarn/patches/@web3-react-metamask-npm-8.2.4-84b10de2d2.patch new file mode 100644 index 0000000..f22a0f8 --- /dev/null +++ b/.yarn/patches/@web3-react-metamask-npm-8.2.4-84b10de2d2.patch @@ -0,0 +1,13 @@ +diff --git a/dist/index.js b/dist/index.js +index c8476dd9b01c0599dfc545f4c86432081bd0fcec..c0bfce759654232df771724e59a650c92b392f78 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -54,7 +54,7 @@ class MetaMask extends types_1.Connector { + return __awaiter(this, void 0, void 0, function* () { + if (this.eagerConnection) + return; +- return (this.eagerConnection = Promise.resolve().then(() => __importStar(require('@metamask/detect-provider'))).then((m) => __awaiter(this, void 0, void 0, function* () { ++ return (this.eagerConnection = Promise.resolve().then(async () => __importStar(await import('@metamask/detect-provider'))).then((m) => __awaiter(this, void 0, void 0, function* () { + var _a, _b; + const provider = yield m.default(this.options); + if (provider) { diff --git a/.yarn/patches/@web3-react-walletconnect-v2-npm-8.5.1-933cac0534.patch b/.yarn/patches/@web3-react-walletconnect-v2-npm-8.5.1-933cac0534.patch new file mode 100644 index 0000000..f42256f --- /dev/null +++ b/.yarn/patches/@web3-react-walletconnect-v2-npm-8.5.1-933cac0534.patch @@ -0,0 +1,28 @@ +diff --git a/dist/index.js b/dist/index.js +index 1a36d14c5d7c9ee55b2eccd11216c8adb6839daf..908b8c57a2d8cd565030e34e15c56caf7d182cfd 100644 +--- a/dist/index.js ++++ b/dist/index.js +@@ -84,7 +84,7 @@ class WalletConnect extends types_1.Connector { + return __awaiter(this, void 0, void 0, function* () { + const rpcMap = this.rpcMap ? (0, utils_1.getBestUrlMap)(this.rpcMap, this.timeout) : undefined; + const chainProps = this.getChainProps(this.chains, this.optionalChains, desiredChainId); +- const ethProviderModule = yield Promise.resolve().then(() => __importStar(require('@walletconnect/ethereum-provider'))); ++ const ethProviderModule = yield Promise.resolve().then(async () => __importStar(await import('@walletconnect/ethereum-provider'))); + this.provider = yield ethProviderModule.default.init(Object.assign(Object.assign(Object.assign({}, this.options), chainProps), { rpcMap: yield rpcMap })); + return this.provider + .on('disconnect', this.disconnectListener) +diff --git a/dist/utils.js b/dist/utils.js +index 17539b6f910e65aeaebcc116395dce56ae4ce193..9ea637118844ebbdc55db71009b44849992603d2 100644 +--- a/dist/utils.js ++++ b/dist/utils.js +@@ -62,8 +62,8 @@ function getBestUrl(urls, timeout) { + if (urls.length === 1) + return urls[0]; + const [HttpConnection, JsonRpcProvider] = yield Promise.all([ +- Promise.resolve().then(() => __importStar(require('@walletconnect/jsonrpc-http-connection'))).then(({ HttpConnection }) => HttpConnection), +- Promise.resolve().then(() => __importStar(require('@walletconnect/jsonrpc-provider'))).then(({ JsonRpcProvider }) => JsonRpcProvider), ++ Promise.resolve().then(async () => __importStar(await import('@walletconnect/jsonrpc-http-connection'))).then(({ HttpConnection }) => HttpConnection), ++ Promise.resolve().then(async () => __importStar(await import('@walletconnect/jsonrpc-provider'))).then(({ JsonRpcProvider }) => JsonRpcProvider), + ]); + // the below returns the first url for which there's been a successful call, prioritized by index + return new Promise((resolve) => { diff --git a/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch b/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch new file mode 100644 index 0000000..540b89a --- /dev/null +++ b/.yarn/patches/detox-npm-20.18.1-b532b310b4.patch @@ -0,0 +1,14 @@ +diff --git a/android/rninfo.gradle b/android/rninfo.gradle +index c09d2af1d219a4134dc0301e9270aef568730d2b..f1b887cf5dcf56c2f66fff3e6f1b674d48704dac 100644 +--- a/android/rninfo.gradle ++++ b/android/rninfo.gradle +@@ -3,7 +3,8 @@ import groovy.json.JsonSlurper + def getRNVersion = { workingDir -> + println("RNInfo: workingDir=$workingDir") + def jsonSlurper = new JsonSlurper() +- def packageFile = "$workingDir/../node_modules/react-native/package.json" ++ // Fixes patch to node_modules in monorepo project ++ def packageFile = "$workingDir/../../../node_modules/react-native/package.json" + println("RNInfo: reading $packageFile") + Map packageJSON = jsonSlurper.parse(new File(packageFile)) + String rnVersion = packageJSON.get('version') diff --git a/.yarn/patches/expo-local-authentication-npm-13.0.2-e7639a51e4.patch b/.yarn/patches/expo-local-authentication-npm-13.0.2-e7639a51e4.patch new file mode 100644 index 0000000..338a6da --- /dev/null +++ b/.yarn/patches/expo-local-authentication-npm-13.0.2-e7639a51e4.patch @@ -0,0 +1,132 @@ +diff --git a/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt b/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt +index 9980dfabbd5179753ba99f50c6ecfadd732cfdb9..589c58e427989181dddaa5ccd4ccf31fcc0e53c7 100644 +--- a/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt ++++ b/android/src/main/java/expo/modules/localauthentication/LocalAuthenticationModule.kt +@@ -7,6 +7,7 @@ import android.content.Context + import android.content.Intent + import android.os.Build + import android.os.Bundle ++import android.provider.Settings + import androidx.biometric.BiometricManager + import androidx.biometric.BiometricPrompt + import androidx.biometric.BiometricPrompt.PromptInfo +@@ -89,7 +90,7 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act + + @ExpoMethod + fun supportedAuthenticationTypesAsync(promise: Promise) { +- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ++ val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + val results: MutableList = ArrayList() + if (result == BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) { + promise.resolve(results) +@@ -122,13 +123,13 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act + + @ExpoMethod + fun hasHardwareAsync(promise: Promise) { +- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ++ val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + promise.resolve(result != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE) + } + + @ExpoMethod + fun isEnrolledAsync(promise: Promise) { +- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ++ val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + promise.resolve(result == BiometricManager.BIOMETRIC_SUCCESS) + } + +@@ -138,13 +139,31 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act + if (isDeviceSecure) { + level = SECURITY_LEVEL_SECRET + } +- val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK) ++ val result = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) + if (result == BiometricManager.BIOMETRIC_SUCCESS) { + level = SECURITY_LEVEL_BIOMETRIC + } + promise.resolve(level) + } + ++ @ExpoMethod ++ fun enrollForAuthentication(promise: Promise) { ++ if (Build.VERSION.SDK_INT >= 30) { ++ val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL) ++ intent.putExtra( ++ Settings.EXTRA_BIOMETRIC_AUTHENTICATORS_ALLOWED, ++ BiometricManager.Authenticators.BIOMETRIC_STRONG ++ ) ++ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) ++ currentActivity!!.startActivity(intent) ++ promise.resolve(true) ++ } else { ++ val intent = Intent(Settings.ACTION_FINGERPRINT_ENROLL) ++ currentActivity!!.startActivity(intent) ++ promise.resolve(true) ++ } ++ } ++ + @ExpoMethod + fun authenticateAsync(options: Map, promise: Promise) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { +@@ -220,10 +239,19 @@ class LocalAuthenticationModule(context: Context) : ExportedModule(context), Act + promptInfoBuilder.setNegativeButtonText(it) + } + } else { +- promptInfoBuilder.setAllowedAuthenticators( +- BiometricManager.Authenticators.BIOMETRIC_WEAK +- or BiometricManager.Authenticators.DEVICE_CREDENTIAL +- ) ++ if (Build.VERSION.SDK_INT >= 30) { ++ promptInfoBuilder.setAllowedAuthenticators( ++ BiometricManager.Authenticators.BIOMETRIC_STRONG ++ or BiometricManager.Authenticators.DEVICE_CREDENTIAL ++ ) ++ } else { ++ promptInfoBuilder.setAllowedAuthenticators( ++ BiometricManager.Authenticators.BIOMETRIC_STRONG ++ ) ++ cancelLabel?.let { ++ promptInfoBuilder.setNegativeButtonText(it) ++ } ++ } + } + promptInfoBuilder.setConfirmationRequired(requireConfirmation) + val promptInfo = promptInfoBuilder.build() +diff --git a/src/LocalAuthentication.ts b/src/LocalAuthentication.ts +index b9f3f0732b61138f790e7c3a33a92d8d726a71f1..b754d8588701c7d74a86a07b8e6c09c2434b9d43 100644 +--- a/src/LocalAuthentication.ts ++++ b/src/LocalAuthentication.ts +@@ -78,11 +78,16 @@ export async function getEnrolledLevelAsync(): Promise { + * @return Returns a promise which fulfils with [`LocalAuthenticationResult`](#localauthenticationresult). + */ + export async function authenticateAsync( +- options: LocalAuthenticationOptions = {} ++ options: LocalAuthenticationOptions + ): Promise { + if (!ExpoLocalAuthentication.authenticateAsync) { + throw new UnavailabilityError('expo-local-authentication', 'authenticateAsync'); + } ++ ++ invariant( ++ typeof options.cancelLabel === 'string' && options.cancelLabel.length, ++ 'LocalAuthentication.authenticateAsync : `options.cancelLabel` must be a non-empty string.' ++ ); + + if (options.hasOwnProperty('promptMessage')) { + invariant( +diff --git a/src/LocalAuthentication.types.ts b/src/LocalAuthentication.types.ts +index a65b16d5bc5d18c03cee3ca8e4d223c9b736659b..d1ae18df61714b2bd44f3b9867718568707c1dc6 100644 +--- a/src/LocalAuthentication.types.ts ++++ b/src/LocalAuthentication.types.ts +@@ -42,9 +42,9 @@ export type LocalAuthenticationOptions = { + */ + promptMessage?: string; + /** +- * Allows to customize the default `Cancel` label shown. ++ * Allows to customize the default `Cancel` label shown. Required to be non-empty for Android or it might cause crashes. + */ +- cancelLabel?: string; ++ cancelLabel: string; + /** + * After several failed attempts the system will fallback to the device passcode. This setting + * allows you to disable this option and instead handle the fallback yourself. This can be diff --git a/.yarn/patches/lightweight-charts-npm-4.1.1-01f161d9b6.patch b/.yarn/patches/lightweight-charts-npm-4.1.1-01f161d9b6.patch new file mode 100644 index 0000000..4ecc164 Binary files /dev/null and b/.yarn/patches/lightweight-charts-npm-4.1.1-01f161d9b6.patch differ diff --git a/.yarn/patches/multiformats-npm-9.4.2-44b8a13c1d.patch b/.yarn/patches/multiformats-npm-9.4.2-44b8a13c1d.patch new file mode 100644 index 0000000..b6bbe10 --- /dev/null +++ b/.yarn/patches/multiformats-npm-9.4.2-44b8a13c1d.patch @@ -0,0 +1,14 @@ +diff --git a/basics b/basics +deleted file mode 100644 +index 1a79016a006c6efd5e4cee071f134128d0ffd100..0000000000000000000000000000000000000000 +--- a/basics ++++ /dev/null +@@ -1 +0,0 @@ +-module.exports = require('./cjs/src/basics.js') +diff --git a/basics.js b/basics.js +new file mode 100644 +index 0000000000000000000000000000000000000000..1a79016a006c6efd5e4cee071f134128d0ffd100 +--- /dev/null ++++ b/basics.js +@@ -0,0 +1 @@ ++module.exports = require('./cjs/src/basics.js') diff --git a/.yarn/patches/react-native-context-menu-view-npm-1.6.0-f5f5410f50.patch b/.yarn/patches/react-native-context-menu-view-npm-1.6.0-f5f5410f50.patch new file mode 100644 index 0000000..5ef2c32 --- /dev/null +++ b/.yarn/patches/react-native-context-menu-view-npm-1.6.0-f5f5410f50.patch @@ -0,0 +1,70 @@ +diff --git a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java +index 4b5b90b7b478668fdff3fd12d5e028d423ada057..af30dc6f700b3b3cfde5c149bf1f865786df3e27 100644 +--- a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java ++++ b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java +@@ -67,6 +67,14 @@ public class ContextMenuView extends ReactViewGroup implements PopupMenu.OnMenuI + contextMenu.show(); + } + } ++ ++ @Override ++ public boolean onDoubleTap(MotionEvent e) { ++ if (dropdownMenuMode) { ++ contextMenu.show(); ++ } ++ return super.onSingleTapConfirmed(e); ++ } + }); + } + +diff --git a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuManager.java b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuManager.java +index 81f7b4b58c946d1b2e14301f9b52ecffa1cd0643..403dac6450be24a8c4d26ffb8293b51a1485f6a8 100644 +--- a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuManager.java ++++ b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuManager.java +@@ -45,6 +45,11 @@ public class ContextMenuManager extends ViewGroupManager { + view.setDropdownMenuMode(enabled); + } + ++ @ReactProp(name = "disabled") ++ public void setDisabled(ContextMenuView view, @Nullable boolean disabled) { ++ view.setDisabled(disabled); ++ } ++ + @androidx.annotation.Nullable + @Override + public Map getExportedCustomDirectEventTypeConstants() { +diff --git a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java +index af30dc6f700b3b3cfde5c149bf1f865786df3e27..aa04fe6d9458601fdcb9bb44f89e16bbc1ad9d39 100644 +--- a/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java ++++ b/android/src/main/java/com/mpiannucci/reactnativecontextmenu/ContextMenuView.java +@@ -43,6 +43,8 @@ public class ContextMenuView extends ReactViewGroup implements PopupMenu.OnMenuI + + boolean cancelled = true; + ++ private boolean disabled = false; ++ + protected boolean dropdownMenuMode = false; + + public ContextMenuView(final Context context) { +@@ -87,13 +89,18 @@ public class ContextMenuView extends ReactViewGroup implements PopupMenu.OnMenuI + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { +- return true; ++ return disabled ? false : true; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { +- gestureDetector.onTouchEvent(ev); +- return true; ++ if (disabled) return false; ++ gestureDetector.onTouchEvent(ev); ++ return true; ++ } ++ ++ public void setDisabled(boolean disabled) { ++ this.disabled = disabled; + } + + public void setActions(@Nullable ReadableArray actions) { diff --git a/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch b/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch new file mode 100644 index 0000000..5187d9c --- /dev/null +++ b/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch @@ -0,0 +1,12 @@ +diff --git a/RNFastImage.podspec b/RNFastImage.podspec +index db0fada63fc06191f8620d336d244edde6c3dba3..286fa816e47996fdff9f25261644d612c682ae0b 100644 +--- a/RNFastImage.podspec ++++ b/RNFastImage.podspec +@@ -16,6 +16,6 @@ Pod::Spec.new do |s| + s.source_files = "ios/**/*.{h,m}" + + s.dependency 'React-Core' +- s.dependency 'SDWebImage', '~> 5.11.1' ++ s.dependency 'SDWebImage', '~> 5.15.5' + s.dependency 'SDWebImageWebPCoder', '~> 0.8.4' + end diff --git a/.yarn/patches/react-native-npm-0.71.13-e260a84bbf.patch b/.yarn/patches/react-native-npm-0.71.13-e260a84bbf.patch new file mode 100644 index 0000000..1fe164d --- /dev/null +++ b/.yarn/patches/react-native-npm-0.71.13-e260a84bbf.patch @@ -0,0 +1,34 @@ +diff --git a/jest/setup.js b/jest/setup.js +index 3738bd2c61e516fa431f61fda47f2474f72dba42..2b3266007b3c9412d99e7ceee205ee52e3008077 100644 +--- a/jest/setup.js ++++ b/jest/setup.js +@@ -17,12 +17,12 @@ jest.requireActual('@react-native/polyfills/error-guard'); + + global.__DEV__ = true; + +-global.performance = { ++if (!global.performance) global.performance = { + now: jest.fn(Date.now), + }; + + global.regeneratorRuntime = jest.requireActual('regenerator-runtime/runtime'); +-global.window = global; ++if (!global.window) global.window = global; + + global.requestAnimationFrame = function (callback) { + return setTimeout(callback, 0); +diff --git a/third-party-podspecs/boost.podspec b/third-party-podspecs/boost.podspec +index 3d9331c95d1217682a0b820a0d9440fdff074ae0..8276eb1a5854f945462363fe8db917e8270b3b6a 100644 +--- a/third-party-podspecs/boost.podspec ++++ b/third-party-podspecs/boost.podspec +@@ -10,8 +10,8 @@ Pod::Spec.new do |spec| + spec.homepage = 'http://www.boost.org' + spec.summary = 'Boost provides free peer-reviewed portable C++ source libraries.' + spec.authors = 'Rene Rivera' +- spec.source = { :http => 'https://boostorg.jfrog.io/artifactory/main/release/1.76.0/source/boost_1_76_0.tar.bz2', +- :sha256 => 'f0397ba6e982c4450f27bf32a2a83292aba035b827a5623a14636ea583318c41' } ++ spec.source = { :http => 'https://sourceforge.net/projects/boost/files/boost/1.83.0/boost_1_83_0.tar.bz2', ++ :sha256 => '6478edfe2f3305127cffe8caf73ea0176c53769f4bf1585be237eb30798c3b8e' } + + # Pinning to the same version as React.podspec. + spec.platforms = { :ios => '11.0' } diff --git a/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch b/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch new file mode 100644 index 0000000..bc7e38b --- /dev/null +++ b/.yarn/patches/react-native-wagmi-charts-npm-2.3.0-8e836a8f3c.patch @@ -0,0 +1,38 @@ +diff --git a/src/charts/line/ChartPath.tsx b/src/charts/line/ChartPath.tsx +index 3807c185c9456d2976c305df94574ff7d948b32a..5cf985422cf49120f943c98084b08c16faf452d9 100644 +--- a/src/charts/line/ChartPath.tsx ++++ b/src/charts/line/ChartPath.tsx +@@ -18,7 +18,6 @@ const BACKGROUND_COMPONENTS = [ + 'LineChartHighlight', + 'LineChartHorizontalLine', + 'LineChartGradient', +- 'LineChartDot', + 'LineChartTooltip', + ]; + const FOREGROUND_COMPONENTS = ['LineChartHighlight', 'LineChartDot']; +@@ -166,10 +165,25 @@ export function LineChartPathWrapper({ + + + ++ ++ ++ ++ ++ ++ ++ + {foregroundChildren} + + ++ + ++ + + ); + } diff --git a/.yarn/patches/rive-react-native-npm-6.1.1-39035aee2b.patch b/.yarn/patches/rive-react-native-npm-6.1.1-39035aee2b.patch new file mode 100644 index 0000000..0bdc0a7 --- /dev/null +++ b/.yarn/patches/rive-react-native-npm-6.1.1-39035aee2b.patch @@ -0,0 +1,18 @@ +diff --git a/android/build.gradle b/android/build.gradle +index 83a10e5c57ba7c8d82db28f8e868a93327790457..dbda4fa4b0391a210f902c8ab17375a29594d22d 100644 +--- a/android/build.gradle ++++ b/android/build.gradle +@@ -50,6 +50,13 @@ android { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } ++ // Resolves library merge issues during detox e2e build ++ packagingOptions { ++ pickFirst 'lib/x86/libc++_shared.so' ++ pickFirst 'lib/x86_64/libc++_shared.so' ++ pickFirst 'lib/armeabi-v7a/libc++_shared.so' ++ pickFirst 'lib/arm64-v8a/libc++_shared.so' ++ } + } + + repositories { diff --git a/.yarn/plugins/@yarnpkg/plugin-constraints.cjs b/.yarn/plugins/@yarnpkg/plugin-constraints.cjs new file mode 100644 index 0000000..54a0e9e --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-constraints.cjs @@ -0,0 +1,52 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-constraints", +factory: function (require) { +var plugin=(()=>{var Wi=Object.create,Je=Object.defineProperty;var Hi=Object.getOwnPropertyDescriptor;var Gi=Object.getOwnPropertyNames;var Yi=Object.getPrototypeOf,Ui=Object.prototype.hasOwnProperty;var Zi=r=>Je(r,"__esModule",{value:!0});var I=(r,u)=>()=>(u||r((u={exports:{}}).exports,u),u.exports),Qi=(r,u)=>{for(var p in u)Je(r,p,{get:u[p],enumerable:!0})},Ji=(r,u,p)=>{if(u&&typeof u=="object"||typeof u=="function")for(let c of Gi(u))!Ui.call(r,c)&&c!=="default"&&Je(r,c,{get:()=>u[c],enumerable:!(p=Hi(u,c))||p.enumerable});return r},G=r=>Ji(Zi(Je(r!=null?Wi(Yi(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var Xr=I((Nu,_r)=>{var Ki;(function(r){var u=function(){return{"append/2":[new r.type.Rule(new r.type.Term("append",[new r.type.Var("X"),new r.type.Var("L")]),new r.type.Term("foldl",[new r.type.Term("append",[]),new r.type.Var("X"),new r.type.Term("[]",[]),new r.type.Var("L")]))],"append/3":[new r.type.Rule(new r.type.Term("append",[new r.type.Term("[]",[]),new r.type.Var("X"),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("append",[new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("S")])]),new r.type.Term("append",[new r.type.Var("T"),new r.type.Var("X"),new r.type.Var("S")]))],"member/2":[new r.type.Rule(new r.type.Term("member",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("_")])]),null),new r.type.Rule(new r.type.Term("member",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("Xs")])]),new r.type.Term("member",[new r.type.Var("X"),new r.type.Var("Xs")]))],"permutation/2":[new r.type.Rule(new r.type.Term("permutation",[new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("permutation",[new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("permutation",[new r.type.Var("T"),new r.type.Var("P")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("P")]),new r.type.Term("append",[new r.type.Var("X"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("Y")]),new r.type.Var("S")])])]))],"maplist/2":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("X")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("Xs")])]))],"maplist/3":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs")])]))],"maplist/4":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs")])]))],"maplist/5":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds")])]))],"maplist/6":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es")])]))],"maplist/7":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")]),new r.type.Term(".",[new r.type.Var("F"),new r.type.Var("Fs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E"),new r.type.Var("F")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es"),new r.type.Var("Fs")])]))],"maplist/8":[new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("A"),new r.type.Var("As")]),new r.type.Term(".",[new r.type.Var("B"),new r.type.Var("Bs")]),new r.type.Term(".",[new r.type.Var("C"),new r.type.Var("Cs")]),new r.type.Term(".",[new r.type.Var("D"),new r.type.Var("Ds")]),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Es")]),new r.type.Term(".",[new r.type.Var("F"),new r.type.Var("Fs")]),new r.type.Term(".",[new r.type.Var("G"),new r.type.Var("Gs")])]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P"),new r.type.Var("A"),new r.type.Var("B"),new r.type.Var("C"),new r.type.Var("D"),new r.type.Var("E"),new r.type.Var("F"),new r.type.Var("G")]),new r.type.Term("maplist",[new r.type.Var("P"),new r.type.Var("As"),new r.type.Var("Bs"),new r.type.Var("Cs"),new r.type.Var("Ds"),new r.type.Var("Es"),new r.type.Var("Fs"),new r.type.Var("Gs")])]))],"include/3":[new r.type.Rule(new r.type.Term("include",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("include",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("A")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("A"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term("[]",[])]),new r.type.Var("B")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("F"),new r.type.Var("B")]),new r.type.Term(",",[new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("F")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("S")])]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("L"),new r.type.Var("S")])]),new r.type.Term("include",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("S")])])])])]))],"exclude/3":[new r.type.Rule(new r.type.Term("exclude",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Term("[]",[])]),null),new r.type.Rule(new r.type.Term("exclude",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("exclude",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("E")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term("[]",[])]),new r.type.Var("Q")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("R"),new r.type.Var("Q")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("R")]),new r.type.Term(",",[new r.type.Term("!",[]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("E")])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("E")])])])])])])]))],"foldl/4":[new r.type.Rule(new r.type.Term("foldl",[new r.type.Var("_"),new r.type.Term("[]",[]),new r.type.Var("I"),new r.type.Var("I")]),null),new r.type.Rule(new r.type.Term("foldl",[new r.type.Var("P"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Var("T")]),new r.type.Var("I"),new r.type.Var("R")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P"),new r.type.Var("L")]),new r.type.Term(",",[new r.type.Term("append",[new r.type.Var("L"),new r.type.Term(".",[new r.type.Var("I"),new r.type.Term(".",[new r.type.Var("H"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])])])]),new r.type.Var("L2")]),new r.type.Term(",",[new r.type.Term("=..",[new r.type.Var("P2"),new r.type.Var("L2")]),new r.type.Term(",",[new r.type.Term("call",[new r.type.Var("P2")]),new r.type.Term("foldl",[new r.type.Var("P"),new r.type.Var("T"),new r.type.Var("X"),new r.type.Var("R")])])])])]))],"select/3":[new r.type.Rule(new r.type.Term("select",[new r.type.Var("E"),new r.type.Term(".",[new r.type.Var("E"),new r.type.Var("Xs")]),new r.type.Var("Xs")]),null),new r.type.Rule(new r.type.Term("select",[new r.type.Var("E"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Ys")])]),new r.type.Term("select",[new r.type.Var("E"),new r.type.Var("Xs"),new r.type.Var("Ys")]))],"sum_list/2":[new r.type.Rule(new r.type.Term("sum_list",[new r.type.Term("[]",[]),new r.type.Num(0,!1)]),null),new r.type.Rule(new r.type.Term("sum_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("sum_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term("is",[new r.type.Var("S"),new r.type.Term("+",[new r.type.Var("X"),new r.type.Var("Y")])])]))],"max_list/2":[new r.type.Rule(new r.type.Term("max_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("max_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("max_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Var("Y")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("X")]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("Y")])])]))],"min_list/2":[new r.type.Rule(new r.type.Term("min_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("min_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("min_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term(";",[new r.type.Term(",",[new r.type.Term("=<",[new r.type.Var("X"),new r.type.Var("Y")]),new r.type.Term(",",[new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("X")]),new r.type.Term("!",[])])]),new r.type.Term("=",[new r.type.Var("S"),new r.type.Var("Y")])])]))],"prod_list/2":[new r.type.Rule(new r.type.Term("prod_list",[new r.type.Term("[]",[]),new r.type.Num(1,!1)]),null),new r.type.Rule(new r.type.Term("prod_list",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("S")]),new r.type.Term(",",[new r.type.Term("prod_list",[new r.type.Var("Xs"),new r.type.Var("Y")]),new r.type.Term("is",[new r.type.Var("S"),new r.type.Term("*",[new r.type.Var("X"),new r.type.Var("Y")])])]))],"last/2":[new r.type.Rule(new r.type.Term("last",[new r.type.Term(".",[new r.type.Var("X"),new r.type.Term("[]",[])]),new r.type.Var("X")]),null),new r.type.Rule(new r.type.Term("last",[new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("Xs")]),new r.type.Var("X")]),new r.type.Term("last",[new r.type.Var("Xs"),new r.type.Var("X")]))],"prefix/2":[new r.type.Rule(new r.type.Term("prefix",[new r.type.Var("Part"),new r.type.Var("Whole")]),new r.type.Term("append",[new r.type.Var("Part"),new r.type.Var("_"),new r.type.Var("Whole")]))],"nth0/3":[new r.type.Rule(new r.type.Term("nth0",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")])]),new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")]),new r.type.Term("!",[])])])]))],"nth1/3":[new r.type.Rule(new r.type.Term("nth1",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")])]),new r.type.Term(",",[new r.type.Term(">",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("_")]),new r.type.Term("!",[])])])]))],"nth0/4":[new r.type.Rule(new r.type.Term("nth0",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")])]),new r.type.Term(",",[new r.type.Term(">=",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(0,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term("!",[])])])]))],"nth1/4":[new r.type.Rule(new r.type.Term("nth1",[new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term(";",[new r.type.Term("->",[new r.type.Term("var",[new r.type.Var("X")]),new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")])]),new r.type.Term(",",[new r.type.Term(">",[new r.type.Var("X"),new r.type.Num(0,!1)]),new r.type.Term(",",[new r.type.Term("nth",[new r.type.Num(1,!1),new r.type.Var("X"),new r.type.Var("Y"),new r.type.Var("Z"),new r.type.Var("W")]),new r.type.Term("!",[])])])]))],"nth/5":[new r.type.Rule(new r.type.Term("nth",[new r.type.Var("N"),new r.type.Var("N"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("X"),new r.type.Var("Xs")]),null),new r.type.Rule(new r.type.Term("nth",[new r.type.Var("N"),new r.type.Var("O"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Xs")]),new r.type.Var("Y"),new r.type.Term(".",[new r.type.Var("X"),new r.type.Var("Ys")])]),new r.type.Term(",",[new r.type.Term("is",[new r.type.Var("M"),new r.type.Term("+",[new r.type.Var("N"),new r.type.Num(1,!1)])]),new r.type.Term("nth",[new r.type.Var("M"),new r.type.Var("O"),new r.type.Var("Xs"),new r.type.Var("Y"),new r.type.Var("Ys")])]))],"length/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(!r.type.is_variable(g)&&!r.type.is_integer(g))c.throw_error(r.error.type("integer",g,_.indicator));else if(r.type.is_integer(g)&&g.value<0)c.throw_error(r.error.domain("not_less_than_zero",g,_.indicator));else{var h=new r.type.Term("length",[v,new r.type.Num(0,!1),g]);r.type.is_integer(g)&&(h=new r.type.Term(",",[h,new r.type.Term("!",[])])),c.prepend([new r.type.State(w.goal.replace(h),w.substitution,w)])}},"length/3":[new r.type.Rule(new r.type.Term("length",[new r.type.Term("[]",[]),new r.type.Var("N"),new r.type.Var("N")]),null),new r.type.Rule(new r.type.Term("length",[new r.type.Term(".",[new r.type.Var("_"),new r.type.Var("X")]),new r.type.Var("A"),new r.type.Var("N")]),new r.type.Term(",",[new r.type.Term("succ",[new r.type.Var("A"),new r.type.Var("B")]),new r.type.Term("length",[new r.type.Var("X"),new r.type.Var("B"),new r.type.Var("N")])]))],"replicate/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_integer(g))c.throw_error(r.error.type("integer",g,_.indicator));else if(g.value<0)c.throw_error(r.error.domain("not_less_than_zero",g,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=new r.type.Term("[]"),T=0;T0;b--)T[b].equals(T[b-1])&&T.splice(b,1);for(var C=new r.type.Term("[]"),b=T.length-1;b>=0;b--)C=new r.type.Term(".",[T[b],C]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,g])),w.substitution,w)])}}},"msort/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else{for(var h=[],x=v;x.indicator==="./2";)h.push(x.args[0]),x=x.args[1];if(r.type.is_variable(x))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_empty_list(x))c.throw_error(r.error.type("list",v,_.indicator));else{for(var T=h.sort(r.compare),b=new r.type.Term("[]"),C=T.length-1;C>=0;C--)b=new r.type.Term(".",[T[C],b]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,g])),w.substitution,w)])}}},"keysort/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else{for(var h=[],x,T=v;T.indicator==="./2";){if(x=T.args[0],r.type.is_variable(x)){c.throw_error(r.error.instantiation(_.indicator));return}else if(!r.type.is_term(x)||x.indicator!=="-/2"){c.throw_error(r.error.type("pair",x,_.indicator));return}x.args[0].pair=x.args[1],h.push(x.args[0]),T=T.args[1]}if(r.type.is_variable(T))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_empty_list(T))c.throw_error(r.error.type("list",v,_.indicator));else{for(var b=h.sort(r.compare),C=new r.type.Term("[]"),N=b.length-1;N>=0;N--)C=new r.type.Term(".",[new r.type.Term("-",[b[N],b[N].pair]),C]),delete b[N].pair;c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,g])),w.substitution,w)])}}},"take/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g)||r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!r.type.is_integer(v))c.throw_error(r.error.type("integer",v,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=v.value,T=[],b=g;x>0&&b.indicator==="./2";)T.push(b.args[0]),b=b.args[1],x--;if(x===0){for(var C=new r.type.Term("[]"),x=T.length-1;x>=0;x--)C=new r.type.Term(".",[T[x],C]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[C,h])),w.substitution,w)])}}},"drop/3":function(c,w,_){var v=_.args[0],g=_.args[1],h=_.args[2];if(r.type.is_variable(g)||r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!r.type.is_integer(v))c.throw_error(r.error.type("integer",v,_.indicator));else if(!r.type.is_variable(h)&&!r.type.is_list(h))c.throw_error(r.error.type("list",h,_.indicator));else{for(var x=v.value,T=[],b=g;x>0&&b.indicator==="./2";)T.push(b.args[0]),b=b.args[1],x--;x===0&&c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,h])),w.substitution,w)])}},"reverse/2":function(c,w,_){var v=_.args[0],g=_.args[1],h=r.type.is_instantiated_list(v),x=r.type.is_instantiated_list(g);if(r.type.is_variable(v)&&r.type.is_variable(g))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_variable(v)&&!r.type.is_fully_list(v))c.throw_error(r.error.type("list",v,_.indicator));else if(!r.type.is_variable(g)&&!r.type.is_fully_list(g))c.throw_error(r.error.type("list",g,_.indicator));else if(!h&&!x)c.throw_error(r.error.instantiation(_.indicator));else{for(var T=h?v:g,b=new r.type.Term("[]",[]);T.indicator==="./2";)b=new r.type.Term(".",[T.args[0],b]),T=T.args[1];c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[b,h?g:v])),w.substitution,w)])}},"list_to_set/2":function(c,w,_){var v=_.args[0],g=_.args[1];if(r.type.is_variable(v))c.throw_error(r.error.instantiation(_.indicator));else{for(var h=v,x=[];h.indicator==="./2";)x.push(h.args[0]),h=h.args[1];if(r.type.is_variable(h))c.throw_error(r.error.instantiation(_.indicator));else if(!r.type.is_term(h)||h.indicator!=="[]/0")c.throw_error(r.error.type("list",v,_.indicator));else{for(var T=[],b=new r.type.Term("[]",[]),C,N=0;N=0;N--)b=new r.type.Term(".",[T[N],b]);c.prepend([new r.type.State(w.goal.replace(new r.type.Term("=",[g,b])),w.substitution,w)])}}}}},p=["append/2","append/3","member/2","permutation/2","maplist/2","maplist/3","maplist/4","maplist/5","maplist/6","maplist/7","maplist/8","include/3","exclude/3","foldl/4","sum_list/2","max_list/2","min_list/2","prod_list/2","last/2","prefix/2","nth0/3","nth1/3","nth0/4","nth1/4","length/2","replicate/3","select/3","sort/2","msort/2","keysort/2","take/3","drop/3","reverse/2","list_to_set/2"];typeof _r!="undefined"?_r.exports=function(c){r=c,new r.type.Module("lists",u(),p)}:new r.type.Module("lists",u(),p)})(Ki)});var et=I(M=>{"use strict";var Ve=process.platform==="win32",wr="aes-256-cbc",ji="sha256",Br="The current environment doesn't support interactive reading from TTY.",z=require("fs"),Fr=process.binding("tty_wrap").TTY,gr=require("child_process"),_e=require("path"),dr={prompt:"> ",hideEchoBack:!1,mask:"*",limit:[],limitMessage:"Input another, please.$<( [)limit(])>",defaultInput:"",trueValue:[],falseValue:[],caseSensitive:!1,keepWhitespace:!1,encoding:"utf8",bufferSize:1024,print:void 0,history:!0,cd:!1,phContent:void 0,preCheck:void 0},fe="none",ue,Ce,zr=!1,we,Ke,vr,es=0,hr="",Se=[],je,Lr=!1,mr=!1,$e=!1;function Wr(r){function u(p){return p.replace(/[^\w\u0080-\uFFFF]/g,function(c){return"#"+c.charCodeAt(0)+";"})}return Ke.concat(function(p){var c=[];return Object.keys(p).forEach(function(w){p[w]==="boolean"?r[w]&&c.push("--"+w):p[w]==="string"&&r[w]&&c.push("--"+w,u(r[w]))}),c}({display:"string",displayOnly:"boolean",keyIn:"boolean",hideEchoBack:"boolean",mask:"string",limit:"string",caseSensitive:"boolean"}))}function rs(r,u){function p(j){var U,Ue="",Ze;for(vr=vr||require("os").tmpdir();;){U=_e.join(vr,j+Ue);try{Ze=z.openSync(U,"wx")}catch(Qe){if(Qe.code==="EEXIST"){Ue++;continue}else throw Qe}z.closeSync(Ze);break}return U}var c,w,_,v={},g,h,x=p("readline-sync.stdout"),T=p("readline-sync.stderr"),b=p("readline-sync.exit"),C=p("readline-sync.done"),N=require("crypto"),L,ee,te;L=N.createHash(ji),L.update(""+process.pid+es+++Math.random()),te=L.digest("hex"),ee=N.createDecipher(wr,te),c=Wr(r),Ve?(w=process.env.ComSpec||"cmd.exe",process.env.Q='"',_=["/V:ON","/S","/C","(%Q%"+w+"%Q% /V:ON /S /C %Q%%Q%"+we+"%Q%"+c.map(function(j){return" %Q%"+j+"%Q%"}).join("")+" & (echo !ERRORLEVEL!)>%Q%"+b+"%Q%%Q%) 2>%Q%"+T+"%Q% |%Q%"+process.execPath+"%Q% %Q%"+__dirname+"\\encrypt.js%Q% %Q%"+wr+"%Q% %Q%"+te+"%Q% >%Q%"+x+"%Q% & (echo 1)>%Q%"+C+"%Q%"]):(w="/bin/sh",_=["-c",'("'+we+'"'+c.map(function(j){return" '"+j.replace(/'/g,"'\\''")+"'"}).join("")+'; echo $?>"'+b+'") 2>"'+T+'" |"'+process.execPath+'" "'+__dirname+'/encrypt.js" "'+wr+'" "'+te+'" >"'+x+'"; echo 1 >"'+C+'"']),$e&&$e("_execFileSync",c);try{gr.spawn(w,_,u)}catch(j){v.error=new Error(j.message),v.error.method="_execFileSync - spawn",v.error.program=w,v.error.args=_}for(;z.readFileSync(C,{encoding:r.encoding}).trim()!=="1";);return(g=z.readFileSync(b,{encoding:r.encoding}).trim())==="0"?v.input=ee.update(z.readFileSync(x,{encoding:"binary"}),"hex",r.encoding)+ee.final(r.encoding):(h=z.readFileSync(T,{encoding:r.encoding}).trim(),v.error=new Error(Br+(h?` +`+h:"")),v.error.method="_execFileSync",v.error.program=w,v.error.args=_,v.error.extMessage=h,v.error.exitCode=+g),z.unlinkSync(x),z.unlinkSync(T),z.unlinkSync(b),z.unlinkSync(C),v}function ts(r){var u,p={},c,w={env:process.env,encoding:r.encoding};if(we||(Ve?process.env.PSModulePath?(we="powershell.exe",Ke=["-ExecutionPolicy","Bypass","-File",__dirname+"\\read.ps1"]):(we="cscript.exe",Ke=["//nologo",__dirname+"\\read.cs.js"]):(we="/bin/sh",Ke=[__dirname+"/read.sh"])),Ve&&!process.env.PSModulePath&&(w.stdio=[process.stdin]),gr.execFileSync){u=Wr(r),$e&&$e("execFileSync",u);try{p.input=gr.execFileSync(we,u,w)}catch(_){c=_.stderr?(_.stderr+"").trim():"",p.error=new Error(Br+(c?` +`+c:"")),p.error.method="execFileSync",p.error.program=we,p.error.args=u,p.error.extMessage=c,p.error.exitCode=_.status,p.error.code=_.code,p.error.signal=_.signal}}else p=rs(r,w);return p.error||(p.input=p.input.replace(/^\s*'|'\s*$/g,""),r.display=""),p}function br(r){var u="",p=r.display,c=!r.display&&r.keyIn&&r.hideEchoBack&&!r.mask;function w(){var _=ts(r);if(_.error)throw _.error;return _.input}return mr&&mr(r),function(){var _,v,g;function h(){return _||(_=process.binding("fs"),v=process.binding("constants")),_}if(typeof fe=="string")if(fe=null,Ve){if(g=function(x){var T=x.replace(/^\D+/,"").split("."),b=0;return(T[0]=+T[0])&&(b+=T[0]*1e4),(T[1]=+T[1])&&(b+=T[1]*100),(T[2]=+T[2])&&(b+=T[2]),b}(process.version),!(g>=20302&&g<40204||g>=5e4&&g<50100||g>=50600&&g<60200)&&process.stdin.isTTY)process.stdin.pause(),fe=process.stdin.fd,Ce=process.stdin._handle;else try{fe=h().open("CONIN$",v.O_RDWR,parseInt("0666",8)),Ce=new Fr(fe,!0)}catch(x){}if(process.stdout.isTTY)ue=process.stdout.fd;else{try{ue=z.openSync("\\\\.\\CON","w")}catch(x){}if(typeof ue!="number")try{ue=h().open("CONOUT$",v.O_RDWR,parseInt("0666",8))}catch(x){}}}else{if(process.stdin.isTTY){process.stdin.pause();try{fe=z.openSync("/dev/tty","r"),Ce=process.stdin._handle}catch(x){}}else try{fe=z.openSync("/dev/tty","r"),Ce=new Fr(fe,!1)}catch(x){}if(process.stdout.isTTY)ue=process.stdout.fd;else try{ue=z.openSync("/dev/tty","w")}catch(x){}}}(),function(){var _,v,g=!r.hideEchoBack&&!r.keyIn,h,x,T,b,C;je="";function N(L){return L===zr?!0:Ce.setRawMode(L)!==0?!1:(zr=L,!0)}if(Lr||!Ce||typeof ue!="number"&&(r.display||!g)){u=w();return}if(r.display&&(z.writeSync(ue,r.display),r.display=""),!r.displayOnly){if(!N(!g)){u=w();return}for(x=r.keyIn?1:r.bufferSize,h=Buffer.allocUnsafe&&Buffer.alloc?Buffer.alloc(x):new Buffer(x),r.keyIn&&r.limit&&(v=new RegExp("[^"+r.limit+"]","g"+(r.caseSensitive?"":"i")));;){T=0;try{T=z.readSync(fe,h,0,x)}catch(L){if(L.code!=="EOF"){N(!1),u+=w();return}}if(T>0?(b=h.toString(r.encoding,0,T),je+=b):(b=` +`,je+=String.fromCharCode(0)),b&&typeof(C=(b.match(/^(.*?)[\r\n]/)||[])[1])=="string"&&(b=C,_=!0),b&&(b=b.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g,"")),b&&v&&(b=b.replace(v,"")),b&&(g||(r.hideEchoBack?r.mask&&z.writeSync(ue,new Array(b.length+1).join(r.mask)):z.writeSync(ue,b)),u+=b),!r.keyIn&&_||r.keyIn&&u.length>=x)break}!g&&!c&&z.writeSync(ue,` +`),N(!1)}}(),r.print&&!c&&r.print(p+(r.displayOnly?"":(r.hideEchoBack?new Array(u.length+1).join(r.mask):u)+` +`),r.encoding),r.displayOnly?"":hr=r.keepWhitespace||r.keyIn?u:u.trim()}function ns(r,u){var p=[];function c(w){w!=null&&(Array.isArray(w)?w.forEach(c):(!u||u(w))&&p.push(w))}return c(r),p}function Tr(r){return r.replace(/[\x00-\x7f]/g,function(u){return"\\x"+("00"+u.charCodeAt().toString(16)).substr(-2)})}function Z(){var r=Array.prototype.slice.call(arguments),u,p;return r.length&&typeof r[0]=="boolean"&&(p=r.shift(),p&&(u=Object.keys(dr),r.unshift(dr))),r.reduce(function(c,w){return w==null||(w.hasOwnProperty("noEchoBack")&&!w.hasOwnProperty("hideEchoBack")&&(w.hideEchoBack=w.noEchoBack,delete w.noEchoBack),w.hasOwnProperty("noTrim")&&!w.hasOwnProperty("keepWhitespace")&&(w.keepWhitespace=w.noTrim,delete w.noTrim),p||(u=Object.keys(w)),u.forEach(function(_){var v;if(!!w.hasOwnProperty(_))switch(v=w[_],_){case"mask":case"limitMessage":case"defaultInput":case"encoding":v=v!=null?v+"":"",v&&_!=="limitMessage"&&(v=v.replace(/[\r\n]/g,"")),c[_]=v;break;case"bufferSize":!isNaN(v=parseInt(v,10))&&typeof v=="number"&&(c[_]=v);break;case"displayOnly":case"keyIn":case"hideEchoBack":case"caseSensitive":case"keepWhitespace":case"history":case"cd":c[_]=!!v;break;case"limit":case"trueValue":case"falseValue":c[_]=ns(v,function(g){var h=typeof g;return h==="string"||h==="number"||h==="function"||g instanceof RegExp}).map(function(g){return typeof g=="string"?g.replace(/[\r\n]/g,""):g});break;case"print":case"phContent":case"preCheck":c[_]=typeof v=="function"?v:void 0;break;case"prompt":case"display":c[_]=v!=null?v:"";break}})),c},{})}function xr(r,u,p){return u.some(function(c){var w=typeof c;return w==="string"?p?r===c:r.toLowerCase()===c.toLowerCase():w==="number"?parseFloat(r)===c:w==="function"?c(r):c instanceof RegExp?c.test(r):!1})}function Vr(r,u){var p=_e.normalize(Ve?(process.env.HOMEDRIVE||"")+(process.env.HOMEPATH||""):process.env.HOME||"").replace(/[\/\\]+$/,"");return r=_e.normalize(r),u?r.replace(/^~(?=\/|\\|$)/,p):r.replace(new RegExp("^"+Tr(p)+"(?=\\/|\\\\|$)",Ve?"i":""),"~")}function Oe(r,u){var p="(?:\\(([\\s\\S]*?)\\))?(\\w+|.-.)(?:\\(([\\s\\S]*?)\\))?",c=new RegExp("(\\$)?(\\$<"+p+">)","g"),w=new RegExp("(\\$)?(\\$\\{"+p+"\\})","g");function _(v,g,h,x,T,b){var C;return g||typeof(C=u(T))!="string"?h:C?(x||"")+C+(b||""):""}return r.replace(c,_).replace(w,_)}function Hr(r,u,p){var c,w=[],_=-1,v=0,g="",h;function x(T,b){return b.length>3?(T.push(b[0]+"..."+b[b.length-1]),h=!0):b.length&&(T=T.concat(b)),T}return c=r.reduce(function(T,b){return T.concat((b+"").split(""))},[]).reduce(function(T,b){var C,N;return u||(b=b.toLowerCase()),C=/^\d$/.test(b)?1:/^[A-Z]$/.test(b)?2:/^[a-z]$/.test(b)?3:0,p&&C===0?g+=b:(N=b.charCodeAt(0),C&&C===_&&N===v+1?w.push(b):(T=x(T,w),w=[b],_=C),v=N),T},[]),c=x(c,w),g&&(c.push(g),h=!0),{values:c,suppressed:h}}function Gr(r,u){return r.join(r.length>2?", ":u?" / ":"/")}function Yr(r,u){var p,c,w={},_;if(u.phContent&&(p=u.phContent(r,u)),typeof p!="string")switch(r){case"hideEchoBack":case"mask":case"defaultInput":case"caseSensitive":case"keepWhitespace":case"encoding":case"bufferSize":case"history":case"cd":p=u.hasOwnProperty(r)?typeof u[r]=="boolean"?u[r]?"on":"off":u[r]+"":"";break;case"limit":case"trueValue":case"falseValue":c=u[u.hasOwnProperty(r+"Src")?r+"Src":r],u.keyIn?(w=Hr(c,u.caseSensitive),c=w.values):c=c.filter(function(v){var g=typeof v;return g==="string"||g==="number"}),p=Gr(c,w.suppressed);break;case"limitCount":case"limitCountNotZero":p=u[u.hasOwnProperty("limitSrc")?"limitSrc":"limit"].length,p=p||r!=="limitCountNotZero"?p+"":"";break;case"lastInput":p=hr;break;case"cwd":case"CWD":case"cwdHome":p=process.cwd(),r==="CWD"?p=_e.basename(p):r==="cwdHome"&&(p=Vr(p));break;case"date":case"time":case"localeDate":case"localeTime":p=new Date()["to"+r.replace(/^./,function(v){return v.toUpperCase()})+"String"]();break;default:typeof(_=(r.match(/^history_m(\d+)$/)||[])[1])=="string"&&(p=Se[Se.length-_]||"")}return p}function Ur(r){var u=/^(.)-(.)$/.exec(r),p="",c,w,_,v;if(!u)return null;for(c=u[1].charCodeAt(0),w=u[2].charCodeAt(0),v=c +And the length must be: $`,trueValue:null,falseValue:null,caseSensitive:!0},u,{history:!1,cd:!1,phContent:function(N){return N==="charlist"?p.text:N==="length"?c+"..."+w:null}}),v,g,h,x,T,b,C;for(u=u||{},v=Oe(u.charlist?u.charlist+"":"$",Ur),(isNaN(c=parseInt(u.min,10))||typeof c!="number")&&(c=12),(isNaN(w=parseInt(u.max,10))||typeof w!="number")&&(w=24),x=new RegExp("^["+Tr(v)+"]{"+c+","+w+"}$"),p=Hr([v],_.caseSensitive,!0),p.text=Gr(p.values,p.suppressed),g=u.confirmMessage!=null?u.confirmMessage:"Reinput a same one to confirm it: ",h=u.unmatchMessage!=null?u.unmatchMessage:"It differs from first one. Hit only the Enter key if you want to retry from first one.",r==null&&(r="Input new password: "),T=_.limitMessage;!C;)_.limit=x,_.limitMessage=T,b=M.question(r,_),_.limit=[b,""],_.limitMessage=h,C=M.question(g,_);return b};function Jr(r,u,p){var c;function w(_){return c=p(_),!isNaN(c)&&typeof c=="number"}return M.question(r,Z({limitMessage:"Input valid number, please."},u,{limit:w,cd:!1})),c}M.questionInt=function(r,u){return Jr(r,u,function(p){return parseInt(p,10)})};M.questionFloat=function(r,u){return Jr(r,u,parseFloat)};M.questionPath=function(r,u){var p,c="",w=Z({hideEchoBack:!1,limitMessage:`$Input valid path, please.$<( Min:)min>$<( Max:)max>`,history:!0,cd:!0},u,{keepWhitespace:!1,limit:function(_){var v,g,h;_=Vr(_,!0),c="";function x(T){T.split(/\/|\\/).reduce(function(b,C){var N=_e.resolve(b+=C+_e.sep);if(!z.existsSync(N))z.mkdirSync(N);else if(!z.statSync(N).isDirectory())throw new Error("Non directory already exists: "+N);return b},"")}try{if(v=z.existsSync(_),p=v?z.realpathSync(_):_e.resolve(_),!u.hasOwnProperty("exists")&&!v||typeof u.exists=="boolean"&&u.exists!==v)return c=(v?"Already exists":"No such file or directory")+": "+p,!1;if(!v&&u.create&&(u.isDirectory?x(p):(x(_e.dirname(p)),z.closeSync(z.openSync(p,"w"))),p=z.realpathSync(p)),v&&(u.min||u.max||u.isFile||u.isDirectory)){if(g=z.statSync(p),u.isFile&&!g.isFile())return c="Not file: "+p,!1;if(u.isDirectory&&!g.isDirectory())return c="Not directory: "+p,!1;if(u.min&&g.size<+u.min||u.max&&g.size>+u.max)return c="Size "+g.size+" is out of range: "+p,!1}if(typeof u.validate=="function"&&(h=u.validate(p))!==!0)return typeof h=="string"&&(c=h),!1}catch(T){return c=T+"",!1}return!0},phContent:function(_){return _==="error"?c:_!=="min"&&_!=="max"?null:u.hasOwnProperty(_)?u[_]+"":""}});return u=u||{},r==null&&(r='Input path (you can "cd" and "pwd"): '),M.question(r,w),p};function Kr(r,u){var p={},c={};return typeof r=="object"?(Object.keys(r).forEach(function(w){typeof r[w]=="function"&&(c[u.caseSensitive?w:w.toLowerCase()]=r[w])}),p.preCheck=function(w){var _;return p.args=Sr(w),_=p.args[0]||"",u.caseSensitive||(_=_.toLowerCase()),p.hRes=_!=="_"&&c.hasOwnProperty(_)?c[_].apply(w,p.args.slice(1)):c.hasOwnProperty("_")?c._.apply(w,p.args):null,{res:w,forceNext:!1}},c.hasOwnProperty("_")||(p.limit=function(){var w=p.args[0]||"";return u.caseSensitive||(w=w.toLowerCase()),c.hasOwnProperty(w)})):p.preCheck=function(w){return p.args=Sr(w),p.hRes=typeof r=="function"?r.apply(w,p.args):!0,{res:w,forceNext:!1}},p}M.promptCL=function(r,u){var p=Z({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},u),c=Kr(r,p);return p.limit=c.limit,p.preCheck=c.preCheck,M.prompt(p),c.args};M.promptLoop=function(r,u){for(var p=Z({hideEchoBack:!1,trueValue:null,falseValue:null,caseSensitive:!1,history:!0},u);!r(M.prompt(p)););};M.promptCLLoop=function(r,u){var p=Z({hideEchoBack:!1,limitMessage:"Requested command is not available.",caseSensitive:!1,history:!0},u),c=Kr(r,p);for(p.limit=c.limit,p.preCheck=c.preCheck;M.prompt(p),!c.hRes;);};M.promptSimShell=function(r){return M.prompt(Z({hideEchoBack:!1,history:!0},r,{prompt:function(){return Ve?"$>":(process.env.USER||"")+(process.env.HOSTNAME?"@"+process.env.HOSTNAME.replace(/\..*$/,""):"")+":$$ "}()}))};function jr(r,u,p){var c;return r==null&&(r="Are you sure? "),(!u||u.guide!==!1)&&(r+="")&&(r=r.replace(/\s*:?\s*$/,"")+" [y/n]: "),c=M.keyIn(r,Z(u,{hideEchoBack:!1,limit:p,trueValue:"y",falseValue:"n",caseSensitive:!1})),typeof c=="boolean"?c:""}M.keyInYN=function(r,u){return jr(r,u)};M.keyInYNStrict=function(r,u){return jr(r,u,"yn")};M.keyInPause=function(r,u){r==null&&(r="Continue..."),(!u||u.guide!==!1)&&(r+="")&&(r=r.replace(/\s+$/,"")+" (Hit any key)"),M.keyIn(r,Z({limit:null},u,{hideEchoBack:!0,mask:""}))};M.keyInSelect=function(r,u,p){var c=Z({hideEchoBack:!1},p,{trueValue:null,falseValue:null,caseSensitive:!1,phContent:function(h){return h==="itemsCount"?r.length+"":h==="firstItem"?(r[0]+"").trim():h==="lastItem"?(r[r.length-1]+"").trim():null}}),w="",_={},v=49,g=` +`;if(!Array.isArray(r)||!r.length||r.length>35)throw"`items` must be Array (max length: 35).";return r.forEach(function(h,x){var T=String.fromCharCode(v);w+=T,_[T]=x,g+="["+T+"] "+(h+"").trim()+` +`,v=v===57?97:v+1}),(!p||p.cancel!==!1)&&(w+="0",_["0"]=-1,g+="[0] "+(p&&p.cancel!=null&&typeof p.cancel!="boolean"?(p.cancel+"").trim():"CANCEL")+` +`),c.limit=w,g+=` +`,u==null&&(u="Choose one from list: "),(u+="")&&((!p||p.guide!==!1)&&(u=u.replace(/\s*:?\s*$/,"")+" [$]: "),g+=u),_[M.keyIn(g,c).toLowerCase()]};M.getRawInput=function(){return je};function De(r,u){var p;return u.length&&(p={},p[r]=u[0]),M.setDefaultOptions(p)[r]}M.setPrint=function(){return De("print",arguments)};M.setPrompt=function(){return De("prompt",arguments)};M.setEncoding=function(){return De("encoding",arguments)};M.setMask=function(){return De("mask",arguments)};M.setBufferSize=function(){return De("bufferSize",arguments)}});var kr=I((Mu,ie)=>{(function(){var r={major:0,minor:2,patch:66,status:"beta"};tau_file_system={files:{},open:function(e,n,t){var s=tau_file_system.files[e];if(!s){if(t==="read")return null;s={path:e,text:"",type:n,get:function(a,l){return l===this.text.length||l>this.text.length?"end_of_file":this.text.substring(l,l+a)},put:function(a,l){return l==="end_of_file"?(this.text+=a,!0):l==="past_end_of_file"?null:(this.text=this.text.substring(0,l)+a+this.text.substring(l+a.length),!0)},get_byte:function(a){if(a==="end_of_stream")return-1;var l=Math.floor(a/2);if(this.text.length<=l)return-1;var f=_(this.text[Math.floor(a/2)],0);return a%2==0?f&255:f/256>>>0},put_byte:function(a,l){var f=l==="end_of_stream"?this.text.length:Math.floor(l/2);if(this.text.length>>0,y=(y&255)<<8|a&255):(y=y&255,y=(a&255)<<8|y&255),this.text.length===f?this.text+=v(y):this.text=this.text.substring(0,f)+v(y)+this.text.substring(f+1),!0},flush:function(){return!0},close:function(){var a=tau_file_system.files[this.path];return a?!0:null}},tau_file_system.files[e]=s}return t==="write"&&(s.text=""),s}},tau_user_input={buffer:"",get:function(e,n){for(var t;tau_user_input.buffer.length\?\@\^\~\\]+|'(?:[^']*?(?:\\(?:x?\d+)?\\)*(?:'')*(?:\\')*)*')/,number:/^(?:0o[0-7]+|0x[0-9a-fA-F]+|0b[01]+|0'(?:''|\\[abfnrtv\\'"`]|\\x?\d+\\|[^\\])|\d+(?:\.\d+(?:[eE][+-]?\d+)?)?)/,string:/^(?:"([^"]|""|\\")*"|`([^`]|``|\\`)*`)/,l_brace:/^(?:\[)/,r_brace:/^(?:\])/,l_bracket:/^(?:\{)/,r_bracket:/^(?:\})/,bar:/^(?:\|)/,l_paren:/^(?:\()/,r_paren:/^(?:\))/};function te(e,n){return e.get_flag("char_conversion").id==="on"?n.replace(/./g,function(t){return e.get_char_conversion(t)}):n}function j(e){this.thread=e,this.text="",this.tokens=[]}j.prototype.set_last_tokens=function(e){return this.tokens=e},j.prototype.new_text=function(e){this.text=e,this.tokens=[]},j.prototype.get_tokens=function(e){var n,t=0,s=0,a=0,l=[],f=!1;if(e){var y=this.tokens[e-1];t=y.len,n=te(this.thread,this.text.substr(y.len)),s=y.line,a=y.start}else n=this.text;if(/^\s*$/.test(n))return null;for(;n!=="";){var d=[],m=!1;if(/^\n/.exec(n)!==null){s++,a=0,t++,n=n.replace(/\n/,""),f=!0;continue}for(var S in ee)if(ee.hasOwnProperty(S)){var P=ee[S].exec(n);P&&d.push({value:P[0],name:S,matches:P})}if(!d.length)return this.set_last_tokens([{value:n,matches:[],name:"lexical",line:s,start:a}]);var y=p(d,function(B,q){return B.value.length>=q.value.length?B:q});switch(y.start=a,y.line=s,n=n.replace(y.value,""),a+=y.value.length,t+=y.value.length,y.name){case"atom":y.raw=y.value,y.value.charAt(0)==="'"&&(y.value=C(y.value.substr(1,y.value.length-2),"'"),y.value===null&&(y.name="lexical",y.value="unknown escape sequence"));break;case"number":y.float=y.value.substring(0,2)!=="0x"&&y.value.match(/[.eE]/)!==null&&y.value!=="0'.",y.value=L(y.value),y.blank=m;break;case"string":var A=y.value.charAt(0);y.value=C(y.value.substr(1,y.value.length-2),A),y.value===null&&(y.name="lexical",y.value="unknown escape sequence");break;case"whitespace":var R=l[l.length-1];R&&(R.space=!0),m=!0;continue;case"r_bracket":l.length>0&&l[l.length-1].name==="l_bracket"&&(y=l.pop(),y.name="atom",y.value="{}",y.raw="{}",y.space=!1);break;case"r_brace":l.length>0&&l[l.length-1].name==="l_brace"&&(y=l.pop(),y.name="atom",y.value="[]",y.raw="[]",y.space=!1);break}y.len=t,l.push(y),m=!1}var k=this.set_last_tokens(l);return k.length===0?null:k};function U(e,n,t,s,a){if(!n[t])return{type:g,value:i.error.syntax(n[t-1],"expression expected",!0)};var l;if(s==="0"){var f=n[t];switch(f.name){case"number":return{type:h,len:t+1,value:new i.type.Num(f.value,f.float)};case"variable":return{type:h,len:t+1,value:new i.type.Var(f.value)};case"string":var y;switch(e.get_flag("double_quotes").id){case"atom":y=new o(f.value,[]);break;case"codes":y=new o("[]",[]);for(var d=f.value.length-1;d>=0;d--)y=new o(".",[new i.type.Num(_(f.value,d),!1),y]);break;case"chars":y=new o("[]",[]);for(var d=f.value.length-1;d>=0;d--)y=new o(".",[new i.type.Term(f.value.charAt(d),[]),y]);break}return{type:h,len:t+1,value:y};case"l_paren":var k=U(e,n,t+1,e.__get_max_priority(),!0);return k.type!==h?k:n[k.len]&&n[k.len].name==="r_paren"?(k.len++,k):{type:g,derived:!0,value:i.error.syntax(n[k.len]?n[k.len]:n[k.len-1],") or operator expected",!n[k.len])};case"l_bracket":var k=U(e,n,t+1,e.__get_max_priority(),!0);return k.type!==h?k:n[k.len]&&n[k.len].name==="r_bracket"?(k.len++,k.value=new o("{}",[k.value]),k):{type:g,derived:!0,value:i.error.syntax(n[k.len]?n[k.len]:n[k.len-1],"} or operator expected",!n[k.len])}}var m=Ue(e,n,t,a);return m.type===h||m.derived||(m=Ze(e,n,t),m.type===h||m.derived)?m:{type:g,derived:!1,value:i.error.syntax(n[t],"unexpected token")}}var S=e.__get_max_priority(),P=e.__get_next_priority(s),A=t;if(n[t].name==="atom"&&n[t+1]&&(n[t].space||n[t+1].name!=="l_paren")){var f=n[t++],R=e.__lookup_operator_classes(s,f.value);if(R&&R.indexOf("fy")>-1){var k=U(e,n,t,s,a);if(k.type!==g)return f.value==="-"&&!f.space&&i.type.is_number(k.value)?{value:new i.type.Num(-k.value.value,k.value.is_float),len:k.len,type:h}:{value:new i.type.Term(f.value,[k.value]),len:k.len,type:h};l=k}else if(R&&R.indexOf("fx")>-1){var k=U(e,n,t,P,a);if(k.type!==g)return{value:new i.type.Term(f.value,[k.value]),len:k.len,type:h};l=k}}t=A;var k=U(e,n,t,P,a);if(k.type===h){t=k.len;var f=n[t];if(n[t]&&(n[t].name==="atom"&&e.__lookup_operator_classes(s,f.value)||n[t].name==="bar"&&e.__lookup_operator_classes(s,"|"))){var W=P,B=s,R=e.__lookup_operator_classes(s,f.value);if(R.indexOf("xf")>-1)return{value:new i.type.Term(f.value,[k.value]),len:++k.len,type:h};if(R.indexOf("xfx")>-1){var q=U(e,n,t+1,W,a);return q.type===h?{value:new i.type.Term(f.value,[k.value,q.value]),len:q.len,type:h}:(q.derived=!0,q)}else if(R.indexOf("xfy")>-1){var q=U(e,n,t+1,B,a);return q.type===h?{value:new i.type.Term(f.value,[k.value,q.value]),len:q.len,type:h}:(q.derived=!0,q)}else if(k.type!==g)for(;;){t=k.len;var f=n[t];if(f&&f.name==="atom"&&e.__lookup_operator_classes(s,f.value)){var R=e.__lookup_operator_classes(s,f.value);if(R.indexOf("yf")>-1)k={value:new i.type.Term(f.value,[k.value]),len:++t,type:h};else if(R.indexOf("yfx")>-1){var q=U(e,n,++t,W,a);if(q.type===g)return q.derived=!0,q;t=q.len,k={value:new i.type.Term(f.value,[k.value,q.value]),len:t,type:h}}else break}else break}}else l={type:g,value:i.error.syntax(n[k.len-1],"operator expected")};return k}return k}function Ue(e,n,t,s){if(!n[t]||n[t].name==="atom"&&n[t].raw==="."&&!s&&(n[t].space||!n[t+1]||n[t+1].name!=="l_paren"))return{type:g,derived:!1,value:i.error.syntax(n[t-1],"unfounded token")};var a=n[t],l=[];if(n[t].name==="atom"&&n[t].raw!==","){if(t++,n[t-1].space)return{type:h,len:t,value:new i.type.Term(a.value,l)};if(n[t]&&n[t].name==="l_paren"){if(n[t+1]&&n[t+1].name==="r_paren")return{type:g,derived:!0,value:i.error.syntax(n[t+1],"argument expected")};var f=U(e,n,++t,"999",!0);if(f.type===g)return f.derived?f:{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],"argument expected",!n[t])};for(l.push(f.value),t=f.len;n[t]&&n[t].name==="atom"&&n[t].value===",";){if(f=U(e,n,t+1,"999",!0),f.type===g)return f.derived?f:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};l.push(f.value),t=f.len}if(n[t]&&n[t].name==="r_paren")t++;else return{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],", or ) expected",!n[t])}}return{type:h,len:t,value:new i.type.Term(a.value,l)}}return{type:g,derived:!1,value:i.error.syntax(n[t],"term expected")}}function Ze(e,n,t){if(!n[t])return{type:g,derived:!1,value:i.error.syntax(n[t-1],"[ expected")};if(n[t]&&n[t].name==="l_brace"){var s=U(e,n,++t,"999",!0),a=[s.value],l=void 0;if(s.type===g)return n[t]&&n[t].name==="r_brace"?{type:h,len:t+1,value:new i.type.Term("[]",[])}:{type:g,derived:!0,value:i.error.syntax(n[t],"] expected")};for(t=s.len;n[t]&&n[t].name==="atom"&&n[t].value===",";){if(s=U(e,n,t+1,"999",!0),s.type===g)return s.derived?s:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};a.push(s.value),t=s.len}var f=!1;if(n[t]&&n[t].name==="bar"){if(f=!0,s=U(e,n,t+1,"999",!0),s.type===g)return s.derived?s:{type:g,derived:!0,value:i.error.syntax(n[t+1]?n[t+1]:n[t],"argument expected",!n[t+1])};l=s.value,t=s.len}return n[t]&&n[t].name==="r_brace"?{type:h,len:t+1,value:he(a,l)}:{type:g,derived:!0,value:i.error.syntax(n[t]?n[t]:n[t-1],f?"] expected":", or | or ] expected",!n[t])}}return{type:g,derived:!1,value:i.error.syntax(n[t],"list expected")}}function Qe(e,n,t){var s=n[t].line,a=U(e,n,t,e.__get_max_priority(),!1),l=null,f;if(a.type!==g)if(t=a.len,n[t]&&n[t].name==="atom"&&n[t].raw===".")if(t++,i.type.is_term(a.value)){if(a.value.indicator===":-/2"?(l=new i.type.Rule(a.value.args[0],ve(a.value.args[1])),f={value:l,len:t,type:h}):a.value.indicator==="-->/2"?(l=Bi(new i.type.Rule(a.value.args[0],a.value.args[1]),e),l.body=ve(l.body),f={value:l,len:t,type:i.type.is_rule(l)?h:g}):(l=new i.type.Rule(a.value,null),f={value:l,len:t,type:h}),l){var y=l.singleton_variables();y.length>0&&e.throw_warning(i.warning.singleton(y,l.head.indicator,s))}return f}else return{type:g,value:i.error.syntax(n[t],"callable expected")};else return{type:g,value:i.error.syntax(n[t]?n[t]:n[t-1],". or operator expected")};return a}function Di(e,n,t){t=t||{},t.from=t.from?t.from:"$tau-js",t.reconsult=t.reconsult!==void 0?t.reconsult:!0;var s=new j(e),a={},l;s.new_text(n);var f=0,y=s.get_tokens(f);do{if(y===null||!y[f])break;var d=Qe(e,y,f);if(d.type===g)return new o("throw",[d.value]);if(d.value.body===null&&d.value.head.indicator==="?-/1"){var m=new X(e.session);m.add_goal(d.value.head.args[0]),m.answer(function(P){i.type.is_error(P)?e.throw_warning(P.args[0]):(P===!1||P===null)&&e.throw_warning(i.warning.failed_goal(d.value.head.args[0],d.len))}),f=d.len;var S=!0}else if(d.value.body===null&&d.value.head.indicator===":-/1"){var S=e.run_directive(d.value.head.args[0]);f=d.len,d.value.head.args[0].indicator==="char_conversion/2"&&(y=s.get_tokens(f),f=0)}else{l=d.value.head.indicator,t.reconsult!==!1&&a[l]!==!0&&!e.is_multifile_predicate(l)&&(e.session.rules[l]=w(e.session.rules[l]||[],function(A){return A.dynamic}),a[l]=!0);var S=e.add_rule(d.value,t);f=d.len}if(!S)return S}while(!0);return!0}function Xi(e,n){var t=new j(e);t.new_text(n);var s=0;do{var a=t.get_tokens(s);if(a===null)break;var l=U(e,a,0,e.__get_max_priority(),!1);if(l.type!==g){var f=l.len,y=f;if(a[f]&&a[f].name==="atom"&&a[f].raw===".")e.add_goal(ve(l.value));else{var d=a[f];return new o("throw",[i.error.syntax(d||a[f-1],". or operator expected",!d)])}s=l.len+1}else return new o("throw",[l.value])}while(!0);return!0}function Bi(e,n){e=e.rename(n);var t=n.next_free_variable(),s=pr(e.body,t,n);return s.error?s.value:(e.body=s.value,e.head.args=e.head.args.concat([t,s.variable]),e.head=new o(e.head.id,e.head.args),e)}function pr(e,n,t){var s;if(i.type.is_term(e)&&e.indicator==="!/0")return{value:e,variable:n,error:!1};if(i.type.is_term(e)&&e.indicator===",/2"){var a=pr(e.args[0],n,t);if(a.error)return a;var l=pr(e.args[1],a.variable,t);return l.error?l:{value:new o(",",[a.value,l.value]),variable:l.variable,error:!1}}else{if(i.type.is_term(e)&&e.indicator==="{}/1")return{value:e.args[0],variable:n,error:!1};if(i.type.is_empty_list(e))return{value:new o("true",[]),variable:n,error:!1};if(i.type.is_list(e)){s=t.next_free_variable();for(var f=e,y;f.indicator==="./2";)y=f,f=f.args[1];return i.type.is_variable(f)?{value:i.error.instantiation("DCG"),variable:n,error:!0}:i.type.is_empty_list(f)?(y.args[1]=s,{value:new o("=",[n,e]),variable:s,error:!1}):{value:i.error.type("list",e,"DCG"),variable:n,error:!0}}else return i.type.is_callable(e)?(s=t.next_free_variable(),e.args=e.args.concat([n,s]),e=new o(e.id,e.args),{value:e,variable:s,error:!1}):{value:i.error.type("callable",e,"DCG"),variable:n,error:!0}}}function ve(e){return i.type.is_variable(e)?new o("call",[e]):i.type.is_term(e)&&[",/2",";/2","->/2"].indexOf(e.indicator)!==-1?new o(e.id,[ve(e.args[0]),ve(e.args[1])]):e}function he(e,n){for(var t=n||new i.type.Term("[]",[]),s=e.length-1;s>=0;s--)t=new i.type.Term(".",[e[s],t]);return t}function Fi(e,n){for(var t=e.length-1;t>=0;t--)e[t]===n&&e.splice(t,1)}function yr(e){for(var n={},t=[],s=0;s=0;n--)if(e.charAt(n)==="/")return new o("/",[new o(e.substring(0,n)),new E(parseInt(e.substring(n+1)),!1)])}function O(e){this.id=e}function E(e,n){this.is_float=n!==void 0?n:parseInt(e)!==e,this.value=this.is_float?e:parseInt(e)}var $r=0;function o(e,n,t){this.ref=t||++$r,this.id=e,this.args=n||[],this.indicator=e+"/"+this.args.length}var Li=0;function ne(e,n,t,s,a,l){this.id=Li++,this.stream=e,this.mode=n,this.alias=t,this.type=s!==void 0?s:"text",this.reposition=a!==void 0?a:!0,this.eof_action=l!==void 0?l:"eof_code",this.position=this.mode==="append"?"end_of_stream":0,this.output=this.mode==="write"||this.mode==="append",this.input=this.mode==="read"}function Y(e){e=e||{},this.links=e}function V(e,n,t){n=n||new Y,t=t||null,this.goal=e,this.substitution=n,this.parent=t}function Q(e,n,t){this.head=e,this.body=n,this.dynamic=t||!1}function D(e){e=e===void 0||e<=0?1e3:e,this.rules={},this.src_predicates={},this.rename=0,this.modules=[],this.thread=new X(this),this.total_threads=1,this.renamed_variables={},this.public_predicates={},this.multifile_predicates={},this.limit=e,this.streams={user_input:new ne(typeof ie!="undefined"&&ie.exports?nodejs_user_input:tau_user_input,"read","user_input","text",!1,"reset"),user_output:new ne(typeof ie!="undefined"&&ie.exports?nodejs_user_output:tau_user_output,"write","user_output","text",!1,"eof_code")},this.file_system=typeof ie!="undefined"&&ie.exports?nodejs_file_system:tau_file_system,this.standard_input=this.streams.user_input,this.standard_output=this.streams.user_output,this.current_input=this.streams.user_input,this.current_output=this.streams.user_output,this.format_success=function(n){return n.substitution},this.format_error=function(n){return n.goal},this.flag={bounded:i.flag.bounded.value,max_integer:i.flag.max_integer.value,min_integer:i.flag.min_integer.value,integer_rounding_function:i.flag.integer_rounding_function.value,char_conversion:i.flag.char_conversion.value,debug:i.flag.debug.value,max_arity:i.flag.max_arity.value,unknown:i.flag.unknown.value,double_quotes:i.flag.double_quotes.value,occurs_check:i.flag.occurs_check.value,dialect:i.flag.dialect.value,version_data:i.flag.version_data.value,nodejs:i.flag.nodejs.value},this.__loaded_modules=[],this.__char_conversion={},this.__operators={1200:{":-":["fx","xfx"],"-->":["xfx"],"?-":["fx"]},1100:{";":["xfy"]},1050:{"->":["xfy"]},1e3:{",":["xfy"]},900:{"\\+":["fy"]},700:{"=":["xfx"],"\\=":["xfx"],"==":["xfx"],"\\==":["xfx"],"@<":["xfx"],"@=<":["xfx"],"@>":["xfx"],"@>=":["xfx"],"=..":["xfx"],is:["xfx"],"=:=":["xfx"],"=\\=":["xfx"],"<":["xfx"],"=<":["xfx"],">":["xfx"],">=":["xfx"]},600:{":":["xfy"]},500:{"+":["yfx"],"-":["yfx"],"/\\":["yfx"],"\\/":["yfx"]},400:{"*":["yfx"],"/":["yfx"],"//":["yfx"],rem:["yfx"],mod:["yfx"],"<<":["yfx"],">>":["yfx"]},200:{"**":["xfx"],"^":["xfy"],"-":["fy"],"+":["fy"],"\\":["fy"]}}}function X(e){this.epoch=Date.now(),this.session=e,this.session.total_threads++,this.total_steps=0,this.cpu_time=0,this.cpu_time_last=0,this.points=[],this.debugger=!1,this.debugger_states=[],this.level="top_level/0",this.__calls=[],this.current_limit=this.session.limit,this.warnings=[]}function Dr(e,n,t){this.id=e,this.rules=n,this.exports=t,i.module[e]=this}Dr.prototype.exports_predicate=function(e){return this.exports.indexOf(e)!==-1},O.prototype.unify=function(e,n){if(n&&u(e.variables(),this.id)!==-1&&!i.type.is_variable(e))return null;var t={};return t[this.id]=e,new Y(t)},E.prototype.unify=function(e,n){return i.type.is_number(e)&&this.value===e.value&&this.is_float===e.is_float?new Y:null},o.prototype.unify=function(e,n){if(i.type.is_term(e)&&this.indicator===e.indicator){for(var t=new Y,s=0;s=0){var s=this.args[0].value,a=Math.floor(s/26),l=s%26;return"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[l]+(a!==0?a:"")}switch(this.indicator){case"[]/0":case"{}/0":case"!/0":return this.id;case"{}/1":return"{"+this.args[0].toString(e)+"}";case"./2":for(var f="["+this.args[0].toString(e),y=this.args[1];y.indicator==="./2";)f+=", "+y.args[0].toString(e),y=y.args[1];return y.indicator!=="[]/0"&&(f+="|"+y.toString(e)),f+="]",f;case",/2":return"("+this.args[0].toString(e)+", "+this.args[1].toString(e)+")";default:var d=this.id,m=e.session?e.session.lookup_operator(this.id,this.args.length):null;if(e.session===void 0||e.ignore_ops||m===null)return e.quoted&&!/^(!|,|;|[a-z][0-9a-zA-Z_]*)$/.test(d)&&d!=="{}"&&d!=="[]"&&(d="'"+N(d)+"'"),d+(this.args.length?"("+c(this.args,function(R){return R.toString(e)}).join(", ")+")":"");var S=m.priority>n.priority||m.priority===n.priority&&(m.class==="xfy"&&this.indicator!==n.indicator||m.class==="yfx"&&this.indicator!==n.indicator||this.indicator===n.indicator&&m.class==="yfx"&&t==="right"||this.indicator===n.indicator&&m.class==="xfy"&&t==="left");m.indicator=this.indicator;var P=S?"(":"",A=S?")":"";return this.args.length===0?"("+this.id+")":["fy","fx"].indexOf(m.class)!==-1?P+d+" "+this.args[0].toString(e,m)+A:["yf","xf"].indexOf(m.class)!==-1?P+this.args[0].toString(e,m)+" "+d+A:P+this.args[0].toString(e,m,"left")+" "+this.id+" "+this.args[1].toString(e,m,"right")+A}},ne.prototype.toString=function(e){return"("+this.id+")"},Y.prototype.toString=function(e){var n="{";for(var t in this.links)!this.links.hasOwnProperty(t)||(n!=="{"&&(n+=", "),n+=t+"/"+this.links[t].toString(e));return n+="}",n},V.prototype.toString=function(e){return this.goal===null?"<"+this.substitution.toString(e)+">":"<"+this.goal.toString(e)+", "+this.substitution.toString(e)+">"},Q.prototype.toString=function(e){return this.body?this.head.toString(e)+" :- "+this.body.toString(e)+".":this.head.toString(e)+"."},D.prototype.toString=function(e){for(var n="",t=0;t=0;a--)s=new o(".",[n[a],s]);return s}return new o(this.id,c(this.args,function(l){return l.apply(e)}),this.ref)},ne.prototype.apply=function(e){return this},Q.prototype.apply=function(e){return new Q(this.head.apply(e),this.body!==null?this.body.apply(e):null)},Y.prototype.apply=function(e){var n,t={};for(n in this.links)!this.links.hasOwnProperty(n)||(t[n]=this.links[n].apply(e));return new Y(t)},o.prototype.select=function(){for(var e=this;e.indicator===",/2";)e=e.args[0];return e},o.prototype.replace=function(e){return this.indicator===",/2"?this.args[0].indicator===",/2"?new o(",",[this.args[0].replace(e),this.args[1]]):e===null?this.args[1]:new o(",",[e,this.args[1]]):e},o.prototype.search=function(e){if(i.type.is_term(e)&&e.ref!==void 0&&this.ref===e.ref)return!0;for(var n=0;nn&&s0&&(n=this.head_point().substitution.domain());u(n,i.format_variable(this.session.rename))!==-1;)this.session.rename++;if(e.id==="_")return new O(i.format_variable(this.session.rename));this.session.renamed_variables[e.id]=i.format_variable(this.session.rename)}return new O(this.session.renamed_variables[e.id])},D.prototype.next_free_variable=function(){return this.thread.next_free_variable()},X.prototype.next_free_variable=function(){this.session.rename++;var e=[];for(this.points.length>0&&(e=this.head_point().substitution.domain());u(e,i.format_variable(this.session.rename))!==-1;)this.session.rename++;return new O(i.format_variable(this.session.rename))},D.prototype.is_public_predicate=function(e){return!this.public_predicates.hasOwnProperty(e)||this.public_predicates[e]===!0},X.prototype.is_public_predicate=function(e){return this.session.is_public_predicate(e)},D.prototype.is_multifile_predicate=function(e){return this.multifile_predicates.hasOwnProperty(e)&&this.multifile_predicates[e]===!0},X.prototype.is_multifile_predicate=function(e){return this.session.is_multifile_predicate(e)},D.prototype.prepend=function(e){return this.thread.prepend(e)},X.prototype.prepend=function(e){for(var n=e.length-1;n>=0;n--)this.points.push(e[n])},D.prototype.success=function(e,n){return this.thread.success(e,n)},X.prototype.success=function(e,n){var n=typeof n=="undefined"?e:n;this.prepend([new V(e.goal.replace(null),e.substitution,n)])},D.prototype.throw_error=function(e){return this.thread.throw_error(e)},X.prototype.throw_error=function(e){this.prepend([new V(new o("throw",[e]),new Y,null,null)])},D.prototype.step_rule=function(e,n){return this.thread.step_rule(e,n)},X.prototype.step_rule=function(e,n){var t=n.indicator;if(e==="user"&&(e=null),e===null&&this.session.rules.hasOwnProperty(t))return this.session.rules[t];for(var s=e===null?this.session.modules:u(this.session.modules,e)===-1?[]:[e],a=0;a1)&&this.again()},D.prototype.answers=function(e,n,t){return this.thread.answers(e,n,t)},X.prototype.answers=function(e,n,t){var s=n||1e3,a=this;if(n<=0){t&&t();return}this.answer(function(l){e(l),l!==!1?setTimeout(function(){a.answers(e,n-1,t)},1):t&&t()})},D.prototype.again=function(e){return this.thread.again(e)},X.prototype.again=function(e){for(var n,t=Date.now();this.__calls.length>0;){for(this.warnings=[],e!==!1&&(this.current_limit=this.session.limit);this.current_limit>0&&this.points.length>0&&this.head_point().goal!==null&&!i.type.is_error(this.head_point().goal);)if(this.current_limit--,this.step()===!0)return;var s=Date.now();this.cpu_time_last=s-t,this.cpu_time+=this.cpu_time_last;var a=this.__calls.shift();this.current_limit<=0?a(null):this.points.length===0?a(!1):i.type.is_error(this.head_point().goal)?(n=this.session.format_error(this.points.pop()),this.points=[],a(n)):(this.debugger&&this.debugger_states.push(this.head_point()),n=this.session.format_success(this.points.pop()),a(n))}},D.prototype.unfold=function(e){if(e.body===null)return!1;var n=e.head,t=e.body,s=t.select(),a=new X(this),l=[];a.add_goal(s),a.step();for(var f=a.points.length-1;f>=0;f--){var y=a.points[f],d=n.apply(y.substitution),m=t.replace(y.goal);m!==null&&(m=m.apply(y.substitution)),l.push(new Q(d,m))}var S=this.rules[n.indicator],P=u(S,e);return l.length>0&&P!==-1?(S.splice.apply(S,[P,1].concat(l)),!0):!1},X.prototype.unfold=function(e){return this.session.unfold(e)},O.prototype.interpret=function(e){return i.error.instantiation(e.level)},E.prototype.interpret=function(e){return this},o.prototype.interpret=function(e){return i.type.is_unitary_list(this)?this.args[0].interpret(e):i.operate(e,this)},O.prototype.compare=function(e){return this.ide.id?1:0},E.prototype.compare=function(e){if(this.value===e.value&&this.is_float===e.is_float)return 0;if(this.valuee.value)return 1},o.prototype.compare=function(e){if(this.args.lengthe.args.length||this.args.length===e.args.length&&this.id>e.id)return 1;for(var n=0;ns)return 1;if(e.constructor===E){if(e.is_float&&n.is_float)return 0;if(e.is_float)return-1;if(n.is_float)return 1}return 0},is_substitution:function(e){return e instanceof Y},is_state:function(e){return e instanceof V},is_rule:function(e){return e instanceof Q},is_variable:function(e){return e instanceof O},is_stream:function(e){return e instanceof ne},is_anonymous_var:function(e){return e instanceof O&&e.id==="_"},is_callable:function(e){return e instanceof o},is_number:function(e){return e instanceof E},is_integer:function(e){return e instanceof E&&!e.is_float},is_float:function(e){return e instanceof E&&e.is_float},is_term:function(e){return e instanceof o},is_atom:function(e){return e instanceof o&&e.args.length===0},is_ground:function(e){if(e instanceof O)return!1;if(e instanceof o){for(var n=0;n0},is_list:function(e){return e instanceof o&&(e.indicator==="[]/0"||e.indicator==="./2")},is_empty_list:function(e){return e instanceof o&&e.indicator==="[]/0"},is_non_empty_list:function(e){return e instanceof o&&e.indicator==="./2"},is_fully_list:function(e){for(;e instanceof o&&e.indicator==="./2";)e=e.args[1];return e instanceof O||e instanceof o&&e.indicator==="[]/0"},is_instantiated_list:function(e){for(;e instanceof o&&e.indicator==="./2";)e=e.args[1];return e instanceof o&&e.indicator==="[]/0"},is_unitary_list:function(e){return e instanceof o&&e.indicator==="./2"&&e.args[1]instanceof o&&e.args[1].indicator==="[]/0"},is_character:function(e){return e instanceof o&&(e.id.length===1||e.id.length>0&&e.id.length<=2&&_(e.id,0)>=65536)},is_character_code:function(e){return e instanceof E&&!e.is_float&&e.value>=0&&e.value<=1114111},is_byte:function(e){return e instanceof E&&!e.is_float&&e.value>=0&&e.value<=255},is_operator:function(e){return e instanceof o&&i.arithmetic.evaluation[e.indicator]},is_directive:function(e){return e instanceof o&&i.directive[e.indicator]!==void 0},is_builtin:function(e){return e instanceof o&&i.predicate[e.indicator]!==void 0},is_error:function(e){return e instanceof o&&e.indicator==="throw/1"},is_predicate_indicator:function(e){return e instanceof o&&e.indicator==="//2"&&e.args[0]instanceof o&&e.args[0].args.length===0&&e.args[1]instanceof E&&e.args[1].is_float===!1},is_flag:function(e){return e instanceof o&&e.args.length===0&&i.flag[e.id]!==void 0},is_value_flag:function(e,n){if(!i.type.is_flag(e))return!1;for(var t in i.flag[e.id].allowed)if(!!i.flag[e.id].allowed.hasOwnProperty(t)&&i.flag[e.id].allowed[t].equals(n))return!0;return!1},is_io_mode:function(e){return i.type.is_atom(e)&&["read","write","append"].indexOf(e.id)!==-1},is_stream_option:function(e){return i.type.is_term(e)&&(e.indicator==="alias/1"&&i.type.is_atom(e.args[0])||e.indicator==="reposition/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="type/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="text"||e.args[0].id==="binary")||e.indicator==="eof_action/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="error"||e.args[0].id==="eof_code"||e.args[0].id==="reset"))},is_stream_position:function(e){return i.type.is_integer(e)&&e.value>=0||i.type.is_atom(e)&&(e.id==="end_of_stream"||e.id==="past_end_of_stream")},is_stream_property:function(e){return i.type.is_term(e)&&(e.indicator==="input/0"||e.indicator==="output/0"||e.indicator==="alias/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0]))||e.indicator==="file_name/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0]))||e.indicator==="position/1"&&(i.type.is_variable(e.args[0])||i.type.is_stream_position(e.args[0]))||e.indicator==="reposition/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false"))||e.indicator==="type/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="text"||e.args[0].id==="binary"))||e.indicator==="mode/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="read"||e.args[0].id==="write"||e.args[0].id==="append"))||e.indicator==="eof_action/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="error"||e.args[0].id==="eof_code"||e.args[0].id==="reset"))||e.indicator==="end_of_stream/1"&&(i.type.is_variable(e.args[0])||i.type.is_atom(e.args[0])&&(e.args[0].id==="at"||e.args[0].id==="past"||e.args[0].id==="not")))},is_streamable:function(e){return e.__proto__.stream!==void 0},is_read_option:function(e){return i.type.is_term(e)&&["variables/1","variable_names/1","singletons/1"].indexOf(e.indicator)!==-1},is_write_option:function(e){return i.type.is_term(e)&&(e.indicator==="quoted/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="ignore_ops/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")||e.indicator==="numbervars/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false"))},is_close_option:function(e){return i.type.is_term(e)&&e.indicator==="force/1"&&i.type.is_atom(e.args[0])&&(e.args[0].id==="true"||e.args[0].id==="false")},is_modifiable_flag:function(e){return i.type.is_flag(e)&&i.flag[e.id].changeable},is_module:function(e){return e instanceof o&&e.indicator==="library/1"&&e.args[0]instanceof o&&e.args[0].args.length===0&&i.module[e.args[0].id]!==void 0}},arithmetic:{evaluation:{"e/0":{type_args:null,type_result:!0,fn:function(e){return Math.E}},"pi/0":{type_args:null,type_result:!0,fn:function(e){return Math.PI}},"tau/0":{type_args:null,type_result:!0,fn:function(e){return 2*Math.PI}},"epsilon/0":{type_args:null,type_result:!0,fn:function(e){return Number.EPSILON}},"+/1":{type_args:null,type_result:null,fn:function(e,n){return e}},"-/1":{type_args:null,type_result:null,fn:function(e,n){return-e}},"\\/1":{type_args:!1,type_result:!1,fn:function(e,n){return~e}},"abs/1":{type_args:null,type_result:null,fn:function(e,n){return Math.abs(e)}},"sign/1":{type_args:null,type_result:null,fn:function(e,n){return Math.sign(e)}},"float_integer_part/1":{type_args:!0,type_result:!1,fn:function(e,n){return parseInt(e)}},"float_fractional_part/1":{type_args:!0,type_result:!0,fn:function(e,n){return e-parseInt(e)}},"float/1":{type_args:null,type_result:!0,fn:function(e,n){return parseFloat(e)}},"floor/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.floor(e)}},"truncate/1":{type_args:!0,type_result:!1,fn:function(e,n){return parseInt(e)}},"round/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.round(e)}},"ceiling/1":{type_args:!0,type_result:!1,fn:function(e,n){return Math.ceil(e)}},"sin/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.sin(e)}},"cos/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.cos(e)}},"tan/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.tan(e)}},"asin/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.asin(e)}},"acos/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.acos(e)}},"atan/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.atan(e)}},"atan2/2":{type_args:null,type_result:!0,fn:function(e,n,t){return Math.atan2(e,n)}},"exp/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.exp(e)}},"sqrt/1":{type_args:null,type_result:!0,fn:function(e,n){return Math.sqrt(e)}},"log/1":{type_args:null,type_result:!0,fn:function(e,n){return e>0?Math.log(e):i.error.evaluation("undefined",n.__call_indicator)}},"+/2":{type_args:null,type_result:null,fn:function(e,n,t){return e+n}},"-/2":{type_args:null,type_result:null,fn:function(e,n,t){return e-n}},"*/2":{type_args:null,type_result:null,fn:function(e,n,t){return e*n}},"//2":{type_args:null,type_result:!0,fn:function(e,n,t){return n?e/n:i.error.evaluation("zero_division",t.__call_indicator)}},"///2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?parseInt(e/n):i.error.evaluation("zero_division",t.__call_indicator)}},"**/2":{type_args:null,type_result:!0,fn:function(e,n,t){return Math.pow(e,n)}},"^/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.pow(e,n)}},"<>/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e>>n}},"/\\/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e&n}},"\\//2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e|n}},"xor/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return e^n}},"rem/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?e%n:i.error.evaluation("zero_division",t.__call_indicator)}},"mod/2":{type_args:!1,type_result:!1,fn:function(e,n,t){return n?e-parseInt(e/n)*n:i.error.evaluation("zero_division",t.__call_indicator)}},"max/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.max(e,n)}},"min/2":{type_args:null,type_result:null,fn:function(e,n,t){return Math.min(e,n)}}}},directive:{"dynamic/1":function(e,n){var t=n.args[0];if(i.type.is_variable(t))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_compound(t)||t.indicator!=="//2")e.throw_error(i.error.type("predicate_indicator",t,n.indicator));else if(i.type.is_variable(t.args[0])||i.type.is_variable(t.args[1]))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_atom(t.args[0]))e.throw_error(i.error.type("atom",t.args[0],n.indicator));else if(!i.type.is_integer(t.args[1]))e.throw_error(i.error.type("integer",t.args[1],n.indicator));else{var s=n.args[0].args[0].id+"/"+n.args[0].args[1].value;e.session.public_predicates[s]=!0,e.session.rules[s]||(e.session.rules[s]=[])}},"multifile/1":function(e,n){var t=n.args[0];i.type.is_variable(t)?e.throw_error(i.error.instantiation(n.indicator)):!i.type.is_compound(t)||t.indicator!=="//2"?e.throw_error(i.error.type("predicate_indicator",t,n.indicator)):i.type.is_variable(t.args[0])||i.type.is_variable(t.args[1])?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_atom(t.args[0])?i.type.is_integer(t.args[1])?e.session.multifile_predicates[n.args[0].args[0].id+"/"+n.args[0].args[1].value]=!0:e.throw_error(i.error.type("integer",t.args[1],n.indicator)):e.throw_error(i.error.type("atom",t.args[0],n.indicator))},"set_prolog_flag/2":function(e,n){var t=n.args[0],s=n.args[1];i.type.is_variable(t)||i.type.is_variable(s)?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_atom(t)?i.type.is_flag(t)?i.type.is_value_flag(t,s)?i.type.is_modifiable_flag(t)?e.session.flag[t.id]=s:e.throw_error(i.error.permission("modify","flag",t)):e.throw_error(i.error.domain("flag_value",new o("+",[t,s]),n.indicator)):e.throw_error(i.error.domain("prolog_flag",t,n.indicator)):e.throw_error(i.error.type("atom",t,n.indicator))},"use_module/1":function(e,n){var t=n.args[0];if(i.type.is_variable(t))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_term(t))e.throw_error(i.error.type("term",t,n.indicator));else if(i.type.is_module(t)){var s=t.args[0].id;u(e.session.modules,s)===-1&&e.session.modules.push(s)}},"char_conversion/2":function(e,n){var t=n.args[0],s=n.args[1];i.type.is_variable(t)||i.type.is_variable(s)?e.throw_error(i.error.instantiation(n.indicator)):i.type.is_character(t)?i.type.is_character(s)?t.id===s.id?delete e.session.__char_conversion[t.id]:e.session.__char_conversion[t.id]=s.id:e.throw_error(i.error.type("character",s,n.indicator)):e.throw_error(i.error.type("character",t,n.indicator))},"op/3":function(e,n){var t=n.args[0],s=n.args[1],a=n.args[2];if(i.type.is_variable(t)||i.type.is_variable(s)||i.type.is_variable(a))e.throw_error(i.error.instantiation(n.indicator));else if(!i.type.is_integer(t))e.throw_error(i.error.type("integer",t,n.indicator));else if(!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,n.indicator));else if(!i.type.is_atom(a))e.throw_error(i.error.type("atom",a,n.indicator));else if(t.value<0||t.value>1200)e.throw_error(i.error.domain("operator_priority",t,n.indicator));else if(a.id===",")e.throw_error(i.error.permission("modify","operator",a,n.indicator));else if(a.id==="|"&&(t.value<1001||s.id.length!==3))e.throw_error(i.error.permission("modify","operator",a,n.indicator));else if(["fy","fx","yf","xf","xfx","yfx","xfy"].indexOf(s.id)===-1)e.throw_error(i.error.domain("operator_specifier",s,n.indicator));else{var l={prefix:null,infix:null,postfix:null};for(var f in e.session.__operators)if(!!e.session.__operators.hasOwnProperty(f)){var y=e.session.__operators[f][a.id];y&&(u(y,"fx")!==-1&&(l.prefix={priority:f,type:"fx"}),u(y,"fy")!==-1&&(l.prefix={priority:f,type:"fy"}),u(y,"xf")!==-1&&(l.postfix={priority:f,type:"xf"}),u(y,"yf")!==-1&&(l.postfix={priority:f,type:"yf"}),u(y,"xfx")!==-1&&(l.infix={priority:f,type:"xfx"}),u(y,"xfy")!==-1&&(l.infix={priority:f,type:"xfy"}),u(y,"yfx")!==-1&&(l.infix={priority:f,type:"yfx"}))}var d;switch(s.id){case"fy":case"fx":d="prefix";break;case"yf":case"xf":d="postfix";break;default:d="infix";break}if(((l.prefix&&d==="prefix"||l.postfix&&d==="postfix"||l.infix&&d==="infix")&&l[d].type!==s.id||l.infix&&d==="postfix"||l.postfix&&d==="infix")&&t.value!==0)e.throw_error(i.error.permission("create","operator",a,n.indicator));else return l[d]&&(Fi(e.session.__operators[l[d].priority][a.id],s.id),e.session.__operators[l[d].priority][a.id].length===0&&delete e.session.__operators[l[d].priority][a.id]),t.value>0&&(e.session.__operators[t.value]||(e.session.__operators[t.value.toString()]={}),e.session.__operators[t.value][a.id]||(e.session.__operators[t.value][a.id]=[]),e.session.__operators[t.value][a.id].push(s.id)),!0}}},predicate:{"op/3":function(e,n,t){i.directive["op/3"](e,t)&&e.success(n)},"current_op/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2],f=[];for(var y in e.session.__operators)for(var d in e.session.__operators[y])for(var m=0;m/2"){var s=e.points,a=e.session.format_success,l=e.session.format_error;e.session.format_success=function(m){return m.substitution},e.session.format_error=function(m){return m.goal},e.points=[new V(t.args[0].args[0],n.substitution,n)];var f=function(m){e.points=s,e.session.format_success=a,e.session.format_error=l,m===!1?e.prepend([new V(n.goal.replace(t.args[1]),n.substitution,n)]):i.type.is_error(m)?e.throw_error(m.args[0]):m===null?(e.prepend([n]),e.__calls.shift()(null)):e.prepend([new V(n.goal.replace(t.args[0].args[1]).apply(m),n.substitution.apply(m),n)])};e.__calls.unshift(f)}else{var y=new V(n.goal.replace(t.args[0]),n.substitution,n),d=new V(n.goal.replace(t.args[1]),n.substitution,n);e.prepend([y,d])}},"!/0":function(e,n,t){var s,a,l=[];for(s=n,a=null;s.parent!==null&&s.parent.goal.search(t);)if(a=s,s=s.parent,s.goal!==null){var f=s.goal.select();if(f&&f.id==="call"&&f.search(t)){s=a;break}}for(var y=e.points.length-1;y>=0;y--){for(var d=e.points[y],m=d.parent;m!==null&&m!==s.parent;)m=m.parent;m===null&&m!==s.parent&&l.push(d)}e.points=l.reverse(),e.success(n)},"\\+/1":function(e,n,t){var s=t.args[0];i.type.is_variable(s)?e.throw_error(i.error.instantiation(e.level)):i.type.is_callable(s)?e.prepend([new V(n.goal.replace(new o(",",[new o(",",[new o("call",[s]),new o("!",[])]),new o("fail",[])])),n.substitution,n),new V(n.goal.replace(null),n.substitution,n)]):e.throw_error(i.error.type("callable",s,e.level))},"->/2":function(e,n,t){var s=n.goal.replace(new o(",",[t.args[0],new o(",",[new o("!"),t.args[1]])]));e.prepend([new V(s,n.substitution,n)])},"fail/0":function(e,n,t){},"false/0":function(e,n,t){},"true/0":function(e,n,t){e.success(n)},"call/1":ye(1),"call/2":ye(2),"call/3":ye(3),"call/4":ye(4),"call/5":ye(5),"call/6":ye(6),"call/7":ye(7),"call/8":ye(8),"once/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("call",[s]),new o("!",[])])),n.substitution,n)])},"forall/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("\\+",[new o(",",[new o("call",[s]),new o("\\+",[new o("call",[a])])])])),n.substitution,n)])},"repeat/0":function(e,n,t){e.prepend([new V(n.goal.replace(null),n.substitution,n),n])},"throw/1":function(e,n,t){i.type.is_variable(t.args[0])?e.throw_error(i.error.instantiation(e.level)):e.throw_error(t.args[0])},"catch/3":function(e,n,t){var s=e.points;e.points=[],e.prepend([new V(t.args[0],n.substitution,n)]);var a=e.session.format_success,l=e.session.format_error;e.session.format_success=function(y){return y.substitution},e.session.format_error=function(y){return y.goal};var f=function(y){var d=e.points;if(e.points=s,e.session.format_success=a,e.session.format_error=l,i.type.is_error(y)){for(var m=[],S=e.points.length-1;S>=0;S--){for(var R=e.points[S],P=R.parent;P!==null&&P!==n.parent;)P=P.parent;P===null&&P!==n.parent&&m.push(R)}e.points=m;var A=e.get_flag("occurs_check").indicator==="true/0",R=new V,k=i.unify(y.args[0],t.args[1],A);k!==null?(R.substitution=n.substitution.apply(k),R.goal=n.goal.replace(t.args[2]).apply(k),R.parent=n,e.prepend([R])):e.throw_error(y.args[0])}else if(y!==!1){for(var W=y===null?[]:[new V(n.goal.apply(y).replace(null),n.substitution.apply(y),n)],B=[],S=d.length-1;S>=0;S--){B.push(d[S]);var q=d[S].goal!==null?d[S].goal.select():null;if(i.type.is_term(q)&&q.indicator==="!/0")break}var F=c(B,function(H){return H.goal===null&&(H.goal=new o("true",[])),H=new V(n.goal.replace(new o("catch",[H.goal,t.args[1],t.args[2]])),n.substitution.apply(H.substitution),H.parent),H.exclude=t.args[0].variables(),H}).reverse();e.prepend(F),e.prepend(W),y===null&&(this.current_limit=0,e.__calls.shift()(null))}};e.__calls.unshift(f)},"=/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=new V,l=i.unify(t.args[0],t.args[1],s);l!==null&&(a.goal=n.goal.apply(l).replace(null),a.substitution=n.substitution.apply(l),a.parent=n,e.prepend([a]))},"unify_with_occurs_check/2":function(e,n,t){var s=new V,a=i.unify(t.args[0],t.args[1],!0);a!==null&&(s.goal=n.goal.apply(a).replace(null),s.substitution=n.substitution.apply(a),s.parent=n,e.prepend([s]))},"\\=/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=i.unify(t.args[0],t.args[1],s);a===null&&e.success(n)},"subsumes_term/2":function(e,n,t){var s=e.get_flag("occurs_check").indicator==="true/0",a=i.unify(t.args[1],t.args[0],s);a!==null&&t.args[1].apply(a).equals(t.args[1])&&e.success(n)},"findall/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(a))e.throw_error(i.error.type("callable",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=e.next_free_variable(),y=new o(",",[a,new o("=",[f,s])]),d=e.points,m=e.session.limit,S=e.session.format_success;e.session.format_success=function(R){return R.substitution},e.add_goal(y,!0,n);var P=[],A=function(R){if(R!==!1&&R!==null&&!i.type.is_error(R))e.__calls.unshift(A),P.push(R.links[f.id]),e.session.limit=e.current_limit;else if(e.points=d,e.session.limit=m,e.session.format_success=S,i.type.is_error(R))e.throw_error(R.args[0]);else if(e.current_limit>0){for(var k=new o("[]"),W=P.length-1;W>=0;W--)k=new o(".",[P[W],k]);e.prepend([new V(n.goal.replace(new o("=",[l,k])),n.substitution,n)])}};e.__calls.unshift(A)}},"bagof/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(l))e.throw_error(i.error.type("callable",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_list(f))e.throw_error(i.error.type("list",f,t.indicator));else{var y=e.next_free_variable(),d;l.indicator==="^/2"?(d=l.args[0].variables(),l=l.args[1]):d=[],d=d.concat(a.variables());for(var m=l.variables().filter(function(F){return u(d,F)===-1}),S=new o("[]"),P=m.length-1;P>=0;P--)S=new o(".",[new O(m[P]),S]);var A=new o(",",[l,new o("=",[y,new o(",",[S,a])])]),R=e.points,k=e.session.limit,W=e.session.format_success;e.session.format_success=function(F){return F.substitution},e.add_goal(A,!0,n);var B=[],q=function(F){if(F!==!1&&F!==null&&!i.type.is_error(F)){e.__calls.unshift(q);var H=!1,J=F.links[y.id].args[0],me=F.links[y.id].args[1];for(var be in B)if(!!B.hasOwnProperty(be)){var Me=B[be];if(Me.variables.equals(J)){Me.answers.push(me),H=!0;break}}H||B.push({variables:J,answers:[me]}),e.session.limit=e.current_limit}else if(e.points=R,e.session.limit=k,e.session.format_success=W,i.type.is_error(F))e.throw_error(F.args[0]);else if(e.current_limit>0){for(var qe=[],ce=0;ce=0;xe--)Te=new o(".",[F[xe],Te]);qe.push(new V(n.goal.replace(new o(",",[new o("=",[S,B[ce].variables]),new o("=",[f,Te])])),n.substitution,n))}e.prepend(qe)}};e.__calls.unshift(q)}},"setof/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(l))e.throw_error(i.error.type("callable",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_list(f))e.throw_error(i.error.type("list",f,t.indicator));else{var y=e.next_free_variable(),d;l.indicator==="^/2"?(d=l.args[0].variables(),l=l.args[1]):d=[],d=d.concat(a.variables());for(var m=l.variables().filter(function(F){return u(d,F)===-1}),S=new o("[]"),P=m.length-1;P>=0;P--)S=new o(".",[new O(m[P]),S]);var A=new o(",",[l,new o("=",[y,new o(",",[S,a])])]),R=e.points,k=e.session.limit,W=e.session.format_success;e.session.format_success=function(F){return F.substitution},e.add_goal(A,!0,n);var B=[],q=function(F){if(F!==!1&&F!==null&&!i.type.is_error(F)){e.__calls.unshift(q);var H=!1,J=F.links[y.id].args[0],me=F.links[y.id].args[1];for(var be in B)if(!!B.hasOwnProperty(be)){var Me=B[be];if(Me.variables.equals(J)){Me.answers.push(me),H=!0;break}}H||B.push({variables:J,answers:[me]}),e.session.limit=e.current_limit}else if(e.points=R,e.session.limit=k,e.session.format_success=W,i.type.is_error(F))e.throw_error(F.args[0]);else if(e.current_limit>0){for(var qe=[],ce=0;ce=0;xe--)Te=new o(".",[F[xe],Te]);qe.push(new V(n.goal.replace(new o(",",[new o("=",[S,B[ce].variables]),new o("=",[f,Te])])),n.substitution,n))}e.prepend(qe)}};e.__calls.unshift(q)}},"functor/3":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2];if(i.type.is_variable(a)&&(i.type.is_variable(l)||i.type.is_variable(f)))e.throw_error(i.error.instantiation("functor/3"));else if(!i.type.is_variable(f)&&!i.type.is_integer(f))e.throw_error(i.error.type("integer",t.args[2],"functor/3"));else if(!i.type.is_variable(l)&&!i.type.is_atomic(l))e.throw_error(i.error.type("atomic",t.args[1],"functor/3"));else if(i.type.is_integer(l)&&i.type.is_integer(f)&&f.value!==0)e.throw_error(i.error.type("atom",t.args[1],"functor/3"));else if(i.type.is_variable(a)){if(t.args[2].value>=0){for(var y=[],d=0;d0&&s<=t.args[1].args.length){var a=new o("=",[t.args[1].args[s-1],t.args[2]]);e.prepend([new V(n.goal.replace(a),n.substitution,n)])}}},"=../2":function(e,n,t){var s;if(i.type.is_variable(t.args[0])&&(i.type.is_variable(t.args[1])||i.type.is_non_empty_list(t.args[1])&&i.type.is_variable(t.args[1].args[0])))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_fully_list(t.args[1]))e.throw_error(i.error.type("list",t.args[1],t.indicator));else if(i.type.is_variable(t.args[0])){if(!i.type.is_variable(t.args[1])){var l=[];for(s=t.args[1].args[1];s.indicator==="./2";)l.push(s.args[0]),s=s.args[1];i.type.is_variable(t.args[0])&&i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):l.length===0&&i.type.is_compound(t.args[1].args[0])?e.throw_error(i.error.type("atomic",t.args[1].args[0],t.indicator)):l.length>0&&(i.type.is_compound(t.args[1].args[0])||i.type.is_number(t.args[1].args[0]))?e.throw_error(i.error.type("atom",t.args[1].args[0],t.indicator)):l.length===0?e.prepend([new V(n.goal.replace(new o("=",[t.args[1].args[0],t.args[0]],n)),n.substitution,n)]):e.prepend([new V(n.goal.replace(new o("=",[new o(t.args[1].args[0].id,l),t.args[0]])),n.substitution,n)])}}else{if(i.type.is_atomic(t.args[0]))s=new o(".",[t.args[0],new o("[]")]);else{s=new o("[]");for(var a=t.args[0].args.length-1;a>=0;a--)s=new o(".",[t.args[0].args[a],s]);s=new o(".",[new o(t.args[0].id),s])}e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n)])}},"copy_term/2":function(e,n,t){var s=t.args[0].rename(e);e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n.parent)])},"term_variables/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(!i.type.is_fully_list(a))e.throw_error(i.error.type("list",a,t.indicator));else{var l=he(c(yr(s.variables()),function(f){return new O(f)}));e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"clause/2":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else if(!i.type.is_variable(t.args[1])&&!i.type.is_callable(t.args[1]))e.throw_error(i.error.type("callable",t.args[1],t.indicator));else if(e.session.rules[t.args[0].indicator]!==void 0)if(e.is_public_predicate(t.args[0].indicator)){var s=[];for(var a in e.session.rules[t.args[0].indicator])if(!!e.session.rules[t.args[0].indicator].hasOwnProperty(a)){var l=e.session.rules[t.args[0].indicator][a];e.session.renamed_variables={},l=l.rename(e),l.body===null&&(l.body=new o("true"));var f=new o(",",[new o("=",[l.head,t.args[0]]),new o("=",[l.body,t.args[1]])]);s.push(new V(n.goal.replace(f),n.substitution,n))}e.prepend(s)}else e.throw_error(i.error.permission("access","private_procedure",t.args[0].indicator,t.indicator))},"current_predicate/1":function(e,n,t){var s=t.args[0];if(!i.type.is_variable(s)&&(!i.type.is_compound(s)||s.indicator!=="//2"))e.throw_error(i.error.type("predicate_indicator",s,t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_variable(s.args[0])&&!i.type.is_atom(s.args[0]))e.throw_error(i.error.type("atom",s.args[0],t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_variable(s.args[1])&&!i.type.is_integer(s.args[1]))e.throw_error(i.error.type("integer",s.args[1],t.indicator));else{var a=[];for(var l in e.session.rules)if(!!e.session.rules.hasOwnProperty(l)){var f=l.lastIndexOf("/"),y=l.substr(0,f),d=parseInt(l.substr(f+1,l.length-(f+1))),m=new o("/",[new o(y),new E(d,!1)]),S=new o("=",[m,s]);a.push(new V(n.goal.replace(S),n.substitution,n))}e.prepend(a)}},"asserta/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=ve(t.args[0].args[1])):(s=t.args[0],a=null),i.type.is_callable(s)?a!==null&&!i.type.is_callable(a)?e.throw_error(i.error.type("callable",a,t.indicator)):e.is_public_predicate(s.indicator)?(e.session.rules[s.indicator]===void 0&&(e.session.rules[s.indicator]=[]),e.session.public_predicates[s.indicator]=!0,e.session.rules[s.indicator]=[new Q(s,a,!0)].concat(e.session.rules[s.indicator]),e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s.indicator,t.indicator)):e.throw_error(i.error.type("callable",s,t.indicator))}},"assertz/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=ve(t.args[0].args[1])):(s=t.args[0],a=null),i.type.is_callable(s)?a!==null&&!i.type.is_callable(a)?e.throw_error(i.error.type("callable",a,t.indicator)):e.is_public_predicate(s.indicator)?(e.session.rules[s.indicator]===void 0&&(e.session.rules[s.indicator]=[]),e.session.public_predicates[s.indicator]=!0,e.session.rules[s.indicator].push(new Q(s,a,!0)),e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s.indicator,t.indicator)):e.throw_error(i.error.type("callable",s,t.indicator))}},"retract/1":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_callable(t.args[0]))e.throw_error(i.error.type("callable",t.args[0],t.indicator));else{var s,a;if(t.args[0].indicator===":-/2"?(s=t.args[0].args[0],a=t.args[0].args[1]):(s=t.args[0],a=new o("true")),typeof n.retract=="undefined")if(e.is_public_predicate(s.indicator)){if(e.session.rules[s.indicator]!==void 0){for(var l=[],f=0;fe.get_flag("max_arity").value)e.throw_error(i.error.representation("max_arity",t.indicator));else{var s=t.args[0].args[0].id+"/"+t.args[0].args[1].value;e.is_public_predicate(s)?(delete e.session.rules[s],e.success(n)):e.throw_error(i.error.permission("modify","static_procedure",s,t.indicator))}},"atom_length/2":function(e,n,t){if(i.type.is_variable(t.args[0]))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_atom(t.args[0]))e.throw_error(i.error.type("atom",t.args[0],t.indicator));else if(!i.type.is_variable(t.args[1])&&!i.type.is_integer(t.args[1]))e.throw_error(i.error.type("integer",t.args[1],t.indicator));else if(i.type.is_integer(t.args[1])&&t.args[1].value<0)e.throw_error(i.error.domain("not_less_than_zero",t.args[1],t.indicator));else{var s=new E(t.args[0].id.length,!1);e.prepend([new V(n.goal.replace(new o("=",[s,t.args[1]])),n.substitution,n)])}},"atom_concat/3":function(e,n,t){var s,a,l=t.args[0],f=t.args[1],y=t.args[2];if(i.type.is_variable(y)&&(i.type.is_variable(l)||i.type.is_variable(f)))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_atom(l))e.throw_error(i.error.type("atom",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_atom(f))e.throw_error(i.error.type("atom",f,t.indicator));else if(!i.type.is_variable(y)&&!i.type.is_atom(y))e.throw_error(i.error.type("atom",y,t.indicator));else{var d=i.type.is_variable(l),m=i.type.is_variable(f);if(!d&&!m)a=new o("=",[y,new o(l.id+f.id)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]);else if(d&&!m)s=y.id.substr(0,y.id.length-f.id.length),s+f.id===y.id&&(a=new o("=",[l,new o(s)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]));else if(m&&!d)s=y.id.substr(l.id.length),l.id+s===y.id&&(a=new o("=",[f,new o(s)]),e.prepend([new V(n.goal.replace(a),n.substitution,n)]));else{for(var S=[],P=0;P<=y.id.length;P++){var A=new o(y.id.substr(0,P)),R=new o(y.id.substr(P));a=new o(",",[new o("=",[A,l]),new o("=",[R,f])]),S.push(new V(n.goal.replace(a),n.substitution,n))}e.prepend(S)}}},"sub_atom/5":function(e,n,t){var s,a=t.args[0],l=t.args[1],f=t.args[2],y=t.args[3],d=t.args[4];if(i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_integer(l))e.throw_error(i.error.type("integer",l,t.indicator));else if(!i.type.is_variable(f)&&!i.type.is_integer(f))e.throw_error(i.error.type("integer",f,t.indicator));else if(!i.type.is_variable(y)&&!i.type.is_integer(y))e.throw_error(i.error.type("integer",y,t.indicator));else if(i.type.is_integer(l)&&l.value<0)e.throw_error(i.error.domain("not_less_than_zero",l,t.indicator));else if(i.type.is_integer(f)&&f.value<0)e.throw_error(i.error.domain("not_less_than_zero",f,t.indicator));else if(i.type.is_integer(y)&&y.value<0)e.throw_error(i.error.domain("not_less_than_zero",y,t.indicator));else{var m=[],S=[],P=[];if(i.type.is_variable(l))for(s=0;s<=a.id.length;s++)m.push(s);else m.push(l.value);if(i.type.is_variable(f))for(s=0;s<=a.id.length;s++)S.push(s);else S.push(f.value);if(i.type.is_variable(y))for(s=0;s<=a.id.length;s++)P.push(s);else P.push(y.value);var A=[];for(var R in m)if(!!m.hasOwnProperty(R)){s=m[R];for(var k in S)if(!!S.hasOwnProperty(k)){var W=S[k],B=a.id.length-s-W;if(u(P,B)!==-1&&s+W+B===a.id.length){var q=a.id.substr(s,W);if(a.id===a.id.substr(0,s)+q+a.id.substr(s+W,B)){var F=new o("=",[new o(q),d]),H=new o("=",[l,new E(s)]),J=new o("=",[f,new E(W)]),me=new o("=",[y,new E(B)]),be=new o(",",[new o(",",[new o(",",[H,J]),me]),F]);A.push(new V(n.goal.replace(be),n.substitution,n))}}}}e.prepend(A)}},"atom_chars/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_variable(s)){for(var y=a,d=i.type.is_variable(s),m="";y.indicator==="./2";){if(i.type.is_character(y.args[0]))m+=y.args[0].id;else if(i.type.is_variable(y.args[0])&&d){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character",y.args[0],t.indicator));return}y=y.args[1]}i.type.is_variable(y)&&d?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_empty_list(y)&&!i.type.is_variable(y)?e.throw_error(i.error.type("list",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[new o(m),s])),n.substitution,n)])}else{for(var l=new o("[]"),f=s.id.length-1;f>=0;f--)l=new o(".",[new o(s.id.charAt(f)),l]);e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"atom_codes/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_variable(s)){for(var y=a,d=i.type.is_variable(s),m="";y.indicator==="./2";){if(i.type.is_character_code(y.args[0]))m+=v(y.args[0].value);else if(i.type.is_variable(y.args[0])&&d){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.representation("character_code",t.indicator));return}y=y.args[1]}i.type.is_variable(y)&&d?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_empty_list(y)&&!i.type.is_variable(y)?e.throw_error(i.error.type("list",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[new o(m),s])),n.substitution,n)])}else{for(var l=new o("[]"),f=s.id.length-1;f>=0;f--)l=new o(".",[new E(_(s.id,f),!1),l]);e.prepend([new V(n.goal.replace(new o("=",[a,l])),n.substitution,n)])}},"char_code/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(i.type.is_variable(s)&&i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_character(s))e.throw_error(i.error.type("character",s,t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_integer(a))e.throw_error(i.error.type("integer",a,t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_character_code(a))e.throw_error(i.error.representation("character_code",t.indicator));else if(i.type.is_variable(a)){var l=new E(_(s.id,0),!1);e.prepend([new V(n.goal.replace(new o("=",[l,a])),n.substitution,n)])}else{var f=new o(v(a.value));e.prepend([new V(n.goal.replace(new o("=",[f,s])),n.substitution,n)])}},"number_chars/2":function(e,n,t){var s,a=t.args[0],l=t.args[1];if(i.type.is_variable(a)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_number(a))e.throw_error(i.error.type("number",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=i.type.is_variable(a);if(!i.type.is_variable(l)){var y=l,d=!0;for(s="";y.indicator==="./2";){if(i.type.is_character(y.args[0]))s+=y.args[0].id;else if(i.type.is_variable(y.args[0]))d=!1;else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character",y.args[0],t.indicator));return}y=y.args[1]}if(d=d&&i.type.is_empty_list(y),!i.type.is_empty_list(y)&&!i.type.is_variable(y)){e.throw_error(i.error.type("list",l,t.indicator));return}if(!d&&f){e.throw_error(i.error.instantiation(t.indicator));return}else if(d)if(i.type.is_variable(y)&&f){e.throw_error(i.error.instantiation(t.indicator));return}else{var m=e.parse(s),S=m.value;!i.type.is_number(S)||m.tokens[m.tokens.length-1].space?e.throw_error(i.error.syntax_by_predicate("parseable_number",t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,S])),n.substitution,n)]);return}}if(!f){s=a.toString();for(var P=new o("[]"),A=s.length-1;A>=0;A--)P=new o(".",[new o(s.charAt(A)),P]);e.prepend([new V(n.goal.replace(new o("=",[l,P])),n.substitution,n)])}}},"number_codes/2":function(e,n,t){var s,a=t.args[0],l=t.args[1];if(i.type.is_variable(a)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(a)&&!i.type.is_number(a))e.throw_error(i.error.type("number",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else{var f=i.type.is_variable(a);if(!i.type.is_variable(l)){var y=l,d=!0;for(s="";y.indicator==="./2";){if(i.type.is_character_code(y.args[0]))s+=v(y.args[0].value);else if(i.type.is_variable(y.args[0]))d=!1;else if(!i.type.is_variable(y.args[0])){e.throw_error(i.error.type("character_code",y.args[0],t.indicator));return}y=y.args[1]}if(d=d&&i.type.is_empty_list(y),!i.type.is_empty_list(y)&&!i.type.is_variable(y)){e.throw_error(i.error.type("list",l,t.indicator));return}if(!d&&f){e.throw_error(i.error.instantiation(t.indicator));return}else if(d)if(i.type.is_variable(y)&&f){e.throw_error(i.error.instantiation(t.indicator));return}else{var m=e.parse(s),S=m.value;!i.type.is_number(S)||m.tokens[m.tokens.length-1].space?e.throw_error(i.error.syntax_by_predicate("parseable_number",t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,S])),n.substitution,n)]);return}}if(!f){s=a.toString();for(var P=new o("[]"),A=s.length-1;A>=0;A--)P=new o(".",[new E(_(s,A),!1),P]);e.prepend([new V(n.goal.replace(new o("=",[l,P])),n.substitution,n)])}}},"upcase_atom/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?!i.type.is_variable(a)&&!i.type.is_atom(a)?e.throw_error(i.error.type("atom",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,new o(s.id.toUpperCase(),[])])),n.substitution,n)]):e.throw_error(i.error.type("atom",s,t.indicator))},"downcase_atom/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?!i.type.is_variable(a)&&!i.type.is_atom(a)?e.throw_error(i.error.type("atom",a,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[a,new o(s.id.toLowerCase(),[])])),n.substitution,n)]):e.throw_error(i.error.type("atom",s,t.indicator))},"atomic_list_concat/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("atomic_list_concat",[s,new o("",[]),a])),n.substitution,n)])},"atomic_list_concat/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(a)||i.type.is_variable(s)&&i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_list(s))e.throw_error(i.error.type("list",s,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_atom(l))e.throw_error(i.error.type("atom",l,t.indicator));else if(i.type.is_variable(l)){for(var y="",d=s;i.type.is_term(d)&&d.indicator==="./2";){if(!i.type.is_atom(d.args[0])&&!i.type.is_number(d.args[0])){e.throw_error(i.error.type("atomic",d.args[0],t.indicator));return}y!==""&&(y+=a.id),i.type.is_atom(d.args[0])?y+=d.args[0].id:y+=""+d.args[0].value,d=d.args[1]}y=new o(y,[]),i.type.is_variable(d)?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_term(d)||d.indicator!=="[]/0"?e.throw_error(i.error.type("list",s,t.indicator)):e.prepend([new V(n.goal.replace(new o("=",[y,l])),n.substitution,n)])}else{var f=he(c(l.id.split(a.id),function(m){return new o(m,[])}));e.prepend([new V(n.goal.replace(new o("=",[f,s])),n.substitution,n)])}},"@=/2":function(e,n,t){i.compare(t.args[0],t.args[1])>0&&e.success(n)},"@>=/2":function(e,n,t){i.compare(t.args[0],t.args[1])>=0&&e.success(n)},"compare/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(i.type.is_atom(s)&&["<",">","="].indexOf(s.id)===-1)e.throw_error(i.type.domain("order",s,t.indicator));else{var f=i.compare(a,l);f=f===0?"=":f===-1?"<":">",e.prepend([new V(n.goal.replace(new o("=",[s,new o(f,[])])),n.substitution,n)])}},"is/2":function(e,n,t){var s=t.args[1].interpret(e);i.type.is_number(s)?e.prepend([new V(n.goal.replace(new o("=",[t.args[0],s],e.level)),n.substitution,n)]):e.throw_error(s)},"between/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2];if(i.type.is_variable(s)||i.type.is_variable(a))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_integer(s))e.throw_error(i.error.type("integer",s,t.indicator));else if(!i.type.is_integer(a))e.throw_error(i.error.type("integer",a,t.indicator));else if(!i.type.is_variable(l)&&!i.type.is_integer(l))e.throw_error(i.error.type("integer",l,t.indicator));else if(i.type.is_variable(l)){var f=[new V(n.goal.replace(new o("=",[l,s])),n.substitution,n)];s.value=l.value&&e.success(n)},"succ/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)&&i.type.is_variable(a)?e.throw_error(i.error.instantiation(t.indicator)):!i.type.is_variable(s)&&!i.type.is_integer(s)?e.throw_error(i.error.type("integer",s,t.indicator)):!i.type.is_variable(a)&&!i.type.is_integer(a)?e.throw_error(i.error.type("integer",a,t.indicator)):!i.type.is_variable(s)&&s.value<0?e.throw_error(i.error.domain("not_less_than_zero",s,t.indicator)):!i.type.is_variable(a)&&a.value<0?e.throw_error(i.error.domain("not_less_than_zero",a,t.indicator)):(i.type.is_variable(a)||a.value>0)&&(i.type.is_variable(s)?e.prepend([new V(n.goal.replace(new o("=",[s,new E(a.value-1,!1)])),n.substitution,n)]):e.prepend([new V(n.goal.replace(new o("=",[a,new E(s.value+1,!1)])),n.substitution,n)]))},"=:=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s===0&&e.success(n)},"=\\=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s!==0&&e.success(n)},"/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s>0&&e.success(n)},">=/2":function(e,n,t){var s=i.arithmetic_compare(e,t.args[0],t.args[1]);i.type.is_term(s)?e.throw_error(s):s>=0&&e.success(n)},"var/1":function(e,n,t){i.type.is_variable(t.args[0])&&e.success(n)},"atom/1":function(e,n,t){i.type.is_atom(t.args[0])&&e.success(n)},"atomic/1":function(e,n,t){i.type.is_atomic(t.args[0])&&e.success(n)},"compound/1":function(e,n,t){i.type.is_compound(t.args[0])&&e.success(n)},"integer/1":function(e,n,t){i.type.is_integer(t.args[0])&&e.success(n)},"float/1":function(e,n,t){i.type.is_float(t.args[0])&&e.success(n)},"number/1":function(e,n,t){i.type.is_number(t.args[0])&&e.success(n)},"nonvar/1":function(e,n,t){i.type.is_variable(t.args[0])||e.success(n)},"ground/1":function(e,n,t){t.variables().length===0&&e.success(n)},"acyclic_term/1":function(e,n,t){for(var s=n.substitution.apply(n.substitution),a=t.args[0].variables(),l=0;l0?k[k.length-1]:null,k!==null&&(A=U(e,k,0,e.__get_max_priority(),!1))}if(A.type===h&&A.len===k.length-1&&W.value==="."){A=A.value.rename(e);var B=new o("=",[a,A]);if(y.variables){var q=he(c(yr(A.variables()),function(F){return new O(F)}));B=new o(",",[B,new o("=",[y.variables,q])])}if(y.variable_names){var q=he(c(yr(A.variables()),function(H){var J;for(J in e.session.renamed_variables)if(e.session.renamed_variables.hasOwnProperty(J)&&e.session.renamed_variables[J]===H)break;return new o("=",[new o(J,[]),new O(H)])}));B=new o(",",[B,new o("=",[y.variable_names,q])])}if(y.singletons){var q=he(c(new Q(A,null).singleton_variables(),function(H){var J;for(J in e.session.renamed_variables)if(e.session.renamed_variables.hasOwnProperty(J)&&e.session.renamed_variables[J]===H)break;return new o("=",[new o(J,[]),new O(H)])}));B=new o(",",[B,new o("=",[y.singletons,q])])}e.prepend([new V(n.goal.replace(B),n.substitution,n)])}else A.type===h?e.throw_error(i.error.syntax(k[A.len],"unexpected token",!1)):e.throw_error(A.value)}}},"write/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write",[new O("S"),s])])),n.substitution,n)])},"write/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("false",[])]),new o(".",[new o("ignore_ops",[new o("false")]),new o(".",[new o("numbervars",[new o("true")]),new o("[]",[])])])])])),n.substitution,n)])},"writeq/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("writeq",[new O("S"),s])])),n.substitution,n)])},"writeq/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("true",[])]),new o(".",[new o("ignore_ops",[new o("false")]),new o(".",[new o("numbervars",[new o("true")]),new o("[]",[])])])])])),n.substitution,n)])},"write_canonical/1":function(e,n,t){var s=t.args[0];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write_canonical",[new O("S"),s])])),n.substitution,n)])},"write_canonical/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o("write_term",[s,a,new o(".",[new o("quoted",[new o("true",[])]),new o(".",[new o("ignore_ops",[new o("true")]),new o(".",[new o("numbervars",[new o("false")]),new o("[]",[])])])])])),n.substitution,n)])},"write_term/2":function(e,n,t){var s=t.args[0],a=t.args[1];e.prepend([new V(n.goal.replace(new o(",",[new o("current_output",[new O("S")]),new o("write_term",[new O("S"),s,a])])),n.substitution,n)])},"write_term/3":function(e,n,t){var s=t.args[0],a=t.args[1],l=t.args[2],f=i.type.is_stream(s)?s:e.get_stream_by_alias(s.id);if(i.type.is_variable(s)||i.type.is_variable(l))e.throw_error(i.error.instantiation(t.indicator));else if(!i.type.is_list(l))e.throw_error(i.error.type("list",l,t.indicator));else if(!i.type.is_stream(s)&&!i.type.is_atom(s))e.throw_error(i.error.domain("stream_or_alias",s,t.indicator));else if(!i.type.is_stream(f)||f.stream===null)e.throw_error(i.error.existence("stream",s,t.indicator));else if(f.input)e.throw_error(i.error.permission("output","stream",s,t.indicator));else if(f.type==="binary")e.throw_error(i.error.permission("output","binary_stream",s,t.indicator));else if(f.position==="past_end_of_stream"&&f.eof_action==="error")e.throw_error(i.error.permission("output","past_end_of_stream",s,t.indicator));else{for(var y={},d=l,m;i.type.is_term(d)&&d.indicator==="./2";){if(m=d.args[0],i.type.is_variable(m)){e.throw_error(i.error.instantiation(t.indicator));return}else if(!i.type.is_write_option(m)){e.throw_error(i.error.domain("write_option",m,t.indicator));return}y[m.id]=m.args[0].id==="true",d=d.args[1]}if(d.indicator!=="[]/0"){i.type.is_variable(d)?e.throw_error(i.error.instantiation(t.indicator)):e.throw_error(i.error.type("list",l,t.indicator));return}else{y.session=e.session;var S=a.toString(y);f.stream.put(S,f.position),typeof f.position=="number"&&(f.position+=S.length),e.success(n)}}},"halt/0":function(e,n,t){e.points=[]},"halt/1":function(e,n,t){var s=t.args[0];i.type.is_variable(s)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_integer(s)?e.points=[]:e.throw_error(i.error.type("integer",s,t.indicator))},"current_prolog_flag/2":function(e,n,t){var s=t.args[0],a=t.args[1];if(!i.type.is_variable(s)&&!i.type.is_atom(s))e.throw_error(i.error.type("atom",s,t.indicator));else if(!i.type.is_variable(s)&&!i.type.is_flag(s))e.throw_error(i.error.domain("prolog_flag",s,t.indicator));else{var l=[];for(var f in i.flag)if(!!i.flag.hasOwnProperty(f)){var y=new o(",",[new o("=",[new o(f),s]),new o("=",[e.get_flag(f),a])]);l.push(new V(n.goal.replace(y),n.substitution,n))}e.prepend(l)}},"set_prolog_flag/2":function(e,n,t){var s=t.args[0],a=t.args[1];i.type.is_variable(s)||i.type.is_variable(a)?e.throw_error(i.error.instantiation(t.indicator)):i.type.is_atom(s)?i.type.is_flag(s)?i.type.is_value_flag(s,a)?i.type.is_modifiable_flag(s)?(e.session.flag[s.id]=a,e.success(n)):e.throw_error(i.error.permission("modify","flag",s)):e.throw_error(i.error.domain("flag_value",new o("+",[s,a]),t.indicator)):e.throw_error(i.error.domain("prolog_flag",s,t.indicator)):e.throw_error(i.error.type("atom",s,t.indicator))}},flag:{bounded:{allowed:[new o("true"),new o("false")],value:new o("true"),changeable:!1},max_integer:{allowed:[new E(Number.MAX_SAFE_INTEGER)],value:new E(Number.MAX_SAFE_INTEGER),changeable:!1},min_integer:{allowed:[new E(Number.MIN_SAFE_INTEGER)],value:new E(Number.MIN_SAFE_INTEGER),changeable:!1},integer_rounding_function:{allowed:[new o("down"),new o("toward_zero")],value:new o("toward_zero"),changeable:!1},char_conversion:{allowed:[new o("on"),new o("off")],value:new o("on"),changeable:!0},debug:{allowed:[new o("on"),new o("off")],value:new o("off"),changeable:!0},max_arity:{allowed:[new o("unbounded")],value:new o("unbounded"),changeable:!1},unknown:{allowed:[new o("error"),new o("fail"),new o("warning")],value:new o("error"),changeable:!0},double_quotes:{allowed:[new o("chars"),new o("codes"),new o("atom")],value:new o("codes"),changeable:!0},occurs_check:{allowed:[new o("false"),new o("true")],value:new o("false"),changeable:!0},dialect:{allowed:[new o("tau")],value:new o("tau"),changeable:!1},version_data:{allowed:[new o("tau",[new E(r.major,!1),new E(r.minor,!1),new E(r.patch,!1),new o(r.status)])],value:new o("tau",[new E(r.major,!1),new E(r.minor,!1),new E(r.patch,!1),new o(r.status)]),changeable:!1},nodejs:{allowed:[new o("yes"),new o("no")],value:new o(typeof ie!="undefined"&&ie.exports?"yes":"no"),changeable:!1}},unify:function(e,n,t){t=t===void 0?!1:t;for(var s=[{left:e,right:n}],a={};s.length!==0;){var l=s.pop();if(e=l.left,n=l.right,i.type.is_term(e)&&i.type.is_term(n)){if(e.indicator!==n.indicator)return null;for(var f=0;fa.value?1:0:a}else return s},operate:function(e,n){if(i.type.is_operator(n)){for(var t=i.type.is_operator(n),s=[],a,l=!1,f=0;fe.get_flag("max_integer").value||a0?e.start+e.matches[0].length:e.start,a=t?new o("token_not_found"):new o("found",[new o(e.value.toString())]),l=new o(".",[new o("line",[new E(e.line+1)]),new o(".",[new o("column",[new E(s+1)]),new o(".",[a,new o("[]",[])])])]);return new o("error",[new o("syntax_error",[new o(n)]),l])},syntax_by_predicate:function(e,n){return new o("error",[new o("syntax_error",[new o(e)]),oe(n)])}},warning:{singleton:function(e,n,t){for(var s=new o("[]"),a=e.length-1;a>=0;a--)s=new o(".",[new O(e[a]),s]);return new o("warning",[new o("singleton_variables",[s,oe(n)]),new o(".",[new o("line",[new E(t,!1)]),new o("[]")])])},failed_goal:function(e,n){return new o("warning",[new o("failed_goal",[e]),new o(".",[new o("line",[new E(n,!1)]),new o("[]")])])}},format_variable:function(e){return"_"+e},format_answer:function(e,n,t){n instanceof D&&(n=n.thread);var t=t||{};if(t.session=n?n.session:void 0,i.type.is_error(e))return"uncaught exception: "+e.args[0].toString();if(e===!1)return"false.";if(e===null)return"limit exceeded ;";var s=0,a="";if(i.type.is_substitution(e)){var l=e.domain(!0);e=e.filter(function(d,m){return!i.type.is_variable(m)||l.indexOf(m.id)!==-1&&d!==m.id})}for(var f in e.links)!e.links.hasOwnProperty(f)||(s++,a!==""&&(a+=", "),a+=f.toString(t)+" = "+e.links[f].toString(t));var y=typeof n=="undefined"||n.points.length>0?" ;":".";return s===0?"true"+y:a+y},flatten_error:function(e){if(!i.type.is_error(e))return null;e=e.args[0];var n={};return n.type=e.args[0].id,n.thrown=n.type==="syntax_error"?null:e.args[1].id,n.expected=null,n.found=null,n.representation=null,n.existence=null,n.existence_type=null,n.line=null,n.column=null,n.permission_operation=null,n.permission_type=null,n.evaluation_type=null,n.type==="type_error"||n.type==="domain_error"?(n.expected=e.args[0].args[0].id,n.found=e.args[0].args[1].toString()):n.type==="syntax_error"?e.args[1].indicator==="./2"?(n.expected=e.args[0].args[0].id,n.found=e.args[1].args[1].args[1].args[0],n.found=n.found.id==="token_not_found"?n.found.id:n.found.args[0].id,n.line=e.args[1].args[0].args[0].value,n.column=e.args[1].args[1].args[0].args[0].value):n.thrown=e.args[1].id:n.type==="permission_error"?(n.found=e.args[0].args[2].toString(),n.permission_operation=e.args[0].args[0].id,n.permission_type=e.args[0].args[1].id):n.type==="evaluation_error"?n.evaluation_type=e.args[0].args[0].id:n.type==="representation_error"?n.representation=e.args[0].args[0].id:n.type==="existence_error"&&(n.existence=e.args[0].args[1].toString(),n.existence_type=e.args[0].args[0].id),n},create:function(e){return new i.type.Session(e)}};typeof ie!="undefined"?ie.exports=i:window.pl=i})()});var er=I((qu,rt)=>{var is=Array.isArray;rt.exports=is});var nt=I(($u,tt)=>{var ss=typeof global=="object"&&global&&global.Object===Object&&global;tt.exports=ss});var rr=I((Du,it)=>{var as=nt(),os=typeof self=="object"&&self&&self.Object===Object&&self,us=as||os||Function("return this")();it.exports=us});var tr=I((Xu,st)=>{var ls=rr(),cs=ls.Symbol;st.exports=cs});var lt=I((Bu,at)=>{var ot=tr(),ut=Object.prototype,fs=ut.hasOwnProperty,ps=ut.toString,Xe=ot?ot.toStringTag:void 0;function ys(r){var u=fs.call(r,Xe),p=r[Xe];try{r[Xe]=void 0;var c=!0}catch(_){}var w=ps.call(r);return c&&(u?r[Xe]=p:delete r[Xe]),w}at.exports=ys});var ft=I((Fu,ct)=>{var _s=Object.prototype,ws=_s.toString;function gs(r){return ws.call(r)}ct.exports=gs});var Pr=I((zu,pt)=>{var yt=tr(),ds=lt(),vs=ft(),hs="[object Null]",ms="[object Undefined]",_t=yt?yt.toStringTag:void 0;function bs(r){return r==null?r===void 0?ms:hs:_t&&_t in Object(r)?ds(r):vs(r)}pt.exports=bs});var gt=I((Lu,wt)=>{function Ts(r){return r!=null&&typeof r=="object"}wt.exports=Ts});var nr=I((Wu,dt)=>{var xs=Pr(),Vs=gt(),Ss="[object Symbol]";function ks(r){return typeof r=="symbol"||Vs(r)&&xs(r)==Ss}dt.exports=ks});var ht=I((Hu,vt)=>{var Ps=er(),Cs=nr(),Os=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Is=/^\w*$/;function Es(r,u){if(Ps(r))return!1;var p=typeof r;return p=="number"||p=="symbol"||p=="boolean"||r==null||Cs(r)?!0:Is.test(r)||!Os.test(r)||u!=null&&r in Object(u)}vt.exports=Es});var ir=I((Gu,mt)=>{function As(r){var u=typeof r;return r!=null&&(u=="object"||u=="function")}mt.exports=As});var Tt=I((Yu,bt)=>{var Ns=Pr(),Rs=ir(),Ms="[object AsyncFunction]",qs="[object Function]",$s="[object GeneratorFunction]",Ds="[object Proxy]";function Xs(r){if(!Rs(r))return!1;var u=Ns(r);return u==qs||u==$s||u==Ms||u==Ds}bt.exports=Xs});var Vt=I((Uu,xt)=>{var Bs=rr(),Fs=Bs["__core-js_shared__"];xt.exports=Fs});var Pt=I((Zu,St)=>{var Cr=Vt(),kt=function(){var r=/[^.]+$/.exec(Cr&&Cr.keys&&Cr.keys.IE_PROTO||"");return r?"Symbol(src)_1."+r:""}();function zs(r){return!!kt&&kt in r}St.exports=zs});var Ot=I((Qu,Ct)=>{var Ls=Function.prototype,Ws=Ls.toString;function Hs(r){if(r!=null){try{return Ws.call(r)}catch(u){}try{return r+""}catch(u){}}return""}Ct.exports=Hs});var Et=I((Ju,It)=>{var Gs=Tt(),Ys=Pt(),Us=ir(),Zs=Ot(),Qs=/[\\^$.*+?()[\]{}|]/g,Js=/^\[object .+?Constructor\]$/,Ks=Function.prototype,js=Object.prototype,ea=Ks.toString,ra=js.hasOwnProperty,ta=RegExp("^"+ea.call(ra).replace(Qs,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function na(r){if(!Us(r)||Ys(r))return!1;var u=Gs(r)?ta:Js;return u.test(Zs(r))}It.exports=na});var Nt=I((Ku,At)=>{function ia(r,u){return r==null?void 0:r[u]}At.exports=ia});var sr=I((ju,Rt)=>{var sa=Et(),aa=Nt();function oa(r,u){var p=aa(r,u);return sa(p)?p:void 0}Rt.exports=oa});var Be=I((el,Mt)=>{var ua=sr(),la=ua(Object,"create");Mt.exports=la});var Dt=I((rl,qt)=>{var $t=Be();function ca(){this.__data__=$t?$t(null):{},this.size=0}qt.exports=ca});var Bt=I((tl,Xt)=>{function fa(r){var u=this.has(r)&&delete this.__data__[r];return this.size-=u?1:0,u}Xt.exports=fa});var zt=I((nl,Ft)=>{var pa=Be(),ya="__lodash_hash_undefined__",_a=Object.prototype,wa=_a.hasOwnProperty;function ga(r){var u=this.__data__;if(pa){var p=u[r];return p===ya?void 0:p}return wa.call(u,r)?u[r]:void 0}Ft.exports=ga});var Wt=I((il,Lt)=>{var da=Be(),va=Object.prototype,ha=va.hasOwnProperty;function ma(r){var u=this.__data__;return da?u[r]!==void 0:ha.call(u,r)}Lt.exports=ma});var Gt=I((sl,Ht)=>{var ba=Be(),Ta="__lodash_hash_undefined__";function xa(r,u){var p=this.__data__;return this.size+=this.has(r)?0:1,p[r]=ba&&u===void 0?Ta:u,this}Ht.exports=xa});var Ut=I((al,Yt)=>{var Va=Dt(),Sa=Bt(),ka=zt(),Pa=Wt(),Ca=Gt();function Ie(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{function Oa(){this.__data__=[],this.size=0}Zt.exports=Oa});var Or=I((ul,Jt)=>{function Ia(r,u){return r===u||r!==r&&u!==u}Jt.exports=Ia});var Fe=I((ll,Kt)=>{var Ea=Or();function Aa(r,u){for(var p=r.length;p--;)if(Ea(r[p][0],u))return p;return-1}Kt.exports=Aa});var en=I((cl,jt)=>{var Na=Fe(),Ra=Array.prototype,Ma=Ra.splice;function qa(r){var u=this.__data__,p=Na(u,r);if(p<0)return!1;var c=u.length-1;return p==c?u.pop():Ma.call(u,p,1),--this.size,!0}jt.exports=qa});var tn=I((fl,rn)=>{var $a=Fe();function Da(r){var u=this.__data__,p=$a(u,r);return p<0?void 0:u[p][1]}rn.exports=Da});var sn=I((pl,nn)=>{var Xa=Fe();function Ba(r){return Xa(this.__data__,r)>-1}nn.exports=Ba});var on=I((yl,an)=>{var Fa=Fe();function za(r,u){var p=this.__data__,c=Fa(p,r);return c<0?(++this.size,p.push([r,u])):p[c][1]=u,this}an.exports=za});var ln=I((_l,un)=>{var La=Qt(),Wa=en(),Ha=tn(),Ga=sn(),Ya=on();function Ee(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{var Ua=sr(),Za=rr(),Qa=Ua(Za,"Map");cn.exports=Qa});var _n=I((gl,pn)=>{var yn=Ut(),Ja=ln(),Ka=fn();function ja(){this.size=0,this.__data__={hash:new yn,map:new(Ka||Ja),string:new yn}}pn.exports=ja});var gn=I((dl,wn)=>{function eo(r){var u=typeof r;return u=="string"||u=="number"||u=="symbol"||u=="boolean"?r!=="__proto__":r===null}wn.exports=eo});var ze=I((vl,dn)=>{var ro=gn();function to(r,u){var p=r.__data__;return ro(u)?p[typeof u=="string"?"string":"hash"]:p.map}dn.exports=to});var hn=I((hl,vn)=>{var no=ze();function io(r){var u=no(this,r).delete(r);return this.size-=u?1:0,u}vn.exports=io});var bn=I((ml,mn)=>{var so=ze();function ao(r){return so(this,r).get(r)}mn.exports=ao});var xn=I((bl,Tn)=>{var oo=ze();function uo(r){return oo(this,r).has(r)}Tn.exports=uo});var Sn=I((Tl,Vn)=>{var lo=ze();function co(r,u){var p=lo(this,r),c=p.size;return p.set(r,u),this.size+=p.size==c?0:1,this}Vn.exports=co});var Pn=I((xl,kn)=>{var fo=_n(),po=hn(),yo=bn(),_o=xn(),wo=Sn();function Ae(r){var u=-1,p=r==null?0:r.length;for(this.clear();++u{var On=Pn(),go="Expected a function";function Ir(r,u){if(typeof r!="function"||u!=null&&typeof u!="function")throw new TypeError(go);var p=function(){var c=arguments,w=u?u.apply(this,c):c[0],_=p.cache;if(_.has(w))return _.get(w);var v=r.apply(this,c);return p.cache=_.set(w,v)||_,v};return p.cache=new(Ir.Cache||On),p}Ir.Cache=On;Cn.exports=Ir});var An=I((Sl,En)=>{var vo=In(),ho=500;function mo(r){var u=vo(r,function(c){return p.size===ho&&p.clear(),c}),p=u.cache;return u}En.exports=mo});var Rn=I((kl,Nn)=>{var bo=An(),To=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,xo=/\\(\\)?/g,Vo=bo(function(r){var u=[];return r.charCodeAt(0)===46&&u.push(""),r.replace(To,function(p,c,w,_){u.push(w?_.replace(xo,"$1"):c||p)}),u});Nn.exports=Vo});var qn=I((Pl,Mn)=>{function So(r,u){for(var p=-1,c=r==null?0:r.length,w=Array(c);++p{var Dn=tr(),ko=qn(),Po=er(),Co=nr(),Oo=1/0,Xn=Dn?Dn.prototype:void 0,Bn=Xn?Xn.toString:void 0;function Fn(r){if(typeof r=="string")return r;if(Po(r))return ko(r,Fn)+"";if(Co(r))return Bn?Bn.call(r):"";var u=r+"";return u=="0"&&1/r==-Oo?"-0":u}$n.exports=Fn});var Wn=I((Ol,Ln)=>{var Io=zn();function Eo(r){return r==null?"":Io(r)}Ln.exports=Eo});var ar=I((Il,Hn)=>{var Ao=er(),No=ht(),Ro=Rn(),Mo=Wn();function qo(r,u){return Ao(r)?r:No(r,u)?[r]:Ro(Mo(r))}Hn.exports=qo});var or=I((El,Gn)=>{var $o=nr(),Do=1/0;function Xo(r){if(typeof r=="string"||$o(r))return r;var u=r+"";return u=="0"&&1/r==-Do?"-0":u}Gn.exports=Xo});var Er=I((Al,Yn)=>{var Bo=ar(),Fo=or();function zo(r,u){u=Bo(u,r);for(var p=0,c=u.length;r!=null&&p{var Lo=Er();function Wo(r,u,p){var c=r==null?void 0:Lo(r,u);return c===void 0?p:c}Un.exports=Wo});var li=I((Ul,ui)=>{var Jo=sr(),Ko=function(){try{var r=Jo(Object,"defineProperty");return r({},"",{}),r}catch(u){}}();ui.exports=Ko});var pi=I((Zl,ci)=>{var fi=li();function jo(r,u,p){u=="__proto__"&&fi?fi(r,u,{configurable:!0,enumerable:!0,value:p,writable:!0}):r[u]=p}ci.exports=jo});var _i=I((Ql,yi)=>{var eu=pi(),ru=Or(),tu=Object.prototype,nu=tu.hasOwnProperty;function iu(r,u,p){var c=r[u];(!(nu.call(r,u)&&ru(c,p))||p===void 0&&!(u in r))&&eu(r,u,p)}yi.exports=iu});var gi=I((Jl,wi)=>{var su=9007199254740991,au=/^(?:0|[1-9]\d*)$/;function ou(r,u){var p=typeof r;return u=u==null?su:u,!!u&&(p=="number"||p!="symbol"&&au.test(r))&&r>-1&&r%1==0&&r{var uu=_i(),lu=ar(),cu=gi(),vi=ir(),fu=or();function pu(r,u,p,c){if(!vi(r))return r;u=lu(u,r);for(var w=-1,_=u.length,v=_-1,g=r;g!=null&&++w<_;){var h=fu(u[w]),x=p;if(h==="__proto__"||h==="constructor"||h==="prototype")return r;if(w!=v){var T=g[h];x=c?c(T,h,g):void 0,x===void 0&&(x=vi(T)?T:cu(u[w+1])?[]:{})}uu(g,h,x),g=g[h]}return r}di.exports=pu});var bi=I((jl,mi)=>{var yu=hi();function _u(r,u,p){return r==null?r:yu(r,u,p)}mi.exports=_u});var xi=I((ec,Ti)=>{function wu(r){var u=r==null?0:r.length;return u?r[u-1]:void 0}Ti.exports=wu});var Si=I((rc,Vi)=>{function gu(r,u,p){var c=-1,w=r.length;u<0&&(u=-u>w?0:w+u),p=p>w?w:p,p<0&&(p+=w),w=u>p?0:p-u>>>0,u>>>=0;for(var _=Array(w);++c{var du=Er(),vu=Si();function hu(r,u){return u.length<2?r:du(r,vu(u,0,-1))}ki.exports=hu});var Oi=I((nc,Ci)=>{var mu=ar(),bu=xi(),Tu=Pi(),xu=or();function Vu(r,u){return u=mu(u,r),r=Tu(r,u),r==null||delete r[xu(bu(u))]}Ci.exports=Vu});var Ei=I((ic,Ii)=>{var Su=Oi();function ku(r,u){return r==null?!0:Su(r,u)}Ii.exports=ku});var Ou={};Qi(Ou,{default:()=>Eu});var $i=G(require("@yarnpkg/core"));var ni=G(require("@yarnpkg/cli")),ur=G(require("@yarnpkg/core")),ii=G(require("@yarnpkg/core")),We=G(require("clipanion"));var ae=G(require("@yarnpkg/core")),le=G(require("@yarnpkg/core")),Ne=G(require("@yarnpkg/fslib")),jn=G(Xr()),Re=G(kr());var Nr=G(require("@yarnpkg/core")),Rr=G(Ar()),re=G(kr()),Zn=G(require("vm")),{is_atom:ge,is_variable:Ho,is_instantiated_list:Go}=re.default.type;function Qn(r,u,p){r.prepend(p.map(c=>new re.default.type.State(u.goal.replace(c),u.substitution,u)))}var Jn=new WeakMap;function Mr(r){let u=Jn.get(r.session);if(u==null)throw new Error("Assertion failed: A project should have been registered for the active session");return u}var Yo=new re.default.type.Module("constraints",{["project_workspaces_by_descriptor/3"]:(r,u,p)=>{let[c,w,_]=p.args;if(!ge(c)||!ge(w)){r.throw_error(re.default.error.instantiation(p.indicator));return}let v=Nr.structUtils.parseIdent(c.id),g=Nr.structUtils.makeDescriptor(v,w.id),x=Mr(r).tryWorkspaceByDescriptor(g);Ho(_)&&x!==null&&Qn(r,u,[new re.default.type.Term("=",[_,new re.default.type.Term(String(x.relativeCwd))])]),ge(_)&&x!==null&&x.relativeCwd===_.id&&r.success(u)},["workspace_field/3"]:(r,u,p)=>{let[c,w,_]=p.args;if(!ge(c)||!ge(w)){r.throw_error(re.default.error.instantiation(p.indicator));return}let g=Mr(r).tryWorkspaceByCwd(c.id);if(g==null)return;let h=(0,Rr.default)(g.manifest.raw,w.id);typeof h!="undefined"&&Qn(r,u,[new re.default.type.Term("=",[_,new re.default.type.Term(typeof h=="object"?JSON.stringify(h):h)])])},["workspace_field_test/3"]:(r,u,p)=>{let[c,w,_]=p.args;r.prepend([new re.default.type.State(u.goal.replace(new re.default.type.Term("workspace_field_test",[c,w,_,new re.default.type.Term("[]",[])])),u.substitution,u)])},["workspace_field_test/4"]:(r,u,p)=>{let[c,w,_,v]=p.args;if(!ge(c)||!ge(w)||!ge(_)||!Go(v)){r.throw_error(re.default.error.instantiation(p.indicator));return}let h=Mr(r).tryWorkspaceByCwd(c.id);if(h==null)return;let x=(0,Rr.default)(h.manifest.raw,w.id);if(typeof x=="undefined")return;let T={$$:x};for(let[C,N]of v.toJavaScript().entries())T[`$${C}`]=N;Zn.default.runInNewContext(_.id,T)&&r.success(u)}},["project_workspaces_by_descriptor/3","workspace_field/3","workspace_field_test/3","workspace_field_test/4"]);function Kn(r,u){Jn.set(r,u),r.consult(`:- use_module(library(${Yo.id})).`)}(0,jn.default)(Re.default);var Le;(function(c){c.Dependencies="dependencies",c.DevDependencies="devDependencies",c.PeerDependencies="peerDependencies"})(Le||(Le={}));var ei=[Le.Dependencies,Le.DevDependencies,Le.PeerDependencies];function K(r){if(r instanceof Re.default.type.Num)return r.value;if(r instanceof Re.default.type.Term)switch(r.indicator){case"throw/1":return K(r.args[0]);case"error/1":return K(r.args[0]);case"error/2":if(r.args[0]instanceof Re.default.type.Term&&r.args[0].indicator==="syntax_error/1")return Object.assign(K(r.args[0]),...K(r.args[1]));{let u=K(r.args[0]);return u.message+=` (in ${K(r.args[1])})`,u}case"syntax_error/1":return new ae.ReportError(ae.MessageName.PROLOG_SYNTAX_ERROR,`Syntax error: ${K(r.args[0])}`);case"existence_error/2":return new ae.ReportError(ae.MessageName.PROLOG_EXISTENCE_ERROR,`Existence error: ${K(r.args[0])} ${K(r.args[1])} not found`);case"instantiation_error/0":return new ae.ReportError(ae.MessageName.PROLOG_INSTANTIATION_ERROR,"Instantiation error: an argument is variable when an instantiated argument was expected");case"line/1":return{line:K(r.args[0])};case"column/1":return{column:K(r.args[0])};case"found/1":return{found:K(r.args[0])};case"./2":return[K(r.args[0])].concat(K(r.args[1]));case"//2":return`${K(r.args[0])}/${K(r.args[1])}`;default:return r.id}throw`couldn't pretty print because of unsupported node ${r}`}function ri(r){let u;try{u=K(r)}catch(p){throw typeof p=="string"?new ae.ReportError(ae.MessageName.PROLOG_UNKNOWN_ERROR,`Unknown error: ${r} (note: ${p})`):p}return typeof u.line!="undefined"&&typeof u.column!="undefined"&&(u.message+=` at line ${u.line}, column ${u.column}`),u}var ti=class{constructor(u,p){let c=1e3*u.workspaces.length;this.session=Re.default.create(c),Kn(this.session,u),this.session.consult(":- use_module(library(lists))."),this.session.consult(p)}fetchNextAnswer(){return new Promise(u=>{this.session.answer(p=>{u(p)})})}async*makeQuery(u){let p=this.session.query(u);if(p!==!0)throw ri(p);for(;;){let c=await this.fetchNextAnswer();if(c===null)throw new ae.ReportError(ae.MessageName.PROLOG_LIMIT_EXCEEDED,"Resolution limit exceeded");if(!c)break;if(c.id==="throw")throw ri(c);yield c}}};function ke(r){return r.id==="null"?null:`${r.toJavaScript()}`}function Uo(r){if(r.id==="null")return null;{let u=r.toJavaScript();if(typeof u!="string")return JSON.stringify(u);try{return JSON.stringify(JSON.parse(u))}catch{return JSON.stringify(u)}}}var pe=class{constructor(u){this.source="";this.project=u;let p=u.configuration.get("constraintsPath");Ne.xfs.existsSync(p)&&(this.source=Ne.xfs.readFileSync(p,"utf8"))}static async find(u){return new pe(u)}getProjectDatabase(){let u="";for(let p of ei)u+=`dependency_type(${p}). +`;for(let p of this.project.workspacesByCwd.values()){let c=p.relativeCwd;u+=`workspace(${de(c)}). +`,u+=`workspace_ident(${de(c)}, ${de(le.structUtils.stringifyIdent(p.locator))}). +`,u+=`workspace_version(${de(c)}, ${de(p.manifest.version)}). +`;for(let w of ei)for(let _ of p.manifest[w].values())u+=`workspace_has_dependency(${de(c)}, ${de(le.structUtils.stringifyIdent(_))}, ${de(_.range)}, ${w}). +`}return u+=`workspace(_) :- false. +`,u+=`workspace_ident(_, _) :- false. +`,u+=`workspace_version(_, _) :- false. +`,u+=`workspace_has_dependency(_, _, _, _) :- false. +`,u}getDeclarations(){let u="";return u+=`gen_enforced_dependency(_, _, _, _) :- false. +`,u+=`gen_enforced_field(_, _, _) :- false. +`,u}get fullSource(){return`${this.getProjectDatabase()} +${this.source} +${this.getDeclarations()}`}createSession(){return new ti(this.project,this.fullSource)}async process(){let u=this.createSession();return{enforcedDependencies:await this.genEnforcedDependencies(u),enforcedFields:await this.genEnforcedFields(u)}}async genEnforcedDependencies(u){let p=[];for await(let c of u.makeQuery("workspace(WorkspaceCwd), dependency_type(DependencyType), gen_enforced_dependency(WorkspaceCwd, DependencyIdent, DependencyRange, DependencyType).")){let w=Ne.ppath.resolve(this.project.cwd,ke(c.links.WorkspaceCwd)),_=ke(c.links.DependencyIdent),v=ke(c.links.DependencyRange),g=ke(c.links.DependencyType);if(w===null||_===null)throw new Error("Invalid rule");let h=this.project.getWorkspaceByCwd(w),x=le.structUtils.parseIdent(_);p.push({workspace:h,dependencyIdent:x,dependencyRange:v,dependencyType:g})}return le.miscUtils.sortMap(p,[({dependencyRange:c})=>c!==null?"0":"1",({workspace:c})=>le.structUtils.stringifyIdent(c.locator),({dependencyIdent:c})=>le.structUtils.stringifyIdent(c)])}async genEnforcedFields(u){let p=[];for await(let c of u.makeQuery("workspace(WorkspaceCwd), gen_enforced_field(WorkspaceCwd, FieldPath, FieldValue).")){let w=Ne.ppath.resolve(this.project.cwd,ke(c.links.WorkspaceCwd)),_=ke(c.links.FieldPath),v=Uo(c.links.FieldValue);if(w===null||_===null)throw new Error("Invalid rule");let g=this.project.getWorkspaceByCwd(w);p.push({workspace:g,fieldPath:_,fieldValue:v})}return le.miscUtils.sortMap(p,[({workspace:c})=>le.structUtils.stringifyIdent(c.locator),({fieldPath:c})=>c])}async*query(u){let p=this.createSession();for await(let c of p.makeQuery(u)){let w={};for(let[_,v]of Object.entries(c.links))_!=="_"&&(w[_]=ke(v));yield w}}};function de(r){return typeof r=="string"?`'${r}'`:"[]"}var He=class extends ni.BaseCommand{constructor(){super(...arguments);this.json=We.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.query=We.Option.String()}async execute(){let u=await ur.Configuration.find(this.context.cwd,this.context.plugins),{project:p}=await ur.Project.find(u,this.context.cwd),c=await pe.find(p),w=this.query;return w.endsWith(".")||(w=`${w}.`),(await ii.StreamReport.start({configuration:u,json:this.json,stdout:this.context.stdout},async v=>{for await(let g of c.query(w)){let h=Array.from(Object.entries(g)),x=h.length,T=h.reduce((b,[C])=>Math.max(b,C.length),0);for(let b=0;b{let v=new Set,g=[];for(let h=0,x=this.fix?10:1;h{await h.persistManifest()}));for(let[h,x]of g)_.reportError(h,x)});return w.hasErrors()?w.exitCode():0}};Ye.paths=[["constraints"]],Ye.usage=fr.Command.Usage({category:"Constraints-related commands",description:"check that the project constraints are met",details:` + This command will run constraints on your project and emit errors for each one that is found but isn't met. If any error is emitted the process will exit with a non-zero exit code. + + If the \`--fix\` flag is used, Yarn will attempt to automatically fix the issues the best it can, following a multi-pass process (with a maximum of 10 iterations). Some ambiguous patterns cannot be autofixed, in which case you'll have to manually specify the right resolution. + + For more information as to how to write constraints, please consult our dedicated page on our website: https://yarnpkg.com/features/constraints. + `,examples:[["Check that all constraints are satisfied","yarn constraints"],["Autofix all unmet constraints","yarn constraints --fix"]]});var qi=Ye;async function Pu(r,u,p,{configuration:c,fix:w}){let _=new Map,v=new Map;for(let{workspace:g,dependencyIdent:h,dependencyRange:x,dependencyType:T}of p){let b=v.get(g);typeof b=="undefined"&&v.set(g,b=new Map);let C=b.get(h.identHash);typeof C=="undefined"&&b.set(h.identHash,C=new Map);let N=C.get(T);typeof N=="undefined"&&C.set(T,N=new Set),_.set(h.identHash,h),N.add(x)}for(let[g,h]of v)for(let[x,T]of h){let b=_.get(x);if(typeof b=="undefined")throw new Error("Assertion failed: The ident should have been registered");for(let[C,N]of T){let L=N.has(null)?[null]:[...N];if(L.length>2)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via conflicting ranges ${L.slice(0,-1).map(ee=>$.structUtils.prettyRange(c,String(ee))).join(", ")}, and ${$.structUtils.prettyRange(c,String(L[L.length-1]))} (in ${C})`]);else if(L.length>1)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via conflicting ranges ${$.structUtils.prettyRange(c,String(L[0]))} and ${$.structUtils.prettyRange(c,String(L[1]))} (in ${C})`]);else{let ee=g.manifest[C].get(b.identHash),[te]=L;te!==null?ee?ee.range!==te&&(w?(g.manifest[C].set(b.identHash,$.structUtils.makeDescriptor(b,te)),r.add(g)):u.push([se.MessageName.CONSTRAINTS_INCOMPATIBLE_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} via ${$.structUtils.prettyRange(c,te)}, but uses ${$.structUtils.prettyRange(c,ee.range)} instead (in ${C})`])):w?(g.manifest[C].set(b.identHash,$.structUtils.makeDescriptor(b,te)),r.add(g)):u.push([se.MessageName.CONSTRAINTS_MISSING_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} must depend on ${$.structUtils.prettyIdent(c,b)} (via ${$.structUtils.prettyRange(c,te)}), but doesn't (in ${C})`]):ee&&(w?(g.manifest[C].delete(b.identHash),r.add(g)):u.push([se.MessageName.CONSTRAINTS_EXTRANEOUS_DEPENDENCY,`${$.structUtils.prettyWorkspace(c,g)} has an extraneous dependency on ${$.structUtils.prettyIdent(c,b)} (in ${C})`]))}}}}async function Cu(r,u,p,{configuration:c,fix:w}){let _=new Map;for(let{workspace:v,fieldPath:g,fieldValue:h}of p){let x=Pe.miscUtils.getMapWithDefault(_,v);Pe.miscUtils.getSetWithDefault(x,g).add(h)}for(let[v,g]of _)for(let[h,x]of g){let T=[...x];if(T.length>2)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to conflicting values ${T.slice(0,-1).map(b=>$.formatUtils.pretty(c,String(b),"magenta")).join(", ")}, or ${$.formatUtils.pretty(c,String(T[T.length-1]),"magenta")}`]);else if(T.length>1)u.push([se.MessageName.CONSTRAINTS_AMBIGUITY,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to conflicting values ${$.formatUtils.pretty(c,String(T[0]),"magenta")} or ${$.formatUtils.pretty(c,String(T[1]),"magenta")}`]);else{let b=(0,Ni.default)(v.manifest.raw,h),[C]=T;C!==null?b===void 0?w?(await qr(v,h,C),r.add(v)):u.push([se.MessageName.CONSTRAINTS_MISSING_FIELD,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,String(C),"magenta")}, but doesn't`]):JSON.stringify(b)!==C&&(w?(await qr(v,h,C),r.add(v)):u.push([se.MessageName.CONSTRAINTS_INCOMPATIBLE_FIELD,`${$.structUtils.prettyWorkspace(c,v)} must have a field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,String(C),"magenta")}, but is set to ${$.formatUtils.pretty(c,JSON.stringify(b),"magenta")} instead`])):b!=null&&(w?(await qr(v,h,null),r.add(v)):u.push([se.MessageName.CONSTRAINTS_EXTRANEOUS_FIELD,`${$.structUtils.prettyWorkspace(c,v)} has an extraneous field ${$.formatUtils.pretty(c,h,"cyan")} set to ${$.formatUtils.pretty(c,JSON.stringify(b),"magenta")}`]))}}}async function qr(r,u,p){p===null?(0,Mi.default)(r.manifest.raw,u):(0,Ri.default)(r.manifest.raw,u,JSON.parse(p))}var Iu={configuration:{constraintsPath:{description:"The path of the constraints file.",type:$i.SettingsType.ABSOLUTE_PATH,default:"./constraints.pro"}},commands:[si,oi,qi]},Eu=Iu;return Ou;})(); +return plugin; +} +}; diff --git a/.yarn/plugins/@yarnpkg/plugin-version.cjs b/.yarn/plugins/@yarnpkg/plugin-version.cjs new file mode 100644 index 0000000..87de4f4 --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-version.cjs @@ -0,0 +1,550 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-version", +factory: function (require) { +var plugin=(()=>{var ZB=Object.create,zy=Object.defineProperty,$B=Object.defineProperties,eU=Object.getOwnPropertyDescriptor,tU=Object.getOwnPropertyDescriptors,nU=Object.getOwnPropertyNames,uS=Object.getOwnPropertySymbols,rU=Object.getPrototypeOf,oS=Object.prototype.hasOwnProperty,iU=Object.prototype.propertyIsEnumerable;var lS=(i,o,f)=>o in i?zy(i,o,{enumerable:!0,configurable:!0,writable:!0,value:f}):i[o]=f,E0=(i,o)=>{for(var f in o||(o={}))oS.call(o,f)&&lS(i,f,o[f]);if(uS)for(var f of uS(o))iU.call(o,f)&&lS(i,f,o[f]);return i},Gf=(i,o)=>$B(i,tU(o)),uU=i=>zy(i,"__esModule",{value:!0});var ce=(i,o)=>()=>(o||i((o={exports:{}}).exports,o),o.exports),sS=(i,o)=>{for(var f in o)zy(i,f,{get:o[f],enumerable:!0})},oU=(i,o,f)=>{if(o&&typeof o=="object"||typeof o=="function")for(let p of nU(o))!oS.call(i,p)&&p!=="default"&&zy(i,p,{get:()=>o[p],enumerable:!(f=eU(o,p))||f.enumerable});return i},Mi=i=>oU(uU(zy(i!=null?ZB(rU(i)):{},"default",i&&i.__esModule&&"default"in i?{get:()=>i.default,enumerable:!0}:{value:i,enumerable:!0})),i);var eD=ce((F$,aS)=>{function lU(i,o){for(var f=-1,p=i==null?0:i.length,E=Array(p);++f{function sU(){this.__data__=[],this.size=0}fS.exports=sU});var tD=ce((P$,dS)=>{function aU(i,o){return i===o||i!==i&&o!==o}dS.exports=aU});var qy=ce((I$,pS)=>{var fU=tD();function cU(i,o){for(var f=i.length;f--;)if(fU(i[f][0],o))return f;return-1}pS.exports=cU});var vS=ce((B$,hS)=>{var dU=qy(),pU=Array.prototype,hU=pU.splice;function vU(i){var o=this.__data__,f=dU(o,i);if(f<0)return!1;var p=o.length-1;return f==p?o.pop():hU.call(o,f,1),--this.size,!0}hS.exports=vU});var yS=ce((U$,mS)=>{var mU=qy();function yU(i){var o=this.__data__,f=mU(o,i);return f<0?void 0:o[f][1]}mS.exports=yU});var _S=ce((j$,gS)=>{var gU=qy();function _U(i){return gU(this.__data__,i)>-1}gS.exports=_U});var DS=ce((z$,ES)=>{var EU=qy();function DU(i,o){var f=this.__data__,p=EU(f,i);return p<0?(++this.size,f.push([i,o])):f[p][1]=o,this}ES.exports=DU});var Hy=ce((q$,wS)=>{var wU=cS(),SU=vS(),TU=yS(),CU=_S(),xU=DS();function jv(i){var o=-1,f=i==null?0:i.length;for(this.clear();++o{var AU=Hy();function RU(){this.__data__=new AU,this.size=0}SS.exports=RU});var xS=ce((W$,CS)=>{function OU(i){var o=this.__data__,f=o.delete(i);return this.size=o.size,f}CS.exports=OU});var RS=ce((V$,AS)=>{function kU(i){return this.__data__.get(i)}AS.exports=kU});var kS=ce((G$,OS)=>{function MU(i){return this.__data__.has(i)}OS.exports=MU});var nD=ce((Y$,MS)=>{var NU=typeof global=="object"&&global&&global.Object===Object&&global;MS.exports=NU});var Yf=ce((K$,NS)=>{var LU=nD(),FU=typeof self=="object"&&self&&self.Object===Object&&self,bU=LU||FU||Function("return this")();NS.exports=bU});var zv=ce((X$,LS)=>{var PU=Yf(),IU=PU.Symbol;LS.exports=IU});var BS=ce((Q$,bS)=>{var PS=zv(),IS=Object.prototype,BU=IS.hasOwnProperty,UU=IS.toString,Wy=PS?PS.toStringTag:void 0;function jU(i){var o=BU.call(i,Wy),f=i[Wy];try{i[Wy]=void 0;var p=!0}catch(t){}var E=UU.call(i);return p&&(o?i[Wy]=f:delete i[Wy]),E}bS.exports=jU});var jS=ce((J$,US)=>{var zU=Object.prototype,qU=zU.toString;function HU(i){return qU.call(i)}US.exports=HU});var Qp=ce((Z$,zS)=>{var qS=zv(),WU=BS(),VU=jS(),GU="[object Null]",YU="[object Undefined]",HS=qS?qS.toStringTag:void 0;function KU(i){return i==null?i===void 0?YU:GU:HS&&HS in Object(i)?WU(i):VU(i)}zS.exports=KU});var qv=ce(($$,WS)=>{function XU(i){var o=typeof i;return i!=null&&(o=="object"||o=="function")}WS.exports=XU});var rD=ce((eee,VS)=>{var QU=Qp(),JU=qv(),ZU="[object AsyncFunction]",$U="[object Function]",ej="[object GeneratorFunction]",tj="[object Proxy]";function nj(i){if(!JU(i))return!1;var o=QU(i);return o==$U||o==ej||o==ZU||o==tj}VS.exports=nj});var YS=ce((tee,GS)=>{var rj=Yf(),ij=rj["__core-js_shared__"];GS.exports=ij});var QS=ce((nee,KS)=>{var iD=YS(),XS=function(){var i=/[^.]+$/.exec(iD&&iD.keys&&iD.keys.IE_PROTO||"");return i?"Symbol(src)_1."+i:""}();function uj(i){return!!XS&&XS in i}KS.exports=uj});var uD=ce((ree,JS)=>{var oj=Function.prototype,lj=oj.toString;function sj(i){if(i!=null){try{return lj.call(i)}catch(o){}try{return i+""}catch(o){}}return""}JS.exports=sj});var $S=ce((iee,ZS)=>{var aj=rD(),fj=QS(),cj=qv(),dj=uD(),pj=/[\\^$.*+?()[\]{}|]/g,hj=/^\[object .+?Constructor\]$/,vj=Function.prototype,mj=Object.prototype,yj=vj.toString,gj=mj.hasOwnProperty,_j=RegExp("^"+yj.call(gj).replace(pj,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function Ej(i){if(!cj(i)||fj(i))return!1;var o=aj(i)?_j:hj;return o.test(dj(i))}ZS.exports=Ej});var tT=ce((uee,eT)=>{function Dj(i,o){return i==null?void 0:i[o]}eT.exports=Dj});var sd=ce((oee,nT)=>{var wj=$S(),Sj=tT();function Tj(i,o){var f=Sj(i,o);return wj(f)?f:void 0}nT.exports=Tj});var L_=ce((lee,rT)=>{var Cj=sd(),xj=Yf(),Aj=Cj(xj,"Map");rT.exports=Aj});var Vy=ce((see,iT)=>{var Rj=sd(),Oj=Rj(Object,"create");iT.exports=Oj});var lT=ce((aee,uT)=>{var oT=Vy();function kj(){this.__data__=oT?oT(null):{},this.size=0}uT.exports=kj});var aT=ce((fee,sT)=>{function Mj(i){var o=this.has(i)&&delete this.__data__[i];return this.size-=o?1:0,o}sT.exports=Mj});var cT=ce((cee,fT)=>{var Nj=Vy(),Lj="__lodash_hash_undefined__",Fj=Object.prototype,bj=Fj.hasOwnProperty;function Pj(i){var o=this.__data__;if(Nj){var f=o[i];return f===Lj?void 0:f}return bj.call(o,i)?o[i]:void 0}fT.exports=Pj});var pT=ce((dee,dT)=>{var Ij=Vy(),Bj=Object.prototype,Uj=Bj.hasOwnProperty;function jj(i){var o=this.__data__;return Ij?o[i]!==void 0:Uj.call(o,i)}dT.exports=jj});var vT=ce((pee,hT)=>{var zj=Vy(),qj="__lodash_hash_undefined__";function Hj(i,o){var f=this.__data__;return this.size+=this.has(i)?0:1,f[i]=zj&&o===void 0?qj:o,this}hT.exports=Hj});var yT=ce((hee,mT)=>{var Wj=lT(),Vj=aT(),Gj=cT(),Yj=pT(),Kj=vT();function Hv(i){var o=-1,f=i==null?0:i.length;for(this.clear();++o{var _T=yT(),Xj=Hy(),Qj=L_();function Jj(){this.size=0,this.__data__={hash:new _T,map:new(Qj||Xj),string:new _T}}gT.exports=Jj});var wT=ce((mee,DT)=>{function Zj(i){var o=typeof i;return o=="string"||o=="number"||o=="symbol"||o=="boolean"?i!=="__proto__":i===null}DT.exports=Zj});var Gy=ce((yee,ST)=>{var $j=wT();function ez(i,o){var f=i.__data__;return $j(o)?f[typeof o=="string"?"string":"hash"]:f.map}ST.exports=ez});var CT=ce((gee,TT)=>{var tz=Gy();function nz(i){var o=tz(this,i).delete(i);return this.size-=o?1:0,o}TT.exports=nz});var AT=ce((_ee,xT)=>{var rz=Gy();function iz(i){return rz(this,i).get(i)}xT.exports=iz});var OT=ce((Eee,RT)=>{var uz=Gy();function oz(i){return uz(this,i).has(i)}RT.exports=oz});var MT=ce((Dee,kT)=>{var lz=Gy();function sz(i,o){var f=lz(this,i),p=f.size;return f.set(i,o),this.size+=f.size==p?0:1,this}kT.exports=sz});var oD=ce((wee,NT)=>{var az=ET(),fz=CT(),cz=AT(),dz=OT(),pz=MT();function Wv(i){var o=-1,f=i==null?0:i.length;for(this.clear();++o{var hz=Hy(),vz=L_(),mz=oD(),yz=200;function gz(i,o){var f=this.__data__;if(f instanceof hz){var p=f.__data__;if(!vz||p.length{var _z=Hy(),Ez=TS(),Dz=xS(),wz=RS(),Sz=kS(),Tz=FT();function Vv(i){var o=this.__data__=new _z(i);this.size=o.size}Vv.prototype.clear=Ez;Vv.prototype.delete=Dz;Vv.prototype.get=wz;Vv.prototype.has=Sz;Vv.prototype.set=Tz;bT.exports=Vv});var BT=ce((Cee,IT)=>{function Cz(i,o){for(var f=-1,p=i==null?0:i.length;++f{var xz=sd(),Az=function(){try{var i=xz(Object,"defineProperty");return i({},"",{}),i}catch(o){}}();UT.exports=Az});var sD=ce((Aee,jT)=>{var zT=lD();function Rz(i,o,f){o=="__proto__"&&zT?zT(i,o,{configurable:!0,enumerable:!0,value:f,writable:!0}):i[o]=f}jT.exports=Rz});var aD=ce((Ree,qT)=>{var Oz=sD(),kz=tD(),Mz=Object.prototype,Nz=Mz.hasOwnProperty;function Lz(i,o,f){var p=i[o];(!(Nz.call(i,o)&&kz(p,f))||f===void 0&&!(o in i))&&Oz(i,o,f)}qT.exports=Lz});var Gv=ce((Oee,HT)=>{var Fz=aD(),bz=sD();function Pz(i,o,f,p){var E=!f;f||(f={});for(var t=-1,k=o.length;++t{function Iz(i,o){for(var f=-1,p=Array(i);++f{function Bz(i){return i!=null&&typeof i=="object"}GT.exports=Bz});var KT=ce((Nee,YT)=>{var Uz=Qp(),jz=ad(),zz="[object Arguments]";function qz(i){return jz(i)&&Uz(i)==zz}YT.exports=qz});var fD=ce((Lee,XT)=>{var QT=KT(),Hz=ad(),JT=Object.prototype,Wz=JT.hasOwnProperty,Vz=JT.propertyIsEnumerable,Gz=QT(function(){return arguments}())?QT:function(i){return Hz(i)&&Wz.call(i,"callee")&&!Vz.call(i,"callee")};XT.exports=Gz});var fd=ce((Fee,ZT)=>{var Yz=Array.isArray;ZT.exports=Yz});var eC=ce((bee,$T)=>{function Kz(){return!1}$T.exports=Kz});var cD=ce((Yy,Yv)=>{var Xz=Yf(),Qz=eC(),tC=typeof Yy=="object"&&Yy&&!Yy.nodeType&&Yy,nC=tC&&typeof Yv=="object"&&Yv&&!Yv.nodeType&&Yv,Jz=nC&&nC.exports===tC,rC=Jz?Xz.Buffer:void 0,Zz=rC?rC.isBuffer:void 0,$z=Zz||Qz;Yv.exports=$z});var uC=ce((Pee,iC)=>{var eq=9007199254740991,tq=/^(?:0|[1-9]\d*)$/;function nq(i,o){var f=typeof i;return o=o==null?eq:o,!!o&&(f=="number"||f!="symbol"&&tq.test(i))&&i>-1&&i%1==0&&i{var rq=9007199254740991;function iq(i){return typeof i=="number"&&i>-1&&i%1==0&&i<=rq}oC.exports=iq});var sC=ce((Bee,lC)=>{var uq=Qp(),oq=dD(),lq=ad(),sq="[object Arguments]",aq="[object Array]",fq="[object Boolean]",cq="[object Date]",dq="[object Error]",pq="[object Function]",hq="[object Map]",vq="[object Number]",mq="[object Object]",yq="[object RegExp]",gq="[object Set]",_q="[object String]",Eq="[object WeakMap]",Dq="[object ArrayBuffer]",wq="[object DataView]",Sq="[object Float32Array]",Tq="[object Float64Array]",Cq="[object Int8Array]",xq="[object Int16Array]",Aq="[object Int32Array]",Rq="[object Uint8Array]",Oq="[object Uint8ClampedArray]",kq="[object Uint16Array]",Mq="[object Uint32Array]",o0={};o0[Sq]=o0[Tq]=o0[Cq]=o0[xq]=o0[Aq]=o0[Rq]=o0[Oq]=o0[kq]=o0[Mq]=!0;o0[sq]=o0[aq]=o0[Dq]=o0[fq]=o0[wq]=o0[cq]=o0[dq]=o0[pq]=o0[hq]=o0[vq]=o0[mq]=o0[yq]=o0[gq]=o0[_q]=o0[Eq]=!1;function Nq(i){return lq(i)&&oq(i.length)&&!!o0[uq(i)]}lC.exports=Nq});var F_=ce((Uee,aC)=>{function Lq(i){return function(o){return i(o)}}aC.exports=Lq});var b_=ce((Ky,Kv)=>{var Fq=nD(),fC=typeof Ky=="object"&&Ky&&!Ky.nodeType&&Ky,Xy=fC&&typeof Kv=="object"&&Kv&&!Kv.nodeType&&Kv,bq=Xy&&Xy.exports===fC,pD=bq&&Fq.process,Pq=function(){try{var i=Xy&&Xy.require&&Xy.require("util").types;return i||pD&&pD.binding&&pD.binding("util")}catch(o){}}();Kv.exports=Pq});var hC=ce((jee,cC)=>{var Iq=sC(),Bq=F_(),dC=b_(),pC=dC&&dC.isTypedArray,Uq=pC?Bq(pC):Iq;cC.exports=Uq});var hD=ce((zee,vC)=>{var jq=VT(),zq=fD(),qq=fd(),Hq=cD(),Wq=uC(),Vq=hC(),Gq=Object.prototype,Yq=Gq.hasOwnProperty;function Kq(i,o){var f=qq(i),p=!f&&zq(i),E=!f&&!p&&Hq(i),t=!f&&!p&&!E&&Vq(i),k=f||p||E||t,L=k?jq(i.length,String):[],N=L.length;for(var C in i)(o||Yq.call(i,C))&&!(k&&(C=="length"||E&&(C=="offset"||C=="parent")||t&&(C=="buffer"||C=="byteLength"||C=="byteOffset")||Wq(C,N)))&&L.push(C);return L}vC.exports=Kq});var P_=ce((qee,mC)=>{var Xq=Object.prototype;function Qq(i){var o=i&&i.constructor,f=typeof o=="function"&&o.prototype||Xq;return i===f}mC.exports=Qq});var vD=ce((Hee,yC)=>{function Jq(i,o){return function(f){return i(o(f))}}yC.exports=Jq});var _C=ce((Wee,gC)=>{var Zq=vD(),$q=Zq(Object.keys,Object);gC.exports=$q});var DC=ce((Vee,EC)=>{var eH=P_(),tH=_C(),nH=Object.prototype,rH=nH.hasOwnProperty;function iH(i){if(!eH(i))return tH(i);var o=[];for(var f in Object(i))rH.call(i,f)&&f!="constructor"&&o.push(f);return o}EC.exports=iH});var mD=ce((Gee,wC)=>{var uH=rD(),oH=dD();function lH(i){return i!=null&&oH(i.length)&&!uH(i)}wC.exports=lH});var I_=ce((Yee,SC)=>{var sH=hD(),aH=DC(),fH=mD();function cH(i){return fH(i)?sH(i):aH(i)}SC.exports=cH});var CC=ce((Kee,TC)=>{var dH=Gv(),pH=I_();function hH(i,o){return i&&dH(o,pH(o),i)}TC.exports=hH});var AC=ce((Xee,xC)=>{function vH(i){var o=[];if(i!=null)for(var f in Object(i))o.push(f);return o}xC.exports=vH});var OC=ce((Qee,RC)=>{var mH=qv(),yH=P_(),gH=AC(),_H=Object.prototype,EH=_H.hasOwnProperty;function DH(i){if(!mH(i))return gH(i);var o=yH(i),f=[];for(var p in i)p=="constructor"&&(o||!EH.call(i,p))||f.push(p);return f}RC.exports=DH});var B_=ce((Jee,kC)=>{var wH=hD(),SH=OC(),TH=mD();function CH(i){return TH(i)?wH(i,!0):SH(i)}kC.exports=CH});var NC=ce((Zee,MC)=>{var xH=Gv(),AH=B_();function RH(i,o){return i&&xH(o,AH(o),i)}MC.exports=RH});var IC=ce((Qy,Xv)=>{var OH=Yf(),LC=typeof Qy=="object"&&Qy&&!Qy.nodeType&&Qy,FC=LC&&typeof Xv=="object"&&Xv&&!Xv.nodeType&&Xv,kH=FC&&FC.exports===LC,bC=kH?OH.Buffer:void 0,PC=bC?bC.allocUnsafe:void 0;function MH(i,o){if(o)return i.slice();var f=i.length,p=PC?PC(f):new i.constructor(f);return i.copy(p),p}Xv.exports=MH});var UC=ce(($ee,BC)=>{function NH(i,o){var f=-1,p=i.length;for(o||(o=Array(p));++f{function LH(i,o){for(var f=-1,p=i==null?0:i.length,E=0,t=[];++f{function FH(){return[]}qC.exports=FH});var U_=ce((nte,HC)=>{var bH=zC(),PH=yD(),IH=Object.prototype,BH=IH.propertyIsEnumerable,WC=Object.getOwnPropertySymbols,UH=WC?function(i){return i==null?[]:(i=Object(i),bH(WC(i),function(o){return BH.call(i,o)}))}:PH;HC.exports=UH});var GC=ce((rte,VC)=>{var jH=Gv(),zH=U_();function qH(i,o){return jH(i,zH(i),o)}VC.exports=qH});var j_=ce((ite,YC)=>{function HH(i,o){for(var f=-1,p=o.length,E=i.length;++f{var WH=vD(),VH=WH(Object.getPrototypeOf,Object);KC.exports=VH});var gD=ce((ote,XC)=>{var GH=j_(),YH=z_(),KH=U_(),XH=yD(),QH=Object.getOwnPropertySymbols,JH=QH?function(i){for(var o=[];i;)GH(o,KH(i)),i=YH(i);return o}:XH;XC.exports=JH});var JC=ce((lte,QC)=>{var ZH=Gv(),$H=gD();function eW(i,o){return ZH(i,$H(i),o)}QC.exports=eW});var _D=ce((ste,ZC)=>{var tW=j_(),nW=fd();function rW(i,o,f){var p=o(i);return nW(i)?p:tW(p,f(i))}ZC.exports=rW});var e6=ce((ate,$C)=>{var iW=_D(),uW=U_(),oW=I_();function lW(i){return iW(i,oW,uW)}$C.exports=lW});var ED=ce((fte,t6)=>{var sW=_D(),aW=gD(),fW=B_();function cW(i){return sW(i,fW,aW)}t6.exports=cW});var r6=ce((cte,n6)=>{var dW=sd(),pW=Yf(),hW=dW(pW,"DataView");n6.exports=hW});var u6=ce((dte,i6)=>{var vW=sd(),mW=Yf(),yW=vW(mW,"Promise");i6.exports=yW});var l6=ce((pte,o6)=>{var gW=sd(),_W=Yf(),EW=gW(_W,"Set");o6.exports=EW});var a6=ce((hte,s6)=>{var DW=sd(),wW=Yf(),SW=DW(wW,"WeakMap");s6.exports=SW});var q_=ce((vte,f6)=>{var DD=r6(),wD=L_(),SD=u6(),TD=l6(),CD=a6(),c6=Qp(),Qv=uD(),d6="[object Map]",TW="[object Object]",p6="[object Promise]",h6="[object Set]",v6="[object WeakMap]",m6="[object DataView]",CW=Qv(DD),xW=Qv(wD),AW=Qv(SD),RW=Qv(TD),OW=Qv(CD),Jp=c6;(DD&&Jp(new DD(new ArrayBuffer(1)))!=m6||wD&&Jp(new wD)!=d6||SD&&Jp(SD.resolve())!=p6||TD&&Jp(new TD)!=h6||CD&&Jp(new CD)!=v6)&&(Jp=function(i){var o=c6(i),f=o==TW?i.constructor:void 0,p=f?Qv(f):"";if(p)switch(p){case CW:return m6;case xW:return d6;case AW:return p6;case RW:return h6;case OW:return v6}return o});f6.exports=Jp});var g6=ce((mte,y6)=>{var kW=Object.prototype,MW=kW.hasOwnProperty;function NW(i){var o=i.length,f=new i.constructor(o);return o&&typeof i[0]=="string"&&MW.call(i,"index")&&(f.index=i.index,f.input=i.input),f}y6.exports=NW});var E6=ce((yte,_6)=>{var LW=Yf(),FW=LW.Uint8Array;_6.exports=FW});var H_=ce((gte,D6)=>{var w6=E6();function bW(i){var o=new i.constructor(i.byteLength);return new w6(o).set(new w6(i)),o}D6.exports=bW});var T6=ce((_te,S6)=>{var PW=H_();function IW(i,o){var f=o?PW(i.buffer):i.buffer;return new i.constructor(f,i.byteOffset,i.byteLength)}S6.exports=IW});var x6=ce((Ete,C6)=>{var BW=/\w*$/;function UW(i){var o=new i.constructor(i.source,BW.exec(i));return o.lastIndex=i.lastIndex,o}C6.exports=UW});var M6=ce((Dte,A6)=>{var R6=zv(),O6=R6?R6.prototype:void 0,k6=O6?O6.valueOf:void 0;function jW(i){return k6?Object(k6.call(i)):{}}A6.exports=jW});var L6=ce((wte,N6)=>{var zW=H_();function qW(i,o){var f=o?zW(i.buffer):i.buffer;return new i.constructor(f,i.byteOffset,i.length)}N6.exports=qW});var b6=ce((Ste,F6)=>{var HW=H_(),WW=T6(),VW=x6(),GW=M6(),YW=L6(),KW="[object Boolean]",XW="[object Date]",QW="[object Map]",JW="[object Number]",ZW="[object RegExp]",$W="[object Set]",eV="[object String]",tV="[object Symbol]",nV="[object ArrayBuffer]",rV="[object DataView]",iV="[object Float32Array]",uV="[object Float64Array]",oV="[object Int8Array]",lV="[object Int16Array]",sV="[object Int32Array]",aV="[object Uint8Array]",fV="[object Uint8ClampedArray]",cV="[object Uint16Array]",dV="[object Uint32Array]";function pV(i,o,f){var p=i.constructor;switch(o){case nV:return HW(i);case KW:case XW:return new p(+i);case rV:return WW(i,f);case iV:case uV:case oV:case lV:case sV:case aV:case fV:case cV:case dV:return YW(i,f);case QW:return new p;case JW:case eV:return new p(i);case ZW:return VW(i);case $W:return new p;case tV:return GW(i)}}F6.exports=pV});var B6=ce((Tte,P6)=>{var hV=qv(),I6=Object.create,vV=function(){function i(){}return function(o){if(!hV(o))return{};if(I6)return I6(o);i.prototype=o;var f=new i;return i.prototype=void 0,f}}();P6.exports=vV});var j6=ce((Cte,U6)=>{var mV=B6(),yV=z_(),gV=P_();function _V(i){return typeof i.constructor=="function"&&!gV(i)?mV(yV(i)):{}}U6.exports=_V});var q6=ce((xte,z6)=>{var EV=q_(),DV=ad(),wV="[object Map]";function SV(i){return DV(i)&&EV(i)==wV}z6.exports=SV});var G6=ce((Ate,H6)=>{var TV=q6(),CV=F_(),W6=b_(),V6=W6&&W6.isMap,xV=V6?CV(V6):TV;H6.exports=xV});var K6=ce((Rte,Y6)=>{var AV=q_(),RV=ad(),OV="[object Set]";function kV(i){return RV(i)&&AV(i)==OV}Y6.exports=kV});var Z6=ce((Ote,X6)=>{var MV=K6(),NV=F_(),Q6=b_(),J6=Q6&&Q6.isSet,LV=J6?NV(J6):MV;X6.exports=LV});var rx=ce((kte,$6)=>{var FV=PT(),bV=BT(),PV=aD(),IV=CC(),BV=NC(),UV=IC(),jV=UC(),zV=GC(),qV=JC(),HV=e6(),WV=ED(),VV=q_(),GV=g6(),YV=b6(),KV=j6(),XV=fd(),QV=cD(),JV=G6(),ZV=qv(),$V=Z6(),eG=I_(),tG=B_(),nG=1,rG=2,iG=4,ex="[object Arguments]",uG="[object Array]",oG="[object Boolean]",lG="[object Date]",sG="[object Error]",tx="[object Function]",aG="[object GeneratorFunction]",fG="[object Map]",cG="[object Number]",nx="[object Object]",dG="[object RegExp]",pG="[object Set]",hG="[object String]",vG="[object Symbol]",mG="[object WeakMap]",yG="[object ArrayBuffer]",gG="[object DataView]",_G="[object Float32Array]",EG="[object Float64Array]",DG="[object Int8Array]",wG="[object Int16Array]",SG="[object Int32Array]",TG="[object Uint8Array]",CG="[object Uint8ClampedArray]",xG="[object Uint16Array]",AG="[object Uint32Array]",Wu={};Wu[ex]=Wu[uG]=Wu[yG]=Wu[gG]=Wu[oG]=Wu[lG]=Wu[_G]=Wu[EG]=Wu[DG]=Wu[wG]=Wu[SG]=Wu[fG]=Wu[cG]=Wu[nx]=Wu[dG]=Wu[pG]=Wu[hG]=Wu[vG]=Wu[TG]=Wu[CG]=Wu[xG]=Wu[AG]=!0;Wu[sG]=Wu[tx]=Wu[mG]=!1;function W_(i,o,f,p,E,t){var k,L=o&nG,N=o&rG,C=o&iG;if(f&&(k=E?f(i,p,E,t):f(i)),k!==void 0)return k;if(!ZV(i))return i;var U=XV(i);if(U){if(k=GV(i),!L)return jV(i,k)}else{var q=VV(i),W=q==tx||q==aG;if(QV(i))return UV(i,L);if(q==nx||q==ex||W&&!E){if(k=N||W?{}:KV(i),!L)return N?qV(i,BV(k,i)):zV(i,IV(k,i))}else{if(!Wu[q])return E?i:{};k=YV(i,q,L)}}t||(t=new FV);var ne=t.get(i);if(ne)return ne;t.set(i,k),$V(i)?i.forEach(function(Se){k.add(W_(Se,o,f,Se,i,t))}):JV(i)&&i.forEach(function(Se,he){k.set(he,W_(Se,o,f,he,i,t))});var m=C?N?WV:HV:N?tG:eG,we=U?void 0:m(i);return bV(we||i,function(Se,he){we&&(he=Se,Se=i[he]),PV(k,he,W_(Se,o,f,he,i,t))}),k}$6.exports=W_});var V_=ce((Mte,ix)=>{var RG=Qp(),OG=ad(),kG="[object Symbol]";function MG(i){return typeof i=="symbol"||OG(i)&&RG(i)==kG}ix.exports=MG});var ox=ce((Nte,ux)=>{var NG=fd(),LG=V_(),FG=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,bG=/^\w*$/;function PG(i,o){if(NG(i))return!1;var f=typeof i;return f=="number"||f=="symbol"||f=="boolean"||i==null||LG(i)?!0:bG.test(i)||!FG.test(i)||o!=null&&i in Object(o)}ux.exports=PG});var ax=ce((Lte,lx)=>{var sx=oD(),IG="Expected a function";function xD(i,o){if(typeof i!="function"||o!=null&&typeof o!="function")throw new TypeError(IG);var f=function(){var p=arguments,E=o?o.apply(this,p):p[0],t=f.cache;if(t.has(E))return t.get(E);var k=i.apply(this,p);return f.cache=t.set(E,k)||t,k};return f.cache=new(xD.Cache||sx),f}xD.Cache=sx;lx.exports=xD});var cx=ce((Fte,fx)=>{var BG=ax(),UG=500;function jG(i){var o=BG(i,function(p){return f.size===UG&&f.clear(),p}),f=o.cache;return o}fx.exports=jG});var px=ce((bte,dx)=>{var zG=cx(),qG=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,HG=/\\(\\)?/g,WG=zG(function(i){var o=[];return i.charCodeAt(0)===46&&o.push(""),i.replace(qG,function(f,p,E,t){o.push(E?t.replace(HG,"$1"):p||f)}),o});dx.exports=WG});var _x=ce((Pte,hx)=>{var vx=zv(),VG=eD(),GG=fd(),YG=V_(),KG=1/0,mx=vx?vx.prototype:void 0,yx=mx?mx.toString:void 0;function gx(i){if(typeof i=="string")return i;if(GG(i))return VG(i,gx)+"";if(YG(i))return yx?yx.call(i):"";var o=i+"";return o=="0"&&1/i==-KG?"-0":o}hx.exports=gx});var Dx=ce((Ite,Ex)=>{var XG=_x();function QG(i){return i==null?"":XG(i)}Ex.exports=QG});var G_=ce((Bte,wx)=>{var JG=fd(),ZG=ox(),$G=px(),eY=Dx();function tY(i,o){return JG(i)?i:ZG(i,o)?[i]:$G(eY(i))}wx.exports=tY});var Tx=ce((Ute,Sx)=>{function nY(i){var o=i==null?0:i.length;return o?i[o-1]:void 0}Sx.exports=nY});var AD=ce((jte,Cx)=>{var rY=V_(),iY=1/0;function uY(i){if(typeof i=="string"||rY(i))return i;var o=i+"";return o=="0"&&1/i==-iY?"-0":o}Cx.exports=uY});var Ax=ce((zte,xx)=>{var oY=G_(),lY=AD();function sY(i,o){o=oY(o,i);for(var f=0,p=o.length;i!=null&&f{function aY(i,o,f){var p=-1,E=i.length;o<0&&(o=-o>E?0:E+o),f=f>E?E:f,f<0&&(f+=E),E=o>f?0:f-o>>>0,o>>>=0;for(var t=Array(E);++p{var fY=Ax(),cY=Ox();function dY(i,o){return o.length<2?i:fY(i,cY(o,0,-1))}kx.exports=dY});var Lx=ce((Wte,Nx)=>{var pY=G_(),hY=Tx(),vY=Mx(),mY=AD();function yY(i,o){return o=pY(o,i),i=vY(i,o),i==null||delete i[mY(hY(o))]}Nx.exports=yY});var Px=ce((Vte,Fx)=>{var gY=Qp(),_Y=z_(),EY=ad(),DY="[object Object]",wY=Function.prototype,SY=Object.prototype,bx=wY.toString,TY=SY.hasOwnProperty,CY=bx.call(Object);function xY(i){if(!EY(i)||gY(i)!=DY)return!1;var o=_Y(i);if(o===null)return!0;var f=TY.call(o,"constructor")&&o.constructor;return typeof f=="function"&&f instanceof f&&bx.call(f)==CY}Fx.exports=xY});var Bx=ce((Gte,Ix)=>{var AY=Px();function RY(i){return AY(i)?void 0:i}Ix.exports=RY});var qx=ce((Yte,Ux)=>{var jx=zv(),OY=fD(),kY=fd(),zx=jx?jx.isConcatSpreadable:void 0;function MY(i){return kY(i)||OY(i)||!!(zx&&i&&i[zx])}Ux.exports=MY});var Vx=ce((Kte,Hx)=>{var NY=j_(),LY=qx();function Wx(i,o,f,p,E){var t=-1,k=i.length;for(f||(f=LY),E||(E=[]);++t0&&f(L)?o>1?Wx(L,o-1,f,p,E):NY(E,L):p||(E[E.length]=L)}return E}Hx.exports=Wx});var Yx=ce((Xte,Gx)=>{var FY=Vx();function bY(i){var o=i==null?0:i.length;return o?FY(i,1):[]}Gx.exports=bY});var Xx=ce((Qte,Kx)=>{function PY(i,o,f){switch(f.length){case 0:return i.call(o);case 1:return i.call(o,f[0]);case 2:return i.call(o,f[0],f[1]);case 3:return i.call(o,f[0],f[1],f[2])}return i.apply(o,f)}Kx.exports=PY});var Zx=ce((Jte,Qx)=>{var IY=Xx(),Jx=Math.max;function BY(i,o,f){return o=Jx(o===void 0?i.length-1:o,0),function(){for(var p=arguments,E=-1,t=Jx(p.length-o,0),k=Array(t);++E{function UY(i){return function(){return i}}$x.exports=UY});var n5=ce(($te,t5)=>{function jY(i){return i}t5.exports=jY});var u5=ce((ene,r5)=>{var zY=e5(),i5=lD(),qY=n5(),HY=i5?function(i,o){return i5(i,"toString",{configurable:!0,enumerable:!1,value:zY(o),writable:!0})}:qY;r5.exports=HY});var l5=ce((tne,o5)=>{var WY=800,VY=16,GY=Date.now;function YY(i){var o=0,f=0;return function(){var p=GY(),E=VY-(p-f);if(f=p,E>0){if(++o>=WY)return arguments[0]}else o=0;return i.apply(void 0,arguments)}}o5.exports=YY});var a5=ce((nne,s5)=>{var KY=u5(),XY=l5(),QY=XY(KY);s5.exports=QY});var c5=ce((rne,f5)=>{var JY=Yx(),ZY=Zx(),$Y=a5();function eK(i){return $Y(ZY(i,void 0,JY),i+"")}f5.exports=eK});var p5=ce((ine,d5)=>{var tK=eD(),nK=rx(),rK=Lx(),iK=G_(),uK=Gv(),oK=Bx(),lK=c5(),sK=ED(),aK=1,fK=2,cK=4,dK=lK(function(i,o){var f={};if(i==null)return f;var p=!1;o=tK(o,function(t){return t=iK(t,i),p||(p=t.length>1),t}),uK(i,sK(i),f),p&&(f=nK(f,aK|fK|cK,oK));for(var E=o.length;E--;)rK(f,o[E]);return f});d5.exports=dK});var eg=ce((vne,y5)=>{"use strict";var g5=Object.getOwnPropertySymbols,_K=Object.prototype.hasOwnProperty,EK=Object.prototype.propertyIsEnumerable;function DK(i){if(i==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(i)}function wK(){try{if(!Object.assign)return!1;var i=new String("abc");if(i[5]="de",Object.getOwnPropertyNames(i)[0]==="5")return!1;for(var o={},f=0;f<10;f++)o["_"+String.fromCharCode(f)]=f;var p=Object.getOwnPropertyNames(o).map(function(t){return o[t]});if(p.join("")!=="0123456789")return!1;var E={};return"abcdefghijklmnopqrst".split("").forEach(function(t){E[t]=t}),Object.keys(Object.assign({},E)).join("")==="abcdefghijklmnopqrst"}catch(t){return!1}}y5.exports=wK()?Object.assign:function(i,o){for(var f,p=DK(i),E,t=1;t{"use strict";var LD=eg(),Kf=typeof Symbol=="function"&&Symbol.for,tg=Kf?Symbol.for("react.element"):60103,SK=Kf?Symbol.for("react.portal"):60106,TK=Kf?Symbol.for("react.fragment"):60107,CK=Kf?Symbol.for("react.strict_mode"):60108,xK=Kf?Symbol.for("react.profiler"):60114,AK=Kf?Symbol.for("react.provider"):60109,RK=Kf?Symbol.for("react.context"):60110,OK=Kf?Symbol.for("react.forward_ref"):60112,kK=Kf?Symbol.for("react.suspense"):60113,MK=Kf?Symbol.for("react.memo"):60115,NK=Kf?Symbol.for("react.lazy"):60116,_5=typeof Symbol=="function"&&Symbol.iterator;function ng(i){for(var o="https://reactjs.org/docs/error-decoder.html?invariant="+i,f=1;fJ_.length&&J_.push(i)}function BD(i,o,f,p){var E=typeof i;(E==="undefined"||E==="boolean")&&(i=null);var t=!1;if(i===null)t=!0;else switch(E){case"string":case"number":t=!0;break;case"object":switch(i.$$typeof){case tg:case SK:t=!0}}if(t)return f(p,i,o===""?"."+UD(i,0):o),1;if(t=0,o=o===""?".":o+":",Array.isArray(i))for(var k=0;k{"use strict";var BK="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";M5.exports=BK});var HD=ce((gne,L5)=>{"use strict";var qD=function(){};process.env.NODE_ENV!=="production"&&(F5=N5(),Z_={},b5=Function.call.bind(Object.prototype.hasOwnProperty),qD=function(i){var o="Warning: "+i;typeof console!="undefined"&&console.error(o);try{throw new Error(o)}catch(f){}});var F5,Z_,b5;function P5(i,o,f,p,E){if(process.env.NODE_ENV!=="production"){for(var t in i)if(b5(i,t)){var k;try{if(typeof i[t]!="function"){var L=Error((p||"React class")+": "+f+" type `"+t+"` is invalid; it must be a function, usually from the `prop-types` package, but received `"+typeof i[t]+"`.");throw L.name="Invariant Violation",L}k=i[t](o,t,p,f,null,F5)}catch(C){k=C}if(k&&!(k instanceof Error)&&qD((p||"React class")+": type specification of "+f+" `"+t+"` is invalid; the type checker function must return `null` or an `Error` but returned a "+typeof k+". You may have forgotten to pass an argument to the type checker creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and shape all require an argument)."),k instanceof Error&&!(k.message in Z_)){Z_[k.message]=!0;var N=E?E():"";qD("Failed "+f+" type: "+k.message+(N!=null?N:""))}}}}P5.resetWarningCache=function(){process.env.NODE_ENV!=="production"&&(Z_={})};L5.exports=P5});var I5=ce(pu=>{"use strict";process.env.NODE_ENV!=="production"&&function(){"use strict";var i=eg(),o=HD(),f="16.13.1",p=typeof Symbol=="function"&&Symbol.for,E=p?Symbol.for("react.element"):60103,t=p?Symbol.for("react.portal"):60106,k=p?Symbol.for("react.fragment"):60107,L=p?Symbol.for("react.strict_mode"):60108,N=p?Symbol.for("react.profiler"):60114,C=p?Symbol.for("react.provider"):60109,U=p?Symbol.for("react.context"):60110,q=p?Symbol.for("react.concurrent_mode"):60111,W=p?Symbol.for("react.forward_ref"):60112,ne=p?Symbol.for("react.suspense"):60113,m=p?Symbol.for("react.suspense_list"):60120,we=p?Symbol.for("react.memo"):60115,Se=p?Symbol.for("react.lazy"):60116,he=p?Symbol.for("react.block"):60121,ge=p?Symbol.for("react.fundamental"):60117,ze=p?Symbol.for("react.responder"):60118,pe=p?Symbol.for("react.scope"):60119,Oe=typeof Symbol=="function"&&Symbol.iterator,le="@@iterator";function Ue(X){if(X===null||typeof X!="object")return null;var _e=Oe&&X[Oe]||X[le];return typeof _e=="function"?_e:null}var Ge={current:null},rt={suspense:null},wt={current:null},xt=/^(.*)[\\\/]/;function $e(X,_e,Ne){var Me="";if(_e){var dt=_e.fileName,Hn=dt.replace(xt,"");if(/^index\./.test(Hn)){var Dn=dt.match(xt);if(Dn){var or=Dn[1];if(or){var mi=or.replace(xt,"");Hn=mi+"/"+Hn}}}Me=" (at "+Hn+":"+_e.lineNumber+")"}else Ne&&(Me=" (created by "+Ne+")");return` + in `+(X||"Unknown")+Me}var ft=1;function Ke(X){return X._status===ft?X._result:null}function jt(X,_e,Ne){var Me=_e.displayName||_e.name||"";return X.displayName||(Me!==""?Ne+"("+Me+")":Ne)}function $t(X){if(X==null)return null;if(typeof X.tag=="number"&&ct("Received an unexpected object in getComponentName(). This is likely a bug in React. Please file an issue."),typeof X=="function")return X.displayName||X.name||null;if(typeof X=="string")return X;switch(X){case k:return"Fragment";case t:return"Portal";case N:return"Profiler";case L:return"StrictMode";case ne:return"Suspense";case m:return"SuspenseList"}if(typeof X=="object")switch(X.$$typeof){case U:return"Context.Consumer";case C:return"Context.Provider";case W:return jt(X,X.render,"ForwardRef");case we:return $t(X.type);case he:return $t(X.render);case Se:{var _e=X,Ne=Ke(_e);if(Ne)return $t(Ne);break}}return null}var at={},Q=null;function ae(X){Q=X}at.getCurrentStack=null,at.getStackAddendum=function(){var X="";if(Q){var _e=$t(Q.type),Ne=Q._owner;X+=$e(_e,Q._source,Ne&&$t(Ne.type))}var Me=at.getCurrentStack;return Me&&(X+=Me()||""),X};var Ce={current:!1},ue={ReactCurrentDispatcher:Ge,ReactCurrentBatchConfig:rt,ReactCurrentOwner:wt,IsSomeRendererActing:Ce,assign:i};i(ue,{ReactDebugCurrentFrame:at,ReactComponentTreeHook:{}});function je(X){{for(var _e=arguments.length,Ne=new Array(_e>1?_e-1:0),Me=1;Me<_e;Me++)Ne[Me-1]=arguments[Me];At("warn",X,Ne)}}function ct(X){{for(var _e=arguments.length,Ne=new Array(_e>1?_e-1:0),Me=1;Me<_e;Me++)Ne[Me-1]=arguments[Me];At("error",X,Ne)}}function At(X,_e,Ne){{var Me=Ne.length>0&&typeof Ne[Ne.length-1]=="string"&&Ne[Ne.length-1].indexOf(` + in`)===0;if(!Me){var dt=ue.ReactDebugCurrentFrame,Hn=dt.getStackAddendum();Hn!==""&&(_e+="%s",Ne=Ne.concat([Hn]))}var Dn=Ne.map(function(Su){return""+Su});Dn.unshift("Warning: "+_e),Function.prototype.apply.call(console[X],console,Dn);try{var or=0,mi="Warning: "+_e.replace(/%s/g,function(){return Ne[or++]});throw new Error(mi)}catch(Su){}}}var en={};function ln(X,_e){{var Ne=X.constructor,Me=Ne&&(Ne.displayName||Ne.name)||"ReactClass",dt=Me+"."+_e;if(en[dt])return;ct("Can't call %s on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to `this.state` directly or define a `state = {};` class property with the desired state in the %s component.",_e,Me),en[dt]=!0}}var An={isMounted:function(X){return!1},enqueueForceUpdate:function(X,_e,Ne){ln(X,"forceUpdate")},enqueueReplaceState:function(X,_e,Ne,Me){ln(X,"replaceState")},enqueueSetState:function(X,_e,Ne,Me){ln(X,"setState")}},nr={};Object.freeze(nr);function un(X,_e,Ne){this.props=X,this.context=_e,this.refs=nr,this.updater=Ne||An}un.prototype.isReactComponent={},un.prototype.setState=function(X,_e){if(!(typeof X=="object"||typeof X=="function"||X==null))throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,X,_e,"setState")},un.prototype.forceUpdate=function(X){this.updater.enqueueForceUpdate(this,X,"forceUpdate")};{var Wt={isMounted:["isMounted","Instead, make sure to clean up subscriptions and pending requests in componentWillUnmount to prevent memory leaks."],replaceState:["replaceState","Refactor your code to use setState instead (see https://github.com/facebook/react/issues/3236)."]},vr=function(X,_e){Object.defineProperty(un.prototype,X,{get:function(){je("%s(...) is deprecated in plain JavaScript React classes. %s",_e[0],_e[1])}})};for(var w in Wt)Wt.hasOwnProperty(w)&&vr(w,Wt[w])}function Ut(){}Ut.prototype=un.prototype;function Vn(X,_e,Ne){this.props=X,this.context=_e,this.refs=nr,this.updater=Ne||An}var fr=Vn.prototype=new Ut;fr.constructor=Vn,i(fr,un.prototype),fr.isPureReactComponent=!0;function Fr(){var X={current:null};return Object.seal(X),X}var ur=Object.prototype.hasOwnProperty,br={key:!0,ref:!0,__self:!0,__source:!0},Kt,vu,a0;a0={};function So(X){if(ur.call(X,"ref")){var _e=Object.getOwnPropertyDescriptor(X,"ref").get;if(_e&&_e.isReactWarning)return!1}return X.ref!==void 0}function Go(X){if(ur.call(X,"key")){var _e=Object.getOwnPropertyDescriptor(X,"key").get;if(_e&&_e.isReactWarning)return!1}return X.key!==void 0}function Os(X,_e){var Ne=function(){Kt||(Kt=!0,ct("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://fb.me/react-special-props)",_e))};Ne.isReactWarning=!0,Object.defineProperty(X,"key",{get:Ne,configurable:!0})}function Yo(X,_e){var Ne=function(){vu||(vu=!0,ct("%s: `ref` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://fb.me/react-special-props)",_e))};Ne.isReactWarning=!0,Object.defineProperty(X,"ref",{get:Ne,configurable:!0})}function Ko(X){if(typeof X.ref=="string"&&wt.current&&X.__self&&wt.current.stateNode!==X.__self){var _e=$t(wt.current.type);a0[_e]||(ct('Component "%s" contains the string ref "%s". Support for string refs will be removed in a future major release. This case cannot be automatically converted to an arrow function. We ask you to manually fix this case by using useRef() or createRef() instead. Learn more about using refs safely here: https://fb.me/react-strict-mode-string-ref',$t(wt.current.type),X.ref),a0[_e]=!0)}}var qt=function(X,_e,Ne,Me,dt,Hn,Dn){var or={$$typeof:E,type:X,key:_e,ref:Ne,props:Dn,_owner:Hn};return or._store={},Object.defineProperty(or._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:!1}),Object.defineProperty(or,"_self",{configurable:!1,enumerable:!1,writable:!1,value:Me}),Object.defineProperty(or,"_source",{configurable:!1,enumerable:!1,writable:!1,value:dt}),Object.freeze&&(Object.freeze(or.props),Object.freeze(or)),or};function _i(X,_e,Ne){var Me,dt={},Hn=null,Dn=null,or=null,mi=null;if(_e!=null){So(_e)&&(Dn=_e.ref,Ko(_e)),Go(_e)&&(Hn=""+_e.key),or=_e.__self===void 0?null:_e.__self,mi=_e.__source===void 0?null:_e.__source;for(Me in _e)ur.call(_e,Me)&&!br.hasOwnProperty(Me)&&(dt[Me]=_e[Me])}var Su=arguments.length-2;if(Su===1)dt.children=Ne;else if(Su>1){for(var bu=Array(Su),Pu=0;Pu1){for(var mu=Array(Pu),yi=0;yi is not supported and will be removed in a future major release. Did you mean to render instead?")),Ne.Provider},set:function(Dn){Ne.Provider=Dn}},_currentValue:{get:function(){return Ne._currentValue},set:function(Dn){Ne._currentValue=Dn}},_currentValue2:{get:function(){return Ne._currentValue2},set:function(Dn){Ne._currentValue2=Dn}},_threadCount:{get:function(){return Ne._threadCount},set:function(Dn){Ne._threadCount=Dn}},Consumer:{get:function(){return Me||(Me=!0,ct("Rendering is not supported and will be removed in a future major release. Did you mean to render instead?")),Ne.Consumer}}}),Ne.Consumer=Hn}return Ne._currentRenderer=null,Ne._currentRenderer2=null,Ne}function Ht(X){var _e={$$typeof:Se,_ctor:X,_status:-1,_result:null};{var Ne,Me;Object.defineProperties(_e,{defaultProps:{configurable:!0,get:function(){return Ne},set:function(dt){ct("React.lazy(...): It is not supported to assign `defaultProps` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),Ne=dt,Object.defineProperty(_e,"defaultProps",{enumerable:!0})}},propTypes:{configurable:!0,get:function(){return Me},set:function(dt){ct("React.lazy(...): It is not supported to assign `propTypes` to a lazy component import. Either specify them where the component is defined, or create a wrapping component around it."),Me=dt,Object.defineProperty(_e,"propTypes",{enumerable:!0})}}})}return _e}function Du(X){return X!=null&&X.$$typeof===we?ct("forwardRef requires a render function but received a `memo` component. Instead of forwardRef(memo(...)), use memo(forwardRef(...))."):typeof X!="function"?ct("forwardRef requires a render function but was given %s.",X===null?"null":typeof X):X.length!==0&&X.length!==2&&ct("forwardRef render functions accept exactly two parameters: props and ref. %s",X.length===1?"Did you forget to use the ref parameter?":"Any additional parameter will be undefined."),X!=null&&(X.defaultProps!=null||X.propTypes!=null)&&ct("forwardRef render functions do not support propTypes or defaultProps. Did you accidentally pass a React component?"),{$$typeof:W,render:X}}function Yi(X){return typeof X=="string"||typeof X=="function"||X===k||X===q||X===N||X===L||X===ne||X===m||typeof X=="object"&&X!==null&&(X.$$typeof===Se||X.$$typeof===we||X.$$typeof===C||X.$$typeof===U||X.$$typeof===W||X.$$typeof===ge||X.$$typeof===ze||X.$$typeof===pe||X.$$typeof===he)}function Y0(X,_e){return Yi(X)||ct("memo: The first argument must be a component. Instead received: %s",X===null?"null":typeof X),{$$typeof:we,type:X,compare:_e===void 0?null:_e}}function Ui(){var X=Ge.current;if(X===null)throw Error(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: +1. You might have mismatching versions of React and the renderer (such as React DOM) +2. You might be breaking the Rules of Hooks +3. You might have more than one copy of React in the same app +See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.`);return X}function Wl(X,_e){var Ne=Ui();if(_e!==void 0&&ct("useContext() second argument is reserved for future use in React. Passing it is not supported. You passed: %s.%s",_e,typeof _e=="number"&&Array.isArray(arguments[2])?` + +Did you call array.map(useContext)? Calling Hooks inside a loop is not supported. Learn more at https://fb.me/rules-of-hooks`:""),X._context!==void 0){var Me=X._context;Me.Consumer===X?ct("Calling useContext(Context.Consumer) is not supported, may cause bugs, and will be removed in a future major release. Did you mean to call useContext(Context) instead?"):Me.Provider===X&&ct("Calling useContext(Context.Provider) is not supported. Did you mean to call useContext(Context) instead?")}return Ne.useContext(X,_e)}function xo(X){var _e=Ui();return _e.useState(X)}function ni(X,_e,Ne){var Me=Ui();return Me.useReducer(X,_e,Ne)}function oo(X){var _e=Ui();return _e.useRef(X)}function Vl(X,_e){var Ne=Ui();return Ne.useEffect(X,_e)}function Ao(X,_e){var Ne=Ui();return Ne.useLayoutEffect(X,_e)}function Ms(X,_e){var Ne=Ui();return Ne.useCallback(X,_e)}function Xn(X,_e){var Ne=Ui();return Ne.useMemo(X,_e)}function Qo(X,_e,Ne){var Me=Ui();return Me.useImperativeHandle(X,_e,Ne)}function lo(X,_e){{var Ne=Ui();return Ne.useDebugValue(X,_e)}}var b0;b0=!1;function yl(){if(wt.current){var X=$t(wt.current.type);if(X)return` + +Check the render method of \``+X+"`."}return""}function Ro(X){if(X!==void 0){var _e=X.fileName.replace(/^.*[\\\/]/,""),Ne=X.lineNumber;return` + +Check your code at `+_e+":"+Ne+"."}return""}function Et(X){return X!=null?Ro(X.__source):""}var Pt={};function Bn(X){var _e=yl();if(!_e){var Ne=typeof X=="string"?X:X.displayName||X.name;Ne&&(_e=` + +Check the top-level render call using <`+Ne+">.")}return _e}function Ir(X,_e){if(!(!X._store||X._store.validated||X.key!=null)){X._store.validated=!0;var Ne=Bn(_e);if(!Pt[Ne]){Pt[Ne]=!0;var Me="";X&&X._owner&&X._owner!==wt.current&&(Me=" It was passed a child from "+$t(X._owner.type)+"."),ae(X),ct('Each child in a list should have a unique "key" prop.%s%s See https://fb.me/react-warning-keys for more information.',Ne,Me),ae(null)}}}function ji(X,_e){if(typeof X=="object"){if(Array.isArray(X))for(var Ne=0;Ne",dt=" Did you accidentally export a JSX literal instead of a component?"):Dn=typeof X,ct("React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: %s.%s",Dn,dt)}var or=_i.apply(this,arguments);if(or==null)return or;if(Me)for(var mi=2;mi{"use strict";process.env.NODE_ENV==="production"?WD.exports=k5():WD.exports=I5()});var B5=ce((nm,rg)=>{(function(){var i,o="4.17.21",f=200,p="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",E="Expected a function",t="Invalid `variable` option passed into `_.template`",k="__lodash_hash_undefined__",L=500,N="__lodash_placeholder__",C=1,U=2,q=4,W=1,ne=2,m=1,we=2,Se=4,he=8,ge=16,ze=32,pe=64,Oe=128,le=256,Ue=512,Ge=30,rt="...",wt=800,xt=16,$e=1,ft=2,Ke=3,jt=1/0,$t=9007199254740991,at=17976931348623157e292,Q=0/0,ae=4294967295,Ce=ae-1,ue=ae>>>1,je=[["ary",Oe],["bind",m],["bindKey",we],["curry",he],["curryRight",ge],["flip",Ue],["partial",ze],["partialRight",pe],["rearg",le]],ct="[object Arguments]",At="[object Array]",en="[object AsyncFunction]",ln="[object Boolean]",An="[object Date]",nr="[object DOMException]",un="[object Error]",Wt="[object Function]",vr="[object GeneratorFunction]",w="[object Map]",Ut="[object Number]",Vn="[object Null]",fr="[object Object]",Fr="[object Promise]",ur="[object Proxy]",br="[object RegExp]",Kt="[object Set]",vu="[object String]",a0="[object Symbol]",So="[object Undefined]",Go="[object WeakMap]",Os="[object WeakSet]",Yo="[object ArrayBuffer]",Ko="[object DataView]",qt="[object Float32Array]",_i="[object Float64Array]",eu="[object Int8Array]",ai="[object Int16Array]",mr="[object Int32Array]",Xo="[object Uint8Array]",W0="[object Uint8ClampedArray]",Lu="[object Uint16Array]",V0="[object Uint32Array]",Hr=/\b__p \+= '';/g,To=/\b(__p \+=) '' \+/g,Co=/(__e\(.*?\)|\b__t\)) \+\n'';/g,L0=/&(?:amp|lt|gt|quot|#39);/g,tu=/[&<>"']/g,Si=RegExp(L0.source),ks=RegExp(tu.source),Hl=/<%-([\s\S]+?)%>/g,F0=/<%([\s\S]+?)%>/g,f0=/<%=([\s\S]+?)%>/g,Pr=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,Ei=/^\w*$/,G0=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,fi=/[\\^$.*+?()[\]{}|]/g,Zt=RegExp(fi.source),Ln=/^\s+/,Di=/\s/,ci=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,Ht=/\{\n\/\* \[wrapped with (.+)\] \*/,Du=/,? & /,Yi=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,Y0=/[()=,{}\[\]\/\s]/,Ui=/\\(\\)?/g,Wl=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,xo=/\w*$/,ni=/^[-+]0x[0-9a-f]+$/i,oo=/^0b[01]+$/i,Vl=/^\[object .+?Constructor\]$/,Ao=/^0o[0-7]+$/i,Ms=/^(?:0|[1-9]\d*)$/,Xn=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Qo=/($^)/,lo=/['\n\r\u2028\u2029\\]/g,b0="\\ud800-\\udfff",yl="\\u0300-\\u036f",Ro="\\ufe20-\\ufe2f",Et="\\u20d0-\\u20ff",Pt=yl+Ro+Et,Bn="\\u2700-\\u27bf",Ir="a-z\\xdf-\\xf6\\xf8-\\xff",ji="\\xac\\xb1\\xd7\\xf7",Wr="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",wu="\\u2000-\\u206f",c0=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Ti="A-Z\\xc0-\\xd6\\xd8-\\xde",d0="\\ufe0e\\ufe0f",as=ji+Wr+wu+c0,St="['\u2019]",so="["+b0+"]",Jo="["+as+"]",Gl="["+Pt+"]",Fu="\\d+",fs="["+Bn+"]",P0="["+Ir+"]",X="[^"+b0+as+Fu+Bn+Ir+Ti+"]",_e="\\ud83c[\\udffb-\\udfff]",Ne="(?:"+Gl+"|"+_e+")",Me="[^"+b0+"]",dt="(?:\\ud83c[\\udde6-\\uddff]){2}",Hn="[\\ud800-\\udbff][\\udc00-\\udfff]",Dn="["+Ti+"]",or="\\u200d",mi="(?:"+P0+"|"+X+")",Su="(?:"+Dn+"|"+X+")",bu="(?:"+St+"(?:d|ll|m|re|s|t|ve))?",Pu="(?:"+St+"(?:D|LL|M|RE|S|T|VE))?",mu=Ne+"?",yi="["+d0+"]?",Oo="(?:"+or+"(?:"+[Me,dt,Hn].join("|")+")"+yi+mu+")*",Tu="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",ao="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Iu=yi+mu+Oo,Oa="(?:"+[fs,dt,Hn].join("|")+")"+Iu,p0="(?:"+[Me+Gl+"?",Gl,dt,Hn,so].join("|")+")",Zs=RegExp(St,"g"),K0=RegExp(Gl,"g"),$s=RegExp(_e+"(?="+_e+")|"+p0+Iu,"g"),ka=RegExp([Dn+"?"+P0+"+"+bu+"(?="+[Jo,Dn,"$"].join("|")+")",Su+"+"+Pu+"(?="+[Jo,Dn+mi,"$"].join("|")+")",Dn+"?"+mi+"+"+bu,Dn+"+"+Pu,ao,Tu,Fu,Oa].join("|"),"g"),cs=RegExp("["+or+b0+Pt+d0+"]"),w0=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Gn=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],ic=-1,ri={};ri[qt]=ri[_i]=ri[eu]=ri[ai]=ri[mr]=ri[Xo]=ri[W0]=ri[Lu]=ri[V0]=!0,ri[ct]=ri[At]=ri[Yo]=ri[ln]=ri[Ko]=ri[An]=ri[un]=ri[Wt]=ri[w]=ri[Ut]=ri[fr]=ri[br]=ri[Kt]=ri[vu]=ri[Go]=!1;var Gr={};Gr[ct]=Gr[At]=Gr[Yo]=Gr[Ko]=Gr[ln]=Gr[An]=Gr[qt]=Gr[_i]=Gr[eu]=Gr[ai]=Gr[mr]=Gr[w]=Gr[Ut]=Gr[fr]=Gr[br]=Gr[Kt]=Gr[vu]=Gr[a0]=Gr[Xo]=Gr[W0]=Gr[Lu]=Gr[V0]=!0,Gr[un]=Gr[Wt]=Gr[Go]=!1;var Yl={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},ea={"&":"&","<":"<",">":">",'"':""","'":"'"},lf={"&":"&","<":"<",">":">",""":'"',"'":"'"},Ns={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Ma=parseFloat,Ls=parseInt,h0=typeof global=="object"&&global&&global.Object===Object&&global,Fs=typeof self=="object"&&self&&self.Object===Object&&self,Ni=h0||Fs||Function("return this")(),B=typeof nm=="object"&&nm&&!nm.nodeType&&nm,z=B&&typeof rg=="object"&&rg&&!rg.nodeType&&rg,G=z&&z.exports===B,$=G&&h0.process,De=function(){try{var Te=z&&z.require&&z.require("util").types;return Te||$&&$.binding&&$.binding("util")}catch(et){}}(),me=De&&De.isArrayBuffer,xe=De&&De.isDate,Z=De&&De.isMap,ke=De&&De.isRegExp,Xe=De&&De.isSet,ht=De&&De.isTypedArray;function ie(Te,et,Ve){switch(Ve.length){case 0:return Te.call(et);case 1:return Te.call(et,Ve[0]);case 2:return Te.call(et,Ve[0],Ve[1]);case 3:return Te.call(et,Ve[0],Ve[1],Ve[2])}return Te.apply(et,Ve)}function qe(Te,et,Ve,Gt){for(var Yt=-1,sr=Te==null?0:Te.length;++Yt-1}function tn(Te,et,Ve){for(var Gt=-1,Yt=Te==null?0:Te.length;++Gt-1;);return Ve}function gl(Te,et){for(var Ve=Te.length;Ve--&&_t(et,Te[Ve],0)>-1;);return Ve}function af(Te,et){for(var Ve=Te.length,Gt=0;Ve--;)Te[Ve]===et&&++Gt;return Gt}var Mo=Yn(Yl),ds=Yn(ea);function bs(Te){return"\\"+Ns[Te]}function No(Te,et){return Te==null?i:Te[et]}function Lo(Te){return cs.test(Te)}function ps(Te){return w0.test(Te)}function Vu(Te){for(var et,Ve=[];!(et=Te.next()).done;)Ve.push(et.value);return Ve}function yu(Te){var et=-1,Ve=Array(Te.size);return Te.forEach(function(Gt,Yt){Ve[++et]=[Yt,Gt]}),Ve}function pi(Te,et){return function(Ve){return Te(et(Ve))}}function T0(Te,et){for(var Ve=-1,Gt=Te.length,Yt=0,sr=[];++Ve-1}function ia(d,v){var x=this.__data__,b=Ql(x,d);return b<0?(++this.size,x.push([d,v])):x[b][1]=v,this}to.prototype.clear=Na,to.prototype.delete=pf,to.prototype.get=uc,to.prototype.has=ms,to.prototype.set=ia;function B0(d){var v=-1,x=d==null?0:d.length;for(this.clear();++v=v?d:v)),d}function U0(d,v,x,b,H,ee){var de,ye=v&C,be=v&U,gt=v&q;if(x&&(de=H?x(d,b,H,ee):x(d)),de!==i)return de;if(!ku(d))return d;var Dt=Jn(d);if(Dt){if(de=Es(d),!ye)return Ji(d,de)}else{var Rt=Ou(d),rn=Rt==Wt||Rt==vr;if(Gs(d))return fc(d,ye);if(Rt==fr||Rt==ct||rn&&!H){if(de=be||rn?{}:vc(d),!ye)return be?Jl(d,tl(de,d)):t0(d,hf(de,d))}else{if(!Gr[Rt])return H?d:{};de=Dh(d,Rt,ye)}}ee||(ee=new el);var Rn=ee.get(d);if(Rn)return Rn;ee.set(d,de),L2(d)?d.forEach(function(ir){de.add(U0(ir,v,x,ir,d,ee))}):gp(d)&&d.forEach(function(ir,Zr){de.set(Zr,U0(ir,v,x,Zr,d,ee))});var $n=gt?be?rr:$c:be?fn:M0,Nr=Dt?i:$n(d);return tt(Nr||d,function(ir,Zr){Nr&&(Zr=ir,ir=d[Zr]),gs(de,Zr,U0(ir,v,x,Zr,d,ee))}),de}function vf(d){var v=M0(d);return function(x){return jc(x,d,v)}}function jc(d,v,x){var b=x.length;if(d==null)return!b;for(d=wn(d);b--;){var H=x[b],ee=v[H],de=d[H];if(de===i&&!(H in d)||!ee(de))return!1}return!0}function lc(d,v,x){if(typeof d!="function")throw new Kr(E);return Wa(function(){d.apply(i,x)},v)}function Sl(d,v,x,b){var H=-1,ee=on,de=!0,ye=d.length,be=[],gt=v.length;if(!ye)return be;x&&(v=Lt(v,di(x))),b?(ee=tn,de=!1):v.length>=f&&(ee=Zo,de=!1,v=new ho(v));e:for(;++HH?0:H+x),b=b===i||b>H?H:Cr(b),b<0&&(b+=H),b=x>b?0:Ep(b);x0&&x(ye)?v>1?bi(ye,v-1,x,b,H):gn(H,ye):b||(H[H.length]=ye)}return H}var g=dc(),y=dc(!0);function A(d,v){return d&&g(d,v,M0)}function F(d,v){return d&&y(d,v,M0)}function I(d,v){return bt(v,function(x){return Ea(d[x])})}function J(d,v){v=Us(v,d);for(var x=0,b=v.length;d!=null&&xv}function Mt(d,v){return d!=null&&ei.call(d,v)}function Er(d,v){return d!=null&&v in wn(d)}function $u(d,v,x){return d>=Wn(v,x)&&d=120&&Dt.length>=120)?new ho(de&&Dt):i}Dt=d[0];var Rt=-1,rn=ye[0];e:for(;++Rt-1;)ye!==d&&R0.call(ye,be,1),R0.call(d,be,1);return d}function u2(d,v){for(var x=d?v.length:0,b=x-1;x--;){var H=v[x];if(x==b||H!==ee){var ee=H;go(H)?R0.call(d,H,1):Cd(d,H)}}return d}function o2(d,v){return d+vs(y0()*(v-d+1))}function wd(d,v,x,b){for(var H=-1,ee=Xr(Ku((v-d)/(x||1)),0),de=Ve(ee);ee--;)de[b?ee:++H]=d,d+=x;return de}function Hc(d,v){var x="";if(!d||v<1||v>$t)return x;do v%2&&(x+=d),v=vs(v/2),v&&(d+=d);while(v);return x}function Mr(d,v){return r1(Nd(d,v,r0),d+"")}function l2(d){return ba(Ac(d))}function s2(d,v){var x=Ac(d);return yc(x,Zu(v,0,x.length))}function ja(d,v,x,b){if(!ku(d))return d;v=Us(v,d);for(var H=-1,ee=v.length,de=ee-1,ye=d;ye!=null&&++HH?0:H+v),x=x>H?H:x,x<0&&(x+=H),H=v>x?0:x-v>>>0,v>>>=0;for(var ee=Ve(H);++b>>1,de=d[ee];de!==null&&!Nl(de)&&(x?de<=v:de=f){var gt=v?null:mm(d);if(gt)return Q0(gt);de=!1,H=Zo,be=new ho}else be=v?[]:ye;e:for(;++b=b?d:rl(d,v,x)}var Kc=hs||function(d){return Ni.clearTimeout(d)};function fc(d,v){if(v)return d.slice();var x=d.length,b=Fi?Fi(x):new d.constructor(x);return d.copy(b),b}function cc(d){var v=new d.constructor(d.byteLength);return new A0(v).set(new A0(d)),v}function f2(d,v){var x=v?cc(d.buffer):d.buffer;return new d.constructor(x,d.byteOffset,d.byteLength)}function yh(d){var v=new d.constructor(d.source,xo.exec(d));return v.lastIndex=d.lastIndex,v}function gf(d){return Sr?wn(Sr.call(d)):{}}function Xc(d,v){var x=v?cc(d.buffer):d.buffer;return new d.constructor(x,d.byteOffset,d.length)}function gh(d,v){if(d!==v){var x=d!==i,b=d===null,H=d===d,ee=Nl(d),de=v!==i,ye=v===null,be=v===v,gt=Nl(v);if(!ye&&!gt&&!ee&&d>v||ee&&de&&be&&!ye&&!gt||b&&de&&be||!x&&be||!H)return 1;if(!b&&!ee&&!gt&&d=ye)return be;var gt=x[b];return be*(gt=="desc"?-1:1)}}return d.index-v.index}function js(d,v,x,b){for(var H=-1,ee=d.length,de=x.length,ye=-1,be=v.length,gt=Xr(ee-de,0),Dt=Ve(be+gt),Rt=!b;++ye1?x[H-1]:i,de=H>2?x[2]:i;for(ee=d.length>3&&typeof ee=="function"?(H--,ee):i,de&&io(x[0],x[1],de)&&(ee=H<3?i:ee,H=1),v=wn(v);++b-1?H[ee?v[de]:de]:i}}function Jc(d){return ol(function(v){var x=v.length,b=x,H=Ur.prototype.thru;for(d&&v.reverse();b--;){var ee=v[b];if(typeof ee!="function")throw new Kr(E);if(H&&!de&&Bo(ee)=="wrapper")var de=new Ur([],!0)}for(b=de?b:x;++b1&&ui.reverse(),Dt&&beye))return!1;var gt=ee.get(d),Dt=ee.get(v);if(gt&&Dt)return gt==v&&Dt==d;var Rt=-1,rn=!0,Rn=x&ne?new ho:i;for(ee.set(d,v),ee.set(v,d);++Rt1?"& ":"")+v[b],v=v.join(x>2?", ":" "),d.replace(ci,`{ +/* [wrapped with `+v+`] */ +`)}function $l(d){return Jn(d)||sl(d)||!!(co&&d&&d[co])}function go(d,v){var x=typeof d;return v=v==null?$t:v,!!v&&(x=="number"||x!="symbol"&&Ms.test(d))&&d>-1&&d%1==0&&d0){if(++v>=wt)return arguments[0]}else v=0;return d.apply(i,arguments)}}function yc(d,v){var x=-1,b=d.length,H=b-1;for(v=v===i?b:v;++x1?d[v-1]:i;return x=typeof x=="function"?(d.pop(),x):i,E2(d,x)});function Bh(d){var v=Y(d);return v.__chain__=!0,v}function Uh(d,v){return v(d),d}function h1(d,v){return v(d)}var Qd=ol(function(d){var v=d.length,x=v?d[0]:0,b=this.__wrapped__,H=function(ee){return Ia(ee,d)};return v>1||this.__actions__.length||!(b instanceof lt)||!go(x)?this.thru(H):(b=b.slice(x,+x+(v?1:0)),b.__actions__.push({func:h1,args:[H],thisArg:i}),new Ur(b,this.__chain__).thru(function(ee){return v&&!ee.length&&ee.push(i),ee}))});function jh(){return Bh(this)}function Jd(){return new Ur(this.value(),this.__chain__)}function zh(){this.__values__===i&&(this.__values__=lv(this.value()));var d=this.__index__>=this.__values__.length,v=d?i:this.__values__[this.__index__++];return{done:d,value:v}}function Cm(){return this}function xm(d){for(var v,x=this;x instanceof Jr;){var b=Fd(x);b.__index__=0,b.__values__=i,v?H.__wrapped__=b:v=b;var H=b;x=x.__wrapped__}return H.__wrapped__=d,v}function Of(){var d=this.__wrapped__;if(d instanceof lt){var v=d;return this.__actions__.length&&(v=new lt(this)),v=v.reverse(),v.__actions__.push({func:h1,args:[Hd],thisArg:i}),new Ur(v,this.__chain__)}return this.thru(Hd)}function kf(){return mh(this.__wrapped__,this.__actions__)}var D2=za(function(d,v,x){ei.call(d,x)?++d[x]:ju(d,x,1)});function Am(d,v,x){var b=Jn(d)?kt:n2;return x&&io(d,v,x)&&(v=i),b(d,zn(v,3))}function Zd(d,v){var x=Jn(d)?bt:zc;return x(d,zn(v,3))}var w2=xl(Bd),$d=xl(u1);function qh(d,v){return bi(v1(d,v),1)}function ep(d,v){return bi(v1(d,v),jt)}function Hh(d,v,x){return x=x===i?1:Cr(x),bi(v1(d,v),x)}function Wh(d,v){var x=Jn(d)?tt:_s;return x(d,zn(v,3))}function tp(d,v){var x=Jn(d)?Tt:oa;return x(d,zn(v,3))}var Rm=za(function(d,v,x){ei.call(d,x)?d[x].push(v):ju(d,x,[v])});function Om(d,v,x,b){d=al(d)?d:Ac(d),x=x&&!b?Cr(x):0;var H=d.length;return x<0&&(x=Xr(H+x,0)),_1(d)?x<=H&&d.indexOf(v,x)>-1:!!H&&_t(d,v,x)>-1}var km=Mr(function(d,v,x){var b=-1,H=typeof v=="function",ee=al(d)?Ve(d.length):[];return _s(d,function(de){ee[++b]=H?ie(v,de,x):Tl(de,v,x)}),ee}),Vh=za(function(d,v,x){ju(d,x,v)});function v1(d,v){var x=Jn(d)?Lt:Ed;return x(d,zn(v,3))}function Mm(d,v,x,b){return d==null?[]:(Jn(v)||(v=v==null?[]:[v]),x=b?i:x,Jn(x)||(x=x==null?[]:[x]),vo(d,v,x))}var np=za(function(d,v,x){d[x?0:1].push(v)},function(){return[[],[]]});function rp(d,v,x){var b=Jn(d)?lr:yr,H=arguments.length<3;return b(d,zn(v,4),x,H,_s)}function Nm(d,v,x){var b=Jn(d)?Qn:yr,H=arguments.length<3;return b(d,zn(v,4),x,H,oa)}function Lm(d,v){var x=Jn(d)?bt:zc;return x(d,C2(zn(v,3)))}function Gh(d){var v=Jn(d)?ba:l2;return v(d)}function Fm(d,v,x){(x?io(d,v,x):v===i)?v=1:v=Cr(v);var b=Jn(d)?Pa:s2;return b(d,v)}function bm(d){var v=Jn(d)?ua:nl;return v(d)}function ip(d){if(d==null)return 0;if(al(d))return _1(d)?Ki(d):d.length;var v=Ou(d);return v==w||v==Kt?d.size:Ba(d).length}function up(d,v,x){var b=Jn(d)?_r:hh;return x&&io(d,v,x)&&(v=i),b(d,zn(v,3))}var ya=Mr(function(d,v){if(d==null)return[];var x=v.length;return x>1&&io(d,v[0],v[1])?v=[]:x>2&&io(v[0],v[1],v[2])&&(v=[v[0]]),vo(d,bi(v,1),[])}),m1=ra||function(){return Ni.Date.now()};function op(d,v){if(typeof v!="function")throw new Kr(E);return d=Cr(d),function(){if(--d<1)return v.apply(this,arguments)}}function Yh(d,v,x){return v=x?i:v,v=d&&v==null?d.length:v,dn(d,Oe,i,i,i,i,v)}function S2(d,v){var x;if(typeof v!="function")throw new Kr(E);return d=Cr(d),function(){return--d>0&&(x=v.apply(this,arguments)),d<=1&&(v=i),x}}var y1=Mr(function(d,v,x){var b=m;if(x.length){var H=T0(x,dr(y1));b|=ze}return dn(d,b,v,x,H)}),Kh=Mr(function(d,v,x){var b=m|we;if(x.length){var H=T0(x,dr(Kh));b|=ze}return dn(v,b,d,x,H)});function lp(d,v,x){v=x?i:v;var b=dn(d,he,i,i,i,i,i,v);return b.placeholder=lp.placeholder,b}function Xh(d,v,x){v=x?i:v;var b=dn(d,ge,i,i,i,i,i,v);return b.placeholder=Xh.placeholder,b}function sp(d,v,x){var b,H,ee,de,ye,be,gt=0,Dt=!1,Rt=!1,rn=!0;if(typeof d!="function")throw new Kr(E);v=fl(v)||0,ku(x)&&(Dt=!!x.leading,Rt="maxWait"in x,ee=Rt?Xr(fl(x.maxWait)||0,v):ee,rn="trailing"in x?!!x.trailing:rn);function Rn(i0){var Ts=b,wo=H;return b=H=i,gt=i0,de=d.apply(wo,Ts),de}function $n(i0){return gt=i0,ye=Wa(Zr,v),Dt?Rn(i0):de}function Nr(i0){var Ts=i0-be,wo=i0-gt,Rv=v-Ts;return Rt?Wn(Rv,ee-wo):Rv}function ir(i0){var Ts=i0-be,wo=i0-gt;return be===i||Ts>=v||Ts<0||Rt&&wo>=ee}function Zr(){var i0=m1();if(ir(i0))return ui(i0);ye=Wa(Zr,Nr(i0))}function ui(i0){return ye=i,rn&&b?Rn(i0):(b=H=i,de)}function bl(){ye!==i&&Kc(ye),gt=0,b=be=H=ye=i}function Wi(){return ye===i?de:ui(m1())}function uo(){var i0=m1(),Ts=ir(i0);if(b=arguments,H=this,be=i0,Ts){if(ye===i)return $n(be);if(Rt)return Kc(ye),ye=Wa(Zr,v),Rn(be)}return ye===i&&(ye=Wa(Zr,v)),de}return uo.cancel=bl,uo.flush=Wi,uo}var Qh=Mr(function(d,v){return lc(d,1,v)}),Jh=Mr(function(d,v,x){return lc(d,fl(v)||0,x)});function ap(d){return dn(d,Ue)}function T2(d,v){if(typeof d!="function"||v!=null&&typeof v!="function")throw new Kr(E);var x=function(){var b=arguments,H=v?v.apply(this,b):b[0],ee=x.cache;if(ee.has(H))return ee.get(H);var de=d.apply(this,b);return x.cache=ee.set(H,de)||ee,de};return x.cache=new(T2.Cache||B0),x}T2.Cache=B0;function C2(d){if(typeof d!="function")throw new Kr(E);return function(){var v=arguments;switch(v.length){case 0:return!d.call(this);case 1:return!d.call(this,v[0]);case 2:return!d.call(this,v[0],v[1]);case 3:return!d.call(this,v[0],v[1],v[2])}return!d.apply(this,v)}}function z0(d){return S2(2,d)}var x2=Rd(function(d,v){v=v.length==1&&Jn(v[0])?Lt(v[0],di(zn())):Lt(bi(v,1),di(zn()));var x=v.length;return Mr(function(b){for(var H=-1,ee=Wn(b.length,x);++H=v}),sl=e0(function(){return arguments}())?e0:function(d){return zu(d)&&ei.call(d,"callee")&&!I0.call(d,"callee")},Jn=Ve.isArray,Vs=me?di(me):He;function al(d){return d!=null&&M2(d.length)&&!Ea(d)}function n0(d){return zu(d)&&al(d)}function ev(d){return d===!0||d===!1||zu(d)&&mt(d)==ln}var Gs=$0||Ip,hp=xe?di(xe):Be;function jm(d){return zu(d)&&d.nodeType===1&&!Ec(d)}function tv(d){if(d==null)return!0;if(al(d)&&(Jn(d)||typeof d=="string"||typeof d.splice=="function"||Gs(d)||Da(d)||sl(d)))return!d.length;var v=Ou(d);if(v==w||v==Kt)return!d.size;if(xf(d))return!Ba(d).length;for(var x in d)if(ei.call(d,x))return!1;return!0}function vp(d,v){return ut(d,v)}function zm(d,v,x){x=typeof x=="function"?x:i;var b=x?x(d,v):i;return b===i?ut(d,v,i,x):!!b}function mp(d){if(!zu(d))return!1;var v=mt(d);return v==un||v==nr||typeof d.message=="string"&&typeof d.name=="string"&&!Ec(d)}function _c(d){return typeof d=="number"&&Xi(d)}function Ea(d){if(!ku(d))return!1;var v=mt(d);return v==Wt||v==vr||v==en||v==ur}function yp(d){return typeof d=="number"&&d==Cr(d)}function M2(d){return typeof d=="number"&&d>-1&&d%1==0&&d<=$t}function ku(d){var v=typeof d;return d!=null&&(v=="object"||v=="function")}function zu(d){return d!=null&&typeof d=="object"}var gp=Z?di(Z):jn;function _p(d,v){return d===v||ti(d,v,Pn(v))}function nv(d,v,x){return x=typeof x=="function"?x:i,ti(d,v,Pn(v),x)}function qm(d){return rv(d)&&d!=+d}function Hm(d){if(Al(d))throw new Yt(p);return tr(d)}function Wm(d){return d===null}function N2(d){return d==null}function rv(d){return typeof d=="number"||zu(d)&&mt(d)==Ut}function Ec(d){if(!zu(d)||mt(d)!=fr)return!1;var v=$o(d);if(v===null)return!0;var x=ei.call(v,"constructor")&&v.constructor;return typeof x=="function"&&x instanceof x&&Au.call(x)==na}var g1=ke?di(ke):ii;function Vm(d){return yp(d)&&d>=-$t&&d<=$t}var L2=Xe?di(Xe):qi;function _1(d){return typeof d=="string"||!Jn(d)&&zu(d)&&mt(d)==vu}function Nl(d){return typeof d=="symbol"||zu(d)&&mt(d)==a0}var Da=ht?di(ht):jr;function iv(d){return d===i}function Gm(d){return zu(d)&&Ou(d)==Go}function uv(d){return zu(d)&&mt(d)==Os}var ov=p2(r2),Ym=p2(function(d,v){return d<=v});function lv(d){if(!d)return[];if(al(d))return _1(d)?Yr(d):Ji(d);if(Ru&&d[Ru])return Vu(d[Ru]());var v=Ou(d),x=v==w?yu:v==Kt?Q0:Ac;return x(d)}function wa(d){if(!d)return d===0?d:0;if(d=fl(d),d===jt||d===-jt){var v=d<0?-1:1;return v*at}return d===d?d:0}function Cr(d){var v=wa(d),x=v%1;return v===v?x?v-x:v:0}function Ep(d){return d?Zu(Cr(d),0,ae):0}function fl(d){if(typeof d=="number")return d;if(Nl(d))return Q;if(ku(d)){var v=typeof d.valueOf=="function"?d.valueOf():d;d=ku(v)?v+"":v}if(typeof d!="string")return d===0?d:+d;d=xu(d);var x=oo.test(d);return x||Ao.test(d)?Ls(d.slice(2),x?2:8):ni.test(d)?Q:+d}function cu(d){return O0(d,fn(d))}function E1(d){return d?Zu(Cr(d),-$t,$t):d===0?d:0}function ki(d){return d==null?"":il(d)}var Dp=no(function(d,v){if(xf(v)||al(v)){O0(v,M0(v),d);return}for(var x in v)ei.call(v,x)&&gs(d,x,v[x])}),F2=no(function(d,v){O0(v,fn(v),d)}),Do=no(function(d,v,x,b){O0(v,fn(v),d,b)}),Ss=no(function(d,v,x,b){O0(v,M0(v),d,b)}),Mf=ol(Ia);function b2(d,v){var x=Qr(d);return v==null?x:hf(x,v)}var wp=Mr(function(d,v){d=wn(d);var x=-1,b=v.length,H=b>2?v[2]:i;for(H&&io(v[0],v[1],H)&&(b=1);++x1),ee}),O0(d,rr(d),x),b&&(x=U0(x,C|U|q,ym));for(var H=v.length;H--;)Cd(x,v[H]);return x});function T1(d,v){return Ka(d,C2(zn(v)))}var Cp=ol(function(d,v){return d==null?{}:dh(d,v)});function Ka(d,v){if(d==null)return{};var x=Lt(rr(d),function(b){return[b]});return v=zn(v),ph(d,x,function(b,H){return v(b,H[0])})}function Km(d,v,x){v=Us(v,d);var b=-1,H=v.length;for(H||(H=1,d=i);++bv){var b=d;d=v,v=b}if(x||d%1||v%1){var H=y0();return Wn(d+H*(v-d+Ma("1e-"+((H+"").length-1))),v)}return o2(d,v)}var q2=_f(function(d,v,x){return v=v.toLowerCase(),d+(x?Uo(v):v)});function Uo(d){return Rp(ki(d).toLowerCase())}function H2(d){return d=ki(d),d&&d.replace(Xn,Mo).replace(K0,"")}function Qm(d,v,x){d=ki(d),v=il(v);var b=d.length;x=x===i?b:Zu(Cr(x),0,b);var H=x;return x-=v.length,x>=0&&d.slice(x,H)==v}function A1(d){return d=ki(d),d&&ks.test(d)?d.replace(tu,ds):d}function Jm(d){return d=ki(d),d&&Zt.test(d)?d.replace(fi,"\\$&"):d}var Zm=_f(function(d,v,x){return d+(x?"-":"")+v.toLowerCase()}),av=_f(function(d,v,x){return d+(x?" ":"")+v.toLowerCase()}),$m=_h("toLowerCase");function fv(d,v,x){d=ki(d),v=Cr(v);var b=v?Ki(d):0;if(!v||b>=v)return d;var H=(v-b)/2;return da(vs(H),x)+d+da(Ku(H),x)}function ey(d,v,x){d=ki(d),v=Cr(v);var b=v?Ki(d):0;return v&&b>>0,x?(d=ki(d),d&&(typeof v=="string"||v!=null&&!g1(v))&&(v=il(v),!v&&Lo(d))?aa(Yr(d),0,x):d.split(v,x)):[]}var bf=_f(function(d,v,x){return d+(x?" ":"")+Rp(v)});function dv(d,v,x){return d=ki(d),x=x==null?0:Zu(Cr(x),0,d.length),v=il(v),d.slice(x,x+v.length)==v}function pv(d,v,x){var b=Y.templateSettings;x&&io(d,v,x)&&(v=i),d=ki(d),v=Do({},v,b,Df);var H=Do({},v.imports,b.imports,Df),ee=M0(H),de=ko(H,ee),ye,be,gt=0,Dt=v.interpolate||Qo,Rt="__p += '",rn=fu((v.escape||Qo).source+"|"+Dt.source+"|"+(Dt===f0?Wl:Qo).source+"|"+(v.evaluate||Qo).source+"|$","g"),Rn="//# sourceURL="+(ei.call(v,"sourceURL")?(v.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++ic+"]")+` +`;d.replace(rn,function(ir,Zr,ui,bl,Wi,uo){return ui||(ui=bl),Rt+=d.slice(gt,uo).replace(lo,bs),Zr&&(ye=!0,Rt+=`' + +__e(`+Zr+`) + +'`),Wi&&(be=!0,Rt+=`'; +`+Wi+`; +__p += '`),ui&&(Rt+=`' + +((__t = (`+ui+`)) == null ? '' : __t) + +'`),gt=uo+ir.length,ir}),Rt+=`'; +`;var $n=ei.call(v,"variable")&&v.variable;if(!$n)Rt=`with (obj) { +`+Rt+` +} +`;else if(Y0.test($n))throw new Yt(t);Rt=(be?Rt.replace(Hr,""):Rt).replace(To,"$1").replace(Co,"$1;"),Rt="function("+($n||"obj")+`) { +`+($n?"":`obj || (obj = {}); +`)+"var __t, __p = ''"+(ye?", __e = _.escape":"")+(be?`, __j = Array.prototype.join; +function print() { __p += __j.call(arguments, '') } +`:`; +`)+Rt+`return __p +}`;var Nr=_v(function(){return sr(ee,Rn+"return "+Rt).apply(i,de)});if(Nr.source=Rt,mp(Nr))throw Nr;return Nr}function hv(d){return ki(d).toLowerCase()}function W2(d){return ki(d).toUpperCase()}function V2(d,v,x){if(d=ki(d),d&&(x||v===i))return xu(d);if(!d||!(v=il(v)))return d;var b=Yr(d),H=Yr(v),ee=sf(b,H),de=gl(b,H)+1;return aa(b,ee,de).join("")}function Ap(d,v,x){if(d=ki(d),d&&(x||v===i))return d.slice(0,fo(d)+1);if(!d||!(v=il(v)))return d;var b=Yr(d),H=gl(b,Yr(v))+1;return aa(b,0,H).join("")}function vv(d,v,x){if(d=ki(d),d&&(x||v===i))return d.replace(Ln,"");if(!d||!(v=il(v)))return d;var b=Yr(d),H=sf(b,Yr(v));return aa(b,H).join("")}function G2(d,v){var x=Ge,b=rt;if(ku(v)){var H="separator"in v?v.separator:H;x="length"in v?Cr(v.length):x,b="omission"in v?il(v.omission):b}d=ki(d);var ee=d.length;if(Lo(d)){var de=Yr(d);ee=de.length}if(x>=ee)return d;var ye=x-Ki(b);if(ye<1)return b;var be=de?aa(de,0,ye).join(""):d.slice(0,ye);if(H===i)return be+b;if(de&&(ye+=be.length-ye),g1(H)){if(d.slice(ye).search(H)){var gt,Dt=be;for(H.global||(H=fu(H.source,ki(xo.exec(H))+"g")),H.lastIndex=0;gt=H.exec(Dt);)var Rt=gt.index;be=be.slice(0,Rt===i?ye:Rt)}}else if(d.indexOf(il(H),ye)!=ye){var rn=be.lastIndexOf(H);rn>-1&&(be=be.slice(0,rn))}return be+b}function mv(d){return d=ki(d),d&&Si.test(d)?d.replace(L0,Oi):d}var yv=_f(function(d,v,x){return d+(x?" ":"")+v.toUpperCase()}),Rp=_h("toUpperCase");function gv(d,v,x){return d=ki(d),v=x?i:v,v===i?ps(d)?cf(d):v0(d):d.match(v)||[]}var _v=Mr(function(d,v){try{return ie(d,i,v)}catch(x){return mp(x)?x:new Yt(x)}}),uy=ol(function(d,v){return tt(v,function(x){x=Rl(x),ju(d,x,y1(d[x],d))}),d});function Ev(d){var v=d==null?0:d.length,x=zn();return d=v?Lt(d,function(b){if(typeof b[1]!="function")throw new Kr(E);return[x(b[0]),b[1]]}):[],Mr(function(b){for(var H=-1;++H$t)return[];var x=ae,b=Wn(d,ae);v=zn(v),d-=ae;for(var H=S0(b,v);++x0||v<0)?new lt(x):(d<0?x=x.takeRight(-d):d&&(x=x.drop(d)),v!==i&&(v=Cr(v),x=v<0?x.dropRight(-v):x.take(v-d)),x)},lt.prototype.takeRightWhile=function(d){return this.reverse().takeWhile(d).reverse()},lt.prototype.toArray=function(){return this.take(ae)},A(lt.prototype,function(d,v){var x=/^(?:filter|find|map|reject)|While$/.test(v),b=/^(?:head|last)$/.test(v),H=Y[b?"take"+(v=="last"?"Right":""):v],ee=b||/^find/.test(v);!H||(Y.prototype[v]=function(){var de=this.__wrapped__,ye=b?[1]:arguments,be=de instanceof lt,gt=ye[0],Dt=be||Jn(de),Rt=function(Zr){var ui=H.apply(Y,gn([Zr],ye));return b&&rn?ui[0]:ui};Dt&&x&&typeof gt=="function"&>.length!=1&&(be=Dt=!1);var rn=this.__chain__,Rn=!!this.__actions__.length,$n=ee&&!rn,Nr=be&&!Rn;if(!ee&&Dt){de=Nr?de:new lt(this);var ir=d.apply(de,ye);return ir.__actions__.push({func:h1,args:[Rt],thisArg:i}),new Ur(ir,rn)}return $n&&Nr?d.apply(this,ye):(ir=this.thru(Rt),$n?b?ir.value()[0]:ir.value():ir)})}),tt(["pop","push","shift","sort","splice","unshift"],function(d){var v=Vr[d],x=/^(?:push|sort|unshift)$/.test(d)?"tap":"thru",b=/^(?:pop|shift)$/.test(d);Y.prototype[d]=function(){var H=arguments;if(b&&!this.__chain__){var ee=this.value();return v.apply(Jn(ee)?ee:[],H)}return this[x](function(de){return v.apply(Jn(de)?de:[],H)})}}),A(lt.prototype,function(d,v){var x=Y[v];if(x){var b=x.name+"";ei.call(xn,b)||(xn[b]=[]),xn[b].push({name:v,func:x})}}),xn[ca(i,we).name]=[{name:"wrapper",func:i}],lt.prototype.clone=hi,lt.prototype.reverse=Qi,lt.prototype.value=g0,Y.prototype.at=Qd,Y.prototype.chain=jh,Y.prototype.commit=Jd,Y.prototype.next=zh,Y.prototype.plant=xm,Y.prototype.reverse=Of,Y.prototype.toJSON=Y.prototype.valueOf=Y.prototype.value=kf,Y.prototype.first=Y.prototype.head,Ru&&(Y.prototype[Ru]=Cm),Y},Z0=J0();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Ni._=Z0,define(function(){return Z0})):z?((z.exports=Z0)._=Z0,B._=Z0):Ni._=Z0}).call(nm)});var GD=ce((Dne,VD)=>{"use strict";var Ai=VD.exports;VD.exports.default=Ai;var hu="[",ig="]",rm="\x07",$_=";",U5=process.env.TERM_PROGRAM==="Apple_Terminal";Ai.cursorTo=(i,o)=>{if(typeof i!="number")throw new TypeError("The `x` argument is required");return typeof o!="number"?hu+(i+1)+"G":hu+(o+1)+";"+(i+1)+"H"};Ai.cursorMove=(i,o)=>{if(typeof i!="number")throw new TypeError("The `x` argument is required");let f="";return i<0?f+=hu+-i+"D":i>0&&(f+=hu+i+"C"),o<0?f+=hu+-o+"A":o>0&&(f+=hu+o+"B"),f};Ai.cursorUp=(i=1)=>hu+i+"A";Ai.cursorDown=(i=1)=>hu+i+"B";Ai.cursorForward=(i=1)=>hu+i+"C";Ai.cursorBackward=(i=1)=>hu+i+"D";Ai.cursorLeft=hu+"G";Ai.cursorSavePosition=U5?"7":hu+"s";Ai.cursorRestorePosition=U5?"8":hu+"u";Ai.cursorGetPosition=hu+"6n";Ai.cursorNextLine=hu+"E";Ai.cursorPrevLine=hu+"F";Ai.cursorHide=hu+"?25l";Ai.cursorShow=hu+"?25h";Ai.eraseLines=i=>{let o="";for(let f=0;f[ig,"8",$_,$_,o,rm,i,ig,"8",$_,$_,rm].join("");Ai.image=(i,o={})=>{let f=`${ig}1337;File=inline=1`;return o.width&&(f+=`;width=${o.width}`),o.height&&(f+=`;height=${o.height}`),o.preserveAspectRatio===!1&&(f+=";preserveAspectRatio=0"),f+":"+i.toString("base64")+rm};Ai.iTerm={setCwd:(i=process.cwd())=>`${ig}50;CurrentDir=${i}${rm}`,annotation:(i,o={})=>{let f=`${ig}1337;`,p=typeof o.x!="undefined",E=typeof o.y!="undefined";if((p||E)&&!(p&&E&&typeof o.length!="undefined"))throw new Error("`x`, `y` and `length` must be defined when `x` or `y` is defined");return i=i.replace(/\|/g,""),f+=o.isHidden?"AddHiddenAnnotation=":"AddAnnotation=",o.length>0?f+=(p?[i,o.length,o.x,o.y]:[o.length,i]).join("|"):f+=i,f+rm}}});var z5=ce((wne,YD)=>{"use strict";var j5=(i,o)=>{for(let f of Reflect.ownKeys(o))Object.defineProperty(i,f,Object.getOwnPropertyDescriptor(o,f));return i};YD.exports=j5;YD.exports.default=j5});var H5=ce((Sne,e4)=>{"use strict";var UK=z5(),t4=new WeakMap,q5=(i,o={})=>{if(typeof i!="function")throw new TypeError("Expected a function");let f,p=0,E=i.displayName||i.name||"",t=function(...k){if(t4.set(t,++p),p===1)f=i.apply(this,k),i=null;else if(o.throw===!0)throw new Error(`Function \`${E}\` can only be called once`);return f};return UK(t,i),t4.set(t,p),t};e4.exports=q5;e4.exports.default=q5;e4.exports.callCount=i=>{if(!t4.has(i))throw new Error(`The given function \`${i.name}\` is not wrapped by the \`onetime\` package`);return t4.get(i)}});var W5=ce((Tne,n4)=>{n4.exports=["SIGABRT","SIGALRM","SIGHUP","SIGINT","SIGTERM"];process.platform!=="win32"&&n4.exports.push("SIGVTALRM","SIGXCPU","SIGXFSZ","SIGUSR2","SIGTRAP","SIGSYS","SIGQUIT","SIGIOT");process.platform==="linux"&&n4.exports.push("SIGIO","SIGPOLL","SIGPWR","SIGSTKFLT","SIGUNUSED")});var JD=ce((Cne,ug)=>{var jK=require("assert"),og=W5(),zK=/^win/i.test(process.platform),r4=require("events");typeof r4!="function"&&(r4=r4.EventEmitter);var zl;process.__signal_exit_emitter__?zl=process.__signal_exit_emitter__:(zl=process.__signal_exit_emitter__=new r4,zl.count=0,zl.emitted={});zl.infinite||(zl.setMaxListeners(Infinity),zl.infinite=!0);ug.exports=function(i,o){jK.equal(typeof i,"function","a callback must be provided for exit handler"),lg===!1&&V5();var f="exit";o&&o.alwaysLast&&(f="afterexit");var p=function(){zl.removeListener(f,i),zl.listeners("exit").length===0&&zl.listeners("afterexit").length===0&&KD()};return zl.on(f,i),p};ug.exports.unload=KD;function KD(){!lg||(lg=!1,og.forEach(function(i){try{process.removeListener(i,XD[i])}catch(o){}}),process.emit=QD,process.reallyExit=G5,zl.count-=1)}function im(i,o,f){zl.emitted[i]||(zl.emitted[i]=!0,zl.emit(i,o,f))}var XD={};og.forEach(function(i){XD[i]=function(){var f=process.listeners(i);f.length===zl.count&&(KD(),im("exit",null,i),im("afterexit",null,i),zK&&i==="SIGHUP"&&(i="SIGINT"),process.kill(process.pid,i))}});ug.exports.signals=function(){return og};ug.exports.load=V5;var lg=!1;function V5(){lg||(lg=!0,zl.count+=1,og=og.filter(function(i){try{return process.on(i,XD[i]),!0}catch(o){return!1}}),process.emit=HK,process.reallyExit=qK)}var G5=process.reallyExit;function qK(i){process.exitCode=i||0,im("exit",process.exitCode,null),im("afterexit",process.exitCode,null),G5.call(process,process.exitCode)}var QD=process.emit;function HK(i,o){if(i==="exit"){o!==void 0&&(process.exitCode=o);var f=QD.apply(this,arguments);return im("exit",process.exitCode,null),im("afterexit",process.exitCode,null),f}else return QD.apply(this,arguments)}});var K5=ce((xne,Y5)=>{"use strict";var WK=H5(),VK=JD();Y5.exports=WK(()=>{VK(()=>{process.stderr.write("[?25h")},{alwaysLast:!0})})});var ZD=ce(um=>{"use strict";var GK=K5(),i4=!1;um.show=(i=process.stderr)=>{!i.isTTY||(i4=!1,i.write("[?25h"))};um.hide=(i=process.stderr)=>{!i.isTTY||(GK(),i4=!0,i.write("[?25l"))};um.toggle=(i,o)=>{i!==void 0&&(i4=i),i4?um.show(o):um.hide(o)}});var Z5=ce(sg=>{"use strict";var X5=sg&&sg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(sg,"__esModule",{value:!0});var Q5=X5(GD()),J5=X5(ZD()),YK=(i,{showCursor:o=!1}={})=>{let f=0,p="",E=!1,t=k=>{!o&&!E&&(J5.default.hide(),E=!0);let L=k+` +`;L!==p&&(p=L,i.write(Q5.default.eraseLines(f)+L),f=L.split(` +`).length)};return t.clear=()=>{i.write(Q5.default.eraseLines(f)),p="",f=0},t.done=()=>{p="",f=0,o||(J5.default.show(),E=!1)},t};sg.default={create:YK}});var e9=ce((One,$5)=>{$5.exports=[{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY_BUILD_BASE",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}}]});var r9=ce(Ra=>{"use strict";var t9=e9(),bc=process.env;Object.defineProperty(Ra,"_vendors",{value:t9.map(function(i){return i.constant})});Ra.name=null;Ra.isPR=null;t9.forEach(function(i){var o=Array.isArray(i.env)?i.env:[i.env],f=o.every(function(p){return n9(p)});if(Ra[i.constant]=f,f)switch(Ra.name=i.name,typeof i.pr){case"string":Ra.isPR=!!bc[i.pr];break;case"object":"env"in i.pr?Ra.isPR=i.pr.env in bc&&bc[i.pr.env]!==i.pr.ne:"any"in i.pr?Ra.isPR=i.pr.any.some(function(p){return!!bc[p]}):Ra.isPR=n9(i.pr);break;default:Ra.isPR=null}});Ra.isCI=!!(bc.CI||bc.CONTINUOUS_INTEGRATION||bc.BUILD_NUMBER||bc.RUN_ID||Ra.name);function n9(i){return typeof i=="string"?!!bc[i]:Object.keys(i).every(function(o){return bc[o]===i[o]})}});var u9=ce((Mne,i9)=>{"use strict";i9.exports=r9().isCI});var l9=ce((Nne,o9)=>{"use strict";var KK=i=>{let o=new Set;do for(let f of Reflect.ownKeys(i))o.add([i,f]);while((i=Reflect.getPrototypeOf(i))&&i!==Object.prototype);return o};o9.exports=(i,{include:o,exclude:f}={})=>{let p=E=>{let t=k=>typeof k=="string"?E===k:k.test(E);return o?o.some(t):f?!f.some(t):!0};for(let[E,t]of KK(i.constructor.prototype)){if(t==="constructor"||!p(t))continue;let k=Reflect.getOwnPropertyDescriptor(E,t);k&&typeof k.value=="function"&&(i[t]=i[t].bind(i))}return i}});var h9=ce($i=>{"use strict";Object.defineProperty($i,"__esModule",{value:!0});var om,ag,u4,o4,$D;typeof window=="undefined"||typeof MessageChannel!="function"?(lm=null,ew=null,tw=function(){if(lm!==null)try{var i=$i.unstable_now();lm(!0,i),lm=null}catch(o){throw setTimeout(tw,0),o}},s9=Date.now(),$i.unstable_now=function(){return Date.now()-s9},om=function(i){lm!==null?setTimeout(om,0,i):(lm=i,setTimeout(tw,0))},ag=function(i,o){ew=setTimeout(i,o)},u4=function(){clearTimeout(ew)},o4=function(){return!1},$D=$i.unstable_forceFrameRate=function(){}):(l4=window.performance,nw=window.Date,a9=window.setTimeout,f9=window.clearTimeout,typeof console!="undefined"&&(c9=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),typeof c9!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills")),typeof l4=="object"&&typeof l4.now=="function"?$i.unstable_now=function(){return l4.now()}:(d9=nw.now(),$i.unstable_now=function(){return nw.now()-d9}),fg=!1,cg=null,s4=-1,rw=5,iw=0,o4=function(){return $i.unstable_now()>=iw},$D=function(){},$i.unstable_forceFrameRate=function(i){0>i||125f4(k,f))N!==void 0&&0>f4(N,k)?(i[p]=N,i[L]=f,p=L):(i[p]=k,i[t]=f,p=t);else if(N!==void 0&&0>f4(N,f))i[p]=N,i[L]=f,p=L;else break e}}return o}return null}function f4(i,o){var f=i.sortIndex-o.sortIndex;return f!==0?f:i.id-o.id}var Xf=[],dd=[],XK=1,Rs=null,ls=3,d4=!1,$p=!1,dg=!1;function p4(i){for(var o=uf(dd);o!==null;){if(o.callback===null)c4(dd);else if(o.startTime<=i)c4(dd),o.sortIndex=o.expirationTime,ow(Xf,o);else break;o=uf(dd)}}function lw(i){if(dg=!1,p4(i),!$p)if(uf(Xf)!==null)$p=!0,om(sw);else{var o=uf(dd);o!==null&&ag(lw,o.startTime-i)}}function sw(i,o){$p=!1,dg&&(dg=!1,u4()),d4=!0;var f=ls;try{for(p4(o),Rs=uf(Xf);Rs!==null&&(!(Rs.expirationTime>o)||i&&!o4());){var p=Rs.callback;if(p!==null){Rs.callback=null,ls=Rs.priorityLevel;var E=p(Rs.expirationTime<=o);o=$i.unstable_now(),typeof E=="function"?Rs.callback=E:Rs===uf(Xf)&&c4(Xf),p4(o)}else c4(Xf);Rs=uf(Xf)}if(Rs!==null)var t=!0;else{var k=uf(dd);k!==null&&ag(lw,k.startTime-o),t=!1}return t}finally{Rs=null,ls=f,d4=!1}}function p9(i){switch(i){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1e4;default:return 5e3}}var QK=$D;$i.unstable_ImmediatePriority=1;$i.unstable_UserBlockingPriority=2;$i.unstable_NormalPriority=3;$i.unstable_IdlePriority=5;$i.unstable_LowPriority=4;$i.unstable_runWithPriority=function(i,o){switch(i){case 1:case 2:case 3:case 4:case 5:break;default:i=3}var f=ls;ls=i;try{return o()}finally{ls=f}};$i.unstable_next=function(i){switch(ls){case 1:case 2:case 3:var o=3;break;default:o=ls}var f=ls;ls=o;try{return i()}finally{ls=f}};$i.unstable_scheduleCallback=function(i,o,f){var p=$i.unstable_now();if(typeof f=="object"&&f!==null){var E=f.delay;E=typeof E=="number"&&0p?(i.sortIndex=E,ow(dd,i),uf(Xf)===null&&i===uf(dd)&&(dg?u4():dg=!0,ag(lw,E-p))):(i.sortIndex=f,ow(Xf,i),$p||d4||($p=!0,om(sw))),i};$i.unstable_cancelCallback=function(i){i.callback=null};$i.unstable_wrapCallback=function(i){var o=ls;return function(){var f=ls;ls=o;try{return i.apply(this,arguments)}finally{ls=f}}};$i.unstable_getCurrentPriorityLevel=function(){return ls};$i.unstable_shouldYield=function(){var i=$i.unstable_now();p4(i);var o=uf(Xf);return o!==Rs&&Rs!==null&&o!==null&&o.callback!==null&&o.startTime<=i&&o.expirationTime{"use strict";process.env.NODE_ENV!=="production"&&function(){"use strict";Object.defineProperty(Ri,"__esModule",{value:!0});var i=!1,o=!1,f=!0,p,E,t,k,L;if(typeof window=="undefined"||typeof MessageChannel!="function"){var N=null,C=null,U=function(){if(N!==null)try{var Et=Ri.unstable_now(),Pt=!0;N(Pt,Et),N=null}catch(Bn){throw setTimeout(U,0),Bn}},q=Date.now();Ri.unstable_now=function(){return Date.now()-q},p=function(Et){N!==null?setTimeout(p,0,Et):(N=Et,setTimeout(U,0))},E=function(Et,Pt){C=setTimeout(Et,Pt)},t=function(){clearTimeout(C)},k=function(){return!1},L=Ri.unstable_forceFrameRate=function(){}}else{var W=window.performance,ne=window.Date,m=window.setTimeout,we=window.clearTimeout;if(typeof console!="undefined"){var Se=window.requestAnimationFrame,he=window.cancelAnimationFrame;typeof Se!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills"),typeof he!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://fb.me/react-polyfills")}if(typeof W=="object"&&typeof W.now=="function")Ri.unstable_now=function(){return W.now()};else{var ge=ne.now();Ri.unstable_now=function(){return ne.now()-ge}}var ze=!1,pe=null,Oe=-1,le=5,Ue=0,Ge=300,rt=!1;if(o&&navigator!==void 0&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0){var wt=navigator.scheduling;k=function(){var Et=Ri.unstable_now();return Et>=Ue?rt||wt.isInputPending()?!0:Et>=Ge:!1},L=function(){rt=!0}}else k=function(){return Ri.unstable_now()>=Ue},L=function(){};Ri.unstable_forceFrameRate=function(Et){if(Et<0||Et>125){console.error("forceFrameRate takes a positive int between 0 and 125, forcing framerates higher than 125 fps is not unsupported");return}Et>0?le=Math.floor(1e3/Et):le=5};var xt=function(){if(pe!==null){var Et=Ri.unstable_now();Ue=Et+le;var Pt=!0;try{var Bn=pe(Pt,Et);Bn?ft.postMessage(null):(ze=!1,pe=null)}catch(Ir){throw ft.postMessage(null),Ir}}else ze=!1;rt=!1},$e=new MessageChannel,ft=$e.port2;$e.port1.onmessage=xt,p=function(Et){pe=Et,ze||(ze=!0,ft.postMessage(null))},E=function(Et,Pt){Oe=m(function(){Et(Ri.unstable_now())},Pt)},t=function(){we(Oe),Oe=-1}}function Ke(Et,Pt){var Bn=Et.length;Et.push(Pt),at(Et,Pt,Bn)}function jt(Et){var Pt=Et[0];return Pt===void 0?null:Pt}function $t(Et){var Pt=Et[0];if(Pt!==void 0){var Bn=Et.pop();return Bn!==Pt&&(Et[0]=Bn,Q(Et,Bn,0)),Pt}else return null}function at(Et,Pt,Bn){for(var Ir=Bn;;){var ji=Math.floor((Ir-1)/2),Wr=Et[ji];if(Wr!==void 0&&ae(Wr,Pt)>0)Et[ji]=Pt,Et[Ir]=Wr,Ir=ji;else return}}function Q(Et,Pt,Bn){for(var Ir=Bn,ji=Et.length;Irur){if(ur*=2,ur>Fr){console.error("Scheduler Profiling: Event log exceeded maximum size. Don't forget to call `stopLoggingProfilingEvents()`."),mr();return}var Bn=new Int32Array(ur*4);Bn.set(Kt),br=Bn.buffer,Kt=Bn}Kt.set(Et,Pt)}}function ai(){ur=fr,br=new ArrayBuffer(ur*4),Kt=new Int32Array(br),vu=0}function mr(){var Et=br;return ur=0,br=null,Kt=null,vu=0,Et}function Xo(Et,Pt){f&&(Wt[Vn]++,Kt!==null&&eu([a0,Pt*1e3,Et.id,Et.priorityLevel]))}function W0(Et,Pt){f&&(Wt[vr]=Ce,Wt[w]=0,Wt[Vn]--,Kt!==null&&eu([So,Pt*1e3,Et.id]))}function Lu(Et,Pt){f&&(Wt[Vn]--,Kt!==null&&eu([Os,Pt*1e3,Et.id]))}function V0(Et,Pt){f&&(Wt[vr]=Ce,Wt[w]=0,Wt[Vn]--,Kt!==null&&eu([Go,Pt*1e3,Et.id]))}function Hr(Et,Pt){f&&(ln++,Wt[vr]=Et.priorityLevel,Wt[w]=Et.id,Wt[Ut]=ln,Kt!==null&&eu([Yo,Pt*1e3,Et.id,ln]))}function To(Et,Pt){f&&(Wt[vr]=Ce,Wt[w]=0,Wt[Ut]=0,Kt!==null&&eu([Ko,Pt*1e3,Et.id,ln]))}function Co(Et){f&&(An++,Kt!==null&&eu([qt,Et*1e3,An]))}function L0(Et){f&&Kt!==null&&eu([_i,Et*1e3,An])}var tu=1073741823,Si=-1,ks=250,Hl=5e3,F0=1e4,f0=tu,Pr=[],Ei=[],G0=1,fi=!1,Zt=null,Ln=ct,Di=!1,ci=!1,Ht=!1;function Du(Et){for(var Pt=jt(Ei);Pt!==null;){if(Pt.callback===null)$t(Ei);else if(Pt.startTime<=Et)$t(Ei),Pt.sortIndex=Pt.expirationTime,Ke(Pr,Pt),f&&(Xo(Pt,Et),Pt.isQueued=!0);else return;Pt=jt(Ei)}}function Yi(Et){if(Ht=!1,Du(Et),!ci)if(jt(Pr)!==null)ci=!0,p(Y0);else{var Pt=jt(Ei);Pt!==null&&E(Yi,Pt.startTime-Et)}}function Y0(Et,Pt){f&&L0(Pt),ci=!1,Ht&&(Ht=!1,t()),Di=!0;var Bn=Ln;try{if(f)try{return Ui(Et,Pt)}catch(Wr){if(Zt!==null){var Ir=Ri.unstable_now();V0(Zt,Ir),Zt.isQueued=!1}throw Wr}else return Ui(Et,Pt)}finally{if(Zt=null,Ln=Bn,Di=!1,f){var ji=Ri.unstable_now();Co(ji)}}}function Ui(Et,Pt){var Bn=Pt;for(Du(Bn),Zt=jt(Pr);Zt!==null&&!(i&&fi)&&!(Zt.expirationTime>Bn&&(!Et||k()));){var Ir=Zt.callback;if(Ir!==null){Zt.callback=null,Ln=Zt.priorityLevel;var ji=Zt.expirationTime<=Bn;Hr(Zt,Bn);var Wr=Ir(ji);Bn=Ri.unstable_now(),typeof Wr=="function"?(Zt.callback=Wr,To(Zt,Bn)):(f&&(W0(Zt,Bn),Zt.isQueued=!1),Zt===jt(Pr)&&$t(Pr)),Du(Bn)}else $t(Pr);Zt=jt(Pr)}if(Zt!==null)return!0;var wu=jt(Ei);return wu!==null&&E(Yi,wu.startTime-Bn),!1}function Wl(Et,Pt){switch(Et){case ue:case je:case ct:case At:case en:break;default:Et=ct}var Bn=Ln;Ln=Et;try{return Pt()}finally{Ln=Bn}}function xo(Et){var Pt;switch(Ln){case ue:case je:case ct:Pt=ct;break;default:Pt=Ln;break}var Bn=Ln;Ln=Pt;try{return Et()}finally{Ln=Bn}}function ni(Et){var Pt=Ln;return function(){var Bn=Ln;Ln=Pt;try{return Et.apply(this,arguments)}finally{Ln=Bn}}}function oo(Et){switch(Et){case ue:return Si;case je:return ks;case en:return f0;case At:return F0;case ct:default:return Hl}}function Vl(Et,Pt,Bn){var Ir=Ri.unstable_now(),ji,Wr;if(typeof Bn=="object"&&Bn!==null){var wu=Bn.delay;typeof wu=="number"&&wu>0?ji=Ir+wu:ji=Ir,Wr=typeof Bn.timeout=="number"?Bn.timeout:oo(Et)}else Wr=oo(Et),ji=Ir;var c0=ji+Wr,Ti={id:G0++,callback:Pt,priorityLevel:Et,startTime:ji,expirationTime:c0,sortIndex:-1};return f&&(Ti.isQueued=!1),ji>Ir?(Ti.sortIndex=ji,Ke(Ei,Ti),jt(Pr)===null&&Ti===jt(Ei)&&(Ht?t():Ht=!0,E(Yi,ji-Ir))):(Ti.sortIndex=c0,Ke(Pr,Ti),f&&(Xo(Ti,Ir),Ti.isQueued=!0),!ci&&!Di&&(ci=!0,p(Y0))),Ti}function Ao(){fi=!0}function Ms(){fi=!1,!ci&&!Di&&(ci=!0,p(Y0))}function Xn(){return jt(Pr)}function Qo(Et){if(f&&Et.isQueued){var Pt=Ri.unstable_now();Lu(Et,Pt),Et.isQueued=!1}Et.callback=null}function lo(){return Ln}function b0(){var Et=Ri.unstable_now();Du(Et);var Pt=jt(Pr);return Pt!==Zt&&Zt!==null&&Pt!==null&&Pt.callback!==null&&Pt.startTime<=Et&&Pt.expirationTime{"use strict";process.env.NODE_ENV==="production"?aw.exports=h9():aw.exports=v9()});var m9=ce((Pne,pg)=>{pg.exports=function i(o){"use strict";var f=eg(),p=su(),E=h4();function t(g){for(var y="https://reactjs.org/docs/error-decoder.html?invariant="+g,A=1;AG0||(g.current=Ei[G0],Ei[G0]=null,G0--)}function Zt(g,y){G0++,Ei[G0]=g.current,g.current=y}var Ln={},Di={current:Ln},ci={current:!1},Ht=Ln;function Du(g,y){var A=g.type.contextTypes;if(!A)return Ln;var F=g.stateNode;if(F&&F.__reactInternalMemoizedUnmaskedChildContext===y)return F.__reactInternalMemoizedMaskedChildContext;var I={},J;for(J in A)I[J]=y[J];return F&&(g=g.stateNode,g.__reactInternalMemoizedUnmaskedChildContext=y,g.__reactInternalMemoizedMaskedChildContext=I),I}function Yi(g){return g=g.childContextTypes,g!=null}function Y0(g){fi(ci,g),fi(Di,g)}function Ui(g){fi(ci,g),fi(Di,g)}function Wl(g,y,A){if(Di.current!==Ln)throw Error(t(168));Zt(Di,y,g),Zt(ci,A,g)}function xo(g,y,A){var F=g.stateNode;if(g=y.childContextTypes,typeof F.getChildContext!="function")return A;F=F.getChildContext();for(var I in F)if(!(I in g))throw Error(t(108,Ge(y)||"Unknown",I));return f({},A,{},F)}function ni(g){var y=g.stateNode;return y=y&&y.__reactInternalMemoizedMergedChildContext||Ln,Ht=Di.current,Zt(Di,y,g),Zt(ci,ci.current,g),!0}function oo(g,y,A){var F=g.stateNode;if(!F)throw Error(t(169));A?(y=xo(g,y,Ht),F.__reactInternalMemoizedMergedChildContext=y,fi(ci,g),fi(Di,g),Zt(Di,y,g)):fi(ci,g),Zt(ci,A,g)}var Vl=E.unstable_runWithPriority,Ao=E.unstable_scheduleCallback,Ms=E.unstable_cancelCallback,Xn=E.unstable_shouldYield,Qo=E.unstable_requestPaint,lo=E.unstable_now,b0=E.unstable_getCurrentPriorityLevel,yl=E.unstable_ImmediatePriority,Ro=E.unstable_UserBlockingPriority,Et=E.unstable_NormalPriority,Pt=E.unstable_LowPriority,Bn=E.unstable_IdlePriority,Ir={},ji=Qo!==void 0?Qo:function(){},Wr=null,wu=null,c0=!1,Ti=lo(),d0=1e4>Ti?lo:function(){return lo()-Ti};function as(){switch(b0()){case yl:return 99;case Ro:return 98;case Et:return 97;case Pt:return 96;case Bn:return 95;default:throw Error(t(332))}}function St(g){switch(g){case 99:return yl;case 98:return Ro;case 97:return Et;case 96:return Pt;case 95:return Bn;default:throw Error(t(332))}}function so(g,y){return g=St(g),Vl(g,y)}function Jo(g,y,A){return g=St(g),Ao(g,y,A)}function Gl(g){return Wr===null?(Wr=[g],wu=Ao(yl,fs)):Wr.push(g),Ir}function Fu(){if(wu!==null){var g=wu;wu=null,Ms(g)}fs()}function fs(){if(!c0&&Wr!==null){c0=!0;var g=0;try{var y=Wr;so(99,function(){for(;g=y&&(fo=!0),g.firstContext=null)}function Tu(g,y){if(Su!==g&&y!==!1&&y!==0)if((typeof y!="number"||y===1073741823)&&(Su=g,y=1073741823),y={context:g,observedBits:y,next:null},mi===null){if(or===null)throw Error(t(308));mi=y,or.dependencies={expirationTime:0,firstContext:y,responders:null}}else mi=mi.next=y;return un?g._currentValue:g._currentValue2}var ao=!1;function Iu(g){return{baseState:g,firstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function Oa(g){return{baseState:g.baseState,firstUpdate:g.firstUpdate,lastUpdate:g.lastUpdate,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null}}function p0(g,y){return{expirationTime:g,suspenseConfig:y,tag:0,payload:null,callback:null,next:null,nextEffect:null}}function Zs(g,y){g.lastUpdate===null?g.firstUpdate=g.lastUpdate=y:(g.lastUpdate.next=y,g.lastUpdate=y)}function K0(g,y){var A=g.alternate;if(A===null){var F=g.updateQueue,I=null;F===null&&(F=g.updateQueue=Iu(g.memoizedState))}else F=g.updateQueue,I=A.updateQueue,F===null?I===null?(F=g.updateQueue=Iu(g.memoizedState),I=A.updateQueue=Iu(A.memoizedState)):F=g.updateQueue=Oa(I):I===null&&(I=A.updateQueue=Oa(F));I===null||F===I?Zs(F,y):F.lastUpdate===null||I.lastUpdate===null?(Zs(F,y),Zs(I,y)):(Zs(F,y),I.lastUpdate=y)}function $s(g,y){var A=g.updateQueue;A=A===null?g.updateQueue=Iu(g.memoizedState):ka(g,A),A.lastCapturedUpdate===null?A.firstCapturedUpdate=A.lastCapturedUpdate=y:(A.lastCapturedUpdate.next=y,A.lastCapturedUpdate=y)}function ka(g,y){var A=g.alternate;return A!==null&&y===A.updateQueue&&(y=g.updateQueue=Oa(y)),y}function cs(g,y,A,F,I,J){switch(A.tag){case 1:return g=A.payload,typeof g=="function"?g.call(J,F,I):g;case 3:g.effectTag=g.effectTag&-4097|64;case 0:if(g=A.payload,I=typeof g=="function"?g.call(J,F,I):g,I==null)break;return f({},F,I);case 2:ao=!0}return F}function w0(g,y,A,F,I){ao=!1,y=ka(g,y);for(var J=y.baseState,fe=null,mt=0,Ct=y.firstUpdate,Mt=J;Ct!==null;){var Er=Ct.expirationTime;Erii?(qi=tr,tr=null):qi=tr.sibling;var jr=iu(He,tr,ut[ii],Jt);if(jr===null){tr===null&&(tr=qi);break}g&&tr&&jr.alternate===null&&y(He,tr),Be=J(jr,Be,ii),ti===null?jn=jr:ti.sibling=jr,ti=jr,tr=qi}if(ii===ut.length)return A(He,tr),jn;if(tr===null){for(;iiii?(qi=tr,tr=null):qi=tr.sibling;var gu=iu(He,tr,jr.value,Jt);if(gu===null){tr===null&&(tr=qi);break}g&&tr&&gu.alternate===null&&y(He,tr),Be=J(gu,Be,ii),ti===null?jn=gu:ti.sibling=gu,ti=gu,tr=qi}if(jr.done)return A(He,tr),jn;if(tr===null){for(;!jr.done;ii++,jr=ut.next())jr=$u(He,jr.value,Jt),jr!==null&&(Be=J(jr,Be,ii),ti===null?jn=jr:ti.sibling=jr,ti=jr);return jn}for(tr=F(He,tr);!jr.done;ii++,jr=ut.next())jr=j0(tr,He,ii,jr.value,Jt),jr!==null&&(g&&jr.alternate!==null&&tr.delete(jr.key===null?ii:jr.key),Be=J(jr,Be,ii),ti===null?jn=jr:ti.sibling=jr,ti=jr);return g&&tr.forEach(function(Ba){return y(He,Ba)}),jn}return function(He,Be,ut,Jt){var jn=typeof ut=="object"&&ut!==null&&ut.type===U&&ut.key===null;jn&&(ut=ut.props.children);var ti=typeof ut=="object"&&ut!==null;if(ti)switch(ut.$$typeof){case N:e:{for(ti=ut.key,jn=Be;jn!==null;){if(jn.key===ti)if(jn.tag===7?ut.type===U:jn.elementType===ut.type){A(He,jn.sibling),Be=I(jn,ut.type===U?ut.props.children:ut.props,Jt),Be.ref=Fs(He,jn,ut),Be.return=He,He=Be;break e}else{A(He,jn);break}else y(He,jn);jn=jn.sibling}ut.type===U?(Be=Zu(ut.props.children,He.mode,Jt,ut.key),Be.return=He,He=Be):(Jt=Ia(ut.type,ut.key,ut.props,null,He.mode,Jt),Jt.ref=Fs(He,Be,ut),Jt.return=He,He=Jt)}return fe(He);case C:e:{for(jn=ut.key;Be!==null;){if(Be.key===jn)if(Be.tag===4&&Be.stateNode.containerInfo===ut.containerInfo&&Be.stateNode.implementation===ut.implementation){A(He,Be.sibling),Be=I(Be,ut.children||[],Jt),Be.return=He,He=Be;break e}else{A(He,Be);break}else y(He,Be);Be=Be.sibling}Be=vf(ut,He.mode,Jt),Be.return=He,He=Be}return fe(He)}if(typeof ut=="string"||typeof ut=="number")return ut=""+ut,Be!==null&&Be.tag===6?(A(He,Be.sibling),Be=I(Be,ut,Jt),Be.return=He,He=Be):(A(He,Be),Be=U0(ut,He.mode,Jt),Be.return=He,He=Be),fe(He);if(h0(ut))return Tl(He,Be,ut,Jt);if(le(ut))return e0(He,Be,ut,Jt);if(ti&&Ni(He,ut),typeof ut=="undefined"&&!jn)switch(He.tag){case 1:case 0:throw He=He.type,Error(t(152,He.displayName||He.name||"Component"))}return A(He,Be)}}var z=B(!0),G=B(!1),$={},De={current:$},me={current:$},xe={current:$};function Z(g){if(g===$)throw Error(t(174));return g}function ke(g,y){Zt(xe,y,g),Zt(me,g,g),Zt(De,$,g),y=jt(y),fi(De,g),Zt(De,y,g)}function Xe(g){fi(De,g),fi(me,g),fi(xe,g)}function ht(g){var y=Z(xe.current),A=Z(De.current);y=$t(A,g.type,y),A!==y&&(Zt(me,g,g),Zt(De,y,g))}function ie(g){me.current===g&&(fi(De,g),fi(me,g))}var qe={current:0};function tt(g){for(var y=g;y!==null;){if(y.tag===13){var A=y.memoizedState;if(A!==null&&(A=A.dehydrated,A===null||Hr(A)||To(A)))return y}else if(y.tag===19&&y.memoizedProps.revealOrder!==void 0){if((y.effectTag&64)!=0)return y}else if(y.child!==null){y.child.return=y,y=y.child;continue}if(y===g)break;for(;y.sibling===null;){if(y.return===null||y.return===g)return null;y=y.return}y.sibling.return=y.return,y=y.sibling}return null}function Tt(g,y){return{responder:g,props:y}}var kt=k.ReactCurrentDispatcher,bt=k.ReactCurrentBatchConfig,on=0,tn=null,Lt=null,gn=null,lr=null,Qn=null,_r=null,Cn=0,Ar=null,v0=0,Rr=!1,nt=null,_t=0;function Ze(){throw Error(t(321))}function Ft(g,y){if(y===null)return!1;for(var A=0;ACn&&(Cn=Er,La(Cn))):(oc(Er,Ct.suspenseConfig),J=Ct.eagerReducer===g?Ct.eagerState:g(J,Ct.action)),fe=Ct,Ct=Ct.next}while(Ct!==null&&Ct!==F);Mt||(mt=fe,I=J),Ne(J,y.memoizedState)||(fo=!0),y.memoizedState=J,y.baseUpdate=mt,y.baseState=I,A.lastRenderedState=J}return[y.memoizedState,A.dispatch]}function S0(g){var y=Yn();return typeof g=="function"&&(g=g()),y.memoizedState=y.baseState=g,g=y.queue={last:null,dispatch:null,lastRenderedReducer:nu,lastRenderedState:g},g=g.dispatch=bs.bind(null,tn,g),[y.memoizedState,g]}function X0(g){return Cu(nu,g)}function xu(g,y,A,F){return g={tag:g,create:y,destroy:A,deps:F,next:null},Ar===null?(Ar={lastEffect:null},Ar.lastEffect=g.next=g):(y=Ar.lastEffect,y===null?Ar.lastEffect=g.next=g:(A=y.next,y.next=g,g.next=A,Ar.lastEffect=g)),g}function di(g,y,A,F){var I=Yn();v0|=g,I.memoizedState=xu(y,A,void 0,F===void 0?null:F)}function ko(g,y,A,F){var I=yr();F=F===void 0?null:F;var J=void 0;if(Lt!==null){var fe=Lt.memoizedState;if(J=fe.destroy,F!==null&&Ft(F,fe.deps)){xu(0,A,J,F);return}}v0|=g,I.memoizedState=xu(y,A,J,F)}function Zo(g,y){return di(516,192,g,y)}function sf(g,y){return ko(516,192,g,y)}function gl(g,y){if(typeof y=="function")return g=g(),y(g),function(){y(null)};if(y!=null)return g=g(),y.current=g,function(){y.current=null}}function af(){}function Mo(g,y){return Yn().memoizedState=[g,y===void 0?null:y],g}function ds(g,y){var A=yr();y=y===void 0?null:y;var F=A.memoizedState;return F!==null&&y!==null&&Ft(y,F[1])?F[0]:(A.memoizedState=[g,y],g)}function bs(g,y,A){if(!(25>_t))throw Error(t(301));var F=g.alternate;if(g===tn||F!==null&&F===tn)if(Rr=!0,g={expirationTime:on,suspenseConfig:null,action:A,eagerReducer:null,eagerState:null,next:null},nt===null&&(nt=new Map),A=nt.get(y),A===void 0)nt.set(y,g);else{for(y=A;y.next!==null;)y=y.next;y.next=g}else{var I=g0(),J=ri.suspense;I=bn(I,g,J),J={expirationTime:I,suspenseConfig:J,action:A,eagerReducer:null,eagerState:null,next:null};var fe=y.last;if(fe===null)J.next=J;else{var mt=fe.next;mt!==null&&(J.next=mt),fe.next=J}if(y.last=J,g.expirationTime===0&&(F===null||F.expirationTime===0)&&(F=y.lastRenderedReducer,F!==null))try{var Ct=y.lastRenderedState,Mt=F(Ct,A);if(J.eagerReducer=F,J.eagerState=Mt,Ne(Mt,Ct))return}catch(Er){}finally{}Qu(g,I)}}var No={readContext:Tu,useCallback:Ze,useContext:Ze,useEffect:Ze,useImperativeHandle:Ze,useLayoutEffect:Ze,useMemo:Ze,useReducer:Ze,useRef:Ze,useState:Ze,useDebugValue:Ze,useResponder:Ze,useDeferredValue:Ze,useTransition:Ze},Lo={readContext:Tu,useCallback:Mo,useContext:Tu,useEffect:Zo,useImperativeHandle:function(g,y,A){return A=A!=null?A.concat([g]):null,di(4,36,gl.bind(null,y,g),A)},useLayoutEffect:function(g,y){return di(4,36,g,y)},useMemo:function(g,y){var A=Yn();return y=y===void 0?null:y,g=g(),A.memoizedState=[g,y],g},useReducer:function(g,y,A){var F=Yn();return y=A!==void 0?A(y):y,F.memoizedState=F.baseState=y,g=F.queue={last:null,dispatch:null,lastRenderedReducer:g,lastRenderedState:y},g=g.dispatch=bs.bind(null,tn,g),[F.memoizedState,g]},useRef:function(g){var y=Yn();return g={current:g},y.memoizedState=g},useState:S0,useDebugValue:af,useResponder:Tt,useDeferredValue:function(g,y){var A=S0(g),F=A[0],I=A[1];return Zo(function(){E.unstable_next(function(){var J=bt.suspense;bt.suspense=y===void 0?null:y;try{I(g)}finally{bt.suspense=J}})},[g,y]),F},useTransition:function(g){var y=S0(!1),A=y[0],F=y[1];return[Mo(function(I){F(!0),E.unstable_next(function(){var J=bt.suspense;bt.suspense=g===void 0?null:g;try{F(!1),I()}finally{bt.suspense=J}})},[g,A]),A]}},ps={readContext:Tu,useCallback:ds,useContext:Tu,useEffect:sf,useImperativeHandle:function(g,y,A){return A=A!=null?A.concat([g]):null,ko(4,36,gl.bind(null,y,g),A)},useLayoutEffect:function(g,y){return ko(4,36,g,y)},useMemo:function(g,y){var A=yr();y=y===void 0?null:y;var F=A.memoizedState;return F!==null&&y!==null&&Ft(y,F[1])?F[0]:(g=g(),A.memoizedState=[g,y],g)},useReducer:Cu,useRef:function(){return yr().memoizedState},useState:X0,useDebugValue:af,useResponder:Tt,useDeferredValue:function(g,y){var A=X0(g),F=A[0],I=A[1];return sf(function(){E.unstable_next(function(){var J=bt.suspense;bt.suspense=y===void 0?null:y;try{I(g)}finally{bt.suspense=J}})},[g,y]),F},useTransition:function(g){var y=X0(!1),A=y[0],F=y[1];return[ds(function(I){F(!0),E.unstable_next(function(){var J=bt.suspense;bt.suspense=g===void 0?null:g;try{F(!1),I()}finally{bt.suspense=J}})},[g,A]),A]}},Vu=null,yu=null,pi=!1;function T0(g,y){var A=Io(5,null,null,0);A.elementType="DELETED",A.type="DELETED",A.stateNode=y,A.return=g,A.effectTag=8,g.lastEffect!==null?(g.lastEffect.nextEffect=A,g.lastEffect=A):g.firstEffect=g.lastEffect=A}function Q0(g,y){switch(g.tag){case 5:return y=Lu(y,g.type,g.pendingProps),y!==null?(g.stateNode=y,!0):!1;case 6:return y=V0(y,g.pendingProps),y!==null?(g.stateNode=y,!0):!1;case 13:return!1;default:return!1}}function Fo(g){if(pi){var y=yu;if(y){var A=y;if(!Q0(g,y)){if(y=Co(A),!y||!Q0(g,y)){g.effectTag=g.effectTag&-1025|2,pi=!1,Vu=g;return}T0(Vu,A)}Vu=g,yu=L0(y)}else g.effectTag=g.effectTag&-1025|2,pi=!1,Vu=g}}function ta(g){for(g=g.return;g!==null&&g.tag!==5&&g.tag!==3&&g.tag!==13;)g=g.return;Vu=g}function Kl(g){if(!w||g!==Vu)return!1;if(!pi)return ta(g),pi=!0,!1;var y=g.type;if(g.tag!==5||y!=="head"&&y!=="body"&&!ct(y,g.memoizedProps))for(y=yu;y;)T0(g,y),y=Co(y);if(ta(g),g.tag===13){if(!w)throw Error(t(316));if(g=g.memoizedState,g=g!==null?g.dehydrated:null,!g)throw Error(t(317));yu=ks(g)}else yu=Vu?Co(g.stateNode):null;return!0}function Ki(){w&&(yu=Vu=null,pi=!1)}var Yr=k.ReactCurrentOwner,fo=!1;function Oi(g,y,A,F){y.child=g===null?G(y,null,A,F):z(y,g.child,A,F)}function gi(g,y,A,F,I){A=A.render;var J=y.ref;return Oo(y,I),F=nn(g,y,A,F,J,I),g!==null&&!fo?(y.updateQueue=g.updateQueue,y.effectTag&=-517,g.expirationTime<=I&&(g.expirationTime=0),fu(g,y,I)):(y.effectTag|=1,Oi(g,y,F,I),y.child)}function ff(g,y,A,F,I,J){if(g===null){var fe=A.type;return typeof fe=="function"&&!hf(fe)&&fe.defaultProps===void 0&&A.compare===null&&A.defaultProps===void 0?(y.tag=15,y.type=fe,cf(g,y,fe,F,I,J)):(g=Ia(A.type,null,F,null,y.mode,J),g.ref=y.ref,g.return=y,y.child=g)}return fe=g.child,Iy)&&Ur.set(g,y)))}}function eo(g,y){g.expirationTimeg?y:g)}function Ju(g){if(g.lastExpiredTime!==0)g.callbackExpirationTime=1073741823,g.callbackPriority=99,g.callbackNode=Gl(to.bind(null,g));else{var y=po(g),A=g.callbackNode;if(y===0)A!==null&&(g.callbackNode=null,g.callbackExpirationTime=0,g.callbackPriority=90);else{var F=g0();if(y===1073741823?F=99:y===1||y===2?F=95:(F=10*(1073741821-y)-10*(1073741821-F),F=0>=F?99:250>=F?98:5250>=F?97:95),A!==null){var I=g.callbackPriority;if(g.callbackExpirationTime===y&&I>=F)return;A!==Ir&&Ms(A)}g.callbackExpirationTime=y,g.callbackPriority=F,y=y===1073741823?Gl(to.bind(null,g)):Jo(F,bo.bind(null,g),{timeout:10*(1073741821-y)-d0()}),g.callbackNode=y}}}function bo(g,y){if(Qi=0,y)return y=g0(),oa(g,y),Ju(g),null;var A=po(g);if(A!==0){if(y=g.callbackNode,(kn&(Xi|ru))!==wr)throw Error(t(327));if(Bs(),g===se&&A===Le||ms(g,A),re!==null){var F=kn;kn|=Xi;var I=B0(g);do try{$1();break}catch(mt){ia(g,mt)}while(1);if(bu(),kn=F,Ku.current=I,Ae===Xr)throw y=ot,ms(g,A),Sl(g,A),Ju(g),y;if(re===null)switch(I=g.finishedWork=g.current.alternate,g.finishedExpirationTime=A,F=Ae,se=null,F){case Ci:case Xr:throw Error(t(345));case Wn:oa(g,2=A){g.lastPingedTime=A,ms(g,A);break}}if(J=po(g),J!==0&&J!==A)break;if(F!==0&&F!==A){g.lastPingedTime=F;break}g.timeoutHandle=ln(Dl.bind(null,g),I);break}Dl(g);break;case m0:if(Sl(g,A),F=g.lastSuspendedTime,A===F&&(g.nextKnownPendingLevel=Uc(I)),yn&&(I=g.lastPingedTime,I===0||I>=A)){g.lastPingedTime=A,ms(g,A);break}if(I=po(g),I!==0&&I!==A)break;if(F!==0&&F!==A){g.lastPingedTime=F;break}if(Xt!==1073741823?F=10*(1073741821-Xt)-d0():vt===1073741823?F=0:(F=10*(1073741821-vt)-5e3,I=d0(),A=10*(1073741821-A)-I,F=I-F,0>F&&(F=0),F=(120>F?120:480>F?480:1080>F?1080:1920>F?1920:3e3>F?3e3:4320>F?4320:1960*df(F/1960))-F,A=F?F=0:(I=fe.busyDelayMs|0,J=d0()-(10*(1073741821-J)-(fe.timeoutMs|0||5e3)),F=J<=I?0:I+F-J),10 component higher in the tree to provide a loading indicator or placeholder to display.`+Pr(I))}Ae!==y0&&(Ae=Wn),J=_l(J,I),Ct=F;do{switch(Ct.tag){case 3:fe=J,Ct.effectTag|=4096,Ct.expirationTime=y;var Be=hs(Ct,fe,y);$s(Ct,Be);break e;case 1:fe=J;var ut=Ct.type,Jt=Ct.stateNode;if((Ct.effectTag&64)==0&&(typeof ut.getDerivedStateFromError=="function"||Jt!==null&&typeof Jt.componentDidCatch=="function"&&(cr===null||!cr.has(Jt)))){Ct.effectTag|=4096,Ct.expirationTime=y;var jn=ra(Ct,fe,y);$s(Ct,jn);break e}}Ct=Ct.return}while(Ct!==null)}re=ho(re)}catch(ti){y=ti;continue}break}while(1)}function B0(){var g=Ku.current;return Ku.current=No,g===null?No:g}function oc(g,y){g_n&&(_n=g)}function gd(){for(;re!==null;)re=e2(re)}function $1(){for(;re!==null&&!Xn();)re=e2(re)}function e2(g){var y=Pa(g.alternate,g,Le);return g.memoizedProps=g.pendingProps,y===null&&(y=ho(g)),vs.current=null,y}function ho(g){re=g;do{var y=re.alternate;if(g=re.return,(re.effectTag&2048)==0){e:{var A=y;y=re;var F=Le,I=y.pendingProps;switch(y.tag){case 2:break;case 16:break;case 15:case 0:break;case 1:Yi(y.type)&&Y0(y);break;case 3:Xe(y),Ui(y),I=y.stateNode,I.pendingContext&&(I.context=I.pendingContext,I.pendingContext=null),(A===null||A.child===null)&&Kl(y)&&Gu(y),Vr(y);break;case 5:ie(y);var J=Z(xe.current);if(F=y.type,A!==null&&y.stateNode!=null)Bu(A,y,F,I,J),A.ref!==y.ref&&(y.effectTag|=128);else if(I){if(A=Z(De.current),Kl(y)){if(I=y,!w)throw Error(t(175));A=tu(I.stateNode,I.type,I.memoizedProps,J,A,I),I.updateQueue=A,A=A!==null,A&&Gu(y)}else{var fe=ae(F,I,J,A,y);Kr(fe,y,!1,!1),y.stateNode=fe,ue(fe,F,I,J,A)&&Gu(y)}y.ref!==null&&(y.effectTag|=128)}else if(y.stateNode===null)throw Error(t(166));break;case 6:if(A&&y.stateNode!=null)Sn(A,y,A.memoizedProps,I);else{if(typeof I!="string"&&y.stateNode===null)throw Error(t(166));if(A=Z(xe.current),J=Z(De.current),Kl(y)){if(A=y,!w)throw Error(t(176));(A=Si(A.stateNode,A.memoizedProps,A))&&Gu(y)}else y.stateNode=en(I,A,J,y)}break;case 11:break;case 13:if(fi(qe,y),I=y.memoizedState,(y.effectTag&64)!=0){y.expirationTime=F;break e}I=I!==null,J=!1,A===null?y.memoizedProps.fallback!==void 0&&Kl(y):(F=A.memoizedState,J=F!==null,I||F===null||(F=A.child.sibling,F!==null&&(fe=y.firstEffect,fe!==null?(y.firstEffect=F,F.nextEffect=fe):(y.firstEffect=y.lastEffect=F,F.nextEffect=null),F.effectTag=8))),I&&!J&&(y.mode&2)!=0&&(A===null&&y.memoizedProps.unstable_avoidThisFallback!==!0||(qe.current&1)!=0?Ae===Ci&&(Ae=Xu):((Ae===Ci||Ae===Xu)&&(Ae=m0),_n!==0&&se!==null&&(Sl(se,Le),_s(se,_n)))),vr&&I&&(y.effectTag|=4),Wt&&(I||J)&&(y.effectTag|=4);break;case 7:break;case 8:break;case 12:break;case 4:Xe(y),Vr(y);break;case 10:mu(y);break;case 9:break;case 14:break;case 17:Yi(y.type)&&Y0(y);break;case 19:if(fi(qe,y),I=y.memoizedState,I===null)break;if(J=(y.effectTag&64)!=0,fe=I.rendering,fe===null){if(J)Au(I,!1);else if(Ae!==Ci||A!==null&&(A.effectTag&64)!=0)for(A=y.child;A!==null;){if(fe=tt(A),fe!==null){for(y.effectTag|=64,Au(I,!1),A=fe.updateQueue,A!==null&&(y.updateQueue=A,y.effectTag|=4),I.lastEffect===null&&(y.firstEffect=null),y.lastEffect=I.lastEffect,A=F,I=y.child;I!==null;)J=I,F=A,J.effectTag&=2,J.nextEffect=null,J.firstEffect=null,J.lastEffect=null,fe=J.alternate,fe===null?(J.childExpirationTime=0,J.expirationTime=F,J.child=null,J.memoizedProps=null,J.memoizedState=null,J.updateQueue=null,J.dependencies=null):(J.childExpirationTime=fe.childExpirationTime,J.expirationTime=fe.expirationTime,J.child=fe.child,J.memoizedProps=fe.memoizedProps,J.memoizedState=fe.memoizedState,J.updateQueue=fe.updateQueue,F=fe.dependencies,J.dependencies=F===null?null:{expirationTime:F.expirationTime,firstContext:F.firstContext,responders:F.responders}),I=I.sibling;Zt(qe,qe.current&1|2,y),y=y.child;break e}A=A.sibling}}else{if(!J)if(A=tt(fe),A!==null){if(y.effectTag|=64,J=!0,A=A.updateQueue,A!==null&&(y.updateQueue=A,y.effectTag|=4),Au(I,!0),I.tail===null&&I.tailMode==="hidden"&&!fe.alternate){y=y.lastEffect=I.lastEffect,y!==null&&(y.nextEffect=null);break}}else d0()>I.tailExpiration&&1I&&(I=F),fe>I&&(I=fe),J=J.sibling;A.childExpirationTime=I}if(y!==null)return y;g!==null&&(g.effectTag&2048)==0&&(g.firstEffect===null&&(g.firstEffect=re.firstEffect),re.lastEffect!==null&&(g.lastEffect!==null&&(g.lastEffect.nextEffect=re.firstEffect),g.lastEffect=re.lastEffect),1g?y:g}function Dl(g){var y=as();return so(99,el.bind(null,g,y)),null}function el(g,y){do Bs();while(Qr!==null);if((kn&(Xi|ru))!==wr)throw Error(t(327));var A=g.finishedWork,F=g.finishedExpirationTime;if(A===null)return null;if(g.finishedWork=null,g.finishedExpirationTime=0,A===g.current)throw Error(t(177));g.callbackNode=null,g.callbackExpirationTime=0,g.callbackPriority=90,g.nextKnownPendingLevel=0;var I=Uc(A);if(g.firstPendingTime=I,F<=g.lastSuspendedTime?g.firstSuspendedTime=g.lastSuspendedTime=g.nextKnownPendingLevel=0:F<=g.firstSuspendedTime&&(g.firstSuspendedTime=F-1),F<=g.lastPingedTime&&(g.lastPingedTime=0),F<=g.lastExpiredTime&&(g.lastExpiredTime=0),g===se&&(re=se=null,Le=0),1=A?Yt(g,y,A):(Zt(qe,qe.current&1,y),y=fu(g,y,A),y!==null?y.sibling:null);Zt(qe,qe.current&1,y);break;case 19:if(F=y.childExpirationTime>=A,(g.effectTag&64)!=0){if(F)return wn(g,y,A);y.effectTag|=64}if(I=y.memoizedState,I!==null&&(I.rendering=null,I.tail=null),Zt(qe,qe.current,y),!F)return null}return fu(g,y,A)}fo=!1}}else fo=!1;switch(y.expirationTime=0,y.tag){case 2:if(F=y.type,g!==null&&(g.alternate=null,y.alternate=null,y.effectTag|=2),g=y.pendingProps,I=Du(y,Di.current),Oo(y,A),I=nn(null,y,F,g,I,A),y.effectTag|=1,typeof I=="object"&&I!==null&&typeof I.render=="function"&&I.$$typeof===void 0){if(y.tag=1,sn(),Yi(F)){var J=!0;ni(y)}else J=!1;y.memoizedState=I.state!==null&&I.state!==void 0?I.state:null;var fe=F.getDerivedStateFromProps;typeof fe=="function"&&Yl(y,F,fe,g),I.updater=ea,y.stateNode=I,I._reactInternalFiber=y,Ls(y,F,g,A),y=et(null,y,F,!0,J,A)}else y.tag=0,Oi(null,y,I,A),y=y.child;return y;case 16:if(I=y.elementType,g!==null&&(g.alternate=null,y.alternate=null,y.effectTag|=2),g=y.pendingProps,Ue(I),I._status!==1)throw I._result;switch(I=I._result,y.type=I,J=y.tag=tl(I),g=Hn(I,g),J){case 0:y=Z0(null,y,I,g,A);break;case 1:y=Te(null,y,I,g,A);break;case 11:y=gi(null,y,I,g,A);break;case 14:y=ff(null,y,I,Hn(I.type,g),F,A);break;default:throw Error(t(306,I,""))}return y;case 0:return F=y.type,I=y.pendingProps,I=y.elementType===F?I:Hn(F,I),Z0(g,y,F,I,A);case 1:return F=y.type,I=y.pendingProps,I=y.elementType===F?I:Hn(F,I),Te(g,y,F,I,A);case 3:if(Ve(y),F=y.updateQueue,F===null)throw Error(t(282));if(I=y.memoizedState,I=I!==null?I.element:null,w0(y,F,y.pendingProps,null,A),F=y.memoizedState.element,F===I)Ki(),y=fu(g,y,A);else{if((I=y.stateNode.hydrate)&&(w?(yu=L0(y.stateNode.containerInfo),Vu=y,I=pi=!0):I=!1),I)for(A=G(y,null,F,A),y.child=A;A;)A.effectTag=A.effectTag&-3|1024,A=A.sibling;else Oi(g,y,F,A),Ki();y=y.child}return y;case 5:return ht(y),g===null&&Fo(y),F=y.type,I=y.pendingProps,J=g!==null?g.memoizedProps:null,fe=I.children,ct(F,I)?fe=null:J!==null&&ct(F,J)&&(y.effectTag|=16),J0(g,y),y.mode&4&&A!==1&&At(F,I)?(y.expirationTime=y.childExpirationTime=1,y=null):(Oi(g,y,fe,A),y=y.child),y;case 6:return g===null&&Fo(y),null;case 13:return Yt(g,y,A);case 4:return ke(y,y.stateNode.containerInfo),F=y.pendingProps,g===null?y.child=z(y,null,F,A):Oi(g,y,F,A),y.child;case 11:return F=y.type,I=y.pendingProps,I=y.elementType===F?I:Hn(F,I),gi(g,y,F,I,A);case 7:return Oi(g,y,y.pendingProps,A),y.child;case 8:return Oi(g,y,y.pendingProps.children,A),y.child;case 12:return Oi(g,y,y.pendingProps.children,A),y.child;case 10:e:{if(F=y.type._context,I=y.pendingProps,fe=y.memoizedProps,J=I.value,Pu(y,J),fe!==null){var mt=fe.value;if(J=Ne(mt,J)?0:(typeof F._calculateChangedBits=="function"?F._calculateChangedBits(mt,J):1073741823)|0,J===0){if(fe.children===I.children&&!ci.current){y=fu(g,y,A);break e}}else for(mt=y.child,mt!==null&&(mt.return=y);mt!==null;){var Ct=mt.dependencies;if(Ct!==null){fe=mt.child;for(var Mt=Ct.firstContext;Mt!==null;){if(Mt.context===F&&(Mt.observedBits&J)!=0){mt.tag===1&&(Mt=p0(A,null),Mt.tag=2,K0(mt,Mt)),mt.expirationTime=y&&g<=y}function Sl(g,y){var A=g.firstSuspendedTime,F=g.lastSuspendedTime;Ay||A===0)&&(g.lastSuspendedTime=y),y<=g.lastPingedTime&&(g.lastPingedTime=0),y<=g.lastExpiredTime&&(g.lastExpiredTime=0)}function _s(g,y){y>g.firstPendingTime&&(g.firstPendingTime=y);var A=g.firstSuspendedTime;A!==0&&(y>=A?g.firstSuspendedTime=g.lastSuspendedTime=g.nextKnownPendingLevel=0:y>=g.lastSuspendedTime&&(g.lastSuspendedTime=y+1),y>g.nextKnownPendingLevel&&(g.nextKnownPendingLevel=y))}function oa(g,y){var A=g.lastExpiredTime;(A===0||A>y)&&(g.lastExpiredTime=y)}function n2(g){var y=g._reactInternalFiber;if(y===void 0)throw typeof g.render=="function"?Error(t(188)):Error(t(268,Object.keys(g)));return g=$e(y),g===null?null:g.stateNode}function la(g,y){g=g.memoizedState,g!==null&&g.dehydrated!==null&&g.retryTime{"use strict";Object.defineProperty(Qf,"__esModule",{value:!0});var JK=0;Qf.__interactionsRef=null;Qf.__subscriberRef=null;Qf.unstable_clear=function(i){return i()};Qf.unstable_getCurrent=function(){return null};Qf.unstable_getThreadID=function(){return++JK};Qf.unstable_trace=function(i,o,f){return f()};Qf.unstable_wrap=function(i){return i};Qf.unstable_subscribe=function(){};Qf.unstable_unsubscribe=function(){}});var g9=ce(au=>{"use strict";process.env.NODE_ENV!=="production"&&function(){"use strict";Object.defineProperty(au,"__esModule",{value:!0});var i=!0,o=0,f=0,p=0;au.__interactionsRef=null,au.__subscriberRef=null,i&&(au.__interactionsRef={current:new Set},au.__subscriberRef={current:null});function E(ge){if(!i)return ge();var ze=au.__interactionsRef.current;au.__interactionsRef.current=new Set;try{return ge()}finally{au.__interactionsRef.current=ze}}function t(){return i?au.__interactionsRef.current:null}function k(){return++p}function L(ge,ze,pe){var Oe=arguments.length>3&&arguments[3]!==void 0?arguments[3]:o;if(!i)return pe();var le={__count:1,id:f++,name:ge,timestamp:ze},Ue=au.__interactionsRef.current,Ge=new Set(Ue);Ge.add(le),au.__interactionsRef.current=Ge;var rt=au.__subscriberRef.current,wt;try{rt!==null&&rt.onInteractionTraced(le)}finally{try{rt!==null&&rt.onWorkStarted(Ge,Oe)}finally{try{wt=pe()}finally{au.__interactionsRef.current=Ue;try{rt!==null&&rt.onWorkStopped(Ge,Oe)}finally{le.__count--,rt!==null&&le.__count===0&&rt.onInteractionScheduledWorkCompleted(le)}}}}return wt}function N(ge){var ze=arguments.length>1&&arguments[1]!==void 0?arguments[1]:o;if(!i)return ge;var pe=au.__interactionsRef.current,Oe=au.__subscriberRef.current;Oe!==null&&Oe.onWorkScheduled(pe,ze),pe.forEach(function(Ge){Ge.__count++});var le=!1;function Ue(){var Ge=au.__interactionsRef.current;au.__interactionsRef.current=pe,Oe=au.__subscriberRef.current;try{var rt;try{Oe!==null&&Oe.onWorkStarted(pe,ze)}finally{try{rt=ge.apply(void 0,arguments)}finally{au.__interactionsRef.current=Ge,Oe!==null&&Oe.onWorkStopped(pe,ze)}}return rt}finally{le||(le=!0,pe.forEach(function(wt){wt.__count--,Oe!==null&&wt.__count===0&&Oe.onInteractionScheduledWorkCompleted(wt)}))}}return Ue.cancel=function(){Oe=au.__subscriberRef.current;try{Oe!==null&&Oe.onWorkCanceled(pe,ze)}finally{pe.forEach(function(rt){rt.__count--,Oe&&rt.__count===0&&Oe.onInteractionScheduledWorkCompleted(rt)})}},Ue}var C=null;i&&(C=new Set);function U(ge){i&&(C.add(ge),C.size===1&&(au.__subscriberRef.current={onInteractionScheduledWorkCompleted:ne,onInteractionTraced:W,onWorkCanceled:he,onWorkScheduled:m,onWorkStarted:we,onWorkStopped:Se}))}function q(ge){i&&(C.delete(ge),C.size===0&&(au.__subscriberRef.current=null))}function W(ge){var ze=!1,pe=null;if(C.forEach(function(Oe){try{Oe.onInteractionTraced(ge)}catch(le){ze||(ze=!0,pe=le)}}),ze)throw pe}function ne(ge){var ze=!1,pe=null;if(C.forEach(function(Oe){try{Oe.onInteractionScheduledWorkCompleted(ge)}catch(le){ze||(ze=!0,pe=le)}}),ze)throw pe}function m(ge,ze){var pe=!1,Oe=null;if(C.forEach(function(le){try{le.onWorkScheduled(ge,ze)}catch(Ue){pe||(pe=!0,Oe=Ue)}}),pe)throw Oe}function we(ge,ze){var pe=!1,Oe=null;if(C.forEach(function(le){try{le.onWorkStarted(ge,ze)}catch(Ue){pe||(pe=!0,Oe=Ue)}}),pe)throw Oe}function Se(ge,ze){var pe=!1,Oe=null;if(C.forEach(function(le){try{le.onWorkStopped(ge,ze)}catch(Ue){pe||(pe=!0,Oe=Ue)}}),pe)throw Oe}function he(ge,ze){var pe=!1,Oe=null;if(C.forEach(function(le){try{le.onWorkCanceled(ge,ze)}catch(Ue){pe||(pe=!0,Oe=Ue)}}),pe)throw Oe}au.unstable_clear=E,au.unstable_getCurrent=t,au.unstable_getThreadID=k,au.unstable_trace=L,au.unstable_wrap=N,au.unstable_subscribe=U,au.unstable_unsubscribe=q}()});var _9=ce((Une,fw)=>{"use strict";process.env.NODE_ENV==="production"?fw.exports=y9():fw.exports=g9()});var E9=ce((jne,hg)=>{"use strict";process.env.NODE_ENV!=="production"&&(hg.exports=function i(o){"use strict";var f=eg(),p=su(),E=HD(),t=h4(),k=_9(),L=0,N=1,C=2,U=3,q=4,W=5,ne=6,m=7,we=8,Se=9,he=10,ge=11,ze=12,pe=13,Oe=14,le=15,Ue=16,Ge=17,rt=18,wt=19,xt=20,$e=21,ft=function(){};ft=function(a,c){for(var _=arguments.length,T=new Array(_>2?_-2:0),R=2;R<_;R++)T[R-2]=arguments[R];if(c===void 0)throw new Error("`warningWithoutStack(condition, format, ...args)` requires a warning message argument");if(T.length>8)throw new Error("warningWithoutStack() currently supports at most 8 arguments.");if(!a){if(typeof console!="undefined"){var j=T.map(function(oe){return""+oe});j.unshift("Warning: "+c),Function.prototype.apply.call(console.error,console,j)}try{var V=0,te="Warning: "+c.replace(/%s/g,function(){return T[V++]});throw new Error(te)}catch(oe){}}};var Ke=ft;function jt(a){return a._reactInternalFiber}function $t(a,c){a._reactInternalFiber=c}var at=p.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;at.hasOwnProperty("ReactCurrentDispatcher")||(at.ReactCurrentDispatcher={current:null}),at.hasOwnProperty("ReactCurrentBatchConfig")||(at.ReactCurrentBatchConfig={suspense:null});var Q=typeof Symbol=="function"&&Symbol.for,ae=Q?Symbol.for("react.element"):60103,Ce=Q?Symbol.for("react.portal"):60106,ue=Q?Symbol.for("react.fragment"):60107,je=Q?Symbol.for("react.strict_mode"):60108,ct=Q?Symbol.for("react.profiler"):60114,At=Q?Symbol.for("react.provider"):60109,en=Q?Symbol.for("react.context"):60110,ln=Q?Symbol.for("react.concurrent_mode"):60111,An=Q?Symbol.for("react.forward_ref"):60112,nr=Q?Symbol.for("react.suspense"):60113,un=Q?Symbol.for("react.suspense_list"):60120,Wt=Q?Symbol.for("react.memo"):60115,vr=Q?Symbol.for("react.lazy"):60116,w=Q?Symbol.for("react.fundamental"):60117,Ut=Q?Symbol.for("react.responder"):60118,Vn=Q?Symbol.for("react.scope"):60119,fr=typeof Symbol=="function"&&Symbol.iterator,Fr="@@iterator";function ur(a){if(a===null||typeof a!="object")return null;var c=fr&&a[fr]||a[Fr];return typeof c=="function"?c:null}var br=Ke;br=function(a,c){if(!a){for(var _=at.ReactDebugCurrentFrame,T=_.getStackAddendum(),R=arguments.length,j=new Array(R>2?R-2:0),V=2;V import('./MyComponent'))`,T),a._status=So,a._result=R}},function(T){a._status===a0&&(a._status=Go,a._result=T)})}}function Ko(a,c,_){var T=c.displayName||c.name||"";return a.displayName||(T!==""?_+"("+T+")":_)}function qt(a){if(a==null)return null;if(typeof a.tag=="number"&&Ke(!1,"Received an unexpected object in getComponentName(). This is likely a bug in React. Please file an issue."),typeof a=="function")return a.displayName||a.name||null;if(typeof a=="string")return a;switch(a){case ue:return"Fragment";case Ce:return"Portal";case ct:return"Profiler";case je:return"StrictMode";case nr:return"Suspense";case un:return"SuspenseList"}if(typeof a=="object")switch(a.$$typeof){case en:return"Context.Consumer";case At:return"Context.Provider";case An:return Ko(a,a.render,"ForwardRef");case Wt:return qt(a.type);case vr:{var c=a,_=Os(c);if(_)return qt(_);break}}return null}var _i=0,eu=1,ai=2,mr=4,Xo=6,W0=8,Lu=16,V0=32,Hr=64,To=128,Co=256,L0=512,tu=1024,Si=1028,ks=932,Hl=2047,F0=2048,f0=4096,Pr=!0,Ei=!0,G0=!0,fi=!0,Zt=!0,Ln=!0,Di=!1,ci=!1,Ht=!1,Du=!1,Yi=!1,Y0=!0,Ui=!1,Wl=!1,xo=!1,ni=!1,oo=!1,Vl=at.ReactCurrentOwner;function Ao(a){var c=a,_=a;if(a.alternate)for(;c.return;)c=c.return;else{var T=c;do c=T,(c.effectTag&(ai|tu))!==_i&&(_=c.return),T=c.return;while(T)}return c.tag===U?_:null}function Ms(a){return Ao(a)===a}function Xn(a){{var c=Vl.current;if(c!==null&&c.tag===N){var _=c,T=_.stateNode;T._warnedAboutRefsInRender||Ke(!1,"%s is accessing isMounted inside its render() function. render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.",qt(_.type)||"A component"),T._warnedAboutRefsInRender=!0}}var R=jt(a);return R?Ao(R)===R:!1}function Qo(a){if(Ao(a)!==a)throw Error("Unable to find node on an unmounted component.")}function lo(a){var c=a.alternate;if(!c){var _=Ao(a);if(_===null)throw Error("Unable to find node on an unmounted component.");return _!==a?null:a}for(var T=a,R=c;;){var j=T.return;if(j===null)break;var V=j.alternate;if(V===null){var te=j.return;if(te!==null){T=R=te;continue}break}if(j.child===V.child){for(var oe=j.child;oe;){if(oe===T)return Qo(j),a;if(oe===R)return Qo(j),c;oe=oe.sibling}throw Error("Unable to find node on an unmounted component.")}if(T.return!==R.return)T=j,R=V;else{for(var Ie=!1,Ye=j.child;Ye;){if(Ye===T){Ie=!0,T=j,R=V;break}if(Ye===R){Ie=!0,R=j,T=V;break}Ye=Ye.sibling}if(!Ie){for(Ye=V.child;Ye;){if(Ye===T){Ie=!0,T=V,R=j;break}if(Ye===R){Ie=!0,R=V,T=j;break}Ye=Ye.sibling}if(!Ie)throw Error("Child was not found in either parent set. This indicates a bug in React related to the return pointer. Please file an issue.")}}if(T.alternate!==R)throw Error("Return fibers should always be each others' alternates. This error is likely caused by a bug in React. Please file an issue.")}if(T.tag!==U)throw Error("Unable to find node on an unmounted component.");return T.stateNode.current===T?a:c}function b0(a){var c=lo(a);if(!c)return null;for(var _=c;;){if(_.tag===W||_.tag===ne)return _;if(_.child){_.child.return=_,_=_.child;continue}if(_===c)return null;for(;!_.sibling;){if(!_.return||_.return===c)return null;_=_.return}_.sibling.return=_.return,_=_.sibling}return null}function yl(a){var c=lo(a);if(!c)return null;for(var _=c;;){if(_.tag===W||_.tag===ne||Ht&&_.tag===xt)return _;if(_.child&&_.tag!==q){_.child.return=_,_=_.child;continue}if(_===c)return null;for(;!_.sibling;){if(!_.return||_.return===c)return null;_=_.return}_.sibling.return=_.return,_=_.sibling}return null}var Ro=o.getPublicInstance,Et=o.getRootHostContext,Pt=o.getChildHostContext,Bn=o.prepareForCommit,Ir=o.resetAfterCommit,ji=o.createInstance,Wr=o.appendInitialChild,wu=o.finalizeInitialChildren,c0=o.prepareUpdate,Ti=o.shouldSetTextContent,d0=o.shouldDeprioritizeSubtree,as=o.createTextInstance,St=o.setTimeout,so=o.clearTimeout,Jo=o.noTimeout,Gl=o.now,Fu=o.isPrimaryRenderer,fs=o.warnsIfNotActing,P0=o.supportsMutation,X=o.supportsPersistence,_e=o.supportsHydration,Ne=o.mountResponderInstance,Me=o.unmountResponderInstance,dt=o.getFundamentalComponentInstance,Hn=o.mountFundamentalComponent,Dn=o.shouldUpdateFundamentalComponent,or=o.getInstanceFromNode,mi=o.appendChild,Su=o.appendChildToContainer,bu=o.commitTextUpdate,Pu=o.commitMount,mu=o.commitUpdate,yi=o.insertBefore,Oo=o.insertInContainerBefore,Tu=o.removeChild,ao=o.removeChildFromContainer,Iu=o.resetTextContent,Oa=o.hideInstance,p0=o.hideTextInstance,Zs=o.unhideInstance,K0=o.unhideTextInstance,$s=o.updateFundamentalComponent,ka=o.unmountFundamentalComponent,cs=o.cloneInstance,w0=o.createContainerChildSet,Gn=o.appendChildToContainerChildSet,ic=o.finalizeContainerChildren,ri=o.replaceContainerChildren,Gr=o.cloneHiddenInstance,Yl=o.cloneHiddenTextInstance,ea=o.cloneInstance,lf=o.canHydrateInstance,Ns=o.canHydrateTextInstance,Ma=o.canHydrateSuspenseInstance,Ls=o.isSuspenseInstancePending,h0=o.isSuspenseInstanceFallback,Fs=o.registerSuspenseInstanceRetry,Ni=o.getNextHydratableSibling,B=o.getFirstHydratableChild,z=o.hydrateInstance,G=o.hydrateTextInstance,$=o.hydrateSuspenseInstance,De=o.getNextHydratableInstanceAfterSuspenseInstance,me=o.commitHydratedContainer,xe=o.commitHydratedSuspenseInstance,Z=o.clearSuspenseBoundary,ke=o.clearSuspenseBoundaryFromContainer,Xe=o.didNotMatchHydratedContainerTextInstance,ht=o.didNotMatchHydratedTextInstance,ie=o.didNotHydrateContainerInstance,qe=o.didNotHydrateInstance,tt=o.didNotFindHydratableContainerInstance,Tt=o.didNotFindHydratableContainerTextInstance,kt=o.didNotFindHydratableContainerSuspenseInstance,bt=o.didNotFindHydratableInstance,on=o.didNotFindHydratableTextInstance,tn=o.didNotFindHydratableSuspenseInstance,Lt=/^(.*)[\\\/]/,gn=function(a,c,_){var T="";if(c){var R=c.fileName,j=R.replace(Lt,"");if(/^index\./.test(j)){var V=R.match(Lt);if(V){var te=V[1];if(te){var oe=te.replace(Lt,"");j=oe+"/"+j}}}T=" (at "+j+":"+c.lineNumber+")"}else _&&(T=" (created by "+_+")");return` + in `+(a||"Unknown")+T},lr=at.ReactDebugCurrentFrame;function Qn(a){switch(a.tag){case U:case q:case ne:case m:case he:case Se:return"";default:var c=a._debugOwner,_=a._debugSource,T=qt(a.type),R=null;return c&&(R=qt(c.type)),gn(T,_,R)}}function _r(a){var c="",_=a;do c+=Qn(_),_=_.return;while(_);return c}var Cn=null,Ar=null;function v0(){{if(Cn===null)return null;var a=Cn._debugOwner;if(a!==null&&typeof a!="undefined")return qt(a.type)}return null}function Rr(){return Cn===null?"":_r(Cn)}function nt(){lr.getCurrentStack=null,Cn=null,Ar=null}function _t(a){lr.getCurrentStack=Rr,Cn=a,Ar=null}function Ze(a){Ar=a}var Ft="\u269B",nn="\u26D4",sn=typeof performance!="undefined"&&typeof performance.mark=="function"&&typeof performance.clearMarks=="function"&&typeof performance.measure=="function"&&typeof performance.clearMeasures=="function",Yn=null,yr=null,nu=null,Cu=!1,S0=!1,X0=!1,xu=0,di=0,ko=new Set,Zo=function(a){return Ft+" "+a},sf=function(a,c){var _=c?nn+" ":Ft+" ",T=c?" Warning: "+c:"";return""+_+a+T},gl=function(a){performance.mark(Zo(a))},af=function(a){performance.clearMarks(Zo(a))},Mo=function(a,c,_){var T=Zo(c),R=sf(a,_);try{performance.measure(R,T)}catch(j){}performance.clearMarks(T),performance.clearMeasures(R)},ds=function(a,c){return a+" (#"+c+")"},bs=function(a,c,_){return _===null?a+" ["+(c?"update":"mount")+"]":a+"."+_},No=function(a,c){var _=qt(a.type)||"Unknown",T=a._debugID,R=a.alternate!==null,j=bs(_,R,c);if(Cu&&ko.has(j))return!1;ko.add(j);var V=ds(j,T);return gl(V),!0},Lo=function(a,c){var _=qt(a.type)||"Unknown",T=a._debugID,R=a.alternate!==null,j=bs(_,R,c),V=ds(j,T);af(V)},ps=function(a,c,_){var T=qt(a.type)||"Unknown",R=a._debugID,j=a.alternate!==null,V=bs(T,j,c),te=ds(V,R);Mo(V,te,_)},Vu=function(a){switch(a.tag){case U:case W:case ne:case q:case m:case he:case Se:case we:return!0;default:return!1}},yu=function(){yr!==null&&nu!==null&&Lo(nu,yr),nu=null,yr=null,X0=!1},pi=function(){for(var a=Yn;a;)a._debugIsCurrentlyTiming&&ps(a,null,null),a=a.return},T0=function(a){a.return!==null&&T0(a.return),a._debugIsCurrentlyTiming&&No(a,null)},Q0=function(){Yn!==null&&T0(Yn)};function Fo(){Pr&&di++}function ta(){Pr&&(Cu&&(S0=!0),yr!==null&&yr!=="componentWillMount"&&yr!=="componentWillReceiveProps"&&(X0=!0))}function Kl(a){if(Pr){if(!sn||Vu(a)||(Yn=a,!No(a,null)))return;a._debugIsCurrentlyTiming=!0}}function Ki(a){if(Pr){if(!sn||Vu(a))return;a._debugIsCurrentlyTiming=!1,Lo(a,null)}}function Yr(a){if(Pr){if(!sn||Vu(a)||(Yn=a.return,!a._debugIsCurrentlyTiming))return;a._debugIsCurrentlyTiming=!1,ps(a,null,null)}}function fo(a){if(Pr){if(!sn||Vu(a)||(Yn=a.return,!a._debugIsCurrentlyTiming))return;a._debugIsCurrentlyTiming=!1;var c=a.tag===pe?"Rendering was suspended":"An error was thrown inside this error boundary";ps(a,null,c)}}function Oi(a,c){if(Pr){if(!sn||(yu(),!No(a,c)))return;nu=a,yr=c}}function gi(){if(Pr){if(!sn)return;if(yr!==null&&nu!==null){var a=X0?"Scheduled a cascading update":null;ps(nu,yr,a)}yr=null,nu=null}}function ff(a){if(Pr){if(Yn=a,!sn)return;xu=0,gl("(React Tree Reconciliation)"),Q0()}}function cf(a,c){if(Pr){if(!sn)return;var _=null;if(a!==null)if(a.tag===U)_="A top-level update interrupted the previous render";else{var T=qt(a.type)||"Unknown";_="An update to "+T+" interrupted the previous render"}else xu>1&&(_="There were cascading updates");xu=0;var R=c?"(React Tree Reconciliation: Completed Root)":"(React Tree Reconciliation: Yielded)";pi(),Mo(R,"(React Tree Reconciliation)",_)}}function J0(){if(Pr){if(!sn)return;Cu=!0,S0=!1,ko.clear(),gl("(Committing Changes)")}}function Z0(){if(Pr){if(!sn)return;var a=null;S0?a="Lifecycle hook scheduled a cascading update":xu>0&&(a="Caused by a cascading update in earlier commit"),S0=!1,xu++,Cu=!1,ko.clear(),Mo("(Committing Changes)","(Committing Changes)",a)}}function Te(){if(Pr){if(!sn)return;di=0,gl("(Committing Snapshot Effects)")}}function et(){if(Pr){if(!sn)return;var a=di;di=0,Mo("(Committing Snapshot Effects: "+a+" Total)","(Committing Snapshot Effects)",null)}}function Ve(){if(Pr){if(!sn)return;di=0,gl("(Committing Host Effects)")}}function Gt(){if(Pr){if(!sn)return;var a=di;di=0,Mo("(Committing Host Effects: "+a+" Total)","(Committing Host Effects)",null)}}function Yt(){if(Pr){if(!sn)return;di=0,gl("(Calling Lifecycle Methods)")}}function sr(){if(Pr){if(!sn)return;var a=di;di=0,Mo("(Calling Lifecycle Methods: "+a+" Total)","(Calling Lifecycle Methods)",null)}}var Br=[],wn;wn=[];var fu=-1;function Gu(a){return{current:a}}function Kr(a,c){if(fu<0){Ke(!1,"Unexpected pop.");return}c!==wn[fu]&&Ke(!1,"Unexpected Fiber popped."),a.current=Br[fu],Br[fu]=null,wn[fu]=null,fu--}function Vr(a,c,_){fu++,Br[fu]=a.current,wn[fu]=_,a.current=c}var Bu;Bu={};var Sn={};Object.freeze(Sn);var C0=Gu(Sn),Au=Gu(!1),ei=Sn;function _l(a,c,_){return ni?Sn:_&&zi(c)?ei:C0.current}function Ps(a,c,_){if(!ni){var T=a.stateNode;T.__reactInternalMemoizedUnmaskedChildContext=c,T.__reactInternalMemoizedMaskedChildContext=_}}function Uu(a,c){if(ni)return Sn;var _=a.type,T=_.contextTypes;if(!T)return Sn;var R=a.stateNode;if(R&&R.__reactInternalMemoizedUnmaskedChildContext===c)return R.__reactInternalMemoizedMaskedChildContext;var j={};for(var V in T)j[V]=c[V];{var te=qt(_)||"Unknown";E(T,j,"context",te,Rr)}return R&&Ps(a,c,j),j}function na(){return ni?!1:Au.current}function zi(a){if(ni)return!1;var c=a.childContextTypes;return c!=null}function Is(a){ni||(Kr(Au,a),Kr(C0,a))}function x0(a){ni||(Kr(Au,a),Kr(C0,a))}function Li(a,c,_){if(!ni){if(C0.current!==Sn)throw Error("Unexpected context found on stack. This error is likely caused by a bug in React. Please file an issue.");Vr(C0,c,a),Vr(Au,_,a)}}function A0(a,c,_){if(ni)return _;var T=a.stateNode,R=c.childContextTypes;if(typeof T.getChildContext!="function"){{var j=qt(c)||"Unknown";Bu[j]||(Bu[j]=!0,Ke(!1,"%s.childContextTypes is specified but there is no getChildContext() method on the instance. You can either define getChildContext() on %s or remove childContextTypes from it.",j,j))}return _}var V;Ze("getChildContext"),Oi(a,"getChildContext"),V=T.getChildContext(),gi(),Ze(null);for(var te in V)if(!(te in R))throw Error((qt(c)||"Unknown")+'.getChildContext(): key "'+te+'" is not defined in childContextTypes.');{var oe=qt(c)||"Unknown";E(R,V,"child context",oe,Rr)}return f({},_,{},V)}function Fi(a){if(ni)return!1;var c=a.stateNode,_=c&&c.__reactInternalMemoizedMergedChildContext||Sn;return ei=C0.current,Vr(C0,_,a),Vr(Au,Au.current,a),!0}function $o(a,c,_){if(!ni){var T=a.stateNode;if(!T)throw Error("Expected to have an instance by this point. This error is likely caused by a bug in React. Please file an issue.");if(_){var R=A0(a,c,ei);T.__reactInternalMemoizedMergedChildContext=R,Kr(Au,a),Kr(C0,a),Vr(C0,R,a),Vr(Au,_,a)}else Kr(Au,a),Vr(Au,_,a)}}function El(a){if(ni)return Sn;if(!(Ms(a)&&a.tag===N))throw Error("Expected subtree parent to be a mounted class component. This error is likely caused by a bug in React. Please file an issue.");var c=a;do{switch(c.tag){case U:return c.stateNode.context;case N:{var _=c.type;if(zi(_))return c.stateNode.__reactInternalMemoizedMergedChildContext;break}}c=c.return}while(c!==null);throw Error("Found unexpected detached subtree parent. This error is likely caused by a bug in React. Please file an issue.")}var I0=1,R0=2,co=t.unstable_runWithPriority,Ru=t.unstable_scheduleCallback,Yu=t.unstable_cancelCallback,Xl=t.unstable_shouldYield,hs=t.unstable_requestPaint,ra=t.unstable_now,df=t.unstable_getCurrentPriorityLevel,Ku=t.unstable_ImmediatePriority,vs=t.unstable_UserBlockingPriority,wr=t.unstable_NormalPriority,$0=t.unstable_LowPriority,Xi=t.unstable_IdlePriority;if(Ln&&!(k.__interactionsRef!=null&&k.__interactionsRef.current!=null))throw Error("It is not supported to run the profiling version of a renderer (for example, `react-dom/profiling`) without also replacing the `scheduler/tracing` module with `scheduler/tracing-profiling`. Your bundler might have a setting for aliasing both modules. Learn more at http://fb.me/react-profiling");var ru={},Ci=99,Xr=98,Wn=97,Xu=96,m0=95,y0=90,kn=Xl,se=hs!==void 0?hs:function(){},re=null,Le=null,Ae=!1,ot=ra(),vt=ot<1e4?ra:function(){return ra()-ot};function Xt(){switch(df()){case Ku:return Ci;case vs:return Xr;case wr:return Wn;case $0:return Xu;case Xi:return m0;default:throw Error("Unknown priority level.")}}function xn(a){switch(a){case Ci:return Ku;case Xr:return vs;case Wn:return wr;case Xu:return $0;case m0:return Xi;default:throw Error("Unknown priority level.")}}function _n(a,c){var _=xn(a);return co(_,c)}function yn(a,c,_){var T=xn(a);return Ru(T,c,_)}function En(a){return re===null?(re=[a],Le=Ru(Ku,xi)):re.push(a),ru}function er(a){a!==ru&&Yu(a)}function It(){if(Le!==null){var a=Le;Le=null,Yu(a)}xi()}function xi(){if(!Ae&&re!==null){Ae=!0;var a=0;try{var c=!0,_=re;_n(Ci,function(){for(;a<_.length;a++){var T=_[a];do T=T(c);while(T!==null)}}),re=null}catch(T){throw re!==null&&(re=re.slice(a+1)),Ru(Ku,It),T}finally{Ae=!1}}}var Sr=0,cr=1,Y=2,Qr=4,Jr=8,Ur=1073741823,lt=0,hi=1,Qi=2,g0=3,bn=Ur,Qu=bn-1,eo=10,po=Qu-1;function Ju(a){return po-(a/eo|0)}function bo(a){return(po-a)*eo}function to(a,c){return((a/c|0)+1)*c}function Na(a,c,_){return po-to(po-a+c/eo,_/eo)}var pf=5e3,uc=250;function ms(a){return Na(a,pf,uc)}function ia(a,c){return Na(a,c,uc)}var B0=500,oc=100;function La(a){return Na(a,B0,oc)}function gd(a){return g0++}function $1(a,c){if(c===bn)return Ci;if(c===hi||c===Qi)return m0;var _=bo(c)-bo(a);return _<=0?Ci:_<=B0+oc?Xr:_<=pf+uc?Wn:m0}function e2(a,c){return a===c&&(a!==0||1/a==1/c)||a!==a&&c!==c}var ho=typeof Object.is=="function"?Object.is:e2,Uc=Object.prototype.hasOwnProperty;function Dl(a,c){if(ho(a,c))return!0;if(typeof a!="object"||a===null||typeof c!="object"||c===null)return!1;var _=Object.keys(a),T=Object.keys(c);if(_.length!==T.length)return!1;for(var R=0;R<_.length;R++)if(!Uc.call(c,_[R])||!ho(a[_[R]],c[_[R]]))return!1;return!0}var el=function(){};{var _d=function(a){for(var c=arguments.length,_=new Array(c>1?c-1:0),T=1;T2?_-2:0),R=2;R<_;R++)T[R-2]=arguments[R];_d.apply(void 0,[c].concat(T))}}}var Bs=el,wl={recordUnsafeLifecycleWarnings:function(a,c){},flushPendingUnsafeLifecycleWarnings:function(){},recordLegacyContextWarning:function(a,c){},flushLegacyContextWarning:function(){},discardPendingWarnings:function(){}};{var t2=function(a){for(var c=null,_=a;_!==null;)_.mode&cr&&(c=_),_=_.return;return c},Po=function(a){var c=[];return a.forEach(function(_){c.push(_)}),c.sort().join(", ")},Fa=[],ba=[],Pa=[],ua=[],ys=[],gs=[],Ql=new Set;wl.recordUnsafeLifecycleWarnings=function(a,c){Ql.has(a.type)||(typeof c.componentWillMount=="function"&&c.componentWillMount.__suppressDeprecationWarning!==!0&&Fa.push(a),a.mode&cr&&typeof c.UNSAFE_componentWillMount=="function"&&ba.push(a),typeof c.componentWillReceiveProps=="function"&&c.componentWillReceiveProps.__suppressDeprecationWarning!==!0&&Pa.push(a),a.mode&cr&&typeof c.UNSAFE_componentWillReceiveProps=="function"&&ua.push(a),typeof c.componentWillUpdate=="function"&&c.componentWillUpdate.__suppressDeprecationWarning!==!0&&ys.push(a),a.mode&cr&&typeof c.UNSAFE_componentWillUpdate=="function"&&gs.push(a))},wl.flushPendingUnsafeLifecycleWarnings=function(){var a=new Set;Fa.length>0&&(Fa.forEach(function(Nt){a.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),Fa=[]);var c=new Set;ba.length>0&&(ba.forEach(function(Nt){c.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),ba=[]);var _=new Set;Pa.length>0&&(Pa.forEach(function(Nt){_.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),Pa=[]);var T=new Set;ua.length>0&&(ua.forEach(function(Nt){T.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),ua=[]);var R=new Set;ys.length>0&&(ys.forEach(function(Nt){R.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),ys=[]);var j=new Set;if(gs.length>0&&(gs.forEach(function(Nt){j.add(qt(Nt.type)||"Component"),Ql.add(Nt.type)}),gs=[]),c.size>0){var V=Po(c);Ke(!1,`Using UNSAFE_componentWillMount in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move code with side effects to componentDidMount, and set initial state in the constructor. + +Please update the following components: %s`,V)}if(T.size>0){var te=Po(T);Ke(!1,`Using UNSAFE_componentWillReceiveProps in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move data fetching code or side effects to componentDidUpdate. +* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state + +Please update the following components: %s`,te)}if(j.size>0){var oe=Po(j);Ke(!1,`Using UNSAFE_componentWillUpdate in strict mode is not recommended and may indicate bugs in your code. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move data fetching code or side effects to componentDidUpdate. + +Please update the following components: %s`,oe)}if(a.size>0){var Ie=Po(a);Bs(!1,`componentWillMount has been renamed, and is not recommended for use. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move code with side effects to componentDidMount, and set initial state in the constructor. +* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder. + +Please update the following components: %s`,Ie)}if(_.size>0){var Ye=Po(_);Bs(!1,`componentWillReceiveProps has been renamed, and is not recommended for use. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move data fetching code or side effects to componentDidUpdate. +* If you're updating state whenever props change, refactor your code to use memoization techniques or move it to static getDerivedStateFromProps. Learn more at: https://fb.me/react-derived-state +* Rename componentWillReceiveProps to UNSAFE_componentWillReceiveProps to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder. + +Please update the following components: %s`,Ye)}if(R.size>0){var pt=Po(R);Bs(!1,`componentWillUpdate has been renamed, and is not recommended for use. See https://fb.me/react-unsafe-component-lifecycles for details. + +* Move data fetching code or side effects to componentDidUpdate. +* Rename componentWillUpdate to UNSAFE_componentWillUpdate to suppress this warning in non-strict mode. In React 17.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run \`npx react-codemod rename-unsafe-lifecycles\` in your project source folder. + +Please update the following components: %s`,pt)}};var Io=new Map,hf=new Set;wl.recordLegacyContextWarning=function(a,c){var _=t2(a);if(_===null){Ke(!1,"Expected to find a StrictMode component in a strict mode tree. This error is likely caused by a bug in React. Please file an issue.");return}if(!hf.has(a.type)){var T=Io.get(_);(a.type.contextTypes!=null||a.type.childContextTypes!=null||c!==null&&typeof c.getChildContext=="function")&&(T===void 0&&(T=[],Io.set(_,T)),T.push(a))}},wl.flushLegacyContextWarning=function(){Io.forEach(function(a,c){var _=new Set;a.forEach(function(j){_.add(qt(j.type)||"Component"),hf.add(j.type)});var T=Po(_),R=_r(c);Ke(!1,`Legacy context API has been detected within a strict-mode tree. + +The old API will be supported in all 16.x releases, but applications using it should migrate to the new version. + +Please update the following components: %s + +Learn more about this warning here: https://fb.me/react-legacy-context%s`,T,R)})},wl.discardPendingWarnings=function(){Fa=[],ba=[],Pa=[],ua=[],ys=[],gs=[],Io=new Map}}var tl=null,ju=null,Ia=function(a){tl=a};function Zu(a){{if(tl===null)return a;var c=tl(a);return c===void 0?a:c.current}}function U0(a){return Zu(a)}function vf(a){{if(tl===null)return a;var c=tl(a);if(c===void 0){if(a!=null&&typeof a.render=="function"){var _=Zu(a.render);if(a.render!==_){var T={$$typeof:An,render:_};return a.displayName!==void 0&&(T.displayName=a.displayName),T}}return a}return c.current}}function jc(a,c){{if(tl===null)return!1;var _=a.elementType,T=c.type,R=!1,j=typeof T=="object"&&T!==null?T.$$typeof:null;switch(a.tag){case N:{typeof T=="function"&&(R=!0);break}case L:{(typeof T=="function"||j===vr)&&(R=!0);break}case ge:{(j===An||j===vr)&&(R=!0);break}case Oe:case le:{(j===Wt||j===vr)&&(R=!0);break}default:return!1}if(R){var V=tl(_);if(V!==void 0&&V===tl(T))return!0}return!1}}function lc(a){{if(tl===null||typeof WeakSet!="function")return;ju===null&&(ju=new WeakSet),ju.add(a)}}var Sl=function(a,c){{if(tl===null)return;var _=c.staleFamilies,T=c.updatedFamilies;Xa(),xp(function(){oa(a.current,T,_)})}},_s=function(a,c){{if(a.context!==Sn)return;Xa(),fv(function(){l_(c,a,null,null)})}};function oa(a,c,_){{var T=a.alternate,R=a.child,j=a.sibling,V=a.tag,te=a.type,oe=null;switch(V){case L:case le:case N:oe=te;break;case ge:oe=te.render;break;default:break}if(tl===null)throw new Error("Expected resolveFamily to be set during hot reload.");var Ie=!1,Ye=!1;if(oe!==null){var pt=tl(oe);pt!==void 0&&(_.has(pt)?Ye=!0:c.has(pt)&&(V===N?Ye=!0:Ie=!0))}ju!==null&&(ju.has(a)||T!==null&&ju.has(T))&&(Ye=!0),Ye&&(a._debugNeedsRemount=!0),(Ye||Ie)&&dl(a,bn),R!==null&&!Ye&&oa(R,c,_),j!==null&&oa(j,c,_)}}var n2=function(a,c){{var _=new Set,T=new Set(c.map(function(R){return R.current}));return la(a.current,T,_),_}};function la(a,c,_){{var T=a.child,R=a.sibling,j=a.tag,V=a.type,te=null;switch(j){case L:case le:case N:te=V;break;case ge:te=V.render;break;default:break}var oe=!1;te!==null&&c.has(te)&&(oe=!0),oe?sc(a,_):T!==null&&la(T,c,_),R!==null&&la(R,c,_)}}function sc(a,c){{var _=zc(a,c);if(_)return;for(var T=a;;){switch(T.tag){case W:c.add(T.stateNode);return;case q:c.add(T.stateNode.containerInfo);return;case U:c.add(T.stateNode.containerInfo);return}if(T.return===null)throw new Error("Expected to reach root first.");T=T.return}}}function zc(a,c){for(var _=a,T=!1;;){if(_.tag===W)T=!0,c.add(_.stateNode);else if(_.child!==null){_.child.return=_,_=_.child;continue}if(_===a)return T;for(;_.sibling===null;){if(_.return===null||_.return===a)return T;_=_.return}_.sibling.return=_.return,_=_.sibling}return!1}function bi(a,c){if(a&&a.defaultProps){var _=f({},c),T=a.defaultProps;for(var R in T)_[R]===void 0&&(_[R]=T[R]);return _}return c}function g(a){if(Yo(a),a._status!==So)throw a._result;return a._result}var y=Gu(null),A;A={};var F=null,I=null,J=null,fe=!1;function mt(){F=null,I=null,J=null,fe=!1}function Ct(){fe=!0}function Mt(){fe=!1}function Er(a,c){var _=a.type._context;Fu?(Vr(y,_._currentValue,a),_._currentValue=c,_._currentRenderer===void 0||_._currentRenderer===null||_._currentRenderer===A||Ke(!1,"Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported."),_._currentRenderer=A):(Vr(y,_._currentValue2,a),_._currentValue2=c,_._currentRenderer2===void 0||_._currentRenderer2===null||_._currentRenderer2===A||Ke(!1,"Detected multiple renderers concurrently rendering the same context provider. This is currently unsupported."),_._currentRenderer2=A)}function $u(a){var c=y.current;Kr(y,a);var _=a.type._context;Fu?_._currentValue=c:_._currentValue2=c}function iu(a,c,_){if(ho(_,c))return 0;var T=typeof a._calculateChangedBits=="function"?a._calculateChangedBits(_,c):Ur;return(T&Ur)!==T&&Kt(!1,"calculateChangedBits: Expected the return value to be a 31-bit integer. Instead received: %s",T),T|0}function j0(a,c){for(var _=a;_!==null;){var T=_.alternate;if(_.childExpirationTime=c&&up(),_.firstContext=null)}}function He(a,c){if(fe&&Kt(!1,"Context can only be read while React is rendering. In classes, you can read it in the render method or getDerivedStateFromProps. In function components, you can read it directly in the function body, but not inside Hooks like useReducer() or useMemo()."),J!==a){if(!(c===!1||c===0)){var _;typeof c!="number"||c===Ur?(J=a,_=Ur):_=c;var T={context:a,observedBits:_,next:null};if(I===null){if(F===null)throw Error("Context can only be read while React is rendering. In classes, you can read it in the render method or getDerivedStateFromProps. In function components, you can read it directly in the function body, but not inside Hooks like useReducer() or useMemo().");I=T,F.dependencies={expirationTime:lt,firstContext:T,responders:null}}else I=I.next=T}}return Fu?a._currentValue:a._currentValue2}var Be=0,ut=1,Jt=2,jn=3,ti=!1,tr,ii;tr=!1,ii=null;function qi(a){var c={baseState:a,firstUpdate:null,lastUpdate:null,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null};return c}function jr(a){var c={baseState:a.baseState,firstUpdate:a.firstUpdate,lastUpdate:a.lastUpdate,firstCapturedUpdate:null,lastCapturedUpdate:null,firstEffect:null,lastEffect:null,firstCapturedEffect:null,lastCapturedEffect:null};return c}function gu(a,c){var _={expirationTime:a,suspenseConfig:c,tag:Be,payload:null,callback:null,next:null,nextEffect:null};return _.priority=Xt(),_}function Ba(a,c){a.lastUpdate===null?a.firstUpdate=a.lastUpdate=c:(a.lastUpdate.next=c,a.lastUpdate=c)}function Ua(a,c){var _=a.alternate,T,R;_===null?(T=a.updateQueue,R=null,T===null&&(T=a.updateQueue=qi(a.memoizedState))):(T=a.updateQueue,R=_.updateQueue,T===null?R===null?(T=a.updateQueue=qi(a.memoizedState),R=_.updateQueue=qi(_.memoizedState)):T=a.updateQueue=jr(R):R===null&&(R=_.updateQueue=jr(T))),R===null||T===R?Ba(T,c):T.lastUpdate===null||R.lastUpdate===null?(Ba(T,c),Ba(R,c)):(Ba(T,c),R.lastUpdate=c),a.tag===N&&(ii===T||R!==null&&ii===R)&&!tr&&(Ke(!1,"An update (setState, replaceState, or forceUpdate) was scheduled from inside an update function. Update functions should be pure, with zero side-effects. Consider using componentDidUpdate or a callback."),tr=!0)}function r2(a,c){var _=a.updateQueue;_===null?_=a.updateQueue=qi(a.memoizedState):_=Ed(a,_),_.lastCapturedUpdate===null?_.firstCapturedUpdate=_.lastCapturedUpdate=c:(_.lastCapturedUpdate.next=c,_.lastCapturedUpdate=c)}function Ed(a,c){var _=a.alternate;return _!==null&&c===_.updateQueue&&(c=a.updateQueue=jr(c)),c}function Dd(a,c,_,T,R,j){switch(_.tag){case ut:{var V=_.payload;if(typeof V=="function"){Ct(),Ei&&a.mode&cr&&V.call(j,T,R);var te=V.call(j,T,R);return Mt(),te}return V}case jn:a.effectTag=a.effectTag&~f0|Hr;case Be:{var oe=_.payload,Ie;return typeof oe=="function"?(Ct(),Ei&&a.mode&cr&&oe.call(j,T,R),Ie=oe.call(j,T,R),Mt()):Ie=oe,Ie==null?T:f({},T,Ie)}case Jt:return ti=!0,T}return T}function mf(a,c,_,T,R){ti=!1,c=Ed(a,c),ii=c;for(var j=c.baseState,V=null,te=lt,oe=c.firstUpdate,Ie=j;oe!==null;){var Ye=oe.expirationTime;if(Ye from render. Or maybe you meant to call this function rather than return it."))}function yh(a){function c(it,Ot){if(!!a){var Je=it.lastEffect;Je!==null?(Je.nextEffect=Ot,it.lastEffect=Ot):it.firstEffect=it.lastEffect=Ot,Ot.nextEffect=null,Ot.effectTag=W0}}function _(it,Ot){if(!a)return null;for(var Je=Ot;Je!==null;)c(it,Je),Je=Je.sibling;return null}function T(it,Ot){for(var Je=new Map,Bt=Ot;Bt!==null;)Bt.key!==null?Je.set(Bt.key,Bt):Je.set(Bt.index,Bt),Bt=Bt.sibling;return Je}function R(it,Ot,Je){var Bt=wo(it,Ot,Je);return Bt.index=0,Bt.sibling=null,Bt}function j(it,Ot,Je){if(it.index=Je,!a)return Ot;var Bt=it.alternate;if(Bt!==null){var Mn=Bt.index;return Mnqr?(_u=ar,ar=null):_u=ar.sibling;var _0=Nt(it,ar,Je[qr],Bt);if(_0===null){ar===null&&(ar=_u);break}a&&ar&&_0.alternate===null&&c(it,ar),ou=j(_0,ou,qr),qu===null?oi=_0:qu.sibling=_0,qu=_0,ar=_u}if(qr===Je.length)return _(it,ar),oi;if(ar===null){for(;qrH0?(Cs=_u,_u=null):Cs=_u.sibling;var pl=Nt(it,_u,Hu.value,Bt);if(pl===null){_u===null&&(_u=Cs);break}a&&_u&&pl.alternate===null&&c(it,_u),_0=j(pl,_0,H0),qr===null?ou=pl:qr.sibling=pl,qr=pl,_u=Cs}if(Hu.done)return _(it,_u),ou;if(_u===null){for(;!Hu.done;H0++,Hu=ar.next()){var Ja=pt(it,Hu.value,Bt);Ja!==null&&(_0=j(Ja,_0,H0),qr===null?ou=Ja:qr.sibling=Ja,qr=Ja)}return ou}for(var jo=T(it,_u);!Hu.done;H0++,Hu=ar.next()){var xs=Vt(jo,it,H0,Hu.value,Bt);xs!==null&&(a&&xs.alternate!==null&&jo.delete(xs.key===null?H0:xs.key),_0=j(xs,_0,H0),qr===null?ou=xs:qr.sibling=xs,qr=xs)}return a&&jo.forEach(function(X2){return c(it,X2)}),ou}function $r(it,Ot,Je,Bt){if(Ot!==null&&Ot.tag===ne){_(it,Ot.sibling);var Mn=R(Ot,Je,Bt);return Mn.return=it,Mn}_(it,Ot);var pn=Cy(Je,it.mode,Bt);return pn.return=it,pn}function wi(it,Ot,Je,Bt){for(var Mn=Je.key,pn=Ot;pn!==null;){if(pn.key===Mn)if(pn.tag===m?Je.type===ue:pn.elementType===Je.type||jc(pn,Je)){_(it,pn.sibling);var Pi=R(pn,Je.type===ue?Je.props.children:Je.props,Bt);return Pi.ref=fc(it,pn,Je),Pi.return=it,Pi._debugSource=Je._source,Pi._debugOwner=Je._owner,Pi}else{_(it,pn);break}else c(it,pn);pn=pn.sibling}if(Je.type===ue){var oi=Qa(Je.props.children,it.mode,Bt,Je.key);return oi.return=it,oi}else{var qu=Ty(Je,it.mode,Bt);return qu.ref=fc(it,Ot,Je),qu.return=it,qu}}function N0(it,Ot,Je,Bt){for(var Mn=Je.key,pn=Ot;pn!==null;){if(pn.key===Mn)if(pn.tag===q&&pn.stateNode.containerInfo===Je.containerInfo&&pn.stateNode.implementation===Je.implementation){_(it,pn.sibling);var Pi=R(pn,Je.children||[],Bt);return Pi.return=it,Pi}else{_(it,pn);break}else c(it,pn);pn=pn.sibling}var oi=xy(Je,it.mode,Bt);return oi.return=it,oi}function Vi(it,Ot,Je,Bt){var Mn=typeof Je=="object"&&Je!==null&&Je.type===ue&&Je.key===null;Mn&&(Je=Je.props.children);var pn=typeof Je=="object"&&Je!==null;if(pn)switch(Je.$$typeof){case ae:return V(wi(it,Ot,Je,Bt));case Ce:return V(N0(it,Ot,Je,Bt))}if(typeof Je=="string"||typeof Je=="number")return V($r(it,Ot,""+Je,Bt));if(Kc(Je))return vn(it,Ot,Je,Bt);if(ur(Je))return xr(it,Ot,Je,Bt);if(pn&&cc(it,Je),typeof Je=="function"&&f2(),typeof Je=="undefined"&&!Mn)switch(it.tag){case N:{var Pi=it.stateNode;if(Pi.render._isMockFunction)break}case L:{var oi=it.type;throw Error((oi.displayName||oi.name||"Component")+"(...): Nothing was returned from render. This usually means a return statement is missing. Or, to render nothing, return null.")}}return _(it,Ot)}return Vi}var gf=yh(!0),Xc=yh(!1);function gh(a,c){if(!(a===null||c.child===a.child))throw Error("Resuming work not yet implemented.");if(c.child!==null){var _=c.child,T=wo(_,_.pendingProps,_.expirationTime);for(c.child=T,T.return=c;_.sibling!==null;)_=_.sibling,T=T.sibling=wo(_,_.pendingProps,_.expirationTime),T.return=c;T.sibling=null}}function vm(a,c){for(var _=a.child;_!==null;)Rv(_,c),_=_.sibling}var js={},fa=Gu(js),Ji=Gu(js),O0=Gu(js);function t0(a){if(a===js)throw Error("Expected host context to exist. This error is likely caused by a bug in React. Please file an issue.");return a}function Jl(){var a=t0(O0.current);return a}function za(a,c){Vr(O0,c,a),Vr(Ji,a,a),Vr(fa,js,a);var _=Et(c);Kr(fa,a),Vr(fa,_,a)}function no(a){Kr(fa,a),Kr(Ji,a),Kr(O0,a)}function ul(){var a=t0(fa.current);return a}function dc(a){var c=t0(O0.current),_=t0(fa.current),T=Pt(_,a.type,c);_!==T&&(Vr(Ji,a,a),Vr(fa,T,a))}function Od(a){Ji.current===a&&(Kr(fa,a),Kr(Ji,a))}var _h=0,_f=1,Ef=1,Qc=2,xl=Gu(_h);function Jc(a,c){return(a&c)!=0}function ca(a){return a&_f}function c2(a,c){return a&_f|c}function d2(a,c){return a|c}function Or(a,c){Vr(xl,c,a)}function da(a){Kr(xl,a)}function kd(a,c){var _=a.memoizedState;if(_!==null)return _.dehydrated!==null;var T=a.memoizedProps;return T.fallback===void 0?!1:T.unstable_avoidThisFallback!==!0?!0:!c}function Zc(a){for(var c=a;c!==null;){if(c.tag===pe){var _=c.memoizedState;if(_!==null){var T=_.dehydrated;if(T===null||Ls(T)||h0(T))return c}}else if(c.tag===wt&&c.memoizedProps.revealOrder!==void 0){var R=(c.effectTag&Hr)!==_i;if(R)return c}else if(c.child!==null){c.child.return=c,c=c.child;continue}if(c===a)return null;for(;c.sibling===null;){if(c.return===null||c.return===a)return null;c=c.return}c.sibling.return=c.return,c=c.sibling}return null}var p2={},vi=Array.isArray;function Md(a,c,_,T){return{fiber:T,props:c,responder:a,rootEventTypes:null,state:_}}function mm(a,c,_,T,R){var j=p2,V=a.getInitialState;V!==null&&(j=V(c));var te=Md(a,c,j,_);if(!R)for(var oe=_;oe!==null;){var Ie=oe.tag;if(Ie===W){R=oe.stateNode;break}else if(Ie===U){R=oe.stateNode.containerInfo;break}oe=oe.return}Ne(a,te,c,j,R),T.set(a,te)}function h2(a,c,_,T,R){var j,V;if(a&&(j=a.responder,V=a.props),!(j&&j.$$typeof===Ut))throw Error("An invalid value was used as an event listener. Expect one or many event listeners created via React.unstable_useResponder().");var te=V;if(_.has(j)){Kt(!1,'Duplicate event responder "%s" found in event listeners. Event listeners passed to elements cannot use the same event responder more than once.',j.displayName);return}_.add(j);var oe=T.get(j);oe===void 0?mm(j,te,c,T,R):(oe.props=te,oe.fiber=c)}function dn(a,c,_){var T=new Set,R=c.dependencies;if(a!=null){R===null&&(R=c.dependencies={expirationTime:lt,firstContext:null,responders:new Map});var j=R.responders;if(j===null&&(j=new Map),vi(a))for(var V=0,te=a.length;V0){var j=R.dispatch;if(Es!==null){var V=Es.get(R);if(V!==void 0){Es.delete(R);var te=T.memoizedState,oe=V;do{var Ie=oe.action;te=a(te,Ie),oe=oe.next}while(oe!==null);return ho(te,T.memoizedState)||up(),T.memoizedState=te,T.baseUpdate===R.last&&(T.baseState=te),R.lastRenderedState=te,[te,j]}}return[T.memoizedState,j]}var Ye=R.last,pt=T.baseUpdate,Nt=T.baseState,Vt;if(pt!==null?(Ye!==null&&(Ye.next=null),Vt=pt.next):Vt=Ye!==null?Ye.next:null,Vt!==null){var zt=Nt,vn=null,xr=null,$r=pt,wi=Vt,N0=!1;do{var Vi=wi.expirationTime;if(ViOu&&(Ou=Vi,G2(Ou));else if(vv(Vi,wi.suspenseConfig),wi.eagerReducer===a)zt=wi.eagerState;else{var it=wi.action;zt=a(zt,it)}$r=wi,wi=wi.next}while(wi!==null&&wi!==Vt);N0||(xr=$r,vn=zt),ho(zt,T.memoizedState)||up(),T.memoizedState=zt,T.baseUpdate=xr,T.baseState=vn,R.lastRenderedState=zt}var Ot=R.dispatch;return[T.memoizedState,Ot]}function Rf(a){var c=mc();typeof a=="function"&&(a=a()),c.memoizedState=c.baseState=a;var _=c.queue={last:null,dispatch:null,lastRenderedReducer:Nd,lastRenderedState:a},T=_.dispatch=u1.bind(null,ll,_);return[c.memoizedState,T]}function n1(a){return t1(Nd,a)}function Wa(a,c,_,T){var R={tag:a,create:c,destroy:_,deps:T,next:null};if(Zl===null)Zl=Ha(),Zl.lastEffect=R.next=R;else{var j=Zl.lastEffect;if(j===null)Zl.lastEffect=R.next=R;else{var V=j.next;j.next=R,R.next=V,Zl.lastEffect=R}}return R}function r1(a){var c=mc(),_={current:a};return Object.seal(_),c.memoizedState=_,_}function Ld(a){var c=e1();return c.memoizedState}function g2(a,c,_,T){var R=mc(),j=T===void 0?null:T;Tf|=a,R.memoizedState=Wa(c,_,void 0,j)}function yc(a,c,_,T){var R=e1(),j=T===void 0?null:T,V=void 0;if(Pn!==null){var te=Pn.memoizedState;if(V=te.destroy,j!==null){var oe=te.deps;if(xf(j,oe)){Wa(wf,_,V,j);return}}}Tf|=a,R.memoizedState=Wa(c,_,V,j)}function i1(a,c){return typeof jest!="undefined"&&Av(ll),g2(mr|L0,rr|$c,a,c)}function Rl(a,c){return typeof jest!="undefined"&&Av(ll),yc(mr|L0,rr|$c,a,c)}function pa(a,c){return g2(mr,Sf|ol,a,c)}function wh(a,c){return yc(mr,Sf|ol,a,c)}function Fd(a,c){if(typeof c=="function"){var _=c,T=a();return _(T),function(){_(null)}}else if(c!=null){var R=c;R.hasOwnProperty("current")||Kt(!1,"Expected useImperativeHandle() first argument to either be a ref callback or React.createRef() object. Instead received: %s.","an object with keys {"+Object.keys(R).join(", ")+"}");var j=a();return R.current=j,function(){R.current=null}}}function bd(a,c,_){typeof c!="function"&&Kt(!1,"Expected useImperativeHandle() second argument to be a function that creates a handle. Instead received: %s.",c!==null?typeof c:"null");var T=_!=null?_.concat([a]):null;return g2(mr,Sf|ol,Fd.bind(null,c,a),T)}function Sh(a,c,_){typeof c!="function"&&Kt(!1,"Expected useImperativeHandle() second argument to be a function that creates a handle. Instead received: %s.",c!==null?typeof c:"null");var T=_!=null?_.concat([a]):null;return yc(mr,Sf|ol,Fd.bind(null,c,a),T)}function _2(a,c){}var Th=_2;function Ol(a,c){var _=mc(),T=c===void 0?null:c;return _.memoizedState=[a,T],a}function es(a,c){var _=e1(),T=c===void 0?null:c,R=_.memoizedState;if(R!==null&&T!==null){var j=R[1];if(xf(T,j))return R[0]}return _.memoizedState=[a,T],a}function Ds(a,c){var _=mc(),T=c===void 0?null:c,R=a();return _.memoizedState=[R,T],R}function zs(a,c){var _=e1(),T=c===void 0?null:c,R=_.memoizedState;if(R!==null&&T!==null){var j=R[1];if(xf(T,j))return R[0]}var V=a();return _.memoizedState=[V,T],V}function Pd(a,c){var _=Rf(a),T=_[0],R=_[1];return i1(function(){t.unstable_next(function(){var j=Bo.suspense;Bo.suspense=c===void 0?null:c;try{R(a)}finally{Bo.suspense=j}})},[a,c]),T}function Ch(a,c){var _=n1(a),T=_[0],R=_[1];return Rl(function(){t.unstable_next(function(){var j=Bo.suspense;Bo.suspense=c===void 0?null:c;try{R(a)}finally{Bo.suspense=j}})},[a,c]),T}function Id(a){var c=Rf(!1),_=c[0],T=c[1],R=Ol(function(j){T(!0),t.unstable_next(function(){var V=Bo.suspense;Bo.suspense=a===void 0?null:a;try{T(!1),j()}finally{Bo.suspense=V}})},[a,_]);return[R,_]}function Bd(a){var c=n1(!1),_=c[0],T=c[1],R=es(function(j){T(!0),t.unstable_next(function(){var V=Bo.suspense;Bo.suspense=a===void 0?null:a;try{T(!1),j()}finally{Bo.suspense=V}})},[a,_]);return[R,_]}function u1(a,c,_){if(!(vc=0){var _=l1()-s1;a.actualDuration+=_,c&&(a.selfBaseDuration=_),s1=-1}}var Ml=null,Ga=null,ha=!1;function qd(){ha&&Kt(!1,"We should not be hydrating here. This is a bug in React. Please file a bug.")}function Hd(a){if(!_e)return!1;var c=a.stateNode.containerInfo;return Ga=B(c),Ml=a,ha=!0,!0}function Em(a,c){return _e?(Ga=Ni(c),Gd(a),ha=!0,!0):!1}function Wd(a,c){switch(a.tag){case U:ie(a.stateNode.containerInfo,c);break;case W:qe(a.type,a.memoizedProps,a.stateNode,c);break}var _=eE();_.stateNode=c,_.return=a,_.effectTag=W0,a.lastEffect!==null?(a.lastEffect.nextEffect=_,a.lastEffect=_):a.firstEffect=a.lastEffect=_}function Mh(a,c){switch(c.effectTag=c.effectTag&~tu|ai,a.tag){case U:{var _=a.stateNode.containerInfo;switch(c.tag){case W:var T=c.type,R=c.pendingProps;tt(_,T,R);break;case ne:var j=c.pendingProps;Tt(_,j);break;case pe:kt(_);break}break}case W:{var V=a.type,te=a.memoizedProps,oe=a.stateNode;switch(c.tag){case W:var Ie=c.type,Ye=c.pendingProps;bt(V,te,oe,Ie,Ye);break;case ne:var pt=c.pendingProps;on(V,te,oe,pt);break;case pe:tn(V,te,oe);break}break}default:return}}function Nh(a,c){switch(a.tag){case W:{var _=a.type,T=a.pendingProps,R=lf(c,_,T);return R!==null?(a.stateNode=R,!0):!1}case ne:{var j=a.pendingProps,V=Ns(c,j);return V!==null?(a.stateNode=V,!0):!1}case pe:{if(Di){var te=Ma(c);if(te!==null){var oe={dehydrated:te,retryTime:hi};a.memoizedState=oe;var Ie=tE(te);return Ie.return=a,a.child=Ie,!0}}return!1}default:return!1}}function Vd(a){if(!!ha){var c=Ga;if(!c){Mh(Ml,a),ha=!1,Ml=a;return}var _=c;if(!Nh(a,c)){if(c=Ni(_),!c||!Nh(a,c)){Mh(Ml,a),ha=!1,Ml=a;return}Wd(Ml,_)}Ml=a,Ga=B(c)}}function Dm(a,c,_){if(!_e)throw Error("Expected prepareToHydrateHostInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.");var T=a.stateNode,R=z(T,a.type,a.memoizedProps,c,_,a);return a.updateQueue=R,R!==null}function wm(a){if(!_e)throw Error("Expected prepareToHydrateHostTextInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.");var c=a.stateNode,_=a.memoizedProps,T=G(c,_,a);if(T){var R=Ml;if(R!==null)switch(R.tag){case U:{var j=R.stateNode.containerInfo;Xe(j,c,_);break}case W:{var V=R.type,te=R.memoizedProps,oe=R.stateNode;ht(V,te,oe,c,_);break}}}return T}function Lh(a){if(!_e)throw Error("Expected prepareToHydrateHostSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.");var c=a.memoizedState,_=c!==null?c.dehydrated:null;if(!_)throw Error("Expected to have a hydrated suspense instance. This error is likely caused by a bug in React. Please file an issue.");$(_,a)}function Sm(a){if(!_e)throw Error("Expected skipPastDehydratedSuspenseInstance() to never be called. This error is likely caused by a bug in React. Please file an issue.");var c=a.memoizedState,_=c!==null?c.dehydrated:null;if(!_)throw Error("Expected to have a hydrated suspense instance. This error is likely caused by a bug in React. Please file an issue.");return De(_)}function Gd(a){for(var c=a.return;c!==null&&c.tag!==W&&c.tag!==U&&c.tag!==pe;)c=c.return;Ml=c}function f1(a){if(!_e||a!==Ml)return!1;if(!ha)return Gd(a),ha=!0,!1;var c=a.type;if(a.tag!==W||c!=="head"&&c!=="body"&&!Ti(c,a.memoizedProps))for(var _=Ga;_;)Wd(a,_),_=Ni(_);return Gd(a),a.tag===pe?Ga=Sm(a):Ga=Ml?Ni(a.stateNode):null,!0}function c1(){!_e||(Ml=null,Ga=null,ha=!1)}var d1=at.ReactCurrentOwner,va=!1,Yd,qs,Hs,Ws,Kd,ma,p1,E2,gc,Xd;Yd={},qs={},Hs={},Ws={},Kd={},ma=!1,p1=!1,E2={},gc={},Xd={};function _o(a,c,_,T){a===null?c.child=Xc(c,null,_,T):c.child=gf(c,a.child,_,T)}function Fh(a,c,_,T){c.child=gf(c,a.child,null,T),c.child=gf(c,null,_,T)}function bh(a,c,_,T,R){if(c.type!==c.elementType){var j=_.propTypes;j&&E(j,T,"prop",qt(_),Rr)}var V=_.render,te=c.ref,oe;return e0(c,R),d1.current=c,Ze("render"),oe=Af(a,c,V,T,te,R),Ei&&c.mode&cr&&c.memoizedState!==null&&(oe=Af(a,c,V,T,te,R)),Ze(null),a!==null&&!va?(v2(a,c,R),ya(a,c,R)):(c.effectTag|=eu,_o(a,c,oe,R),c.child)}function Ph(a,c,_,T,R,j){if(a===null){var V=_.type;if(i0(V)&&_.compare===null&&_.defaultProps===void 0){var te=V;return te=Zu(V),c.tag=le,c.type=te,Zd(c,V),Ih(a,c,te,T,R,j)}{var oe=V.propTypes;oe&&E(oe,T,"prop",qt(V),Rr)}var Ie=Sy(_.type,null,T,null,c.mode,j);return Ie.ref=c.ref,Ie.return=c,c.child=Ie,Ie}{var Ye=_.type,pt=Ye.propTypes;pt&&E(pt,T,"prop",qt(Ye),Rr)}var Nt=a.child;if(R component appears to have a render method, but doesn't extend React.Component. This is likely to cause errors. Change %s to extend React.Component instead.",oe,oe),Yd[oe]=!0)}c.mode&cr&&wl.recordLegacyContextWarning(c,null),d1.current=c,te=Af(null,c,_,R,j,T)}if(c.effectTag|=eu,typeof te=="object"&&te!==null&&typeof te.render=="function"&&te.$$typeof===void 0){{var Ie=qt(_)||"Unknown";qs[Ie]||(Ke(!1,"The <%s /> component appears to be a function component that returns a class instance. Change %s to a class that extends React.Component instead. If you can't use a class try assigning the prototype on the function as a workaround. `%s.prototype = React.Component.prototype`. Don't use an arrow function since it cannot be called with `new` by React.",Ie,Ie,Ie),qs[Ie]=!0)}c.tag=N,m2();var Ye=!1;zi(_)?(Ye=!0,Fi(c)):Ye=!1,c.memoizedState=te.state!==null&&te.state!==void 0?te.state:null;var pt=_.getDerivedStateFromProps;return typeof pt=="function"&&yf(c,_,pt,R),il(c,te),ac(c,_,R,T),Jd(null,c,_,!0,Ye,T)}else return c.tag=L,ni&&_.contextTypes&&Ke(!1,"%s uses the legacy contextTypes API which is no longer supported. Use React.createContext() with React.useContext() instead.",qt(_)||"Unknown"),Ei&&c.mode&cr&&c.memoizedState!==null&&(te=Af(null,c,_,R,j,T)),_o(null,c,te,T),Zd(c,_),c.child}function Zd(a,c){if(c&&c.childContextTypes&&Ke(!1,"%s(...): childContextTypes cannot be defined on a function component.",c.displayName||c.name||"Component"),a.ref!==null){var _="",T=v0();T&&(_+=` + +Check the render method of \``+T+"`.");var R=T||a._debugID||"",j=a._debugSource;j&&(R=j.fileName+":"+j.lineNumber),Kd[R]||(Kd[R]=!0,Kt(!1,"Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?%s",_))}if(Wl&&c.defaultProps!==void 0){var V=qt(c)||"Unknown";Xd[V]||(Ke(!1,"%s: Support for defaultProps will be removed from function components in a future major release. Use JavaScript default parameters instead.",V),Xd[V]=!0)}if(typeof c.getDerivedStateFromProps=="function"){var te=qt(c)||"Unknown";Ws[te]||(Ke(!1,"%s: Function components do not support getDerivedStateFromProps.",te),Ws[te]=!0)}if(typeof c.contextType=="object"&&c.contextType!==null){var oe=qt(c)||"Unknown";Hs[oe]||(Ke(!1,"%s: Function components do not support contextType.",oe),Hs[oe]=!0)}}var w2={dehydrated:null,retryTime:lt};function $d(a,c,_){return Jc(a,Qc)&&(c===null||c.memoizedState!==null)}function qh(a,c,_){var T=c.mode,R=c.pendingProps;a_(c)&&(c.effectTag|=Hr);var j=xl.current,V=!1,te=(c.effectTag&Hr)!==_i;if(te||$d(j,a,c)?(V=!0,c.effectTag&=~Hr):(a===null||a.memoizedState!==null)&&R.fallback!==void 0&&R.unstable_avoidThisFallback!==!0&&(j=d2(j,Ef)),j=ca(j),Or(c,j),"maxDuration"in R&&(p1||(p1=!0,Kt(!1,"maxDuration has been removed from React. Remove the maxDuration prop."))),a===null){if(R.fallback!==void 0&&(Vd(c),Di)){var oe=c.memoizedState;if(oe!==null){var Ie=oe.dehydrated;if(Ie!==null)return Hh(c,Ie,_)}}if(V){var Ye=R.fallback,pt=Qa(null,T,lt,null);if(pt.return=c,(c.mode&Y)===Sr){var Nt=c.memoizedState,Vt=Nt!==null?c.child.child:c.child;pt.child=Vt;for(var zt=Vt;zt!==null;)zt.return=pt,zt=zt.sibling}var vn=Qa(Ye,T,_,null);return vn.return=c,pt.sibling=vn,c.memoizedState=w2,c.child=pt,vn}else{var xr=R.children;return c.memoizedState=null,c.child=Xc(c,null,xr,_)}}else{var $r=a.memoizedState;if($r!==null){if(Di){var wi=$r.dehydrated;if(wi!==null)if(te){if(c.memoizedState!==null)return c.child=a.child,c.effectTag|=Hr,null;var N0=R.fallback,Vi=Qa(null,T,lt,null);if(Vi.return=c,Vi.child=null,(c.mode&Y)===Sr)for(var it=Vi.child=c.child;it!==null;)it.return=Vi,it=it.sibling;else gf(c,a.child,null,_);if(Zt&&c.mode&Jr){for(var Ot=0,Je=Vi.child;Je!==null;)Ot+=Je.treeBaseDuration,Je=Je.sibling;Vi.treeBaseDuration=Ot}var Bt=Qa(N0,T,_,null);return Bt.return=c,Vi.sibling=Bt,Bt.effectTag|=ai,Vi.childExpirationTime=lt,c.memoizedState=w2,c.child=Vi,Bt}else return Wh(a,c,wi,$r,_)}var Mn=a.child,pn=Mn.sibling;if(V){var Pi=R.fallback,oi=wo(Mn,Mn.pendingProps,lt);if(oi.return=c,(c.mode&Y)===Sr){var qu=c.memoizedState,ar=qu!==null?c.child.child:c.child;if(ar!==Mn.child){oi.child=ar;for(var ou=ar;ou!==null;)ou.return=oi,ou=ou.sibling}}if(Zt&&c.mode&Jr){for(var qr=0,_u=oi.child;_u!==null;)qr+=_u.treeBaseDuration,_u=_u.sibling;oi.treeBaseDuration=qr}var _0=wo(pn,Pi,pn.expirationTime);return _0.return=c,oi.sibling=_0,oi.childExpirationTime=lt,c.memoizedState=w2,c.child=oi,_0}else{var H0=R.children,Cs=Mn.child,Hu=gf(c,Cs,H0,_);return c.memoizedState=null,c.child=Hu}}else{var pl=a.child;if(V){var Ja=R.fallback,jo=Qa(null,T,lt,null);if(jo.return=c,jo.child=pl,pl!==null&&(pl.return=jo),(c.mode&Y)===Sr){var xs=c.memoizedState,X2=xs!==null?c.child.child:c.child;jo.child=X2;for(var Uf=X2;Uf!==null;)Uf.return=jo,Uf=Uf.sibling}if(Zt&&c.mode&Jr){for(var Rc=0,Pl=jo.child;Pl!==null;)Rc+=Pl.treeBaseDuration,Pl=Pl.sibling;jo.treeBaseDuration=Rc}var zo=Qa(Ja,T,_,null);return zo.return=c,jo.sibling=zo,zo.effectTag|=ai,jo.childExpirationTime=lt,c.memoizedState=w2,c.child=jo,zo}else{c.memoizedState=null;var O1=R.children;return c.child=gf(c,pl,O1,_)}}}}function ep(a,c,_){c.memoizedState=null;var T=c.pendingProps,R=T.children;return _o(a,c,R,_),c.child}function Hh(a,c,_){if((a.mode&Y)===Sr)Kt(!1,"Cannot hydrate Suspense in legacy mode. Switch from ReactDOM.hydrate(element, container) to ReactDOM.createBlockingRoot(container, { hydrate: true }).render(element) or remove the Suspense components from the server rendered components."),a.expirationTime=bn;else if(h0(c)){var T=Fl(),R=ms(T);Ln&&x(R),a.expirationTime=R}else a.expirationTime=hi,Ln&&x(hi);return null}function Wh(a,c,_,T,R){if(qd(),(c.mode&Y)===Sr||h0(_))return ep(a,c,R);var j=a.childExpirationTime>=R;if(va||j){if(R. Use lowercase "%s" instead.',a,a.toLowerCase());break}case"forward":case"backward":{Kt(!1,'"%s" is not a valid value for revealOrder on . React uses the -s suffix in the spelling. Use "%ss" instead.',a,a.toLowerCase());break}default:Kt(!1,'"%s" is not a supported revealOrder on . Did you mean "together", "forwards" or "backwards"?',a);break}else Kt(!1,'%s is not a supported value for revealOrder on . Did you mean "together", "forwards" or "backwards"?',a)}function Vh(a,c){a!==void 0&&!gc[a]&&(a!=="collapsed"&&a!=="hidden"?(gc[a]=!0,Kt(!1,'"%s" is not a supported value for tail on . Did you mean "collapsed" or "hidden"?',a)):c!=="forwards"&&c!=="backwards"&&(gc[a]=!0,Kt(!1,' is only valid if revealOrder is "forwards" or "backwards". Did you mean to specify revealOrder="forwards"?',a)))}function v1(a,c){{var _=Array.isArray(a),T=!_&&typeof ur(a)=="function";if(_||T){var R=_?"array":"iterable";return Kt(!1,"A nested %s was passed to row #%s in . Wrap it in an additional SuspenseList to configure its revealOrder: ... {%s} ... ",R,c,R),!1}}return!0}function Mm(a,c){if((c==="forwards"||c==="backwards")&&a!==void 0&&a!==null&&a!==!1)if(Array.isArray(a)){for(var _=0;_. This is not useful since it needs multiple rows. Did you mean to pass multiple children or an array?',c)}}function np(a,c,_,T,R,j){var V=a.memoizedState;V===null?a.memoizedState={isBackwards:c,rendering:null,last:T,tail:_,tailExpiration:0,tailMode:R,lastEffect:j}:(V.isBackwards=c,V.rendering=null,V.last=T,V.tail=_,V.tailExpiration=0,V.tailMode=R,V.lastEffect=j)}function rp(a,c,_){var T=c.pendingProps,R=T.revealOrder,j=T.tail,V=T.children;km(R),Vh(j,R),Mm(V,R),_o(a,c,V,_);var te=xl.current,oe=Jc(te,Qc);if(oe)te=c2(te,Qc),c.effectTag|=Hr;else{var Ie=a!==null&&(a.effectTag&Hr)!==_i;Ie&&Rm(c,c.child,_),te=ca(te)}if(Or(c,te),(c.mode&Y)===Sr)c.memoizedState=null;else switch(R){case"forwards":{var Ye=Om(c.child),pt;Ye===null?(pt=c.child,c.child=null):(pt=Ye.sibling,Ye.sibling=null),np(c,!1,pt,Ye,j,c.lastEffect);break}case"backwards":{var Nt=null,Vt=c.child;for(c.child=null;Vt!==null;){var zt=Vt.alternate;if(zt!==null&&Zc(zt)===null){c.child=Vt;break}var vn=Vt.sibling;Vt.sibling=Nt,Nt=Vt,Vt=vn}np(c,!0,Nt,null,j,c.lastEffect);break}case"together":{np(c,!1,null,null,void 0,c.lastEffect);break}default:c.memoizedState=null}return c.child}function Nm(a,c,_){za(c,c.stateNode.containerInfo);var T=c.pendingProps;return a===null?c.child=gf(c,null,T,_):_o(a,c,T,_),c.child}function Lm(a,c,_){var T=c.type,R=T._context,j=c.pendingProps,V=c.memoizedProps,te=j.value;{var oe=c.type.propTypes;oe&&E(oe,j,"prop","Context.Provider",Rr)}if(Er(c,te),V!==null){var Ie=V.value,Ye=iu(R,te,Ie);if(Ye===0){if(V.children===j.children&&!na())return ya(a,c,_)}else Tl(c,R,Ye,_)}var pt=j.children;return _o(a,c,pt,_),c.child}var Gh=!1;function Fm(a,c,_){var T=c.type;T._context===void 0?T!==T.Consumer&&(Gh||(Gh=!0,Kt(!1,"Rendering directly is not supported and will be removed in a future major release. Did you mean to render instead?"))):T=T._context;var R=c.pendingProps,j=R.children;typeof j!="function"&&Ke(!1,"A context consumer was rendered with multiple children, or a child that isn't a function. A context consumer expects a single child that is a function. If you did pass a function, make sure there is no trailing or leading whitespace around it."),e0(c,_);var V=He(T,R.unstable_observedBits),te;return d1.current=c,Ze("render"),te=j(V),Ze(null),c.effectTag|=eu,_o(a,c,te,_),c.child}function bm(a,c,_){var T=c.type.impl;if(T.reconcileChildren===!1)return null;var R=c.pendingProps,j=R.children;return _o(a,c,j,_),c.child}function ip(a,c,_){var T=c.pendingProps,R=T.children;return _o(a,c,R,_),c.child}function up(){va=!0}function ya(a,c,_){Ki(c),a!==null&&(c.dependencies=a.dependencies),Zt&&kh(c);var T=c.expirationTime;T!==lt&&G2(T);var R=c.childExpirationTime;return R<_?null:(gh(a,c),c.child)}function m1(a,c,_){{var T=c.return;if(T===null)throw new Error("Cannot swap the root fiber.");if(a.alternate=null,c.alternate=null,_.index=c.index,_.sibling=c.sibling,_.return=c.return,_.ref=c.ref,c===T.child)T.child=_;else{var R=T.child;if(R===null)throw new Error("Expected parent to have a child.");for(;R.sibling!==c;)if(R=R.sibling,R===null)throw new Error("Expected to find the previous sibling.");R.sibling=_}var j=T.lastEffect;return j!==null?(j.nextEffect=a,T.lastEffect=a):T.firstEffect=T.lastEffect=a,a.nextEffect=null,a.effectTag=W0,_.effectTag|=ai,_}}function op(a,c,_){var T=c.expirationTime;if(c._debugNeedsRemount&&a!==null)return m1(a,c,Sy(c.type,c.key,c.pendingProps,c._debugOwner||null,c.mode,c.expirationTime));if(a!==null){var R=a.memoizedProps,j=c.pendingProps;if(R!==j||na()||c.type!==a.type)va=!0;else if(T<_){switch(va=!1,c.tag){case U:zh(c),c1();break;case W:if(dc(c),c.mode&Qr&&_!==hi&&d0(c.type,j))return Ln&&x(hi),c.expirationTime=c.childExpirationTime=hi,null;break;case N:{var V=c.type;zi(V)&&Fi(c);break}case q:za(c,c.stateNode.containerInfo);break;case he:{var te=c.memoizedProps.value;Er(c,te);break}case ze:if(Zt){var oe=c.childExpirationTime>=_;oe&&(c.effectTag|=mr)}break;case pe:{var Ie=c.memoizedState;if(Ie!==null){if(Di&&Ie.dehydrated!==null){Or(c,ca(xl.current)),c.effectTag|=Hr;break}var Ye=c.child,pt=Ye.childExpirationTime;if(pt!==lt&&pt>=_)return qh(a,c,_);Or(c,ca(xl.current));var Nt=ya(a,c,_);return Nt!==null?Nt.sibling:null}else Or(c,ca(xl.current));break}case wt:{var Vt=(a.effectTag&Hr)!==_i,zt=c.childExpirationTime>=_;if(Vt){if(zt)return rp(a,c,_);c.effectTag|=Hr}var vn=c.memoizedState;if(vn!==null&&(vn.rendering=null,vn.tail=null),Or(c,xl.current),zt)break;return null}}return ya(a,c,_)}else va=!1}else va=!1;switch(c.expirationTime=lt,c.tag){case C:return Am(a,c,c.type,_);case Ue:{var xr=c.elementType;return kf(a,c,xr,T,_)}case L:{var $r=c.type,wi=c.pendingProps,N0=c.elementType===$r?wi:bi($r,wi);return Qd(a,c,$r,N0,_)}case N:{var Vi=c.type,it=c.pendingProps,Ot=c.elementType===Vi?it:bi(Vi,it);return jh(a,c,Vi,Ot,_)}case U:return Cm(a,c,_);case W:return xm(a,c,_);case ne:return Of(a,c);case pe:return qh(a,c,_);case q:return Nm(a,c,_);case ge:{var Je=c.type,Bt=c.pendingProps,Mn=c.elementType===Je?Bt:bi(Je,Bt);return bh(a,c,Je,Mn,_)}case m:return Tm(a,c,_);case we:return Bh(a,c,_);case ze:return Uh(a,c,_);case he:return Lm(a,c,_);case Se:return Fm(a,c,_);case Oe:{var pn=c.type,Pi=c.pendingProps,oi=bi(pn,Pi);if(c.type!==c.elementType){var qu=pn.propTypes;qu&&E(qu,oi,"prop",qt(pn),Rr)}return oi=bi(pn.type,oi),Ph(a,c,pn,oi,T,_)}case le:return Ih(a,c,c.type,c.pendingProps,T,_);case Ge:{var ar=c.type,ou=c.pendingProps,qr=c.elementType===ar?ou:bi(ar,ou);return D2(a,c,ar,qr,_)}case wt:return rp(a,c,_);case xt:{if(Ht)return bm(a,c,_);break}case $e:{if(Du)return ip(a,c,_);break}}throw Error("Unknown unit of work tag ("+c.tag+"). This error is likely caused by a bug in React. Please file an issue.")}function Yh(a,c,_,T){return{currentFiber:a,impl:_,instance:null,prevProps:null,props:c,state:T}}function S2(a){return a.tag===pe&&a.memoizedState!==null}function y1(a){return a.child.sibling.child}var Kh={};function lp(a,c,_){if(Du){if(a.tag===W){var T=a.type,R=a.memoizedProps,j=a.stateNode,V=Ro(j);V!==null&&c(T,R||Kh,V)===!0&&_.push(V)}var te=a.child;S2(a)&&(te=y1(a)),te!==null&&sp(te,c,_)}}function Xh(a,c){if(Du){if(a.tag===W){var _=a.type,T=a.memoizedProps,R=a.stateNode,j=Ro(R);if(j!==null&&c(_,T,j)===!0)return j}var V=a.child;if(S2(a)&&(V=y1(a)),V!==null)return Qh(V,c)}return null}function sp(a,c,_){for(var T=a;T!==null;)lp(T,c,_),T=T.sibling}function Qh(a,c){for(var _=a;_!==null;){var T=Xh(_,c);if(T!==null)return T;_=_.sibling}return null}function Jh(a,c,_){if(T2(a,c))_.push(a.stateNode.methods);else{var T=a.child;S2(a)&&(T=y1(a)),T!==null&&ap(T,c,_)}}function ap(a,c,_){for(var T=a;T!==null;)Jh(T,c,_),T=T.sibling}function T2(a,c){return a.tag===$e&&a.type===c&&a.stateNode!==null}function C2(a,c){return{getChildren:function(){var _=c.fiber,T=_.child,R=[];return T!==null&&ap(T,a,R),R.length===0?null:R},getChildrenFromRoot:function(){for(var _=c.fiber,T=_;T!==null;){var R=T.return;if(R===null||(T=R,T.tag===$e&&T.type===a))break}var j=[];return ap(T.child,a,j),j.length===0?null:j},getParent:function(){for(var _=c.fiber.return;_!==null;){if(_.tag===$e&&_.type===a)return _.stateNode.methods;_=_.return}return null},getProps:function(){var _=c.fiber;return _.memoizedProps},queryAllNodes:function(_){var T=c.fiber,R=T.child,j=[];return R!==null&&sp(R,_,j),j.length===0?null:j},queryFirstNode:function(_){var T=c.fiber,R=T.child;return R!==null?Qh(R,_):null},containsNode:function(_){for(var T=or(_);T!==null;){if(T.tag===$e&&T.type===a&&T.stateNode===c)return!0;T=T.return}return!1}}}function z0(a){a.effectTag|=mr}function x2(a){a.effectTag|=To}var ga,Ya,A2,R2;if(P0)ga=function(a,c,_,T){for(var R=c.child;R!==null;){if(R.tag===W||R.tag===ne)Wr(a,R.stateNode);else if(Ht&&R.tag===xt)Wr(a,R.stateNode.instance);else if(R.tag!==q){if(R.child!==null){R.child.return=R,R=R.child;continue}}if(R===c)return;for(;R.sibling===null;){if(R.return===null||R.return===c)return;R=R.return}R.sibling.return=R.return,R=R.sibling}},Ya=function(a){},A2=function(a,c,_,T,R){var j=a.memoizedProps;if(j!==T){var V=c.stateNode,te=ul(),oe=c0(V,_,j,T,R,te);c.updateQueue=oe,oe&&z0(c)}},R2=function(a,c,_,T){_!==T&&z0(c)};else if(X){ga=function(a,c,_,T){for(var R=c.child;R!==null;){e:if(R.tag===W){var j=R.stateNode;if(_&&T){var V=R.memoizedProps,te=R.type;j=Gr(j,te,V,R)}Wr(a,j)}else if(R.tag===ne){var oe=R.stateNode;if(_&&T){var Ie=R.memoizedProps;oe=Yl(oe,Ie,R)}Wr(a,oe)}else if(Ht&&R.tag===xt){var Ye=R.stateNode.instance;if(_&&T){var pt=R.memoizedProps,Nt=R.type;Ye=Gr(Ye,Nt,pt,R)}Wr(a,Ye)}else if(R.tag!==q){if(R.tag===pe){if((R.effectTag&mr)!==_i){var Vt=R.memoizedState!==null;if(Vt){var zt=R.child;if(zt!==null){zt.child!==null&&(zt.child.return=zt,ga(a,zt,!0,Vt));var vn=zt.sibling;if(vn!==null){vn.return=R,R=vn;continue}}}}if(R.child!==null){R.child.return=R,R=R.child;continue}}else if(R.child!==null){R.child.return=R,R=R.child;continue}}if(R=R,R===c)return;for(;R.sibling===null;){if(R.return===null||R.return===c)return;R=R.return}R.sibling.return=R.return,R=R.sibling}};var fp=function(a,c,_,T){for(var R=c.child;R!==null;){e:if(R.tag===W){var j=R.stateNode;if(_&&T){var V=R.memoizedProps,te=R.type;j=Gr(j,te,V,R)}Gn(a,j)}else if(R.tag===ne){var oe=R.stateNode;if(_&&T){var Ie=R.memoizedProps;oe=Yl(oe,Ie,R)}Gn(a,oe)}else if(Ht&&R.tag===xt){var Ye=R.stateNode.instance;if(_&&T){var pt=R.memoizedProps,Nt=R.type;Ye=Gr(Ye,Nt,pt,R)}Gn(a,Ye)}else if(R.tag!==q){if(R.tag===pe){if((R.effectTag&mr)!==_i){var Vt=R.memoizedState!==null;if(Vt){var zt=R.child;if(zt!==null){zt.child!==null&&(zt.child.return=zt,fp(a,zt,!0,Vt));var vn=zt.sibling;if(vn!==null){vn.return=R,R=vn;continue}}}}if(R.child!==null){R.child.return=R,R=R.child;continue}}else if(R.child!==null){R.child.return=R,R=R.child;continue}}if(R=R,R===c)return;for(;R.sibling===null;){if(R.return===null||R.return===c)return;R=R.return}R.sibling.return=R.return,R=R.sibling}};Ya=function(a){var c=a.stateNode,_=a.firstEffect===null;if(!_){var T=c.containerInfo,R=w0(T);fp(R,a,!1,!1),c.pendingChildren=R,z0(a),ic(T,R)}},A2=function(a,c,_,T,R){var j=a.stateNode,V=a.memoizedProps,te=c.firstEffect===null;if(te&&V===T){c.stateNode=j;return}var oe=c.stateNode,Ie=ul(),Ye=null;if(V!==T&&(Ye=c0(oe,_,V,T,R,Ie)),te&&Ye===null){c.stateNode=j;return}var pt=cs(j,Ye,_,V,T,c,te,oe);wu(pt,_,T,R,Ie)&&z0(c),c.stateNode=pt,te?z0(c):ga(pt,c,!1,!1)},R2=function(a,c,_,T){if(_!==T){var R=Jl(),j=ul();c.stateNode=as(T,R,j,c),z0(c)}}}else Ya=function(a){},A2=function(a,c,_,T,R){},R2=function(a,c,_,T){};function O2(a,c){switch(a.tailMode){case"hidden":{for(var _=a.tail,T=null;_!==null;)_.alternate!==null&&(T=_),_=_.sibling;T===null?a.tail=null:T.sibling=null;break}case"collapsed":{for(var R=a.tail,j=null;R!==null;)R.alternate!==null&&(j=R),R=R.sibling;j===null?!c&&a.tail!==null?a.tail.sibling=null:a.tail=null:j.sibling=null;break}}}function Zh(a,c,_){var T=c.pendingProps;switch(c.tag){case C:break;case Ue:break;case le:case L:break;case N:{var R=c.type;zi(R)&&Is(c);break}case U:{no(c),x0(c);var j=c.stateNode;if(j.pendingContext&&(j.context=j.pendingContext,j.pendingContext=null),a===null||a.child===null){var V=f1(c);V&&z0(c)}Ya(c);break}case W:{Od(c);var te=Jl(),oe=c.type;if(a!==null&&c.stateNode!=null){if(A2(a,c,oe,T,te),ci){var Ie=a.memoizedProps.listeners,Ye=T.listeners;Ie!==Ye&&z0(c)}a.ref!==c.ref&&x2(c)}else{if(!T){if(c.stateNode===null)throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.");break}var pt=ul(),Nt=f1(c);if(Nt){if(Dm(c,te,pt)&&z0(c),ci){var Vt=T.listeners;Vt!=null&&dn(Vt,c,te)}}else{var zt=ji(oe,T,te,pt,c);if(ga(zt,c,!1,!1),c.stateNode=zt,ci){var vn=T.listeners;vn!=null&&dn(vn,c,te)}wu(zt,oe,T,te,pt)&&z0(c)}c.ref!==null&&x2(c)}break}case ne:{var xr=T;if(a&&c.stateNode!=null){var $r=a.memoizedProps;R2(a,c,$r,xr)}else{if(typeof xr!="string"&&c.stateNode===null)throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.");var wi=Jl(),N0=ul(),Vi=f1(c);Vi?wm(c)&&z0(c):c.stateNode=as(xr,wi,N0,c)}break}case ge:break;case pe:{da(c);var it=c.memoizedState;if(Di&&it!==null&&it.dehydrated!==null)if(a===null){var Ot=f1(c);if(!Ot)throw Error("A dehydrated suspense component was completed without a hydrated node. This is probably a bug in React.");return Lh(c),Ln&&x(hi),null}else return c1(),(c.effectTag&Hr)===_i&&(c.memoizedState=null),c.effectTag|=mr,null;if((c.effectTag&Hr)!==_i)return c.expirationTime=_,c;var Je=it!==null,Bt=!1;if(a===null)c.memoizedProps.fallback!==void 0&&f1(c);else{var Mn=a.memoizedState;if(Bt=Mn!==null,!Je&&Mn!==null){var pn=a.child.sibling;if(pn!==null){var Pi=c.firstEffect;Pi!==null?(c.firstEffect=pn,pn.nextEffect=Pi):(c.firstEffect=c.lastEffect=pn,pn.nextEffect=null),pn.effectTag=W0}}}if(Je&&!Bt&&(c.mode&Y)!==Sr){var oi=a===null&&c.memoizedProps.unstable_avoidThisFallback!==!0;oi||Jc(xl.current,Ef)?mv():yv()}X&&Je&&(c.effectTag|=mr),P0&&(Je||Bt)&&(c.effectTag|=mr),Ui&&c.updateQueue!==null&&c.memoizedProps.suspenseCallback!=null&&(c.effectTag|=mr);break}case m:break;case we:break;case ze:break;case q:no(c),Ya(c);break;case he:$u(c);break;case Se:break;case Oe:break;case Ge:{var qu=c.type;zi(qu)&&Is(c);break}case wt:{da(c);var ar=c.memoizedState;if(ar===null)break;var ou=(c.effectTag&Hr)!==_i,qr=ar.rendering;if(qr===null)if(ou)O2(ar,!1);else{var _u=gv()&&(a===null||(a.effectTag&Hr)===_i);if(!_u)for(var _0=c.child;_0!==null;){var H0=Zc(_0);if(H0!==null){ou=!0,c.effectTag|=Hr,O2(ar,!1);var Cs=H0.updateQueue;return Cs!==null&&(c.updateQueue=Cs,c.effectTag|=mr),ar.lastEffect===null&&(c.firstEffect=null),c.lastEffect=ar.lastEffect,vm(c,_),Or(c,c2(xl.current,Qc)),c.child}_0=_0.sibling}}else{if(!ou){var Hu=Zc(qr);if(Hu!==null){c.effectTag|=Hr,ou=!0;var pl=Hu.updateQueue;if(pl!==null&&(c.updateQueue=pl,c.effectTag|=mr),O2(ar,!0),ar.tail===null&&ar.tailMode==="hidden"&&!qr.alternate){var Ja=c.lastEffect=ar.lastEffect;return Ja!==null&&(Ja.nextEffect=null),null}}else if(vt()>ar.tailExpiration&&_>hi){c.effectTag|=Hr,ou=!0,O2(ar,!1);var jo=_-1;c.expirationTime=c.childExpirationTime=jo,Ln&&x(jo)}}if(ar.isBackwards)qr.sibling=c.child,c.child=qr;else{var xs=ar.last;xs!==null?xs.sibling=qr:c.child=qr,ar.last=qr}}if(ar.tail!==null){if(ar.tailExpiration===0){var X2=500;ar.tailExpiration=vt()+X2}var Uf=ar.tail;ar.rendering=Uf,ar.tail=Uf.sibling,ar.lastEffect=c.lastEffect,Uf.sibling=null;var Rc=xl.current;return ou?Rc=c2(Rc,Qc):Rc=ca(Rc),Or(c,Rc),Uf}break}case xt:{if(Ht){var Pl=c.type.impl,zo=c.stateNode;if(zo===null){var O1=Pl.getInitialState,m_;O1!==void 0&&(m_=O1(T)),zo=c.stateNode=Yh(c,T,Pl,m_||{});var y_=dt(zo);if(zo.instance=y_,Pl.reconcileChildren===!1)return null;ga(y_,c,!1,!1),Hn(zo)}else{var yE=zo.props;if(zo.prevProps=yE,zo.props=T,zo.currentFiber=c,X){var g_=ea(zo);zo.instance=g_,ga(g_,c,!1,!1)}var gE=Dn(zo);gE&&z0(c)}}break}case $e:{if(Du)if(a===null){var _E=c.type,Ly={fiber:c,methods:null};if(c.stateNode=Ly,Ly.methods=C2(_E,Ly),ci){var __=T.listeners;if(__!=null){var EE=Jl();dn(__,c,EE)}}c.ref!==null&&(x2(c),z0(c))}else{if(ci){var DE=a.memoizedProps.listeners,wE=T.listeners;(DE!==wE||c.ref!==null)&&z0(c)}else c.ref!==null&&z0(c);a.ref!==c.ref&&x2(c)}break}default:throw Error("Unknown unit of work tag ("+c.tag+"). This error is likely caused by a bug in React. Please file an issue.")}return null}function Pm(a,c){switch(a.tag){case N:{var _=a.type;zi(_)&&Is(a);var T=a.effectTag;return T&f0?(a.effectTag=T&~f0|Hr,a):null}case U:{no(a),x0(a);var R=a.effectTag;if((R&Hr)!==_i)throw Error("The root failed to unmount after an error. This is likely a bug in React. Please file an issue.");return a.effectTag=R&~f0|Hr,a}case W:return Od(a),null;case pe:{if(da(a),Di){var j=a.memoizedState;if(j!==null&&j.dehydrated!==null){if(a.alternate===null)throw Error("Threw in newly mounted dehydrated component. This is likely a bug in React. Please file an issue.");c1()}}var V=a.effectTag;return V&f0?(a.effectTag=V&~f0|Hr,a):null}case wt:return da(a),null;case q:return no(a),null;case he:return $u(a),null;default:return null}}function $h(a){switch(a.tag){case N:{var c=a.type.childContextTypes;c!=null&&Is(a);break}case U:{no(a),x0(a);break}case W:{Od(a);break}case q:no(a);break;case pe:da(a);break;case wt:da(a);break;case he:$u(a);break;default:break}}function cp(a,c){return{value:a,source:c,stack:_r(c)}}var dp=function(a,c,_,T,R,j,V,te,oe){var Ie=Array.prototype.slice.call(arguments,3);try{c.apply(_,Ie)}catch(Ye){this.onError(Ye)}};if(typeof window!="undefined"&&typeof window.dispatchEvent=="function"&&typeof document!="undefined"&&typeof document.createEvent=="function"){var pp=document.createElement("react"),Im=function(a,c,_,T,R,j,V,te,oe){if(typeof document=="undefined")throw Error("The `document` global was defined when React was initialized, but is not defined anymore. This can happen in a test environment if a component schedules an update from an asynchronous callback, but the test has already finished running. To solve this, you can either unmount the component at the end of your test (and ensure that any asynchronous operations get canceled in `componentWillUnmount`), or you can change the test itself to be asynchronous.");var Ie=document.createEvent("Event"),Ye=!0,pt=window.event,Nt=Object.getOwnPropertyDescriptor(window,"event"),Vt=Array.prototype.slice.call(arguments,3);function zt(){pp.removeEventListener(N0,zt,!1),typeof window.event!="undefined"&&window.hasOwnProperty("event")&&(window.event=pt),c.apply(_,Vt),Ye=!1}var vn,xr=!1,$r=!1;function wi(Vi){if(vn=Vi.error,xr=!0,vn===null&&Vi.colno===0&&Vi.lineno===0&&($r=!0),Vi.defaultPrevented&&vn!=null&&typeof vn=="object")try{vn._suppressLogging=!0}catch(it){}}var N0="react-"+(a||"invokeguardedcallback");window.addEventListener("error",wi),pp.addEventListener(N0,zt,!1),Ie.initEvent(N0,!1,!1),pp.dispatchEvent(Ie),Nt&&Object.defineProperty(window,"event",Nt),Ye&&(xr?$r&&(vn=new Error("A cross-origin error was thrown. React doesn't have access to the actual error object in development. See https://fb.me/react-crossorigin-error for more information.")):vn=new Error(`An error was thrown inside one of your components, but React doesn't know what it was. This is likely due to browser flakiness. React does its best to preserve the "Pause on exceptions" behavior of the DevTools, which requires some DEV-mode only tricks. It's possible that these don't work in your browser. Try triggering the error in production mode, or switching to a modern browser. If you suspect that this is actually an issue with React, please file an issue.`),this.onError(vn)),window.removeEventListener("error",wi)};dp=Im}var Bm=dp,Eo=!1,k2=null,Um={onError:function(a){Eo=!0,k2=a}};function sl(a,c,_,T,R,j,V,te,oe){Eo=!1,k2=null,Bm.apply(Um,arguments)}function Jn(){return Eo}function Vs(){if(Eo){var a=k2;return Eo=!1,k2=null,a}else throw Error("clearCaughtError was called but no error was captured. This error is likely caused by a bug in React. Please file an issue.")}function al(a){return!0}function n0(a){var c=al(a);if(c!==!1){var _=a.error;{var T=a.componentName,R=a.componentStack,j=a.errorBoundaryName,V=a.errorBoundaryFound,te=a.willRetry;if(_!=null&&_._suppressLogging){if(V&&te)return;console.error(_)}var oe=T?"The above error occurred in the <"+T+"> component:":"The above error occurred in one of your React components:",Ie;V&&j?te?Ie="React will try to recreate this component tree from scratch "+("using the error boundary you provided, "+j+"."):Ie="This error was initially handled by the error boundary "+j+`. +Recreating the tree from scratch failed so React will unmount the tree.`:Ie=`Consider adding an error boundary to your tree to customize error handling behavior. +Visit https://fb.me/react-error-boundaries to learn more about error boundaries.`;var Ye=""+oe+R+` + +`+(""+Ie);console.error(Ye)}}}var ev=null;ev=new Set;var Gs=typeof WeakSet=="function"?WeakSet:Set;function hp(a,c){var _=c.source,T=c.stack;T===null&&_!==null&&(T=_r(_));var R={componentName:_!==null?qt(_.type):null,componentStack:T!==null?T:"",error:c.value,errorBoundary:null,errorBoundaryName:null,errorBoundaryFound:!1,willRetry:!1};a!==null&&a.tag===N&&(R.errorBoundary=a.stateNode,R.errorBoundaryName=qt(a.type),R.errorBoundaryFound=!0,R.willRetry=!0);try{n0(R)}catch(j){setTimeout(function(){throw j})}}var jm=function(a,c){Oi(a,"componentWillUnmount"),c.props=a.memoizedProps,c.state=a.memoizedState,c.componentWillUnmount(),gi()};function tv(a,c){if(sl(null,jm,null,a,c),Jn()){var _=Vs();Pf(a,_)}}function vp(a){var c=a.ref;if(c!==null)if(typeof c=="function"){if(sl(null,c,null,null),Jn()){var _=Vs();Pf(a,_)}}else c.current=null}function zm(a,c){if(sl(null,c,null),Jn()){var _=Vs();Pf(a,_)}}function mp(a,c){switch(c.tag){case L:case ge:case le:{_c(ym,wf,c);return}case N:{if(c.effectTag&Co&&a!==null){var _=a.memoizedProps,T=a.memoizedState;Oi(c,"getSnapshotBeforeUpdate");var R=c.stateNode;c.type===c.elementType&&!ma&&(R.props!==c.memoizedProps&&Kt(!1,"Expected %s props to match memoized props before getSnapshotBeforeUpdate. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(c.type)||"instance"),R.state!==c.memoizedState&&Kt(!1,"Expected %s state to match memoized state before getSnapshotBeforeUpdate. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(c.type)||"instance"));var j=R.getSnapshotBeforeUpdate(c.elementType===c.type?_:bi(c.type,_),T);{var V=ev;j===void 0&&!V.has(c.type)&&(V.add(c.type),Ke(!1,"%s.getSnapshotBeforeUpdate(): A snapshot value (or null) must be returned. You have returned undefined.",qt(c.type)))}R.__reactInternalSnapshotBeforeUpdate=j,gi()}return}case U:case W:case ne:case q:case Ge:return;default:throw Error("This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.")}}function _c(a,c,_){var T=_.updateQueue,R=T!==null?T.lastEffect:null;if(R!==null){var j=R.next,V=j;do{if((V.tag&a)!==wf){var te=V.destroy;V.destroy=void 0,te!==void 0&&te()}if((V.tag&c)!==wf){var oe=V.create;V.destroy=oe();{var Ie=V.destroy;if(Ie!==void 0&&typeof Ie!="function"){var Ye=void 0;Ie===null?Ye=" You returned null. If your effect does not require clean up, return undefined (or nothing).":typeof Ie.then=="function"?Ye=` + +It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately: + +useEffect(() => { + async function fetchData() { + // You can await here + const response = await MyAPI.getData(someId); + // ... + } + fetchData(); +}, [someId]); // Or [] if effect doesn't need props or state + +Learn more about data fetching with Hooks: https://fb.me/react-hooks-data-fetching`:Ye=" You returned: "+Ie,Ke(!1,"An effect function must not return anything besides a function, which is used for clean-up.%s%s",Ye,_r(_))}}}V=V.next}while(V!==j)}}function Ea(a){if((a.effectTag&L0)!==_i)switch(a.tag){case L:case ge:case le:{_c(rr,wf,a),_c(wf,$c,a);break}default:break}}function yp(a,c,_,T){switch(_.tag){case L:case ge:case le:{_c(gm,ol,_);break}case N:{var R=_.stateNode;if(_.effectTag&mr)if(c===null)Oi(_,"componentDidMount"),_.type===_.elementType&&!ma&&(R.props!==_.memoizedProps&&Kt(!1,"Expected %s props to match memoized props before componentDidMount. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance"),R.state!==_.memoizedState&&Kt(!1,"Expected %s state to match memoized state before componentDidMount. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance")),R.componentDidMount(),gi();else{var j=_.elementType===_.type?c.memoizedProps:bi(_.type,c.memoizedProps),V=c.memoizedState;Oi(_,"componentDidUpdate"),_.type===_.elementType&&!ma&&(R.props!==_.memoizedProps&&Kt(!1,"Expected %s props to match memoized props before componentDidUpdate. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance"),R.state!==_.memoizedState&&Kt(!1,"Expected %s state to match memoized state before componentDidUpdate. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance")),R.componentDidUpdate(j,V,R.__reactInternalSnapshotBeforeUpdate),gi()}var te=_.updateQueue;te!==null&&(_.type===_.elementType&&!ma&&(R.props!==_.memoizedProps&&Kt(!1,"Expected %s props to match memoized props before processing the update queue. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance"),R.state!==_.memoizedState&&Kt(!1,"Expected %s state to match memoized state before processing the update queue. This might either be because of a bug in React, or because a component reassigns its own `this.props`. Please file an issue.",qt(_.type)||"instance")),vo(_,te,R,T));return}case U:{var oe=_.updateQueue;if(oe!==null){var Ie=null;if(_.child!==null)switch(_.child.tag){case W:Ie=Ro(_.child.stateNode);break;case N:Ie=_.child.stateNode;break}vo(_,oe,Ie,T)}return}case W:{var Ye=_.stateNode;if(c===null&&_.effectTag&mr){var pt=_.type,Nt=_.memoizedProps;Pu(Ye,pt,Nt,_)}return}case ne:return;case q:return;case ze:{if(Zt){var Vt=_.memoizedProps.onRender;typeof Vt=="function"&&(Ln?Vt(_.memoizedProps.id,c===null?"mount":"update",_.actualDuration,_.treeBaseDuration,_.actualStartTime,kl(),a.memoizedInteractions):Vt(_.memoizedProps.id,c===null?"mount":"update",_.actualDuration,_.treeBaseDuration,_.actualStartTime,kl()))}return}case pe:{Nl(a,_);return}case wt:case Ge:case xt:case $e:return;default:throw Error("This unit of work tag should not have side-effects. This error is likely caused by a bug in React. Please file an issue.")}}function M2(a,c){if(P0)for(var _=a;;){if(_.tag===W){var T=_.stateNode;c?Oa(T):Zs(_.stateNode,_.memoizedProps)}else if(_.tag===ne){var R=_.stateNode;c?p0(R):K0(R,_.memoizedProps)}else if(_.tag===pe&&_.memoizedState!==null&&_.memoizedState.dehydrated===null){var j=_.child.sibling;j.return=_,_=j;continue}else if(_.child!==null){_.child.return=_,_=_.child;continue}if(_===a)return;for(;_.sibling===null;){if(_.return===null||_.return===a)return;_=_.return}_.sibling.return=_.return,_=_.sibling}}function ku(a){var c=a.ref;if(c!==null){var _=a.stateNode,T;switch(a.tag){case W:T=Ro(_);break;default:T=_}Du&&a.tag===$e&&(T=_.methods),typeof c=="function"?c(T):(c.hasOwnProperty("current")||Ke(!1,"Unexpected ref object provided for %s. Use either a ref-setter function or React.createRef().%s",qt(a.type),_r(a)),c.current=T)}}function zu(a){var c=a.ref;c!==null&&(typeof c=="function"?c(null):c.current=null)}function gp(a,c,_){switch(Rn(c),c.tag){case L:case ge:case Oe:case le:{var T=c.updateQueue;if(T!==null){var R=T.lastEffect;if(R!==null){var j=R.next,V=_>Wn?Wn:_;_n(V,function(){var $r=j;do{var wi=$r.destroy;wi!==void 0&&zm(c,wi),$r=$r.next}while($r!==j)})}}break}case N:{vp(c);var te=c.stateNode;typeof te.componentWillUnmount=="function"&&tv(c,te);return}case W:{if(ci){var oe=c.dependencies;if(oe!==null){var Ie=oe.responders;if(Ie!==null){for(var Ye=Array.from(Ie.values()),pt=0,Nt=Ye.length;pt component higher in the tree to provide a loading indicator or placeholder to display.`+_r(_))}Rp(),T=cp(T,_);var Nt=c;do{switch(Nt.tag){case U:{var Vt=T;Nt.effectTag|=f0,Nt.expirationTime=R;var zt=uv(Nt,Vt,R);r2(Nt,zt);return}case N:var vn=T,xr=Nt.type,$r=Nt.stateNode;if((Nt.effectTag&Hr)===_i&&(typeof xr.getDerivedStateFromError=="function"||$r!==null&&typeof $r.componentDidCatch=="function"&&!Lp($r))){Nt.effectTag|=f0,Nt.expirationTime=R;var wi=ov(Nt,vn,R);r2(Nt,wi);return}break;default:break}Nt=Nt.return}while(Nt!==null)}var wa=Math.ceil,Cr=at.ReactCurrentDispatcher,Ep=at.ReactCurrentOwner,fl=at.IsSomeRendererActing,cu=0,E1=1,ki=2,Dp=4,F2=8,Do=16,Ss=32,Mf=0,b2=1,wp=2,D1=3,w1=4,Sp=5,Zn=cu,cl=null,qn=null,q0=lt,k0=Mf,P2=null,Ll=bn,S1=bn,Dc=null,wc=lt,I2=!1,Tp=0,M0=500,fn=null,B2=!1,U2=null,Sc=null,Tc=!1,Cc=null,T1=y0,Cp=lt,Ka=null,Km=50,xc=0,j2=null,sv=50,C1=0,Nf=null,Lf=null,x1=lt;function Fl(){return(Zn&(Do|Ss))!==cu?Ju(vt()):(x1!==lt||(x1=Ju(vt())),x1)}function Ac(){return Ju(vt())}function Ff(a,c,_){var T=c.mode;if((T&Y)===Sr)return bn;var R=Xt();if((T&Qr)===Sr)return R===Ci?bn:Qu;if((Zn&Do)!==cu)return q0;var j;if(_!==null)j=ia(a,_.timeoutMs|0||pf);else switch(R){case Ci:j=bn;break;case Xr:j=La(a);break;case Wn:case Xu:j=ms(a);break;case m0:j=Qi;break;default:throw Error("Expected a valid priority level")}return cl!==null&&j===q0&&(j-=1),j}function Xm(a,c){hy(),gy(a);var _=z2(a,c);if(_===null){my(a);return}Up(a,c),ta();var T=Xt();if(c===bn?(Zn&F2)!==cu&&(Zn&(Do|Ss))===cu?(H(_,c),A1(_)):(Uo(_),H(_,c),Zn===cu&&It()):(Uo(_),H(_,c)),(Zn&Dp)!==cu&&(T===Xr||T===Ci))if(Ka===null)Ka=new Map([[_,c]]);else{var R=Ka.get(_);(R===void 0||R>c)&&Ka.set(_,c)}}var dl=Xm;function z2(a,c){a.expirationTimeR?T:R}function Uo(a){var c=a.lastExpiredTime;if(c!==lt){a.callbackExpirationTime=bn,a.callbackPriority=Ci,a.callbackNode=En(A1.bind(null,a));return}var _=q2(a),T=a.callbackNode;if(_===lt){T!==null&&(a.callbackNode=null,a.callbackExpirationTime=lt,a.callbackPriority=y0);return}var R=Fl(),j=$1(R,_);if(T!==null){var V=a.callbackPriority,te=a.callbackExpirationTime;if(te===_&&V>=j)return;er(T)}a.callbackExpirationTime=_,a.callbackPriority=j;var oe;_===bn?oe=En(A1.bind(null,a)):oo?oe=yn(j,H2.bind(null,a)):oe=yn(j,H2.bind(null,a),{timeout:bo(_)-vt()}),a.callbackNode=oe}function H2(a,c){if(x1=lt,c){var _=Fl();return qp(a,_),Uo(a),null}var T=q2(a);if(T!==lt){var R=a.callbackNode;if((Zn&(Do|Ss))!==cu)throw Error("Should not already be working.");if(Xa(),(a!==cl||T!==q0)&&(bf(a,T),ee(a,T)),qn!==null){var j=Zn;Zn|=Do;var V=pv(a),te=W2(a);ff(qn);do try{oy();break}catch(Ye){dv(a,Ye)}while(!0);if(mt(),Zn=j,hv(V),Ln&&V2(te),k0===b2){var oe=P2;throw Bp(),bf(a,T),Bf(a,T),Uo(a),oe}if(qn!==null)Bp();else{Tv();var Ie=a.finishedWork=a.current.alternate;a.finishedExpirationTime=T,Qm(a,Ie,k0,T)}if(Uo(a),a.callbackNode===R)return H2.bind(null,a)}}return null}function Qm(a,c,_,T){switch(cl=null,_){case Mf:case b2:throw Error("Root did not complete. This is a bug in React.");case wp:{qp(a,T>Qi?Qi:T);break}case D1:{Bf(a,T);var R=a.lastSuspendedTime;T===R&&(a.nextKnownPendingLevel=Op(c)),d();var j=Ll===bn;if(j&&!(Y0&&If.current)){var V=Tp+M0-vt();if(V>10){if(I2){var te=a.lastPingedTime;if(te===lt||te>=T){a.lastPingedTime=T,bf(a,T);break}}var oe=q2(a);if(oe!==lt&&oe!==T)break;if(R!==lt&&R!==T){a.lastPingedTime=R;break}a.timeoutHandle=St(r0.bind(null,a),V);break}}r0(a);break}case w1:{Bf(a,T);var Ie=a.lastSuspendedTime;if(T===Ie&&(a.nextKnownPendingLevel=Op(c)),d(),!(Y0&&If.current)){if(I2){var Ye=a.lastPingedTime;if(Ye===lt||Ye>=T){a.lastPingedTime=T,bf(a,T);break}}var pt=q2(a);if(pt!==lt&&pt!==T)break;if(Ie!==lt&&Ie!==T){a.lastPingedTime=Ie;break}var Nt;if(S1!==bn)Nt=bo(S1)-vt();else if(Ll===bn)Nt=0;else{var Vt=_v(Ll),zt=vt(),vn=bo(T)-zt,xr=zt-Vt;xr<0&&(xr=0),Nt=Pp(xr)-xr,vn10){a.timeoutHandle=St(r0.bind(null,a),Nt);break}}r0(a);break}case Sp:{if(!(Y0&&If.current)&&Ll!==bn&&Dc!==null){var $r=Ip(Ll,T,Dc);if($r>10){Bf(a,T),a.timeoutHandle=St(r0.bind(null,a),$r);break}}r0(a);break}default:throw Error("Unknown root exit status.")}}function A1(a){var c=a.lastExpiredTime,_=c!==lt?c:bn;if(a.finishedExpirationTime===_)r0(a);else{if((Zn&(Do|Ss))!==cu)throw Error("Should not already be working.");if(Xa(),(a!==cl||_!==q0)&&(bf(a,_),ee(a,_)),qn!==null){var T=Zn;Zn|=Do;var R=pv(a),j=W2(a);ff(qn);do try{Ev();break}catch(te){dv(a,te)}while(!0);if(mt(),Zn=T,hv(R),Ln&&V2(j),k0===b2){var V=P2;throw Bp(),bf(a,_),Bf(a,_),Uo(a),V}if(qn!==null)throw Error("Cannot commit an incomplete root. This error is likely caused by a bug in React. Please file an issue.");Tv(),a.finishedWork=a.current.alternate,a.finishedExpirationTime=_,Jm(a,k0,_),Uo(a)}}return null}function Jm(a,c,_){cl=null,(c===D1||c===w1)&&d(),r0(a)}function Zm(a,c){qp(a,c),Uo(a),(Zn&(Do|Ss))===cu&&It()}function av(){if((Zn&(E1|Do|Ss))!==cu){(Zn&Do)!==cu&&Kt(!1,"unstable_flushDiscreteUpdates: Cannot flush updates when React is already rendering.");return}ey(),Xa()}function $m(a){return _n(Wn,a)}function fv(a,c,_,T){return _n(Ci,a.bind(null,c,_,T))}function ey(){if(Ka!==null){var a=Ka;Ka=null,a.forEach(function(c,_){qp(_,c),Uo(_)}),It()}}function ty(a,c){var _=Zn;Zn|=E1;try{return a(c)}finally{Zn=_,Zn===cu&&It()}}function ny(a,c){var _=Zn;Zn|=ki;try{return a(c)}finally{Zn=_,Zn===cu&&It()}}function cv(a,c,_,T){var R=Zn;Zn|=Dp;try{return _n(Xr,a.bind(null,c,_,T))}finally{Zn=R,Zn===cu&&It()}}function ry(a,c){var _=Zn;Zn&=~E1,Zn|=F2;try{return a(c)}finally{Zn=_,Zn===cu&&It()}}function xp(a,c){if((Zn&(Do|Ss))!==cu)throw Error("flushSync was called from inside a lifecycle method. It cannot be called when React is already rendering.");var _=Zn;Zn|=E1;try{return _n(Ci,a.bind(null,c))}finally{Zn=_,It()}}function iy(a){var c=Zn;Zn|=E1;try{_n(Ci,a)}finally{Zn=c,Zn===cu&&It()}}function bf(a,c){a.finishedWork=null,a.finishedExpirationTime=lt;var _=a.timeoutHandle;if(_!==Jo&&(a.timeoutHandle=Jo,so(_)),qn!==null)for(var T=qn.return;T!==null;)$h(T),T=T.return;cl=a,qn=wo(a.current,null,c),q0=c,k0=Mf,P2=null,Ll=bn,S1=bn,Dc=null,wc=lt,I2=!1,Ln&&(Lf=null),wl.discardPendingWarnings(),Ys=null}function dv(a,c){do{try{if(mt(),m2(),nt(),qn===null||qn.return===null)return k0=b2,P2=c,null;Zt&&qn.mode&Jr&&a1(qn,!0),lv(a,qn.return,qn,c,q0),qn=Dv(qn)}catch(_){c=_;continue}return}while(!0)}function pv(a){var c=Cr.current;return Cr.current=o1,c===null?o1:c}function hv(a){Cr.current=a}function W2(a){if(Ln){var c=k.__interactionsRef.current;return k.__interactionsRef.current=a.memoizedInteractions,c}return null}function V2(a){Ln&&(k.__interactionsRef.current=a)}function Ap(){Tp=vt()}function vv(a,c){aQi&&(Ll=a),c!==null&&aQi&&(S1=a,Dc=c)}function G2(a){a>wc&&(wc=a)}function mv(){k0===Mf&&(k0=D1)}function yv(){(k0===Mf||k0===D1)&&(k0=w1),wc!==lt&&cl!==null&&(Bf(cl,q0),o_(cl,wc))}function Rp(){k0!==Sp&&(k0=wp)}function gv(){return k0===Mf}function _v(a){var c=bo(a);return c-pf}function uy(a,c){var _=bo(a);return _-(c.timeoutMs|0||pf)}function Ev(){for(;qn!==null;)qn=Y2(qn)}function oy(){for(;qn!==null&&!kn();)qn=Y2(qn)}function Y2(a){var c=a.alternate;Kl(a),_t(a);var _;return Zt&&(a.mode&Jr)!==Sr?(zd(a),_=R1(c,a,q0),a1(a,!0)):_=R1(c,a,q0),nt(),a.memoizedProps=a.pendingProps,_===null&&(_=Dv(a)),Ep.current=null,_}function Dv(a){qn=a;do{var c=qn.alternate,_=qn.return;if((qn.effectTag&F0)===_i){_t(qn);var T=void 0;if(!Zt||(qn.mode&Jr)===Sr?T=Zh(c,qn,q0):(zd(qn),T=Zh(c,qn,q0),a1(qn,!1)),Yr(qn),nt(),ly(qn),T!==null)return T;if(_!==null&&(_.effectTag&F0)===_i){_.firstEffect===null&&(_.firstEffect=qn.firstEffect),qn.lastEffect!==null&&(_.lastEffect!==null&&(_.lastEffect.nextEffect=qn.firstEffect),_.lastEffect=qn.lastEffect);var R=qn.effectTag;R>eu&&(_.lastEffect!==null?_.lastEffect.nextEffect=qn:_.firstEffect=qn,_.lastEffect=qn)}}else{var j=Pm(qn,q0);if(Zt&&(qn.mode&Jr)!==Sr){a1(qn,!1);for(var V=qn.actualDuration,te=qn.child;te!==null;)V+=te.actualDuration,te=te.sibling;qn.actualDuration=V}if(j!==null)return fo(qn),j.effectTag&=Hl,j;Yr(qn),_!==null&&(_.firstEffect=_.lastEffect=null,_.effectTag|=F0)}var oe=qn.sibling;if(oe!==null)return oe;qn=_}while(qn!==null);return k0===Mf&&(k0=Sp),null}function Op(a){var c=a.expirationTime,_=a.childExpirationTime;return c>_?c:_}function ly(a){if(!(q0!==hi&&a.childExpirationTime===hi)){var c=lt;if(Zt&&(a.mode&Jr)!==Sr){for(var _=a.actualDuration,T=a.selfBaseDuration,R=a.alternate===null||a.child!==a.alternate.child,j=a.child;j!==null;){var V=j.expirationTime,te=j.childExpirationTime;V>c&&(c=V),te>c&&(c=te),R&&(_+=j.actualDuration),T+=j.treeBaseDuration,j=j.sibling}a.actualDuration=_,a.treeBaseDuration=T}else for(var oe=a.child;oe!==null;){var Ie=oe.expirationTime,Ye=oe.childExpirationTime;Ie>c&&(c=Ie),Ye>c&&(c=Ye),oe=oe.sibling}a.childExpirationTime=c}}function r0(a){var c=Xt();return _n(Ci,kp.bind(null,a,c)),null}function kp(a,c){do Xa();while(Cc!==null);if(vy(),(Zn&(Do|Ss))!==cu)throw Error("Should not already be working.");var _=a.finishedWork,T=a.finishedExpirationTime;if(_===null)return null;if(a.finishedWork=null,a.finishedExpirationTime=lt,_===a.current)throw Error("Cannot commit the same tree as before. This error is likely caused by a bug in React. Please file an issue.");a.callbackNode=null,a.callbackExpirationTime=lt,a.callbackPriority=y0,a.nextKnownPendingLevel=lt,J0();var R=Op(_);iE(a,T,R),a===cl&&(cl=null,qn=null,q0=lt);var j;if(_.effectTag>eu?_.lastEffect!==null?(_.lastEffect.nextEffect=_,j=_.firstEffect):j=_:j=_.firstEffect,j!==null){var V=Zn;Zn|=Ss;var te=W2(a);Ep.current=null,Te(),Bn(a.containerInfo),fn=j;do if(sl(null,sy,null),Jn()){if(fn===null)throw Error("Should be working on an effect.");var oe=Vs();Pf(fn,oe),fn=fn.nextEffect}while(fn!==null);et(),Zt&&Oh(),Ve(),fn=j;do if(sl(null,ay,null,a,c),Jn()){if(fn===null)throw Error("Should be working on an effect.");var Ie=Vs();Pf(fn,Ie),fn=fn.nextEffect}while(fn!==null);Gt(),Ir(a.containerInfo),a.current=_,Yt(),fn=j;do if(sl(null,Mp,null,a,T),Jn()){if(fn===null)throw Error("Should be working on an effect.");var Ye=Vs();Pf(fn,Ye),fn=fn.nextEffect}while(fn!==null);sr(),fn=null,se(),Ln&&V2(te),Zn=V}else a.current=_,Te(),et(),Zt&&Oh(),Ve(),Gt(),Yt(),sr();Z0();var pt=Tc;if(Tc)Tc=!1,Cc=a,Cp=T,T1=c;else for(fn=j;fn!==null;){var Nt=fn.nextEffect;fn.nextEffect=null,fn=Nt}var Vt=a.firstPendingTime;if(Vt!==lt){if(Ln){if(Lf!==null){var zt=Lf;Lf=null;for(var vn=0;vnWn?Wn:T1;return T1=y0,_n(a,Np)}}function Np(){if(Cc===null)return!1;var a=Cc,c=Cp;if(Cc=null,Cp=lt,(Zn&(Do|Ss))!==cu)throw Error("Cannot flush passive effects while already rendering.");var _=Zn;Zn|=Ss;for(var T=W2(a),R=a.current.firstEffect;R!==null;){{if(_t(R),sl(null,Ea,null,R),Jn()){if(R===null)throw Error("Should be working on an effect.");var j=Vs();Pf(R,j)}nt()}var V=R.nextEffect;R.nextEffect=null,R=V}return Ln&&(V2(T),de(a,c)),Zn=_,It(),C1=Cc===null?0:C1+1,!0}function Lp(a){return Sc!==null&&Sc.has(a)}function Fp(a){Sc===null?Sc=new Set([a]):Sc.add(a)}function fy(a){B2||(B2=!0,U2=a)}var cy=fy;function wv(a,c,_){var T=cp(_,c),R=uv(a,T,bn);Ua(a,R);var j=z2(a,bn);j!==null&&(Uo(j),H(j,bn))}function Pf(a,c){if(a.tag===U){wv(a,a,c);return}for(var _=a.return;_!==null;){if(_.tag===U){wv(_,a,c);return}else if(_.tag===N){var T=_.type,R=_.stateNode;if(typeof T.getDerivedStateFromError=="function"||typeof R.componentDidCatch=="function"&&!Lp(R)){var j=cp(c,a),V=ov(_,j,bn);Ua(_,V);var te=z2(_,bn);te!==null&&(Uo(te),H(te,bn));return}}_=_.return}}function bp(a,c,_){var T=a.pingCache;if(T!==null&&T.delete(c),cl===a&&q0===_){k0===w1||k0===D1&&Ll===bn&&vt()-TpKm)throw xc=0,j2=null,Error("Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.");C1>sv&&(C1=0,Kt(!1,"Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render."))}function vy(){wl.flushLegacyContextWarning(),fi&&wl.flushPendingUnsafeLifecycleWarnings()}function Tv(){var a=!0;cf(Nf,a),Nf=null}function Bp(){var a=!1;cf(Nf,a),Nf=null}function Up(a,c){Pr&&cl!==null&&c>q0&&(Nf=a)}var K2=null;function my(a){{var c=a.tag;if(c!==U&&c!==N&&c!==L&&c!==ge&&c!==Oe&&c!==le)return;var _=qt(a.type)||"ReactComponent";if(K2!==null){if(K2.has(_))return;K2.add(_)}else K2=new Set([_]);Ke(!1,"Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s",c===N?"the componentWillUnmount method":"a useEffect cleanup function",_r(a))}}var R1;if(G0){var yy=null;R1=function(a,c,_){var T=i_(yy,c);try{return op(a,c,_)}catch(j){if(j!==null&&typeof j=="object"&&typeof j.then=="function")throw j;if(mt(),m2(),$h(c),i_(c,T),Zt&&c.mode&Jr&&zd(c),sl(null,op,null,a,c,_),Jn()){var R=Vs();throw R}else throw j}}}else R1=op;var Cv=!1,xv=!1;function gy(a){if(a.tag===N)switch(Ar){case"getChildContext":if(xv)return;Ke(!1,"setState(...): Cannot call setState() inside getChildContext()"),xv=!0;break;case"render":if(Cv)return;Ke(!1,"Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state."),Cv=!0;break}}var If={current:!1};function jp(a){fs===!0&&fl.current===!0&&If.current!==!0&&Ke(!1,`It looks like you're using the wrong act() around your test interactions. +Be sure to use the matching version of act() corresponding to your renderer: + +// for react-dom: +import {act} from 'react-dom/test-utils'; +// ... +act(() => ...); + +// for react-test-renderer: +import TestRenderer from 'react-test-renderer'; +const {act} = TestRenderer; +// ... +act(() => ...);%s`,_r(a))}function Av(a){fs===!0&&(a.mode&cr)!==Sr&&fl.current===!1&&If.current===!1&&Ke(!1,`An update to %s ran an effect, but was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act%s`,qt(a.type),_r(a))}function _y(a){fs===!0&&Zn===cu&&fl.current===!1&&If.current===!1&&Ke(!1,`An update to %s inside a test was not wrapped in act(...). + +When testing, code that causes React state updates should be wrapped into act(...): + +act(() => { + /* fire events that update state */ +}); +/* assert on the output */ + +This ensures that you're testing the behavior the user would see in the browser. Learn more at https://fb.me/react-wrap-tests-with-act%s`,qt(a.type),_r(a))}var Ey=_y,zp=!1;function Dy(a){zp===!1&&t.unstable_flushAllWithoutAsserting===void 0&&(a.mode&Y||a.mode&Qr?(zp=!0,Ke(!1,`In Concurrent or Sync modes, the "scheduler" module needs to be mocked to guarantee consistent behaviour across tests and browsers. For example, with jest: +jest.mock('scheduler', () => require('scheduler/unstable_mock')); + +For more info, visit https://fb.me/react-mock-scheduler`)):Yi===!0&&(zp=!0,Ke(!1,`Starting from React v17, the "scheduler" module will need to be mocked to guarantee consistent behaviour across tests and browsers. For example, with jest: +jest.mock('scheduler', () => require('scheduler/unstable_mock')); + +For more info, visit https://fb.me/react-mock-scheduler`)))}var Ys=null;function wy(a){{var c=Xt();if((a.mode&Qr)!==_i&&(c===Xr||c===Ci))for(var _=a;_!==null;){var T=_.alternate;if(T!==null)switch(_.tag){case N:var R=T.updateQueue;if(R!==null)for(var j=R.firstUpdate;j!==null;){var V=j.priority;if(V===Xr||V===Ci){Ys===null?Ys=new Set([qt(_.type)]):Ys.add(qt(_.type));break}j=j.next}break;case L:case ge:case le:if(_.memoizedState!==null&&_.memoizedState.baseUpdate!==null)for(var te=_.memoizedState.baseUpdate;te!==null;){var oe=te.priority;if(oe===Xr||oe===Ci){Ys===null?Ys=new Set([qt(_.type)]):Ys.add(qt(_.type));break}if(te.next===_.memoizedState.baseUpdate)break;te=te.next}break;default:break}_=_.return}}}function d(){if(Ys!==null){var a=[];Ys.forEach(function(c){return a.push(c)}),Ys=null,a.length>0&&Ke(!1,`%s triggered a user-blocking update that suspended. + +The fix is to split the update into multiple parts: a user-blocking update to provide immediate feedback, and another update that triggers the bulk of the changes. + +Refer to the documentation for useTransition to learn how to implement this pattern.`,a.sort().join(", "))}}function v(a,c){return c*1e3+a.interactionThreadID}function x(a){!Ln||(Lf===null?Lf=[a]:Lf.push(a))}function b(a,c,_){if(!!Ln&&_.size>0){var T=a.pendingInteractionMap,R=T.get(c);R!=null?_.forEach(function(te){R.has(te)||te.__count++,R.add(te)}):(T.set(c,new Set(_)),_.forEach(function(te){te.__count++}));var j=k.__subscriberRef.current;if(j!==null){var V=v(a,c);j.onWorkScheduled(_,V)}}}function H(a,c){!Ln||b(a,c,k.__interactionsRef.current)}function ee(a,c){if(!!Ln){var _=new Set;if(a.pendingInteractionMap.forEach(function(j,V){V>=c&&j.forEach(function(te){return _.add(te)})}),a.memoizedInteractions=_,_.size>0){var T=k.__subscriberRef.current;if(T!==null){var R=v(a,c);try{T.onWorkStarted(_,R)}catch(j){yn(Ci,function(){throw j})}}}}}function de(a,c){if(!!Ln){var _=a.firstPendingTime,T;try{if(T=k.__subscriberRef.current,T!==null&&a.memoizedInteractions.size>0){var R=v(a,c);T.onWorkStopped(a.memoizedInteractions,R)}}catch(V){yn(Ci,function(){throw V})}finally{var j=a.pendingInteractionMap;j.forEach(function(V,te){te>_&&(j.delete(te),V.forEach(function(oe){if(oe.__count--,T!==null&&oe.__count===0)try{T.onInteractionScheduledWorkCompleted(oe)}catch(Ie){yn(Ci,function(){throw Ie})}}))})}}}var ye=null,be=null,gt=!1,Dt=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__!="undefined";function Rt(a){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined")return!1;var c=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(c.isDisabled)return!0;if(!c.supportsFiber)return Ke(!1,"The installed version of React DevTools is too old and will not work with the current version of React. Please update React DevTools. https://fb.me/react-devtools"),!0;try{var _=c.inject(a);ye=function(T,R){try{var j=(T.current.effectTag&Hr)===Hr;if(Zt){var V=Ac(),te=$1(V,R);c.onCommitFiberRoot(_,T,te,j)}else c.onCommitFiberRoot(_,T,void 0,j)}catch(oe){gt||(gt=!0,Ke(!1,"React DevTools encountered an error: %s",oe))}},be=function(T){try{c.onCommitFiberUnmount(_,T)}catch(R){gt||(gt=!0,Ke(!1,"React DevTools encountered an error: %s",R))}}}catch(T){Ke(!1,"React DevTools encountered an error: %s.",T)}return!0}function rn(a,c){typeof ye=="function"&&ye(a,c)}function Rn(a){typeof be=="function"&&be(a)}var $n;{$n=!1;try{var Nr=Object.preventExtensions({}),ir=new Map([[Nr,null]]),Zr=new Set([Nr]);ir.set(0,0),Zr.add(0)}catch(a){$n=!0}}var ui=1;function bl(a,c,_,T){this.tag=a,this.key=_,this.elementType=null,this.type=null,this.stateNode=null,this.return=null,this.child=null,this.sibling=null,this.index=0,this.ref=null,this.pendingProps=c,this.memoizedProps=null,this.updateQueue=null,this.memoizedState=null,this.dependencies=null,this.mode=T,this.effectTag=_i,this.nextEffect=null,this.firstEffect=null,this.lastEffect=null,this.expirationTime=lt,this.childExpirationTime=lt,this.alternate=null,Zt&&(this.actualDuration=Number.NaN,this.actualStartTime=Number.NaN,this.selfBaseDuration=Number.NaN,this.treeBaseDuration=Number.NaN,this.actualDuration=0,this.actualStartTime=-1,this.selfBaseDuration=0,this.treeBaseDuration=0),Pr&&(this._debugID=ui++,this._debugIsCurrentlyTiming=!1),this._debugSource=null,this._debugOwner=null,this._debugNeedsRemount=!1,this._debugHookTypes=null,!$n&&typeof Object.preventExtensions=="function"&&Object.preventExtensions(this)}var Wi=function(a,c,_,T){return new bl(a,c,_,T)};function uo(a){var c=a.prototype;return!!(c&&c.isReactComponent)}function i0(a){return typeof a=="function"&&!uo(a)&&a.defaultProps===void 0}function Ts(a){if(typeof a=="function")return uo(a)?N:L;if(a!=null){var c=a.$$typeof;if(c===An)return ge;if(c===Wt)return Oe}return C}function wo(a,c,_){var T=a.alternate;T===null?(T=Wi(a.tag,c,a.key,a.mode),T.elementType=a.elementType,T.type=a.type,T.stateNode=a.stateNode,T._debugID=a._debugID,T._debugSource=a._debugSource,T._debugOwner=a._debugOwner,T._debugHookTypes=a._debugHookTypes,T.alternate=a,a.alternate=T):(T.pendingProps=c,T.effectTag=_i,T.nextEffect=null,T.firstEffect=null,T.lastEffect=null,Zt&&(T.actualDuration=0,T.actualStartTime=-1)),T.childExpirationTime=a.childExpirationTime,T.expirationTime=a.expirationTime,T.child=a.child,T.memoizedProps=a.memoizedProps,T.memoizedState=a.memoizedState,T.updateQueue=a.updateQueue;var R=a.dependencies;switch(T.dependencies=R===null?null:{expirationTime:R.expirationTime,firstContext:R.firstContext,responders:R.responders},T.sibling=a.sibling,T.index=a.index,T.ref=a.ref,Zt&&(T.selfBaseDuration=a.selfBaseDuration,T.treeBaseDuration=a.treeBaseDuration),T._debugNeedsRemount=a._debugNeedsRemount,T.tag){case C:case L:case le:T.type=Zu(a.type);break;case N:T.type=U0(a.type);break;case ge:T.type=vf(a.type);break;default:break}return T}function Rv(a,c){a.effectTag&=ai,a.nextEffect=null,a.firstEffect=null,a.lastEffect=null;var _=a.alternate;if(_===null)a.childExpirationTime=lt,a.expirationTime=c,a.child=null,a.memoizedProps=null,a.memoizedState=null,a.updateQueue=null,a.dependencies=null,Zt&&(a.selfBaseDuration=0,a.treeBaseDuration=0);else{a.childExpirationTime=_.childExpirationTime,a.expirationTime=_.expirationTime,a.child=_.child,a.memoizedProps=_.memoizedProps,a.memoizedState=_.memoizedState,a.updateQueue=_.updateQueue;var T=_.dependencies;a.dependencies=T===null?null:{expirationTime:T.expirationTime,firstContext:T.firstContext,responders:T.responders},Zt&&(a.selfBaseDuration=_.selfBaseDuration,a.treeBaseDuration=_.treeBaseDuration)}return a}function X4(a){var c;return a===R0?c=Qr|Y|cr:a===I0?c=Y|cr:c=Sr,Zt&&Dt&&(c|=Jr),Wi(U,null,null,c)}function Sy(a,c,_,T,R,j){var V,te=C,oe=a;if(typeof a=="function")uo(a)?(te=N,oe=U0(oe)):oe=Zu(oe);else if(typeof a=="string")te=W;else{e:switch(a){case ue:return Qa(_.children,R,j,c);case ln:te=we,R|=Qr|Y|cr;break;case je:te=we,R|=cr;break;case ct:return J4(_,R,j,c);case nr:return Z4(_,R,j,c);case un:return $4(_,R,j,c);default:{if(typeof a=="object"&&a!==null)switch(a.$$typeof){case At:te=he;break e;case en:te=Se;break e;case An:te=ge,oe=vf(oe);break e;case Wt:te=Oe;break e;case vr:te=Ue,oe=null;break e;case w:if(Ht)return r_(a,_,R,j,c);break;case Vn:if(Du)return Q4(a,_,R,j,c)}var Ie="";{(a===void 0||typeof a=="object"&&a!==null&&Object.keys(a).length===0)&&(Ie+=" You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.");var Ye=T?qt(T.type):null;Ye&&(Ie+=` + +Check the render method of \``+Ye+"`.")}throw Error("Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: "+(a==null?a:typeof a)+"."+Ie)}}}return V=Wi(te,_,c,R),V.elementType=a,V.type=oe,V.expirationTime=j,V}function Ty(a,c,_){var T=null;T=a._owner;var R=a.type,j=a.key,V=a.props,te=Sy(R,j,V,T,c,_);return te._debugSource=a._source,te._debugOwner=a._owner,te}function Qa(a,c,_,T){var R=Wi(m,a,T,c);return R.expirationTime=_,R}function r_(a,c,_,T,R){var j=Wi(xt,c,R,_);return j.elementType=a,j.type=a,j.expirationTime=T,j}function Q4(a,c,_,T,R){var j=Wi($e,c,R,_);return j.type=a,j.elementType=a,j.expirationTime=T,j}function J4(a,c,_,T){(typeof a.id!="string"||typeof a.onRender!="function")&&Ke(!1,'Profiler must specify an "id" string and "onRender" function as props');var R=Wi(ze,a,T,c|Jr);return R.elementType=ct,R.type=ct,R.expirationTime=_,R}function Z4(a,c,_,T){var R=Wi(pe,a,T,c);return R.type=nr,R.elementType=nr,R.expirationTime=_,R}function $4(a,c,_,T){var R=Wi(wt,a,T,c);return R.type=un,R.elementType=un,R.expirationTime=_,R}function Cy(a,c,_){var T=Wi(ne,a,null,c);return T.expirationTime=_,T}function eE(){var a=Wi(W,null,null,Sr);return a.elementType="DELETED",a.type="DELETED",a}function tE(a){var c=Wi(rt,null,null,Sr);return c.stateNode=a,c}function xy(a,c,_){var T=a.children!==null?a.children:[],R=Wi(q,T,a.key,c);return R.expirationTime=_,R.stateNode={containerInfo:a.containerInfo,pendingChildren:null,implementation:a.implementation},R}function i_(a,c){return a===null&&(a=Wi(C,null,null,Sr)),a.tag=c.tag,a.key=c.key,a.elementType=c.elementType,a.type=c.type,a.stateNode=c.stateNode,a.return=c.return,a.child=c.child,a.sibling=c.sibling,a.index=c.index,a.ref=c.ref,a.pendingProps=c.pendingProps,a.memoizedProps=c.memoizedProps,a.updateQueue=c.updateQueue,a.memoizedState=c.memoizedState,a.dependencies=c.dependencies,a.mode=c.mode,a.effectTag=c.effectTag,a.nextEffect=c.nextEffect,a.firstEffect=c.firstEffect,a.lastEffect=c.lastEffect,a.expirationTime=c.expirationTime,a.childExpirationTime=c.childExpirationTime,a.alternate=c.alternate,Zt&&(a.actualDuration=c.actualDuration,a.actualStartTime=c.actualStartTime,a.selfBaseDuration=c.selfBaseDuration,a.treeBaseDuration=c.treeBaseDuration),a._debugID=c._debugID,a._debugSource=c._debugSource,a._debugOwner=c._debugOwner,a._debugIsCurrentlyTiming=c._debugIsCurrentlyTiming,a._debugNeedsRemount=c._debugNeedsRemount,a._debugHookTypes=c._debugHookTypes,a}function nE(a,c,_){this.tag=c,this.current=null,this.containerInfo=a,this.pendingChildren=null,this.pingCache=null,this.finishedExpirationTime=lt,this.finishedWork=null,this.timeoutHandle=Jo,this.context=null,this.pendingContext=null,this.hydrate=_,this.callbackNode=null,this.callbackPriority=y0,this.firstPendingTime=lt,this.firstSuspendedTime=lt,this.lastSuspendedTime=lt,this.nextKnownPendingLevel=lt,this.lastPingedTime=lt,this.lastExpiredTime=lt,Ln&&(this.interactionThreadID=k.unstable_getThreadID(),this.memoizedInteractions=new Set,this.pendingInteractionMap=new Map),Ui&&(this.hydrationCallbacks=null)}function rE(a,c,_,T){var R=new nE(a,c,_);Ui&&(R.hydrationCallbacks=T);var j=X4(c);return R.current=j,j.stateNode=R,R}function u_(a,c){var _=a.firstSuspendedTime,T=a.lastSuspendedTime;return _!==lt&&_>=c&&T<=c}function Bf(a,c){var _=a.firstSuspendedTime,T=a.lastSuspendedTime;_c||_===lt)&&(a.lastSuspendedTime=c),c<=a.lastPingedTime&&(a.lastPingedTime=lt),c<=a.lastExpiredTime&&(a.lastExpiredTime=lt)}function o_(a,c){var _=a.firstPendingTime;c>_&&(a.firstPendingTime=c);var T=a.firstSuspendedTime;T!==lt&&(c>=T?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=lt:c>=a.lastSuspendedTime&&(a.lastSuspendedTime=c+1),c>a.nextKnownPendingLevel&&(a.nextKnownPendingLevel=c))}function iE(a,c,_){a.firstPendingTime=_,c<=a.lastSuspendedTime?a.firstSuspendedTime=a.lastSuspendedTime=a.nextKnownPendingLevel=lt:c<=a.firstSuspendedTime&&(a.firstSuspendedTime=c-1),c<=a.lastPingedTime&&(a.lastPingedTime=lt),c<=a.lastExpiredTime&&(a.lastExpiredTime=lt)}function qp(a,c){var _=a.lastExpiredTime;(_===lt||_>c)&&(a.lastExpiredTime=c)}var uE={debugTool:null},Ov=uE,Ay,Ry;Ay=!1,Ry={};function oE(a){if(!a)return Sn;var c=jt(a),_=El(c);if(c.tag===N){var T=c.type;if(zi(T))return A0(c,T,_)}return _}function Oy(a){var c=jt(a);if(c===void 0)throw typeof a.render=="function"?Error("Unable to find node on an unmounted component."):Error("Argument appears to not be a ReactComponent. Keys: "+Object.keys(a));var _=b0(c);return _===null?null:_.stateNode}function lE(a,c){{var _=jt(a);if(_===void 0)throw typeof a.render=="function"?Error("Unable to find node on an unmounted component."):Error("Argument appears to not be a ReactComponent. Keys: "+Object.keys(a));var T=b0(_);if(T===null)return null;if(T.mode&cr){var R=qt(_.type)||"Component";Ry[R]||(Ry[R]=!0,_.mode&cr?Ke(!1,"%s is deprecated in StrictMode. %s was passed an instance of %s which is inside StrictMode. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://fb.me/react-strict-mode-find-node%s",c,c,R,_r(T)):Ke(!1,"%s is deprecated in StrictMode. %s was passed an instance of %s which renders StrictMode children. Instead, add a ref directly to the element you want to reference. Learn more about using refs safely here: https://fb.me/react-strict-mode-find-node%s",c,c,R,_r(T)))}return T.stateNode}return Oy(a)}function sE(a,c,_,T){return rE(a,c,_,T)}function l_(a,c,_,T){var R=c.current,j=Fl();typeof jest!="undefined"&&(Dy(R),jp(R));var V=mo(),te=Ff(j,R,V);Ov.debugTool&&(R.alternate===null?Ov.debugTool.onMountContainer(c):a===null?Ov.debugTool.onUnmountContainer(c):Ov.debugTool.onUpdateContainer(c));var oe=oE(_);c.context===null?c.context=oe:c.pendingContext=oe,Ar==="render"&&Cn!==null&&!Ay&&(Ay=!0,Ke(!1,`Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate. + +Check the render method of %s.`,qt(Cn.type)||"Unknown"));var Ie=gu(te,V);return Ie.payload={element:a},T=T===void 0?null:T,T!==null&&(typeof T!="function"&&Ke(!1,"render(...): Expected the last optional `callback` argument to be a function. Instead received: %s.",T),Ie.callback=T),Ua(R,Ie),dl(R,te),te}function aE(a){var c=a.current;if(!c.child)return null;switch(c.child.tag){case W:return Ro(c.child.stateNode);default:return c.child.stateNode}}function fE(a){switch(a.tag){case U:var c=a.stateNode;c.hydrate&&Zm(c,c.firstPendingTime);break;case pe:xp(function(){return dl(a,bn)});var _=La(Fl());kv(a,_);break}}function s_(a,c){var _=a.memoizedState;_!==null&&_.dehydrated!==null&&_.retryTime=c.length)return T;var R=c[_],j=Array.isArray(a)?a.slice():f({},a);return j[R]=Ny(a[R],c,_+1,T),j},h_=function(a,c,_){return Ny(a,c,0,_)};f_=function(a,c,_,T){for(var R=a.memoizedState;R!==null&&c>0;)R=R.next,c--;if(R!==null){var j=h_(R.memoizedState,_,T);R.memoizedState=j,R.baseState=j,a.memoizedProps=f({},a.memoizedProps),dl(a,bn)}},c_=function(a,c,_){a.pendingProps=h_(a.memoizedProps,c,_),a.alternate&&(a.alternate.pendingProps=a.pendingProps),dl(a,bn)},d_=function(a){dl(a,bn)},p_=function(a){My=a}}function hE(a){var c=a.findFiberByHostInstance,_=at.ReactCurrentDispatcher;return Rt(f({},a,{overrideHookState:f_,overrideProps:c_,setSuspenseHandler:p_,scheduleUpdate:d_,currentDispatcherRef:_,findHostInstanceByFiber:function(T){var R=b0(T);return R===null?null:R.stateNode},findFiberByHostInstance:function(T){return c?c(T):null},findHostInstancesForRefresh:n2,scheduleRefresh:Sl,scheduleRoot:_s,setRefreshHandler:Ia,getCurrentFiber:function(){return Cn}}))}var v_=Object.freeze({createContainer:sE,updateContainer:l_,batchedEventUpdates:ny,batchedUpdates:ty,unbatchedUpdates:ry,deferredUpdates:$m,syncUpdates:fv,discreteUpdates:cv,flushDiscreteUpdates:av,flushControlled:iy,flushSync:xp,flushPassiveEffects:Xa,IsThisRendererActing:If,getPublicRootInstance:aE,attemptSynchronousHydration:fE,attemptUserBlockingHydration:cE,attemptContinuousHydration:ky,attemptHydrationAtCurrentPriority:dE,findHostInstance:Oy,findHostInstanceWithWarning:lE,findHostInstanceWithNoPortals:pE,shouldSuspend:a_,injectIntoDevTools:hE}),vE=v_.default||v_;hg.exports=vE;var mE=hg.exports;return hg.exports=i,mE})});var D9=ce((zne,cw)=>{"use strict";process.env.NODE_ENV==="production"?cw.exports=m9():cw.exports=E9()});var S9=ce((qne,w9)=>{"use strict";var ZK={ALIGN_COUNT:8,ALIGN_AUTO:0,ALIGN_FLEX_START:1,ALIGN_CENTER:2,ALIGN_FLEX_END:3,ALIGN_STRETCH:4,ALIGN_BASELINE:5,ALIGN_SPACE_BETWEEN:6,ALIGN_SPACE_AROUND:7,DIMENSION_COUNT:2,DIMENSION_WIDTH:0,DIMENSION_HEIGHT:1,DIRECTION_COUNT:3,DIRECTION_INHERIT:0,DIRECTION_LTR:1,DIRECTION_RTL:2,DISPLAY_COUNT:2,DISPLAY_FLEX:0,DISPLAY_NONE:1,EDGE_COUNT:9,EDGE_LEFT:0,EDGE_TOP:1,EDGE_RIGHT:2,EDGE_BOTTOM:3,EDGE_START:4,EDGE_END:5,EDGE_HORIZONTAL:6,EDGE_VERTICAL:7,EDGE_ALL:8,EXPERIMENTAL_FEATURE_COUNT:1,EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS:0,FLEX_DIRECTION_COUNT:4,FLEX_DIRECTION_COLUMN:0,FLEX_DIRECTION_COLUMN_REVERSE:1,FLEX_DIRECTION_ROW:2,FLEX_DIRECTION_ROW_REVERSE:3,JUSTIFY_COUNT:6,JUSTIFY_FLEX_START:0,JUSTIFY_CENTER:1,JUSTIFY_FLEX_END:2,JUSTIFY_SPACE_BETWEEN:3,JUSTIFY_SPACE_AROUND:4,JUSTIFY_SPACE_EVENLY:5,LOG_LEVEL_COUNT:6,LOG_LEVEL_ERROR:0,LOG_LEVEL_WARN:1,LOG_LEVEL_INFO:2,LOG_LEVEL_DEBUG:3,LOG_LEVEL_VERBOSE:4,LOG_LEVEL_FATAL:5,MEASURE_MODE_COUNT:3,MEASURE_MODE_UNDEFINED:0,MEASURE_MODE_EXACTLY:1,MEASURE_MODE_AT_MOST:2,NODE_TYPE_COUNT:2,NODE_TYPE_DEFAULT:0,NODE_TYPE_TEXT:1,OVERFLOW_COUNT:3,OVERFLOW_VISIBLE:0,OVERFLOW_HIDDEN:1,OVERFLOW_SCROLL:2,POSITION_TYPE_COUNT:2,POSITION_TYPE_RELATIVE:0,POSITION_TYPE_ABSOLUTE:1,PRINT_OPTIONS_COUNT:3,PRINT_OPTIONS_LAYOUT:1,PRINT_OPTIONS_STYLE:2,PRINT_OPTIONS_CHILDREN:4,UNIT_COUNT:4,UNIT_UNDEFINED:0,UNIT_POINT:1,UNIT_PERCENT:2,UNIT_AUTO:3,WRAP_COUNT:3,WRAP_NO_WRAP:0,WRAP_WRAP:1,WRAP_WRAP_REVERSE:2};w9.exports=ZK});var A9=ce((Hne,T9)=>{"use strict";var $K=Object.assign||function(i){for(var o=1;o"}}]),i}(),C9=function(){v4(i,null,[{key:"fromJS",value:function(f){var p=f.width,E=f.height;return new i(p,E)}}]);function i(o,f){pw(this,i),this.width=o,this.height=f}return v4(i,[{key:"fromJS",value:function(f){f(this.width,this.height)}},{key:"toString",value:function(){return""}}]),i}(),x9=function(){function i(o,f){pw(this,i),this.unit=o,this.value=f}return v4(i,[{key:"fromJS",value:function(f){f(this.unit,this.value)}},{key:"toString",value:function(){switch(this.unit){case Jf.UNIT_POINT:return String(this.value);case Jf.UNIT_PERCENT:return this.value+"%";case Jf.UNIT_AUTO:return"auto";default:return this.value+"?"}}},{key:"valueOf",value:function(){return this.value}}]),i}();T9.exports=function(i,o){function f(k,L,N){var C=k[L];k[L]=function(){for(var U=arguments.length,q=Array(U),W=0;W1?q-1:0),ne=1;ne1&&arguments[1]!==void 0?arguments[1]:NaN,N=arguments.length>2&&arguments[2]!==void 0?arguments[2]:NaN,C=arguments.length>3&&arguments[3]!==void 0?arguments[3]:Jf.DIRECTION_LTR;return k.call(this,L,N,C)}),$K({Config:o.Config,Node:o.Node,Layout:i("Layout",eX),Size:i("Size",C9),Value:i("Value",x9),getInstanceCount:function(){return o.getInstanceCount.apply(o,arguments)}},Jf)}});var R9=ce((exports,module)=>{(function(i,o){typeof define=="function"&&define.amd?define([],function(){return o}):typeof module=="object"&&module.exports?module.exports=o:(i.nbind=i.nbind||{}).init=o})(exports,function(Module,cb){typeof Module=="function"&&(cb=Module,Module={}),Module.onRuntimeInitialized=function(i,o){return function(){i&&i.apply(this,arguments);try{Module.ccall("nbind_init")}catch(f){o(f);return}o(null,{bind:Module._nbind_value,reflect:Module.NBind.reflect,queryType:Module.NBind.queryType,toggleLightGC:Module.toggleLightGC,lib:Module})}}(Module.onRuntimeInitialized,cb);var Module;Module||(Module=(typeof Module!="undefined"?Module:null)||{});var moduleOverrides={};for(var key in Module)Module.hasOwnProperty(key)&&(moduleOverrides[key]=Module[key]);var ENVIRONMENT_IS_WEB=!1,ENVIRONMENT_IS_WORKER=!1,ENVIRONMENT_IS_NODE=!1,ENVIRONMENT_IS_SHELL=!1;if(Module.ENVIRONMENT)if(Module.ENVIRONMENT==="WEB")ENVIRONMENT_IS_WEB=!0;else if(Module.ENVIRONMENT==="WORKER")ENVIRONMENT_IS_WORKER=!0;else if(Module.ENVIRONMENT==="NODE")ENVIRONMENT_IS_NODE=!0;else if(Module.ENVIRONMENT==="SHELL")ENVIRONMENT_IS_SHELL=!0;else throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.");else ENVIRONMENT_IS_WEB=typeof window=="object",ENVIRONMENT_IS_WORKER=typeof importScripts=="function",ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof require=="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER,ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){Module.print||(Module.print=console.log),Module.printErr||(Module.printErr=console.warn);var nodeFS,nodePath;Module.read=function(o,f){nodeFS||(nodeFS={}("")),nodePath||(nodePath={}("")),o=nodePath.normalize(o);var p=nodeFS.readFileSync(o);return f?p:p.toString()},Module.readBinary=function(o){var f=Module.read(o,!0);return f.buffer||(f=new Uint8Array(f)),assert(f.buffer),f},Module.load=function(o){globalEval(read(o))},Module.thisProgram||(process.argv.length>1?Module.thisProgram=process.argv[1].replace(/\\/g,"/"):Module.thisProgram="unknown-program"),Module.arguments=process.argv.slice(2),typeof module!="undefined"&&(module.exports=Module),Module.inspect=function(){return"[Emscripten Module object]"}}else if(ENVIRONMENT_IS_SHELL)Module.print||(Module.print=print),typeof printErr!="undefined"&&(Module.printErr=printErr),typeof read!="undefined"?Module.read=read:Module.read=function(){throw"no read() available"},Module.readBinary=function(o){if(typeof readbuffer=="function")return new Uint8Array(readbuffer(o));var f=read(o,"binary");return assert(typeof f=="object"),f},typeof scriptArgs!="undefined"?Module.arguments=scriptArgs:typeof arguments!="undefined"&&(Module.arguments=arguments),typeof quit=="function"&&(Module.quit=function(i,o){quit(i)});else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(Module.read=function(o){var f=new XMLHttpRequest;return f.open("GET",o,!1),f.send(null),f.responseText},ENVIRONMENT_IS_WORKER&&(Module.readBinary=function(o){var f=new XMLHttpRequest;return f.open("GET",o,!1),f.responseType="arraybuffer",f.send(null),new Uint8Array(f.response)}),Module.readAsync=function(o,f,p){var E=new XMLHttpRequest;E.open("GET",o,!0),E.responseType="arraybuffer",E.onload=function(){E.status==200||E.status==0&&E.response?f(E.response):p()},E.onerror=p,E.send(null)},typeof arguments!="undefined"&&(Module.arguments=arguments),typeof console!="undefined")Module.print||(Module.print=function(o){console.log(o)}),Module.printErr||(Module.printErr=function(o){console.warn(o)});else{var TRY_USE_DUMP=!1;Module.print||(Module.print=TRY_USE_DUMP&&typeof dump!="undefined"?function(i){dump(i)}:function(i){})}ENVIRONMENT_IS_WORKER&&(Module.load=importScripts),typeof Module.setWindowTitle=="undefined"&&(Module.setWindowTitle=function(i){document.title=i})}else throw"Unknown runtime environment. Where are we?";function globalEval(i){eval.call(null,i)}!Module.load&&Module.read&&(Module.load=function(o){globalEval(Module.read(o))}),Module.print||(Module.print=function(){}),Module.printErr||(Module.printErr=Module.print),Module.arguments||(Module.arguments=[]),Module.thisProgram||(Module.thisProgram="./this.program"),Module.quit||(Module.quit=function(i,o){throw o}),Module.print=Module.print,Module.printErr=Module.printErr,Module.preRun=[],Module.postRun=[];for(var key in moduleOverrides)moduleOverrides.hasOwnProperty(key)&&(Module[key]=moduleOverrides[key]);moduleOverrides=void 0;var Runtime={setTempRet0:function(i){return tempRet0=i,i},getTempRet0:function(){return tempRet0},stackSave:function(){return STACKTOP},stackRestore:function(i){STACKTOP=i},getNativeTypeSize:function(i){switch(i){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(i[i.length-1]==="*")return Runtime.QUANTUM_SIZE;if(i[0]==="i"){var o=parseInt(i.substr(1));return assert(o%8==0),o/8}else return 0}}},getNativeFieldSize:function(i){return Math.max(Runtime.getNativeTypeSize(i),Runtime.QUANTUM_SIZE)},STACK_ALIGN:16,prepVararg:function(i,o){return o==="double"||o==="i64"?i&7&&(assert((i&7)==4),i+=4):assert((i&3)==0),i},getAlignSize:function(i,o,f){return!f&&(i=="i64"||i=="double")?8:i?Math.min(o||(i?Runtime.getNativeFieldSize(i):0),Runtime.QUANTUM_SIZE):Math.min(o,8)},dynCall:function(i,o,f){return f&&f.length?Module["dynCall_"+i].apply(null,[o].concat(f)):Module["dynCall_"+i].call(null,o)},functionPointers:[],addFunction:function(i){for(var o=0;o>2],f=(o+i+15|0)&-16;if(HEAP32[DYNAMICTOP_PTR>>2]=f,f>=TOTAL_MEMORY){var p=enlargeMemory();if(!p)return HEAP32[DYNAMICTOP_PTR>>2]=o,0}return o},alignMemory:function(i,o){var f=i=Math.ceil(i/(o||16))*(o||16);return f},makeBigInt:function(i,o,f){var p=f?+(i>>>0)+ +(o>>>0)*4294967296:+(i>>>0)+ +(o|0)*4294967296;return p},GLOBAL_BASE:8,QUANTUM_SIZE:4,__dummy__:0};Module.Runtime=Runtime;var ABORT=0,EXITSTATUS=0;function assert(i,o){i||abort("Assertion failed: "+o)}function getCFunc(ident){var func=Module["_"+ident];if(!func)try{func=eval("_"+ident)}catch(i){}return assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)"),func}var cwrap,ccall;(function(){var JSfuncs={stackSave:function(){Runtime.stackSave()},stackRestore:function(){Runtime.stackRestore()},arrayToC:function(i){var o=Runtime.stackAlloc(i.length);return writeArrayToMemory(i,o),o},stringToC:function(i){var o=0;if(i!=null&&i!==0){var f=(i.length<<2)+1;o=Runtime.stackAlloc(f),stringToUTF8(i,o,f)}return o}},toC={string:JSfuncs.stringToC,array:JSfuncs.arrayToC};ccall=function(o,f,p,E,t){var k=getCFunc(o),L=[],N=0;if(E)for(var C=0;C>0]=o;break;case"i8":HEAP8[i>>0]=o;break;case"i16":HEAP16[i>>1]=o;break;case"i32":HEAP32[i>>2]=o;break;case"i64":tempI64=[o>>>0,(tempDouble=o,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[i>>2]=tempI64[0],HEAP32[i+4>>2]=tempI64[1];break;case"float":HEAPF32[i>>2]=o;break;case"double":HEAPF64[i>>3]=o;break;default:abort("invalid type for setValue: "+f)}}Module.setValue=setValue;function getValue(i,o,f){switch(o=o||"i8",o.charAt(o.length-1)==="*"&&(o="i32"),o){case"i1":return HEAP8[i>>0];case"i8":return HEAP8[i>>0];case"i16":return HEAP16[i>>1];case"i32":return HEAP32[i>>2];case"i64":return HEAP32[i>>2];case"float":return HEAPF32[i>>2];case"double":return HEAPF64[i>>3];default:abort("invalid type for setValue: "+o)}return null}Module.getValue=getValue;var ALLOC_NORMAL=0,ALLOC_STACK=1,ALLOC_STATIC=2,ALLOC_DYNAMIC=3,ALLOC_NONE=4;Module.ALLOC_NORMAL=ALLOC_NORMAL,Module.ALLOC_STACK=ALLOC_STACK,Module.ALLOC_STATIC=ALLOC_STATIC,Module.ALLOC_DYNAMIC=ALLOC_DYNAMIC,Module.ALLOC_NONE=ALLOC_NONE;function allocate(i,o,f,p){var E,t;typeof i=="number"?(E=!0,t=i):(E=!1,t=i.length);var k=typeof o=="string"?o:null,L;if(f==ALLOC_NONE?L=p:L=[typeof _malloc=="function"?_malloc:Runtime.staticAlloc,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][f===void 0?ALLOC_STATIC:f](Math.max(t,k?1:o.length)),E){var p=L,N;for(assert((L&3)==0),N=L+(t&~3);p>2]=0;for(N=L+t;p>0]=0;return L}if(k==="i8")return i.subarray||i.slice?HEAPU8.set(i,L):HEAPU8.set(new Uint8Array(i),L),L;for(var C=0,U,q,W;C>0],f|=p,!(p==0&&!o||(E++,o&&E==o)););o||(o=E);var t="";if(f<128){for(var k=1024,L;o>0;)L=String.fromCharCode.apply(String,HEAPU8.subarray(i,i+Math.min(o,k))),t=t?t+L:L,i+=k,o-=k;return t}return Module.UTF8ToString(i)}Module.Pointer_stringify=Pointer_stringify;function AsciiToString(i){for(var o="";;){var f=HEAP8[i++>>0];if(!f)return o;o+=String.fromCharCode(f)}}Module.AsciiToString=AsciiToString;function stringToAscii(i,o){return writeAsciiToMemory(i,o,!1)}Module.stringToAscii=stringToAscii;var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):void 0;function UTF8ArrayToString(i,o){for(var f=o;i[f];)++f;if(f-o>16&&i.subarray&&UTF8Decoder)return UTF8Decoder.decode(i.subarray(o,f));for(var p,E,t,k,L,N,C="";;){if(p=i[o++],!p)return C;if(!(p&128)){C+=String.fromCharCode(p);continue}if(E=i[o++]&63,(p&224)==192){C+=String.fromCharCode((p&31)<<6|E);continue}if(t=i[o++]&63,(p&240)==224?p=(p&15)<<12|E<<6|t:(k=i[o++]&63,(p&248)==240?p=(p&7)<<18|E<<12|t<<6|k:(L=i[o++]&63,(p&252)==248?p=(p&3)<<24|E<<18|t<<12|k<<6|L:(N=i[o++]&63,p=(p&1)<<30|E<<24|t<<18|k<<12|L<<6|N))),p<65536)C+=String.fromCharCode(p);else{var U=p-65536;C+=String.fromCharCode(55296|U>>10,56320|U&1023)}}}Module.UTF8ArrayToString=UTF8ArrayToString;function UTF8ToString(i){return UTF8ArrayToString(HEAPU8,i)}Module.UTF8ToString=UTF8ToString;function stringToUTF8Array(i,o,f,p){if(!(p>0))return 0;for(var E=f,t=f+p-1,k=0;k=55296&&L<=57343&&(L=65536+((L&1023)<<10)|i.charCodeAt(++k)&1023),L<=127){if(f>=t)break;o[f++]=L}else if(L<=2047){if(f+1>=t)break;o[f++]=192|L>>6,o[f++]=128|L&63}else if(L<=65535){if(f+2>=t)break;o[f++]=224|L>>12,o[f++]=128|L>>6&63,o[f++]=128|L&63}else if(L<=2097151){if(f+3>=t)break;o[f++]=240|L>>18,o[f++]=128|L>>12&63,o[f++]=128|L>>6&63,o[f++]=128|L&63}else if(L<=67108863){if(f+4>=t)break;o[f++]=248|L>>24,o[f++]=128|L>>18&63,o[f++]=128|L>>12&63,o[f++]=128|L>>6&63,o[f++]=128|L&63}else{if(f+5>=t)break;o[f++]=252|L>>30,o[f++]=128|L>>24&63,o[f++]=128|L>>18&63,o[f++]=128|L>>12&63,o[f++]=128|L>>6&63,o[f++]=128|L&63}}return o[f]=0,f-E}Module.stringToUTF8Array=stringToUTF8Array;function stringToUTF8(i,o,f){return stringToUTF8Array(i,HEAPU8,o,f)}Module.stringToUTF8=stringToUTF8;function lengthBytesUTF8(i){for(var o=0,f=0;f=55296&&p<=57343&&(p=65536+((p&1023)<<10)|i.charCodeAt(++f)&1023),p<=127?++o:p<=2047?o+=2:p<=65535?o+=3:p<=2097151?o+=4:p<=67108863?o+=5:o+=6}return o}Module.lengthBytesUTF8=lengthBytesUTF8;var UTF16Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf-16le"):void 0;function demangle(i){var o=Module.___cxa_demangle||Module.__cxa_demangle;if(o){try{var f=i.substr(1),p=lengthBytesUTF8(f)+1,E=_malloc(p);stringToUTF8(f,E,p);var t=_malloc(4),k=o(E,0,0,t);if(getValue(t,"i32")===0&&k)return Pointer_stringify(k)}catch(L){}finally{E&&_free(E),t&&_free(t),k&&_free(k)}return i}return Runtime.warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling"),i}function demangleAll(i){var o=/__Z[\w\d_]+/g;return i.replace(o,function(f){var p=demangle(f);return f===p?f:f+" ["+p+"]"})}function jsStackTrace(){var i=new Error;if(!i.stack){try{throw new Error(0)}catch(o){i=o}if(!i.stack)return"(no stack trace available)"}return i.stack.toString()}function stackTrace(){var i=jsStackTrace();return Module.extraStackTrace&&(i+=` +`+Module.extraStackTrace()),demangleAll(i)}Module.stackTrace=stackTrace;var HEAP,buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferViews(){Module.HEAP8=HEAP8=new Int8Array(buffer),Module.HEAP16=HEAP16=new Int16Array(buffer),Module.HEAP32=HEAP32=new Int32Array(buffer),Module.HEAPU8=HEAPU8=new Uint8Array(buffer),Module.HEAPU16=HEAPU16=new Uint16Array(buffer),Module.HEAPU32=HEAPU32=new Uint32Array(buffer),Module.HEAPF32=HEAPF32=new Float32Array(buffer),Module.HEAPF64=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed,STACK_BASE,STACKTOP,STACK_MAX,DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0,staticSealed=!1;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime but prevents some optimizations, (3) set Module.TOTAL_MEMORY to a higher value before the program runs, or (4) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}function enlargeMemory(){abortOnCannotGrowMemory()}var TOTAL_STACK=Module.TOTAL_STACK||5242880,TOTAL_MEMORY=Module.TOTAL_MEMORY||134217728;TOTAL_MEMORY0;){var o=i.shift();if(typeof o=="function"){o();continue}var f=o.func;typeof f=="number"?o.arg===void 0?Module.dynCall_v(f):Module.dynCall_vi(f,o.arg):f(o.arg===void 0?null:o.arg)}}var __ATPRERUN__=[],__ATINIT__=[],__ATMAIN__=[],__ATEXIT__=[],__ATPOSTRUN__=[],runtimeInitialized=!1,runtimeExited=!1;function preRun(){if(Module.preRun)for(typeof Module.preRun=="function"&&(Module.preRun=[Module.preRun]);Module.preRun.length;)addOnPreRun(Module.preRun.shift());callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){runtimeInitialized||(runtimeInitialized=!0,callRuntimeCallbacks(__ATINIT__))}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__),runtimeExited=!0}function postRun(){if(Module.postRun)for(typeof Module.postRun=="function"&&(Module.postRun=[Module.postRun]);Module.postRun.length;)addOnPostRun(Module.postRun.shift());callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(i){__ATPRERUN__.unshift(i)}Module.addOnPreRun=addOnPreRun;function addOnInit(i){__ATINIT__.unshift(i)}Module.addOnInit=addOnInit;function addOnPreMain(i){__ATMAIN__.unshift(i)}Module.addOnPreMain=addOnPreMain;function addOnExit(i){__ATEXIT__.unshift(i)}Module.addOnExit=addOnExit;function addOnPostRun(i){__ATPOSTRUN__.unshift(i)}Module.addOnPostRun=addOnPostRun;function intArrayFromString(i,o,f){var p=f>0?f:lengthBytesUTF8(i)+1,E=new Array(p),t=stringToUTF8Array(i,E,0,E.length);return o&&(E.length=t),E}Module.intArrayFromString=intArrayFromString;function intArrayToString(i){for(var o=[],f=0;f255&&(p&=255),o.push(String.fromCharCode(p))}return o.join("")}Module.intArrayToString=intArrayToString;function writeStringToMemory(i,o,f){Runtime.warnOnce("writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!");var p,E;f&&(E=o+lengthBytesUTF8(i),p=HEAP8[E]),stringToUTF8(i,o,Infinity),f&&(HEAP8[E]=p)}Module.writeStringToMemory=writeStringToMemory;function writeArrayToMemory(i,o){HEAP8.set(i,o)}Module.writeArrayToMemory=writeArrayToMemory;function writeAsciiToMemory(i,o,f){for(var p=0;p>0]=i.charCodeAt(p);f||(HEAP8[o>>0]=0)}if(Module.writeAsciiToMemory=writeAsciiToMemory,(!Math.imul||Math.imul(4294967295,5)!==-5)&&(Math.imul=function(o,f){var p=o>>>16,E=o&65535,t=f>>>16,k=f&65535;return E*k+(p*k+E*t<<16)|0}),Math.imul=Math.imul,!Math.fround){var froundBuffer=new Float32Array(1);Math.fround=function(i){return froundBuffer[0]=i,froundBuffer[0]}}Math.fround=Math.fround,Math.clz32||(Math.clz32=function(i){i=i>>>0;for(var o=0;o<32;o++)if(i&1<<31-o)return o;return 32}),Math.clz32=Math.clz32,Math.trunc||(Math.trunc=function(i){return i<0?Math.ceil(i):Math.floor(i)}),Math.trunc=Math.trunc;var Math_abs=Math.abs,Math_cos=Math.cos,Math_sin=Math.sin,Math_tan=Math.tan,Math_acos=Math.acos,Math_asin=Math.asin,Math_atan=Math.atan,Math_atan2=Math.atan2,Math_exp=Math.exp,Math_log=Math.log,Math_sqrt=Math.sqrt,Math_ceil=Math.ceil,Math_floor=Math.floor,Math_pow=Math.pow,Math_imul=Math.imul,Math_fround=Math.fround,Math_round=Math.round,Math_min=Math.min,Math_clz32=Math.clz32,Math_trunc=Math.trunc,runDependencies=0,runDependencyWatcher=null,dependenciesFulfilled=null;function getUniqueRunDependency(i){return i}function addRunDependency(i){runDependencies++,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies)}Module.addRunDependency=addRunDependency;function removeRunDependency(i){if(runDependencies--,Module.monitorRunDependencies&&Module.monitorRunDependencies(runDependencies),runDependencies==0&&(runDependencyWatcher!==null&&(clearInterval(runDependencyWatcher),runDependencyWatcher=null),dependenciesFulfilled)){var o=dependenciesFulfilled;dependenciesFulfilled=null,o()}}Module.removeRunDependency=removeRunDependency,Module.preloadedImages={},Module.preloadedAudios={};var ASM_CONSTS=[function(i,o,f,p,E,t,k,L){return _nbind.callbackSignatureList[i].apply(this,arguments)}];function _emscripten_asm_const_iiiiiiii(i,o,f,p,E,t,k,L){return ASM_CONSTS[i](o,f,p,E,t,k,L)}function _emscripten_asm_const_iiiii(i,o,f,p,E){return ASM_CONSTS[i](o,f,p,E)}function _emscripten_asm_const_iiidddddd(i,o,f,p,E,t,k,L,N){return ASM_CONSTS[i](o,f,p,E,t,k,L,N)}function _emscripten_asm_const_iiididi(i,o,f,p,E,t,k){return ASM_CONSTS[i](o,f,p,E,t,k)}function _emscripten_asm_const_iiii(i,o,f,p){return ASM_CONSTS[i](o,f,p)}function _emscripten_asm_const_iiiid(i,o,f,p,E){return ASM_CONSTS[i](o,f,p,E)}function _emscripten_asm_const_iiiiii(i,o,f,p,E,t){return ASM_CONSTS[i](o,f,p,E,t)}STATIC_BASE=Runtime.GLOBAL_BASE,STATICTOP=STATIC_BASE+12800,__ATINIT__.push({func:function(){__GLOBAL__sub_I_Yoga_cpp()}},{func:function(){__GLOBAL__sub_I_nbind_cc()}},{func:function(){__GLOBAL__sub_I_common_cc()}},{func:function(){__GLOBAL__sub_I_Binding_cc()}}),allocate([0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,192,127,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,3,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,192,127,0,0,192,127,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,128,191,0,0,128,191,0,0,192,127,0,0,0,0,0,0,0,0,0,0,128,63,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,3,0,0,0,1,0,0,0,2,0,0,0,0,0,0,0,190,12,0,0,200,12,0,0,208,12,0,0,216,12,0,0,230,12,0,0,242,12,0,0,1,0,0,0,3,0,0,0,0,0,0,0,2,0,0,0,0,0,192,127,3,0,0,0,180,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,182,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,3,0,0,0,1,0,0,0,4,0,0,0,183,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,181,45,0,0,184,45,0,0,185,45,0,0,181,45,0,0,181,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,148,4,0,0,3,0,0,0,187,45,0,0,164,4,0,0,188,45,0,0,2,0,0,0,189,45,0,0,164,4,0,0,188,45,0,0,185,45,0,0,164,4,0,0,185,45,0,0,164,4,0,0,188,45,0,0,181,45,0,0,182,45,0,0,181,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,5,0,0,0,6,0,0,0,1,0,0,0,7,0,0,0,183,45,0,0,182,45,0,0,181,45,0,0,190,45,0,0,190,45,0,0,182,45,0,0,182,45,0,0,185,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,181,45,0,0,185,45,0,0,182,45,0,0,185,45,0,0,48,5,0,0,3,0,0,0,56,5,0,0,1,0,0,0,189,45,0,0,185,45,0,0,164,4,0,0,76,5,0,0,2,0,0,0,191,45,0,0,186,45,0,0,182,45,0,0,185,45,0,0,192,45,0,0,185,45,0,0,182,45,0,0,186,45,0,0,185,45,0,0,76,5,0,0,76,5,0,0,136,5,0,0,182,45,0,0,181,45,0,0,2,0,0,0,190,45,0,0,136,5,0,0,56,19,0,0,156,5,0,0,2,0,0,0,184,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,8,0,0,0,9,0,0,0,1,0,0,0,10,0,0,0,204,5,0,0,181,45,0,0,181,45,0,0,2,0,0,0,180,45,0,0,204,5,0,0,2,0,0,0,195,45,0,0,236,5,0,0,97,19,0,0,198,45,0,0,211,45,0,0,212,45,0,0,213,45,0,0,214,45,0,0,215,45,0,0,188,45,0,0,182,45,0,0,216,45,0,0,217,45,0,0,218,45,0,0,219,45,0,0,192,45,0,0,181,45,0,0,0,0,0,0,185,45,0,0,110,19,0,0,186,45,0,0,115,19,0,0,221,45,0,0,120,19,0,0,148,4,0,0,132,19,0,0,96,6,0,0,145,19,0,0,222,45,0,0,164,19,0,0,223,45,0,0,173,19,0,0,0,0,0,0,3,0,0,0,104,6,0,0,1,0,0,0,187,45,0,0,0,0,0,0,0,0,0,0,1,0,0,0,11,0,0,0,12,0,0,0,1,0,0,0,13,0,0,0,185,45,0,0,224,45,0,0,164,6,0,0,188,45,0,0,172,6,0,0,180,6,0,0,2,0,0,0,188,6,0,0,7,0,0,0,224,45,0,0,7,0,0,0,164,6,0,0,1,0,0,0,213,45,0,0,185,45,0,0,224,45,0,0,172,6,0,0,185,45,0,0,224,45,0,0,164,6,0,0,185,45,0,0,224,45,0,0,211,45,0,0,211,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,222,45,0,0,211,45,0,0,224,45,0,0,172,6,0,0,222,45,0,0,211,45,0,0,224,45,0,0,188,45,0,0,222,45,0,0,211,45,0,0,40,7,0,0,188,45,0,0,2,0,0,0,224,45,0,0,185,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,188,45,0,0,222,45,0,0,224,45,0,0,148,4,0,0,185,45,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,148,4,0,0,185,45,0,0,164,6,0,0,148,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,14,0,0,0,15,0,0,0,1,0,0,0,16,0,0,0,148,7,0,0,2,0,0,0,225,45,0,0,183,45,0,0,188,45,0,0,168,7,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,2,0,0,0,234,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,148,45,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,28,9,0,0,5,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,0,2,0,0,0,242,45,0,0,0,4,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,110,111,100,101,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,119,104,105,99,104,32,115,116,105,108,108,32,104,97,115,32,99,104,105,108,100,114,101,110,32,97,116,116,97,99,104,101,100,0,67,97,110,110,111,116,32,114,101,115,101,116,32,97,32,110,111,100,101,32,115,116,105,108,108,32,97,116,116,97,99,104,101,100,32,116,111,32,97,32,112,97,114,101,110,116,0,67,111,117,108,100,32,110,111,116,32,97,108,108,111,99,97,116,101,32,109,101,109,111,114,121,32,102,111,114,32,99,111,110,102,105,103,0,67,97,110,110,111,116,32,115,101,116,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,67,104,105,108,100,32,97,108,114,101,97,100,121,32,104,97,115,32,97,32,112,97,114,101,110,116,44,32,105,116,32,109,117,115,116,32,98,101,32,114,101,109,111,118,101,100,32,102,105,114,115,116,46,0,67,97,110,110,111,116,32,97,100,100,32,99,104,105,108,100,58,32,78,111,100,101,115,32,119,105,116,104,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,32,99,97,110,110,111,116,32,104,97,118,101,32,99,104,105,108,100,114,101,110,46,0,79,110,108,121,32,108,101,97,102,32,110,111,100,101,115,32,119,105,116,104,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,115,115,104,111,117,108,100,32,109,97,110,117,97,108,108,121,32,109,97,114,107,32,116,104,101,109,115,101,108,118,101,115,32,97,115,32,100,105,114,116,121,0,67,97,110,110,111,116,32,103,101,116,32,108,97,121,111,117,116,32,112,114,111,112,101,114,116,105,101,115,32,111,102,32,109,117,108,116,105,45,101,100,103,101,32,115,104,111,114,116,104,97,110,100,115,0,37,115,37,100,46,123,91,115,107,105,112,112,101,100,93,32,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,61,62,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,37,115,37,100,46,123,37,115,0,42,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,97,119,58,32,37,102,32,97,104,58,32,37,102,32,37,115,10,0,37,115,37,100,46,125,37,115,0,119,109,58,32,37,115,44,32,104,109,58,32,37,115,44,32,100,58,32,40,37,102,44,32,37,102,41,32,37,115,10,0,79,117,116,32,111,102,32,99,97,99,104,101,32,101,110,116,114,105,101,115,33,10,0,83,99,97,108,101,32,102,97,99,116,111,114,32,115,104,111,117,108,100,32,110,111,116,32,98,101,32,108,101,115,115,32,116,104,97,110,32,122,101,114,111,0,105,110,105,116,105,97,108,0,37,115,10,0,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,0,85,78,68,69,70,73,78,69,68,0,69,88,65,67,84,76,89,0,65,84,95,77,79,83,84,0,76,65,89,95,85,78,68,69,70,73,78,69,68,0,76,65,89,95,69,88,65,67,84,76,89,0,76,65,89,95,65,84,95,77,79,83,84,0,97,118,97,105,108,97,98,108,101,87,105,100,116,104,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,119,105,100,116,104,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,97,118,97,105,108,97,98,108,101,72,101,105,103,104,116,32,105,115,32,105,110,100,101,102,105,110,105,116,101,32,115,111,32,104,101,105,103,104,116,77,101,97,115,117,114,101,77,111,100,101,32,109,117,115,116,32,98,101,32,89,71,77,101,97,115,117,114,101,77,111,100,101,85,110,100,101,102,105,110,101,100,0,102,108,101,120,0,115,116,114,101,116,99,104,0,109,117,108,116,105,108,105,110,101,45,115,116,114,101,116,99,104,0,69,120,112,101,99,116,101,100,32,110,111,100,101,32,116,111,32,104,97,118,101,32,99,117,115,116,111,109,32,109,101,97,115,117,114,101,32,102,117,110,99,116,105,111,110,0,109,101,97,115,117,114,101,0,69,120,112,101,99,116,32,99,117,115,116,111,109,32,98,97,115,101,108,105,110,101,32,102,117,110,99,116,105,111,110,32,116,111,32,110,111,116,32,114,101,116,117,114,110,32,78,97,78,0,97,98,115,45,109,101,97,115,117,114,101,0,97,98,115,45,108,97,121,111,117,116,0,78,111,100,101,0,99,114,101,97,116,101,68,101,102,97,117,108,116,0,99,114,101,97,116,101,87,105,116,104,67,111,110,102,105,103,0,100,101,115,116,114,111,121,0,114,101,115,101,116,0,99,111,112,121,83,116,121,108,101,0,115,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,115,101,116,80,111,115,105,116,105,111,110,0,115,101,116,80,111,115,105,116,105,111,110,80,101,114,99,101,110,116,0,115,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,115,101,116,65,108,105,103,110,73,116,101,109,115,0,115,101,116,65,108,105,103,110,83,101,108,102,0,115,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,115,101,116,70,108,101,120,87,114,97,112,0,115,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,115,101,116,77,97,114,103,105,110,0,115,101,116,77,97,114,103,105,110,80,101,114,99,101,110,116,0,115,101,116,77,97,114,103,105,110,65,117,116,111,0,115,101,116,79,118,101,114,102,108,111,119,0,115,101,116,68,105,115,112,108,97,121,0,115,101,116,70,108,101,120,0,115,101,116,70,108,101,120,66,97,115,105,115,0,115,101,116,70,108,101,120,66,97,115,105,115,80,101,114,99,101,110,116,0,115,101,116,70,108,101,120,71,114,111,119,0,115,101,116,70,108,101,120,83,104,114,105,110,107,0,115,101,116,87,105,100,116,104,0,115,101,116,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,87,105,100,116,104,65,117,116,111,0,115,101,116,72,101,105,103,104,116,0,115,101,116,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,72,101,105,103,104,116,65,117,116,111,0,115,101,116,77,105,110,87,105,100,116,104,0,115,101,116,77,105,110,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,105,110,72,101,105,103,104,116,0,115,101,116,77,105,110,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,77,97,120,87,105,100,116,104,0,115,101,116,77,97,120,87,105,100,116,104,80,101,114,99,101,110,116,0,115,101,116,77,97,120,72,101,105,103,104,116,0,115,101,116,77,97,120,72,101,105,103,104,116,80,101,114,99,101,110,116,0,115,101,116,65,115,112,101,99,116,82,97,116,105,111,0,115,101,116,66,111,114,100,101,114,0,115,101,116,80,97,100,100,105,110,103,0,115,101,116,80,97,100,100,105,110,103,80,101,114,99,101,110,116,0,103,101,116,80,111,115,105,116,105,111,110,84,121,112,101,0,103,101,116,80,111,115,105,116,105,111,110,0,103,101,116,65,108,105,103,110,67,111,110,116,101,110,116,0,103,101,116,65,108,105,103,110,73,116,101,109,115,0,103,101,116,65,108,105,103,110,83,101,108,102,0,103,101,116,70,108,101,120,68,105,114,101,99,116,105,111,110,0,103,101,116,70,108,101,120,87,114,97,112,0,103,101,116,74,117,115,116,105,102,121,67,111,110,116,101,110,116,0,103,101,116,77,97,114,103,105,110,0,103,101,116,70,108,101,120,66,97,115,105,115,0,103,101,116,70,108,101,120,71,114,111,119,0,103,101,116,70,108,101,120,83,104,114,105,110,107,0,103,101,116,87,105,100,116,104,0,103,101,116,72,101,105,103,104,116,0,103,101,116,77,105,110,87,105,100,116,104,0,103,101,116,77,105,110,72,101,105,103,104,116,0,103,101,116,77,97,120,87,105,100,116,104,0,103,101,116,77,97,120,72,101,105,103,104,116,0,103,101,116,65,115,112,101,99,116,82,97,116,105,111,0,103,101,116,66,111,114,100,101,114,0,103,101,116,79,118,101,114,102,108,111,119,0,103,101,116,68,105,115,112,108,97,121,0,103,101,116,80,97,100,100,105,110,103,0,105,110,115,101,114,116,67,104,105,108,100,0,114,101,109,111,118,101,67,104,105,108,100,0,103,101,116,67,104,105,108,100,67,111,117,110,116,0,103,101,116,80,97,114,101,110,116,0,103,101,116,67,104,105,108,100,0,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,117,110,115,101,116,77,101,97,115,117,114,101,70,117,110,99,0,109,97,114,107,68,105,114,116,121,0,105,115,68,105,114,116,121,0,99,97,108,99,117,108,97,116,101,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,76,101,102,116,0,103,101,116,67,111,109,112,117,116,101,100,82,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,84,111,112,0,103,101,116,67,111,109,112,117,116,101,100,66,111,116,116,111,109,0,103,101,116,67,111,109,112,117,116,101,100,87,105,100,116,104,0,103,101,116,67,111,109,112,117,116,101,100,72,101,105,103,104,116,0,103,101,116,67,111,109,112,117,116,101,100,76,97,121,111,117,116,0,103,101,116,67,111,109,112,117,116,101,100,77,97,114,103,105,110,0,103,101,116,67,111,109,112,117,116,101,100,66,111,114,100,101,114,0,103,101,116,67,111,109,112,117,116,101,100,80,97,100,100,105,110,103,0,67,111,110,102,105,103,0,99,114,101,97,116,101,0,115,101,116,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,115,101,116,80,111,105,110,116,83,99,97,108,101,70,97,99,116,111,114,0,105,115,69,120,112,101,114,105,109,101,110,116,97,108,70,101,97,116,117,114,101,69,110,97,98,108,101,100,0,86,97,108,117,101,0,76,97,121,111,117,116,0,83,105,122,101,0,103,101,116,73,110,115,116,97,110,99,101,67,111,117,110,116,0,73,110,116,54,52,0,1,1,1,2,2,4,4,4,4,8,8,4,8,118,111,105,100,0,98,111,111,108,0,115,116,100,58,58,115,116,114,105,110,103,0,99,98,70,117,110,99,116,105,111,110,32,38,0,99,111,110,115,116,32,99,98,70,117,110,99,116,105,111,110,32,38,0,69,120,116,101,114,110,97,108,0,66,117,102,102,101,114,0,78,66,105,110,100,73,68,0,78,66,105,110,100,0,98,105,110,100,95,118,97,108,117,101,0,114,101,102,108,101,99,116,0,113,117,101,114,121,84,121,112,101,0,108,97,108,108,111,99,0,108,114,101,115,101,116,0,123,114,101,116,117,114,110,40,95,110,98,105,110,100,46,99,97,108,108,98,97,99,107,83,105,103,110,97,116,117,114,101,76,105,115,116,91,36,48,93,46,97,112,112,108,121,40,116,104,105,115,44,97,114,103,117,109,101,110,116,115,41,41,59,125,0,95,110,98,105,110,100,95,110,101,119,0,17,0,10,0,17,17,17,0,0,0,0,5,0,0,0,0,0,0,9,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,15,10,17,17,17,3,10,7,0,1,19,9,11,11,0,0,9,6,11,0,0,11,0,6,17,0,0,0,17,17,17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,17,0,10,10,17,17,17,0,10,0,0,2,0,9,11,0,0,0,9,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,14,0,0,0,0,0,0,0,0,0,0,0,13,0,0,0,4,13,0,0,0,0,9,14,0,0,0,0,0,14,0,0,14,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,0,0,0,0,0,0,0,15,0,0,0,0,15,0,0,0,0,9,16,0,0,0,0,0,16,0,0,16,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,18,0,0,0,18,18,18,0,0,0,0,0,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,11,0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,10,0,0,0,0,9,11,0,0,0,0,0,11,0,0,11,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,0,0,0,0,0,0,0,12,0,0,0,0,12,0,0,0,0,9,12,0,0,0,0,0,12,0,0,12,0,0,45,43,32,32,32,48,88,48,120,0,40,110,117,108,108,41,0,45,48,88,43,48,88,32,48,88,45,48,120,43,48,120,32,48,120,0,105,110,102,0,73,78,70,0,110,97,110,0,78,65,78,0,48,49,50,51,52,53,54,55,56,57,65,66,67,68,69,70,46,0,84,33,34,25,13,1,2,3,17,75,28,12,16,4,11,29,18,30,39,104,110,111,112,113,98,32,5,6,15,19,20,21,26,8,22,7,40,36,23,24,9,10,14,27,31,37,35,131,130,125,38,42,43,60,61,62,63,67,71,74,77,88,89,90,91,92,93,94,95,96,97,99,100,101,102,103,105,106,107,108,114,115,116,121,122,123,124,0,73,108,108,101,103,97,108,32,98,121,116,101,32,115,101,113,117,101,110,99,101,0,68,111,109,97,105,110,32,101,114,114,111,114,0,82,101,115,117,108,116,32,110,111,116,32,114,101,112,114,101,115,101,110,116,97,98,108,101,0,78,111,116,32,97,32,116,116,121,0,80,101,114,109,105,115,115,105,111,110,32,100,101,110,105,101,100,0,79,112,101,114,97,116,105,111,110,32,110,111,116,32,112,101,114,109,105,116,116,101,100,0,78,111,32,115,117,99,104,32,102,105,108,101,32,111,114,32,100,105,114,101,99,116,111,114,121,0,78,111,32,115,117,99,104,32,112,114,111,99,101,115,115,0,70,105,108,101,32,101,120,105,115,116,115,0,86,97,108,117,101,32,116,111,111,32,108,97,114,103,101,32,102,111,114,32,100,97,116,97,32,116,121,112,101,0,78,111,32,115,112,97,99,101,32,108,101,102,116,32,111,110,32,100,101,118,105,99,101,0,79,117,116,32,111,102,32,109,101,109,111,114,121,0,82,101,115,111,117,114,99,101,32,98,117,115,121,0,73,110,116,101,114,114,117,112,116,101,100,32,115,121,115,116,101,109,32,99,97,108,108,0,82,101,115,111,117,114,99,101,32,116,101,109,112,111,114,97,114,105,108,121,32,117,110,97,118,97,105,108,97,98,108,101,0,73,110,118,97,108,105,100,32,115,101,101,107,0,67,114,111,115,115,45,100,101,118,105,99,101,32,108,105,110,107,0,82,101,97,100,45,111,110,108,121,32,102,105,108,101,32,115,121,115,116,101,109,0,68,105,114,101,99,116,111,114,121,32,110,111,116,32,101,109,112,116,121,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,112,101,101,114,0,79,112,101,114,97,116,105,111,110,32,116,105,109,101,100,32,111,117,116,0,67,111,110,110,101,99,116,105,111,110,32,114,101,102,117,115,101,100,0,72,111,115,116,32,105,115,32,100,111,119,110,0,72,111,115,116,32,105,115,32,117,110,114,101,97,99,104,97,98,108,101,0,65,100,100,114,101,115,115,32,105,110,32,117,115,101,0,66,114,111,107,101,110,32,112,105,112,101,0,73,47,79,32,101,114,114,111,114,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,32,111,114,32,97,100,100,114,101,115,115,0,66,108,111,99,107,32,100,101,118,105,99,101,32,114,101,113,117,105,114,101,100,0,78,111,32,115,117,99,104,32,100,101,118,105,99,101,0,78,111,116,32,97,32,100,105,114,101,99,116,111,114,121,0,73,115,32,97,32,100,105,114,101,99,116,111,114,121,0,84,101,120,116,32,102,105,108,101,32,98,117,115,121,0,69,120,101,99,32,102,111,114,109,97,116,32,101,114,114,111,114,0,73,110,118,97,108,105,100,32,97,114,103,117,109,101,110,116,0,65,114,103,117,109,101,110,116,32,108,105,115,116,32,116,111,111,32,108,111,110,103,0,83,121,109,98,111,108,105,99,32,108,105,110,107,32,108,111,111,112,0,70,105,108,101,110,97,109,101,32,116,111,111,32,108,111,110,103,0,84,111,111,32,109,97,110,121,32,111,112,101,110,32,102,105,108,101,115,32,105,110,32,115,121,115,116,101,109,0,78,111,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,115,32,97,118,97,105,108,97,98,108,101,0,66,97,100,32,102,105,108,101,32,100,101,115,99,114,105,112,116,111,114,0,78,111,32,99,104,105,108,100,32,112,114,111,99,101,115,115,0,66,97,100,32,97,100,100,114,101,115,115,0,70,105,108,101,32,116,111,111,32,108,97,114,103,101,0,84,111,111,32,109,97,110,121,32,108,105,110,107,115,0,78,111,32,108,111,99,107,115,32,97,118,97,105,108,97,98,108,101,0,82,101,115,111,117,114,99,101,32,100,101,97,100,108,111,99,107,32,119,111,117,108,100,32,111,99,99,117,114,0,83,116,97,116,101,32,110,111,116,32,114,101,99,111,118,101,114,97,98,108,101,0,80,114,101,118,105,111,117,115,32,111,119,110,101,114,32,100,105,101,100,0,79,112,101,114,97,116,105,111,110,32,99,97,110,99,101,108,101,100,0,70,117,110,99,116,105,111,110,32,110,111,116,32,105,109,112,108,101,109,101,110,116,101,100,0,78,111,32,109,101,115,115,97,103,101,32,111,102,32,100,101,115,105,114,101,100,32,116,121,112,101,0,73,100,101,110,116,105,102,105,101,114,32,114,101,109,111,118,101,100,0,68,101,118,105,99,101,32,110,111,116,32,97,32,115,116,114,101,97,109,0,78,111,32,100,97,116,97,32,97,118,97,105,108,97,98,108,101,0,68,101,118,105,99,101,32,116,105,109,101,111,117,116,0,79,117,116,32,111,102,32,115,116,114,101,97,109,115,32,114,101,115,111,117,114,99,101,115,0,76,105,110,107,32,104,97,115,32,98,101,101,110,32,115,101,118,101,114,101,100,0,80,114,111,116,111,99,111,108,32,101,114,114,111,114,0,66,97,100,32,109,101,115,115,97,103,101,0,70,105,108,101,32,100,101,115,99,114,105,112,116,111,114,32,105,110,32,98,97,100,32,115,116,97,116,101,0,78,111,116,32,97,32,115,111,99,107,101,116,0,68,101,115,116,105,110,97,116,105,111,110,32,97,100,100,114,101,115,115,32,114,101,113,117,105,114,101,100,0,77,101,115,115,97,103,101,32,116,111,111,32,108,97,114,103,101,0,80,114,111,116,111,99,111,108,32,119,114,111,110,103,32,116,121,112,101,32,102,111,114,32,115,111,99,107,101,116,0,80,114,111,116,111,99,111,108,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,80,114,111,116,111,99,111,108,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,83,111,99,107,101,116,32,116,121,112,101,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,78,111,116,32,115,117,112,112,111,114,116,101,100,0,80,114,111,116,111,99,111,108,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,0,65,100,100,114,101,115,115,32,102,97,109,105,108,121,32,110,111,116,32,115,117,112,112,111,114,116,101,100,32,98,121,32,112,114,111,116,111,99,111,108,0,65,100,100,114,101,115,115,32,110,111,116,32,97,118,97,105,108,97,98,108,101,0,78,101,116,119,111,114,107,32,105,115,32,100,111,119,110,0,78,101,116,119,111,114,107,32,117,110,114,101,97,99,104,97,98,108,101,0,67,111,110,110,101,99,116,105,111,110,32,114,101,115,101,116,32,98,121,32,110,101,116,119,111,114,107,0,67,111,110,110,101,99,116,105,111,110,32,97,98,111,114,116,101,100,0,78,111,32,98,117,102,102,101,114,32,115,112,97,99,101,32,97,118,97,105,108,97,98,108,101,0,83,111,99,107,101,116,32,105,115,32,99,111,110,110,101,99,116,101,100,0,83,111,99,107,101,116,32,110,111,116,32,99,111,110,110,101,99,116,101,100,0,67,97,110,110,111,116,32,115,101,110,100,32,97,102,116,101,114,32,115,111,99,107,101,116,32,115,104,117,116,100,111,119,110,0,79,112,101,114,97,116,105,111,110,32,97,108,114,101,97,100,121,32,105,110,32,112,114,111,103,114,101,115,115,0,79,112,101,114,97,116,105,111,110,32,105,110,32,112,114,111,103,114,101,115,115,0,83,116,97,108,101,32,102,105,108,101,32,104,97,110,100,108,101,0,82,101,109,111,116,101,32,73,47,79,32,101,114,114,111,114,0,81,117,111,116,97,32,101,120,99,101,101,100,101,100,0,78,111,32,109,101,100,105,117,109,32,102,111,117,110,100,0,87,114,111,110,103,32,109,101,100,105,117,109,32,116,121,112,101,0,78,111,32,101,114,114,111,114,32,105,110,102,111,114,109,97,116,105,111,110,0,0],"i8",ALLOC_NONE,Runtime.GLOBAL_BASE);var tempDoublePtr=STATICTOP;STATICTOP+=16;function _atexit(i,o){__ATEXIT__.unshift({func:i,arg:o})}function ___cxa_atexit(){return _atexit.apply(null,arguments)}function _abort(){Module.abort()}function __ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj(){Module.printErr("missing function: _ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj"),abort(-1)}function __decorate(i,o,f,p){var E=arguments.length,t=E<3?o:p===null?p=Object.getOwnPropertyDescriptor(o,f):p,k;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")t=Reflect.decorate(i,o,f,p);else for(var L=i.length-1;L>=0;L--)(k=i[L])&&(t=(E<3?k(t):E>3?k(o,f,t):k(o,f))||t);return E>3&&t&&Object.defineProperty(o,f,t),t}function _defineHidden(i){return function(o,f){Object.defineProperty(o,f,{configurable:!1,enumerable:!1,value:i,writable:!0})}}var _nbind={};function __nbind_free_external(i){_nbind.externalList[i].dereference(i)}function __nbind_reference_external(i){_nbind.externalList[i].reference()}function _llvm_stackrestore(i){var o=_llvm_stacksave,f=o.LLVM_SAVEDSTACKS[i];o.LLVM_SAVEDSTACKS.splice(i,1),Runtime.stackRestore(f)}function __nbind_register_pool(i,o,f,p){_nbind.Pool.pageSize=i,_nbind.Pool.usedPtr=o/4,_nbind.Pool.rootPtr=f,_nbind.Pool.pagePtr=p/4,HEAP32[o/4]=16909060,HEAP8[o]==1&&(_nbind.bigEndian=!0),HEAP32[o/4]=0,_nbind.makeTypeKindTbl=(t={},t[1024]=_nbind.PrimitiveType,t[64]=_nbind.Int64Type,t[2048]=_nbind.BindClass,t[3072]=_nbind.BindClassPtr,t[4096]=_nbind.SharedClassPtr,t[5120]=_nbind.ArrayType,t[6144]=_nbind.ArrayType,t[7168]=_nbind.CStringType,t[9216]=_nbind.CallbackType,t[10240]=_nbind.BindType,t),_nbind.makeTypeNameTbl={Buffer:_nbind.BufferType,External:_nbind.ExternalType,Int64:_nbind.Int64Type,_nbind_new:_nbind.CreateValueType,bool:_nbind.BooleanType,"cbFunction &":_nbind.CallbackType,"const cbFunction &":_nbind.CallbackType,"const std::string &":_nbind.StringType,"std::string":_nbind.StringType},Module.toggleLightGC=_nbind.toggleLightGC,_nbind.callUpcast=Module.dynCall_ii;var E=_nbind.makeType(_nbind.constructType,{flags:2048,id:0,name:""});E.proto=Module,_nbind.BindClass.list.push(E);var t}function _emscripten_set_main_loop_timing(i,o){if(Browser.mainLoop.timingMode=i,Browser.mainLoop.timingValue=o,!Browser.mainLoop.func)return 1;if(i==0)Browser.mainLoop.scheduler=function(){var k=Math.max(0,Browser.mainLoop.tickStartTime+o-_emscripten_get_now())|0;setTimeout(Browser.mainLoop.runner,k)},Browser.mainLoop.method="timeout";else if(i==1)Browser.mainLoop.scheduler=function(){Browser.requestAnimationFrame(Browser.mainLoop.runner)},Browser.mainLoop.method="rAF";else if(i==2){if(!window.setImmediate){let t=function(k){k.source===window&&k.data===p&&(k.stopPropagation(),f.shift()())};var E=t,f=[],p="setimmediate";window.addEventListener("message",t,!0),window.setImmediate=function(L){f.push(L),ENVIRONMENT_IS_WORKER?(Module.setImmediates===void 0&&(Module.setImmediates=[]),Module.setImmediates.push(L),window.postMessage({target:p})):window.postMessage(p,"*")}}Browser.mainLoop.scheduler=function(){window.setImmediate(Browser.mainLoop.runner)},Browser.mainLoop.method="immediate"}return 0}function _emscripten_get_now(){abort()}function _emscripten_set_main_loop(i,o,f,p,E){Module.noExitRuntime=!0,assert(!Browser.mainLoop.func,"emscripten_set_main_loop: there can only be one main loop function at once: call emscripten_cancel_main_loop to cancel the previous one before setting a new one with different parameters."),Browser.mainLoop.func=i,Browser.mainLoop.arg=p;var t;typeof p!="undefined"?t=function(){Module.dynCall_vi(i,p)}:t=function(){Module.dynCall_v(i)};var k=Browser.mainLoop.currentlyRunningMainloop;if(Browser.mainLoop.runner=function(){if(!ABORT){if(Browser.mainLoop.queue.length>0){var N=Date.now(),C=Browser.mainLoop.queue.shift();if(C.func(C.arg),Browser.mainLoop.remainingBlockers){var U=Browser.mainLoop.remainingBlockers,q=U%1==0?U-1:Math.floor(U);C.counted?Browser.mainLoop.remainingBlockers=q:(q=q+.5,Browser.mainLoop.remainingBlockers=(8*U+q)/9)}if(console.log('main loop blocker "'+C.name+'" took '+(Date.now()-N)+" ms"),Browser.mainLoop.updateStatus(),k1&&Browser.mainLoop.currentFrameNumber%Browser.mainLoop.timingValue!=0){Browser.mainLoop.scheduler();return}else Browser.mainLoop.timingMode==0&&(Browser.mainLoop.tickStartTime=_emscripten_get_now());Browser.mainLoop.method==="timeout"&&Module.ctx&&(Module.printErr("Looks like you are rendering without using requestAnimationFrame for the main loop. You should use 0 for the frame rate in emscripten_set_main_loop in order to use requestAnimationFrame, as that can greatly improve your frame rates!"),Browser.mainLoop.method=""),Browser.mainLoop.runIter(t),!(k0?_emscripten_set_main_loop_timing(0,1e3/o):_emscripten_set_main_loop_timing(1,1),Browser.mainLoop.scheduler()),f)throw"SimulateInfiniteLoop"}var Browser={mainLoop:{scheduler:null,method:"",currentlyRunningMainloop:0,func:null,arg:0,timingMode:0,timingValue:0,currentFrameNumber:0,queue:[],pause:function(){Browser.mainLoop.scheduler=null,Browser.mainLoop.currentlyRunningMainloop++},resume:function(){Browser.mainLoop.currentlyRunningMainloop++;var i=Browser.mainLoop.timingMode,o=Browser.mainLoop.timingValue,f=Browser.mainLoop.func;Browser.mainLoop.func=null,_emscripten_set_main_loop(f,0,!1,Browser.mainLoop.arg,!0),_emscripten_set_main_loop_timing(i,o),Browser.mainLoop.scheduler()},updateStatus:function(){if(Module.setStatus){var i=Module.statusMessage||"Please wait...",o=Browser.mainLoop.remainingBlockers,f=Browser.mainLoop.expectedBlockers;o?o=6;){var rt=le>>Ue-6&63;Ue-=6,Oe+=ze[rt]}return Ue==2?(Oe+=ze[(le&3)<<4],Oe+=pe+pe):Ue==4&&(Oe+=ze[(le&15)<<2],Oe+=pe),Oe}m.src="data:audio/x-"+k.substr(-3)+";base64,"+he(t),U(m)},m.src=ne,Browser.safeSetTimeout(function(){U(m)},1e4)}else return q()},Module.preloadPlugins.push(o);function f(){Browser.pointerLock=document.pointerLockElement===Module.canvas||document.mozPointerLockElement===Module.canvas||document.webkitPointerLockElement===Module.canvas||document.msPointerLockElement===Module.canvas}var p=Module.canvas;p&&(p.requestPointerLock=p.requestPointerLock||p.mozRequestPointerLock||p.webkitRequestPointerLock||p.msRequestPointerLock||function(){},p.exitPointerLock=document.exitPointerLock||document.mozExitPointerLock||document.webkitExitPointerLock||document.msExitPointerLock||function(){},p.exitPointerLock=p.exitPointerLock.bind(document),document.addEventListener("pointerlockchange",f,!1),document.addEventListener("mozpointerlockchange",f,!1),document.addEventListener("webkitpointerlockchange",f,!1),document.addEventListener("mspointerlockchange",f,!1),Module.elementPointerLock&&p.addEventListener("click",function(E){!Browser.pointerLock&&Module.canvas.requestPointerLock&&(Module.canvas.requestPointerLock(),E.preventDefault())},!1))},createContext:function(i,o,f,p){if(o&&Module.ctx&&i==Module.canvas)return Module.ctx;var E,t;if(o){var k={antialias:!1,alpha:!1};if(p)for(var L in p)k[L]=p[L];t=GL.createContext(i,k),t&&(E=GL.getContext(t).GLctx)}else E=i.getContext("2d");return E?(f&&(o||assert(typeof GLctx=="undefined","cannot set in module if GLctx is used, but we are a non-GL context that would replace it"),Module.ctx=E,o&&GL.makeContextCurrent(t),Module.useWebGL=o,Browser.moduleContextCreatedCallbacks.forEach(function(N){N()}),Browser.init()),E):null},destroyContext:function(i,o,f){},fullscreenHandlersInstalled:!1,lockPointer:void 0,resizeCanvas:void 0,requestFullscreen:function(i,o,f){Browser.lockPointer=i,Browser.resizeCanvas=o,Browser.vrDevice=f,typeof Browser.lockPointer=="undefined"&&(Browser.lockPointer=!0),typeof Browser.resizeCanvas=="undefined"&&(Browser.resizeCanvas=!1),typeof Browser.vrDevice=="undefined"&&(Browser.vrDevice=null);var p=Module.canvas;function E(){Browser.isFullscreen=!1;var k=p.parentNode;(document.fullscreenElement||document.mozFullScreenElement||document.msFullscreenElement||document.webkitFullscreenElement||document.webkitCurrentFullScreenElement)===k?(p.exitFullscreen=document.exitFullscreen||document.cancelFullScreen||document.mozCancelFullScreen||document.msExitFullscreen||document.webkitCancelFullScreen||function(){},p.exitFullscreen=p.exitFullscreen.bind(document),Browser.lockPointer&&p.requestPointerLock(),Browser.isFullscreen=!0,Browser.resizeCanvas&&Browser.setFullscreenCanvasSize()):(k.parentNode.insertBefore(p,k),k.parentNode.removeChild(k),Browser.resizeCanvas&&Browser.setWindowedCanvasSize()),Module.onFullScreen&&Module.onFullScreen(Browser.isFullscreen),Module.onFullscreen&&Module.onFullscreen(Browser.isFullscreen),Browser.updateCanvasDimensions(p)}Browser.fullscreenHandlersInstalled||(Browser.fullscreenHandlersInstalled=!0,document.addEventListener("fullscreenchange",E,!1),document.addEventListener("mozfullscreenchange",E,!1),document.addEventListener("webkitfullscreenchange",E,!1),document.addEventListener("MSFullscreenChange",E,!1));var t=document.createElement("div");p.parentNode.insertBefore(t,p),t.appendChild(p),t.requestFullscreen=t.requestFullscreen||t.mozRequestFullScreen||t.msRequestFullscreen||(t.webkitRequestFullscreen?function(){t.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}:null)||(t.webkitRequestFullScreen?function(){t.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT)}:null),f?t.requestFullscreen({vrDisplay:f}):t.requestFullscreen()},requestFullScreen:function(i,o,f){return Module.printErr("Browser.requestFullScreen() is deprecated. Please call Browser.requestFullscreen instead."),Browser.requestFullScreen=function(p,E,t){return Browser.requestFullscreen(p,E,t)},Browser.requestFullscreen(i,o,f)},nextRAF:0,fakeRequestAnimationFrame:function(i){var o=Date.now();if(Browser.nextRAF===0)Browser.nextRAF=o+1e3/60;else for(;o+2>=Browser.nextRAF;)Browser.nextRAF+=1e3/60;var f=Math.max(Browser.nextRAF-o,0);setTimeout(i,f)},requestAnimationFrame:function(o){typeof window=="undefined"?Browser.fakeRequestAnimationFrame(o):(window.requestAnimationFrame||(window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame||window.oRequestAnimationFrame||Browser.fakeRequestAnimationFrame),window.requestAnimationFrame(o))},safeCallback:function(i){return function(){if(!ABORT)return i.apply(null,arguments)}},allowAsyncCallbacks:!0,queuedAsyncCallbacks:[],pauseAsyncCallbacks:function(){Browser.allowAsyncCallbacks=!1},resumeAsyncCallbacks:function(){if(Browser.allowAsyncCallbacks=!0,Browser.queuedAsyncCallbacks.length>0){var i=Browser.queuedAsyncCallbacks;Browser.queuedAsyncCallbacks=[],i.forEach(function(o){o()})}},safeRequestAnimationFrame:function(i){return Browser.requestAnimationFrame(function(){ABORT||(Browser.allowAsyncCallbacks?i():Browser.queuedAsyncCallbacks.push(i))})},safeSetTimeout:function(i,o){return Module.noExitRuntime=!0,setTimeout(function(){ABORT||(Browser.allowAsyncCallbacks?i():Browser.queuedAsyncCallbacks.push(i))},o)},safeSetInterval:function(i,o){return Module.noExitRuntime=!0,setInterval(function(){ABORT||Browser.allowAsyncCallbacks&&i()},o)},getMimetype:function(i){return{jpg:"image/jpeg",jpeg:"image/jpeg",png:"image/png",bmp:"image/bmp",ogg:"audio/ogg",wav:"audio/wav",mp3:"audio/mpeg"}[i.substr(i.lastIndexOf(".")+1)]},getUserMedia:function(i){window.getUserMedia||(window.getUserMedia=navigator.getUserMedia||navigator.mozGetUserMedia),window.getUserMedia(i)},getMovementX:function(i){return i.movementX||i.mozMovementX||i.webkitMovementX||0},getMovementY:function(i){return i.movementY||i.mozMovementY||i.webkitMovementY||0},getMouseWheelDelta:function(i){var o=0;switch(i.type){case"DOMMouseScroll":o=i.detail;break;case"mousewheel":o=i.wheelDelta;break;case"wheel":o=i.deltaY;break;default:throw"unrecognized mouse wheel event: "+i.type}return o},mouseX:0,mouseY:0,mouseMovementX:0,mouseMovementY:0,touches:{},lastTouches:{},calculateMouseEvent:function(i){if(Browser.pointerLock)i.type!="mousemove"&&"mozMovementX"in i?Browser.mouseMovementX=Browser.mouseMovementY=0:(Browser.mouseMovementX=Browser.getMovementX(i),Browser.mouseMovementY=Browser.getMovementY(i)),typeof SDL!="undefined"?(Browser.mouseX=SDL.mouseX+Browser.mouseMovementX,Browser.mouseY=SDL.mouseY+Browser.mouseMovementY):(Browser.mouseX+=Browser.mouseMovementX,Browser.mouseY+=Browser.mouseMovementY);else{var o=Module.canvas.getBoundingClientRect(),f=Module.canvas.width,p=Module.canvas.height,E=typeof window.scrollX!="undefined"?window.scrollX:window.pageXOffset,t=typeof window.scrollY!="undefined"?window.scrollY:window.pageYOffset;if(i.type==="touchstart"||i.type==="touchend"||i.type==="touchmove"){var k=i.touch;if(k===void 0)return;var L=k.pageX-(E+o.left),N=k.pageY-(t+o.top);L=L*(f/o.width),N=N*(p/o.height);var C={x:L,y:N};if(i.type==="touchstart")Browser.lastTouches[k.identifier]=C,Browser.touches[k.identifier]=C;else if(i.type==="touchend"||i.type==="touchmove"){var U=Browser.touches[k.identifier];U||(U=C),Browser.lastTouches[k.identifier]=U,Browser.touches[k.identifier]=C}return}var q=i.pageX-(E+o.left),W=i.pageY-(t+o.top);q=q*(f/o.width),W=W*(p/o.height),Browser.mouseMovementX=q-Browser.mouseX,Browser.mouseMovementY=W-Browser.mouseY,Browser.mouseX=q,Browser.mouseY=W}},asyncLoad:function(i,o,f,p){var E=p?"":getUniqueRunDependency("al "+i);Module.readAsync(i,function(t){assert(t,'Loading data file "'+i+'" failed (no arrayBuffer).'),o(new Uint8Array(t)),E&&removeRunDependency(E)},function(t){if(f)f();else throw'Loading data file "'+i+'" failed.'}),E&&addRunDependency(E)},resizeListeners:[],updateResizeListeners:function(){var i=Module.canvas;Browser.resizeListeners.forEach(function(o){o(i.width,i.height)})},setCanvasSize:function(i,o,f){var p=Module.canvas;Browser.updateCanvasDimensions(p,i,o),f||Browser.updateResizeListeners()},windowedWidth:0,windowedHeight:0,setFullscreenCanvasSize:function(){if(typeof SDL!="undefined"){var i=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];i=i|8388608,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=i}Browser.updateResizeListeners()},setWindowedCanvasSize:function(){if(typeof SDL!="undefined"){var i=HEAPU32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2];i=i&~8388608,HEAP32[SDL.screen+Runtime.QUANTUM_SIZE*0>>2]=i}Browser.updateResizeListeners()},updateCanvasDimensions:function(i,o,f){o&&f?(i.widthNative=o,i.heightNative=f):(o=i.widthNative,f=i.heightNative);var p=o,E=f;if(Module.forcedAspectRatio&&Module.forcedAspectRatio>0&&(p/E>2];return o},getStr:function(){var i=Pointer_stringify(SYSCALLS.get());return i},get64:function(){var i=SYSCALLS.get(),o=SYSCALLS.get();return i>=0?assert(o===0):assert(o===-1),i},getZero:function(){assert(SYSCALLS.get()===0)}};function ___syscall6(i,o){SYSCALLS.varargs=o;try{var f=SYSCALLS.getStreamFromFD();return FS.close(f),0}catch(p){return(typeof FS=="undefined"||!(p instanceof FS.ErrnoError))&&abort(p),-p.errno}}function ___syscall54(i,o){SYSCALLS.varargs=o;try{return 0}catch(f){return(typeof FS=="undefined"||!(f instanceof FS.ErrnoError))&&abort(f),-f.errno}}function _typeModule(i){var o=[[0,1,"X"],[1,1,"const X"],[128,1,"X *"],[256,1,"X &"],[384,1,"X &&"],[512,1,"std::shared_ptr"],[640,1,"std::unique_ptr"],[5120,1,"std::vector"],[6144,2,"std::array"],[9216,-1,"std::function"]];function f(N,C,U,q,W,ne){if(C==1){var m=q&896;(m==128||m==256||m==384)&&(N="X const")}var we;return ne?we=U.replace("X",N).replace("Y",W):we=N.replace("X",U).replace("Y",W),we.replace(/([*&]) (?=[*&])/g,"$1")}function p(N,C,U,q,W){throw new Error(N+" type "+U.replace("X",C+"?")+(q?" with flag "+q:"")+" in "+W)}function E(N,C,U,q,W,ne,m,we){ne===void 0&&(ne="X"),we===void 0&&(we=1);var Se=U(N);if(Se)return Se;var he=q(N),ge=he.placeholderFlag,ze=o[ge];m&&ze&&(ne=f(m[2],m[0],ne,ze[0],"?",!0));var pe;ge==0&&(pe="Unbound"),ge>=10&&(pe="Corrupt"),we>20&&(pe="Deeply nested"),pe&&p(pe,N,ne,ge,W||"?");var Oe=he.paramList[0],le=E(Oe,C,U,q,W,ne,ze,we+1),Ue,Ge={flags:ze[0],id:N,name:"",paramList:[le]},rt=[],wt="?";switch(he.placeholderFlag){case 1:Ue=le.spec;break;case 2:if((le.flags&15360)==1024&&le.spec.ptrSize==1){Ge.flags=7168;break}case 3:case 6:case 5:Ue=le.spec,(le.flags&15360)!=2048;break;case 8:wt=""+he.paramList[1],Ge.paramList.push(he.paramList[1]);break;case 9:for(var xt=0,$e=he.paramList[1];xt<$e.length;xt++){var ft=$e[xt],Ke=E(ft,C,U,q,W,ne,ze,we+1);rt.push(Ke.name),Ge.paramList.push(Ke)}wt=rt.join(", ");break;default:break}if(Ge.name=f(ze[2],ze[0],le.name,le.flags,wt),Ue){for(var jt=0,$t=Object.keys(Ue);jt<$t.length;jt++){var at=$t[jt];Ge[at]=Ge[at]||Ue[at]}Ge.flags|=Ue.flags}return t(C,Ge)}function t(N,C){var U=C.flags,q=U&896,W=U&15360;return!C.name&&W==1024&&(C.ptrSize==1?C.name=(U&16?"":(U&8?"un":"")+"signed ")+"char":C.name=(U&8?"u":"")+(U&32?"float":"int")+(C.ptrSize*8+"_t")),C.ptrSize==8&&!(U&32)&&(W=64),W==2048&&(q==512||q==640?W=4096:q&&(W=3072)),N(W,C)}var k=function(){function N(C){this.id=C.id,this.name=C.name,this.flags=C.flags,this.spec=C}return N.prototype.toString=function(){return this.name},N}(),L={Type:k,getComplexType:E,makeType:t,structureList:o};return i.output=L,i.output||L}function __nbind_register_type(i,o){var f=_nbind.readAsciiString(o),p={flags:10240,id:i,name:f};_nbind.makeType(_nbind.constructType,p)}function __nbind_register_callback_signature(i,o){var f=_nbind.readTypeIdList(i,o),p=_nbind.callbackSignatureList.length;return _nbind.callbackSignatureList[p]=_nbind.makeJSCaller(f),p}function __extends(i,o){for(var f in o)o.hasOwnProperty(f)&&(i[f]=o[f]);function p(){this.constructor=i}p.prototype=o.prototype,i.prototype=new p}function __nbind_register_class(i,o,f,p,E,t,k){var L=_nbind.readAsciiString(k),N=_nbind.readPolicyList(o),C=HEAPU32.subarray(i/4,i/4+2),U={flags:2048|(N.Value?2:0),id:C[0],name:L},q=_nbind.makeType(_nbind.constructType,U);q.ptrType=_nbind.getComplexType(C[1],_nbind.constructType,_nbind.getType,_nbind.queryType),q.destroy=_nbind.makeMethodCaller(q.ptrType,{boundID:U.id,flags:0,name:"destroy",num:0,ptr:t,title:q.name+".free",typeList:["void","uint32_t","uint32_t"]}),E&&(q.superIdList=Array.prototype.slice.call(HEAPU32.subarray(f/4,f/4+E)),q.upcastList=Array.prototype.slice.call(HEAPU32.subarray(p/4,p/4+E))),Module[q.name]=q.makeBound(N),_nbind.BindClass.list.push(q)}function _removeAccessorPrefix(i){var o=/^[Gg]et_?([A-Z]?([A-Z]?))/;return i.replace(o,function(f,p,E){return E?p:p.toLowerCase()})}function __nbind_register_function(i,o,f,p,E,t,k,L,N,C){var U=_nbind.getType(i),q=_nbind.readPolicyList(o),W=_nbind.readTypeIdList(f,p),ne;if(k==5)ne=[{direct:E,name:"__nbindConstructor",ptr:0,title:U.name+" constructor",typeList:["uint32_t"].concat(W.slice(1))},{direct:t,name:"__nbindValueConstructor",ptr:0,title:U.name+" value constructor",typeList:["void","uint32_t"].concat(W.slice(1))}];else{var m=_nbind.readAsciiString(L),we=(U.name&&U.name+".")+m;(k==3||k==4)&&(m=_removeAccessorPrefix(m)),ne=[{boundID:i,direct:t,name:m,ptr:E,title:we,typeList:W}]}for(var Se=0,he=ne;Se>2]=i),i}function _llvm_stacksave(){var i=_llvm_stacksave;return i.LLVM_SAVEDSTACKS||(i.LLVM_SAVEDSTACKS=[]),i.LLVM_SAVEDSTACKS.push(Runtime.stackSave()),i.LLVM_SAVEDSTACKS.length-1}function ___syscall140(i,o){SYSCALLS.varargs=o;try{var f=SYSCALLS.getStreamFromFD(),p=SYSCALLS.get(),E=SYSCALLS.get(),t=SYSCALLS.get(),k=SYSCALLS.get(),L=E;return FS.llseek(f,L,k),HEAP32[t>>2]=f.position,f.getdents&&L===0&&k===0&&(f.getdents=null),0}catch(N){return(typeof FS=="undefined"||!(N instanceof FS.ErrnoError))&&abort(N),-N.errno}}function ___syscall146(i,o){SYSCALLS.varargs=o;try{var f=SYSCALLS.get(),p=SYSCALLS.get(),E=SYSCALLS.get(),t=0;___syscall146.buffer||(___syscall146.buffers=[null,[],[]],___syscall146.printChar=function(U,q){var W=___syscall146.buffers[U];assert(W),q===0||q===10?((U===1?Module.print:Module.printErr)(UTF8ArrayToString(W,0)),W.length=0):W.push(q)});for(var k=0;k>2],N=HEAP32[p+(k*8+4)>>2],C=0;Ci.pageSize/2||o>i.pageSize-f){var p=_nbind.typeNameTbl.NBind.proto;return p.lalloc(o)}else return HEAPU32[i.usedPtr]=f+o,i.rootPtr+f},i.lreset=function(o,f){var p=HEAPU32[i.pagePtr];if(p){var E=_nbind.typeNameTbl.NBind.proto;E.lreset(o,f)}else HEAPU32[i.usedPtr]=o},i}();_nbind.Pool=Pool;function constructType(i,o){var f=i==10240?_nbind.makeTypeNameTbl[o.name]||_nbind.BindType:_nbind.makeTypeKindTbl[i],p=new f(o);return typeIdTbl[o.id]=p,_nbind.typeNameTbl[o.name]=p,p}_nbind.constructType=constructType;function getType(i){return typeIdTbl[i]}_nbind.getType=getType;function queryType(i){var o=HEAPU8[i],f=_nbind.structureList[o][1];i/=4,f<0&&(++i,f=HEAPU32[i]+1);var p=Array.prototype.slice.call(HEAPU32.subarray(i+1,i+1+f));return o==9&&(p=[p[0],p.slice(1)]),{paramList:p,placeholderFlag:o}}_nbind.queryType=queryType;function getTypes(i,o){return i.map(function(f){return typeof f=="number"?_nbind.getComplexType(f,constructType,getType,queryType,o):_nbind.typeNameTbl[f]})}_nbind.getTypes=getTypes;function readTypeIdList(i,o){return Array.prototype.slice.call(HEAPU32,i/4,i/4+o)}_nbind.readTypeIdList=readTypeIdList;function readAsciiString(i){for(var o=i;HEAPU8[o++];);return String.fromCharCode.apply("",HEAPU8.subarray(i,o-1))}_nbind.readAsciiString=readAsciiString;function readPolicyList(i){var o={};if(i)for(;;){var f=HEAPU32[i/4];if(!f)break;o[readAsciiString(f)]=!0,i+=4}return o}_nbind.readPolicyList=readPolicyList;function getDynCall(i,o){var f={float32_t:"d",float64_t:"d",int64_t:"d",uint64_t:"d",void:"v"},p=i.map(function(t){return f[t.name]||"i"}).join(""),E=Module["dynCall_"+p];if(!E)throw new Error("dynCall_"+p+" not found for "+o+"("+i.map(function(t){return t.name}).join(", ")+")");return E}_nbind.getDynCall=getDynCall;function addMethod(i,o,f,p){var E=i[o];i.hasOwnProperty(o)&&E?((E.arity||E.arity===0)&&(E=_nbind.makeOverloader(E,E.arity),i[o]=E),E.addMethod(f,p)):(f.arity=p,i[o]=f)}_nbind.addMethod=addMethod;function throwError(i){throw new Error(i)}_nbind.throwError=throwError,_nbind.bigEndian=!1,_a=_typeModule(_typeModule),_nbind.Type=_a.Type,_nbind.makeType=_a.makeType,_nbind.getComplexType=_a.getComplexType,_nbind.structureList=_a.structureList;var BindType=function(i){__extends(o,i);function o(){var f=i!==null&&i.apply(this,arguments)||this;return f.heap=HEAPU32,f.ptrSize=4,f}return o.prototype.needsWireRead=function(f){return!!this.wireRead||!!this.makeWireRead},o.prototype.needsWireWrite=function(f){return!!this.wireWrite||!!this.makeWireWrite},o}(_nbind.Type);_nbind.BindType=BindType;var PrimitiveType=function(i){__extends(o,i);function o(f){var p=i.call(this,f)||this,E=f.flags&32?{32:HEAPF32,64:HEAPF64}:f.flags&8?{8:HEAPU8,16:HEAPU16,32:HEAPU32}:{8:HEAP8,16:HEAP16,32:HEAP32};return p.heap=E[f.ptrSize*8],p.ptrSize=f.ptrSize,p}return o.prototype.needsWireWrite=function(f){return!!f&&!!f.Strict},o.prototype.makeWireWrite=function(f,p){return p&&p.Strict&&function(E){if(typeof E=="number")return E;throw new Error("Type mismatch")}},o}(BindType);_nbind.PrimitiveType=PrimitiveType;function pushCString(i,o){if(i==null){if(o&&o.Nullable)return 0;throw new Error("Type mismatch")}if(o&&o.Strict){if(typeof i!="string")throw new Error("Type mismatch")}else i=i.toString();var f=Module.lengthBytesUTF8(i)+1,p=_nbind.Pool.lalloc(f);return Module.stringToUTF8Array(i,HEAPU8,p,f),p}_nbind.pushCString=pushCString;function popCString(i){return i===0?null:Module.Pointer_stringify(i)}_nbind.popCString=popCString;var CStringType=function(i){__extends(o,i);function o(){var f=i!==null&&i.apply(this,arguments)||this;return f.wireRead=popCString,f.wireWrite=pushCString,f.readResources=[_nbind.resources.pool],f.writeResources=[_nbind.resources.pool],f}return o.prototype.makeWireWrite=function(f,p){return function(E){return pushCString(E,p)}},o}(BindType);_nbind.CStringType=CStringType;var BooleanType=function(i){__extends(o,i);function o(){var f=i!==null&&i.apply(this,arguments)||this;return f.wireRead=function(p){return!!p},f}return o.prototype.needsWireWrite=function(f){return!!f&&!!f.Strict},o.prototype.makeWireRead=function(f){return"!!("+f+")"},o.prototype.makeWireWrite=function(f,p){return p&&p.Strict&&function(E){if(typeof E=="boolean")return E;throw new Error("Type mismatch")}||f},o}(BindType);_nbind.BooleanType=BooleanType;var Wrapper=function(){function i(){}return i.prototype.persist=function(){this.__nbindState|=1},i}();_nbind.Wrapper=Wrapper;function makeBound(i,o){var f=function(p){__extends(E,p);function E(t,k,L,N){var C=p.call(this)||this;if(!(C instanceof E))return new(Function.prototype.bind.apply(E,Array.prototype.concat.apply([null],arguments)));var U=k,q=L,W=N;if(t!==_nbind.ptrMarker){var ne=C.__nbindConstructor.apply(C,arguments);U=4096|512,W=HEAPU32[ne/4],q=HEAPU32[ne/4+1]}var m={configurable:!0,enumerable:!1,value:null,writable:!1},we={__nbindFlags:U,__nbindPtr:q};W&&(we.__nbindShared=W,_nbind.mark(C));for(var Se=0,he=Object.keys(we);Se>=1;var f=_nbind.valueList[i];return _nbind.valueList[i]=firstFreeValue,firstFreeValue=i,f}else{if(o)return _nbind.popShared(i,o);throw new Error("Invalid value slot "+i)}}_nbind.popValue=popValue;var valueBase=18446744073709552e3;function push64(i){return typeof i=="number"?i:pushValue(i)*4096+valueBase}function pop64(i){return i=3?k=Buffer.from(t):k=new Buffer(t),k.copy(p)}else getBuffer(p).set(t)}}_nbind.commitBuffer=commitBuffer;var dirtyList=[],gcTimer=0;function sweep(){for(var i=0,o=dirtyList;i>2]=DYNAMIC_BASE,staticSealed=!0;function invoke_viiiii(i,o,f,p,E,t){try{Module.dynCall_viiiii(i,o,f,p,E,t)}catch(k){if(typeof k!="number"&&k!=="longjmp")throw k;Module.setThrew(1,0)}}function invoke_vif(i,o,f){try{Module.dynCall_vif(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_vid(i,o,f){try{Module.dynCall_vid(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_fiff(i,o,f,p){try{return Module.dynCall_fiff(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_vi(i,o){try{Module.dynCall_vi(i,o)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_vii(i,o,f){try{Module.dynCall_vii(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_ii(i,o){try{return Module.dynCall_ii(i,o)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_viddi(i,o,f,p,E){try{Module.dynCall_viddi(i,o,f,p,E)}catch(t){if(typeof t!="number"&&t!=="longjmp")throw t;Module.setThrew(1,0)}}function invoke_vidd(i,o,f,p){try{Module.dynCall_vidd(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_iiii(i,o,f,p){try{return Module.dynCall_iiii(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_diii(i,o,f,p){try{return Module.dynCall_diii(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_di(i,o){try{return Module.dynCall_di(i,o)}catch(f){if(typeof f!="number"&&f!=="longjmp")throw f;Module.setThrew(1,0)}}function invoke_iid(i,o,f){try{return Module.dynCall_iid(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_iii(i,o,f){try{return Module.dynCall_iii(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_viiddi(i,o,f,p,E,t){try{Module.dynCall_viiddi(i,o,f,p,E,t)}catch(k){if(typeof k!="number"&&k!=="longjmp")throw k;Module.setThrew(1,0)}}function invoke_viiiiii(i,o,f,p,E,t,k){try{Module.dynCall_viiiiii(i,o,f,p,E,t,k)}catch(L){if(typeof L!="number"&&L!=="longjmp")throw L;Module.setThrew(1,0)}}function invoke_dii(i,o,f){try{return Module.dynCall_dii(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_i(i){try{return Module.dynCall_i(i)}catch(o){if(typeof o!="number"&&o!=="longjmp")throw o;Module.setThrew(1,0)}}function invoke_iiiiii(i,o,f,p,E,t){try{return Module.dynCall_iiiiii(i,o,f,p,E,t)}catch(k){if(typeof k!="number"&&k!=="longjmp")throw k;Module.setThrew(1,0)}}function invoke_viiid(i,o,f,p,E){try{Module.dynCall_viiid(i,o,f,p,E)}catch(t){if(typeof t!="number"&&t!=="longjmp")throw t;Module.setThrew(1,0)}}function invoke_viififi(i,o,f,p,E,t,k){try{Module.dynCall_viififi(i,o,f,p,E,t,k)}catch(L){if(typeof L!="number"&&L!=="longjmp")throw L;Module.setThrew(1,0)}}function invoke_viii(i,o,f,p){try{Module.dynCall_viii(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_v(i){try{Module.dynCall_v(i)}catch(o){if(typeof o!="number"&&o!=="longjmp")throw o;Module.setThrew(1,0)}}function invoke_viid(i,o,f,p){try{Module.dynCall_viid(i,o,f,p)}catch(E){if(typeof E!="number"&&E!=="longjmp")throw E;Module.setThrew(1,0)}}function invoke_idd(i,o,f){try{return Module.dynCall_idd(i,o,f)}catch(p){if(typeof p!="number"&&p!=="longjmp")throw p;Module.setThrew(1,0)}}function invoke_viiii(i,o,f,p,E){try{Module.dynCall_viiii(i,o,f,p,E)}catch(t){if(typeof t!="number"&&t!=="longjmp")throw t;Module.setThrew(1,0)}}Module.asmGlobalArg={Math,Int8Array,Int16Array,Int32Array,Uint8Array,Uint16Array,Uint32Array,Float32Array,Float64Array,NaN:NaN,Infinity:Infinity},Module.asmLibraryArg={abort,assert,enlargeMemory,getTotalMemory,abortOnCannotGrowMemory,invoke_viiiii,invoke_vif,invoke_vid,invoke_fiff,invoke_vi,invoke_vii,invoke_ii,invoke_viddi,invoke_vidd,invoke_iiii,invoke_diii,invoke_di,invoke_iid,invoke_iii,invoke_viiddi,invoke_viiiiii,invoke_dii,invoke_i,invoke_iiiiii,invoke_viiid,invoke_viififi,invoke_viii,invoke_v,invoke_viid,invoke_idd,invoke_viiii,_emscripten_asm_const_iiiii,_emscripten_asm_const_iiidddddd,_emscripten_asm_const_iiiid,__nbind_reference_external,_emscripten_asm_const_iiiiiiii,_removeAccessorPrefix,_typeModule,__nbind_register_pool,__decorate,_llvm_stackrestore,___cxa_atexit,__extends,__nbind_get_value_object,__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,_emscripten_set_main_loop_timing,__nbind_register_primitive,__nbind_register_type,_emscripten_memcpy_big,__nbind_register_function,___setErrNo,__nbind_register_class,__nbind_finish,_abort,_nbind_value,_llvm_stacksave,___syscall54,_defineHidden,_emscripten_set_main_loop,_emscripten_get_now,__nbind_register_callback_signature,_emscripten_asm_const_iiiiii,__nbind_free_external,_emscripten_asm_const_iiii,_emscripten_asm_const_iiididi,___syscall6,_atexit,___syscall140,___syscall146,DYNAMICTOP_PTR,tempDoublePtr,ABORT,STACKTOP,STACK_MAX,cttz_i8,___dso_handle};var asm=function(i,o,f){var p=new i.Int8Array(f),E=new i.Int16Array(f),t=new i.Int32Array(f),k=new i.Uint8Array(f),L=new i.Uint16Array(f),N=new i.Uint32Array(f),C=new i.Float32Array(f),U=new i.Float64Array(f),q=o.DYNAMICTOP_PTR|0,W=o.tempDoublePtr|0,ne=o.ABORT|0,m=o.STACKTOP|0,we=o.STACK_MAX|0,Se=o.cttz_i8|0,he=o.___dso_handle|0,ge=0,ze=0,pe=0,Oe=0,le=i.NaN,Ue=i.Infinity,Ge=0,rt=0,wt=0,xt=0,$e=0,ft=0,Ke=i.Math.floor,jt=i.Math.abs,$t=i.Math.sqrt,at=i.Math.pow,Q=i.Math.cos,ae=i.Math.sin,Ce=i.Math.tan,ue=i.Math.acos,je=i.Math.asin,ct=i.Math.atan,At=i.Math.atan2,en=i.Math.exp,ln=i.Math.log,An=i.Math.ceil,nr=i.Math.imul,un=i.Math.min,Wt=i.Math.max,vr=i.Math.clz32,w=i.Math.fround,Ut=o.abort,Vn=o.assert,fr=o.enlargeMemory,Fr=o.getTotalMemory,ur=o.abortOnCannotGrowMemory,br=o.invoke_viiiii,Kt=o.invoke_vif,vu=o.invoke_vid,a0=o.invoke_fiff,So=o.invoke_vi,Go=o.invoke_vii,Os=o.invoke_ii,Yo=o.invoke_viddi,Ko=o.invoke_vidd,qt=o.invoke_iiii,_i=o.invoke_diii,eu=o.invoke_di,ai=o.invoke_iid,mr=o.invoke_iii,Xo=o.invoke_viiddi,W0=o.invoke_viiiiii,Lu=o.invoke_dii,V0=o.invoke_i,Hr=o.invoke_iiiiii,To=o.invoke_viiid,Co=o.invoke_viififi,L0=o.invoke_viii,tu=o.invoke_v,Si=o.invoke_viid,ks=o.invoke_idd,Hl=o.invoke_viiii,F0=o._emscripten_asm_const_iiiii,f0=o._emscripten_asm_const_iiidddddd,Pr=o._emscripten_asm_const_iiiid,Ei=o.__nbind_reference_external,G0=o._emscripten_asm_const_iiiiiiii,fi=o._removeAccessorPrefix,Zt=o._typeModule,Ln=o.__nbind_register_pool,Di=o.__decorate,ci=o._llvm_stackrestore,Ht=o.___cxa_atexit,Du=o.__extends,Yi=o.__nbind_get_value_object,Y0=o.__ZN8facebook4yoga14YGNodeToStringEPNSt3__212basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEEP6YGNode14YGPrintOptionsj,Ui=o._emscripten_set_main_loop_timing,Wl=o.__nbind_register_primitive,xo=o.__nbind_register_type,ni=o._emscripten_memcpy_big,oo=o.__nbind_register_function,Vl=o.___setErrNo,Ao=o.__nbind_register_class,Ms=o.__nbind_finish,Xn=o._abort,Qo=o._nbind_value,lo=o._llvm_stacksave,b0=o.___syscall54,yl=o._defineHidden,Ro=o._emscripten_set_main_loop,Et=o._emscripten_get_now,Pt=o.__nbind_register_callback_signature,Bn=o._emscripten_asm_const_iiiiii,Ir=o.__nbind_free_external,ji=o._emscripten_asm_const_iiii,Wr=o._emscripten_asm_const_iiididi,wu=o.___syscall6,c0=o._atexit,Ti=o.___syscall140,d0=o.___syscall146,as=w(0);let St=w(0);function so(e){e=e|0;var n=0;return n=m,m=m+e|0,m=m+15&-16,n|0}function Jo(){return m|0}function Gl(e){e=e|0,m=e}function Fu(e,n){e=e|0,n=n|0,m=e,we=n}function fs(e,n){e=e|0,n=n|0,ge||(ge=e,ze=n)}function P0(e){e=e|0,ft=e}function X(){return ft|0}function _e(){var e=0,n=0;pr(8104,8,400)|0,pr(8504,408,540)|0,e=9044,n=e+44|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));p[9088]=0,p[9089]=1,t[2273]=0,t[2274]=948,t[2275]=948,Ht(17,8104,he|0)|0}function Ne(e){e=e|0,ic(e+948|0)}function Me(e){return e=w(e),((cr(e)|0)&2147483647)>>>0>2139095040|0}function dt(e,n,r){e=e|0,n=n|0,r=r|0;e:do if(t[e+(n<<3)+4>>2]|0)e=e+(n<<3)|0;else{if((n|2|0)==3?t[e+60>>2]|0:0){e=e+56|0;break}switch(n|0){case 0:case 2:case 4:case 5:{if(t[e+52>>2]|0){e=e+48|0;break e}break}default:}if(t[e+68>>2]|0){e=e+64|0;break}else{e=(n|1|0)==5?948:r;break}}while(0);return e|0}function Hn(e){e=e|0;var n=0;return n=C_(1e3)|0,Dn(e,(n|0)!=0,2456),t[2276]=(t[2276]|0)+1,pr(n|0,8104,1e3)|0,p[e+2>>0]|0&&(t[n+4>>2]=2,t[n+12>>2]=4),t[n+976>>2]=e,n|0}function Dn(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;l=m,m=m+16|0,u=l,n||(t[u>>2]=r,_l(e,5,3197,u)),m=l}function or(){return Hn(956)|0}function mi(e){e=e|0;var n=0;return n=cn(1e3)|0,Su(n,e),Dn(t[e+976>>2]|0,1,2456),t[2276]=(t[2276]|0)+1,t[n+944>>2]=0,n|0}function Su(e,n){e=e|0,n=n|0;var r=0;pr(e|0,n|0,948)|0,na(e+948|0,n+948|0),r=e+960|0,e=n+960|0,n=r+40|0;do t[r>>2]=t[e>>2],r=r+4|0,e=e+4|0;while((r|0)<(n|0))}function bu(e){e=e|0;var n=0,r=0,u=0,l=0;if(n=e+944|0,r=t[n>>2]|0,r|0&&(Pu(r+948|0,e)|0,t[n>>2]=0),r=mu(e)|0,r|0){n=0;do t[(yi(e,n)|0)+944>>2]=0,n=n+1|0;while((n|0)!=(r|0))}r=e+948|0,u=t[r>>2]|0,l=e+952|0,n=t[l>>2]|0,(n|0)!=(u|0)&&(t[l>>2]=n+(~((n+-4-u|0)>>>2)<<2)),Oo(r),x_(e),t[2276]=(t[2276]|0)+-1}function Pu(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0;u=t[e>>2]|0,D=e+4|0,r=t[D>>2]|0,s=r;e:do if((u|0)==(r|0))l=u,h=4;else for(e=u;;){if((t[e>>2]|0)==(n|0)){l=e,h=4;break e}if(e=e+4|0,(e|0)==(r|0)){e=0;break}}while(0);return(h|0)==4&&((l|0)!=(r|0)?(u=l+4|0,e=s-u|0,n=e>>2,n&&(Iy(l|0,u|0,e|0)|0,r=t[D>>2]|0),e=l+(n<<2)|0,(r|0)==(e|0)||(t[D>>2]=r+(~((r+-4-e|0)>>>2)<<2)),e=1):e=0),e|0}function mu(e){return e=e|0,(t[e+952>>2]|0)-(t[e+948>>2]|0)>>2|0}function yi(e,n){e=e|0,n=n|0;var r=0;return r=t[e+948>>2]|0,(t[e+952>>2]|0)-r>>2>>>0>n>>>0?e=t[r+(n<<2)>>2]|0:e=0,e|0}function Oo(e){e=e|0;var n=0,r=0,u=0,l=0;u=m,m=m+32|0,n=u,l=t[e>>2]|0,r=(t[e+4>>2]|0)-l|0,((t[e+8>>2]|0)-l|0)>>>0>r>>>0&&(l=r>>2,Y(n,l,l,e+8|0),Qr(e,n),Jr(n)),m=u}function Tu(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0;M=mu(e)|0;do if(M|0){if((t[(yi(e,0)|0)+944>>2]|0)==(e|0)){if(!(Pu(e+948|0,n)|0))break;pr(n+400|0,8504,540)|0,t[n+944>>2]=0,Gn(e);break}h=t[(t[e+976>>2]|0)+12>>2]|0,D=e+948|0,S=(h|0)==0,r=0,s=0;do u=t[(t[D>>2]|0)+(s<<2)>>2]|0,(u|0)==(n|0)?Gn(e):(l=mi(u)|0,t[(t[D>>2]|0)+(r<<2)>>2]=l,t[l+944>>2]=e,S||$E[h&15](u,l,e,r),r=r+1|0),s=s+1|0;while((s|0)!=(M|0));if(r>>>0>>0){S=e+948|0,D=e+952|0,h=r,r=t[D>>2]|0;do s=(t[S>>2]|0)+(h<<2)|0,u=s+4|0,l=r-u|0,n=l>>2,n&&(Iy(s|0,u|0,l|0)|0,r=t[D>>2]|0),l=r,u=s+(n<<2)|0,(l|0)!=(u|0)&&(r=l+(~((l+-4-u|0)>>>2)<<2)|0,t[D>>2]=r),h=h+1|0;while((h|0)!=(M|0))}}while(0)}function ao(e){e=e|0;var n=0,r=0,u=0,l=0;Iu(e,(mu(e)|0)==0,2491),Iu(e,(t[e+944>>2]|0)==0,2545),n=e+948|0,r=t[n>>2]|0,u=e+952|0,l=t[u>>2]|0,(l|0)!=(r|0)&&(t[u>>2]=l+(~((l+-4-r|0)>>>2)<<2)),Oo(n),n=e+976|0,r=t[n>>2]|0,pr(e|0,8104,1e3)|0,p[r+2>>0]|0&&(t[e+4>>2]=2,t[e+12>>2]=4),t[n>>2]=r}function Iu(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;l=m,m=m+16|0,u=l,n||(t[u>>2]=r,sr(e,5,3197,u)),m=l}function Oa(){return t[2276]|0}function p0(){var e=0;return e=C_(20)|0,Zs((e|0)!=0,2592),t[2277]=(t[2277]|0)+1,t[e>>2]=t[239],t[e+4>>2]=t[240],t[e+8>>2]=t[241],t[e+12>>2]=t[242],t[e+16>>2]=t[243],e|0}function Zs(e,n){e=e|0,n=n|0;var r=0,u=0;u=m,m=m+16|0,r=u,e||(t[r>>2]=n,sr(0,5,3197,r)),m=u}function K0(e){e=e|0,x_(e),t[2277]=(t[2277]|0)+-1}function $s(e,n){e=e|0,n=n|0;var r=0;n?(Iu(e,(mu(e)|0)==0,2629),r=1):(r=0,n=0),t[e+964>>2]=n,t[e+988>>2]=r}function ka(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,s=u+8|0,l=u+4|0,h=u,t[l>>2]=n,Iu(e,(t[n+944>>2]|0)==0,2709),Iu(e,(t[e+964>>2]|0)==0,2763),cs(e),n=e+948|0,t[h>>2]=(t[n>>2]|0)+(r<<2),t[s>>2]=t[h>>2],w0(n,s,l)|0,t[(t[l>>2]|0)+944>>2]=e,Gn(e),m=u}function cs(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0;if(r=mu(e)|0,r|0?(t[(yi(e,0)|0)+944>>2]|0)!=(e|0):0){u=t[(t[e+976>>2]|0)+12>>2]|0,l=e+948|0,s=(u|0)==0,n=0;do h=t[(t[l>>2]|0)+(n<<2)>>2]|0,D=mi(h)|0,t[(t[l>>2]|0)+(n<<2)>>2]=D,t[D+944>>2]=e,s||$E[u&15](h,D,e,n),n=n+1|0;while((n|0)!=(r|0))}}function w0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0;Qe=m,m=m+64|0,P=Qe+52|0,D=Qe+48|0,K=Qe+28|0,Pe=Qe+24|0,Ee=Qe+20|0,ve=Qe,u=t[e>>2]|0,s=u,n=u+((t[n>>2]|0)-s>>2<<2)|0,u=e+4|0,l=t[u>>2]|0,h=e+8|0;do if(l>>>0<(t[h>>2]|0)>>>0){if((n|0)==(l|0)){t[n>>2]=t[r>>2],t[u>>2]=(t[u>>2]|0)+4;break}Ur(e,n,l,n+4|0),n>>>0<=r>>>0&&(r=(t[u>>2]|0)>>>0>r>>>0?r+4|0:r),t[n>>2]=t[r>>2]}else{u=(l-s>>2)+1|0,l=x0(e)|0,l>>>0>>0&&li(e),O=t[e>>2]|0,M=(t[h>>2]|0)-O|0,s=M>>1,Y(ve,M>>2>>>0>>1>>>0?s>>>0>>0?u:s:l,n-O>>2,e+8|0),O=ve+8|0,u=t[O>>2]|0,s=ve+12|0,M=t[s>>2]|0,h=M,S=u;do if((u|0)==(M|0)){if(M=ve+4|0,u=t[M>>2]|0,We=t[ve>>2]|0,l=We,u>>>0<=We>>>0){u=h-l>>1,u=(u|0)==0?1:u,Y(K,u,u>>>2,t[ve+16>>2]|0),t[Pe>>2]=t[M>>2],t[Ee>>2]=t[O>>2],t[D>>2]=t[Pe>>2],t[P>>2]=t[Ee>>2],hi(K,D,P),u=t[ve>>2]|0,t[ve>>2]=t[K>>2],t[K>>2]=u,u=K+4|0,We=t[M>>2]|0,t[M>>2]=t[u>>2],t[u>>2]=We,u=K+8|0,We=t[O>>2]|0,t[O>>2]=t[u>>2],t[u>>2]=We,u=K+12|0,We=t[s>>2]|0,t[s>>2]=t[u>>2],t[u>>2]=We,Jr(K),u=t[O>>2]|0;break}s=u,h=((s-l>>2)+1|0)/-2|0,D=u+(h<<2)|0,l=S-s|0,s=l>>2,s&&(Iy(D|0,u|0,l|0)|0,u=t[M>>2]|0),We=D+(s<<2)|0,t[O>>2]=We,t[M>>2]=u+(h<<2),u=We}while(0);t[u>>2]=t[r>>2],t[O>>2]=(t[O>>2]|0)+4,n=lt(e,ve,n)|0,Jr(ve)}while(0);return m=Qe,n|0}function Gn(e){e=e|0;var n=0;do{if(n=e+984|0,p[n>>0]|0)break;p[n>>0]=1,C[e+504>>2]=w(le),e=t[e+944>>2]|0}while((e|0)!=0)}function ic(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-4-u|0)>>>2)<<2)),yt(r))}function ri(e){return e=e|0,t[e+944>>2]|0}function Gr(e){e=e|0,Iu(e,(t[e+964>>2]|0)!=0,2832),Gn(e)}function Yl(e){return e=e|0,(p[e+984>>0]|0)!=0|0}function ea(e,n){e=e|0,n=n|0,MI(e,n,400)|0&&(pr(e|0,n|0,400)|0,Gn(e))}function lf(e){e=e|0;var n=St;return n=w(C[e+44>>2]),e=Me(n)|0,w(e?w(0):n)}function Ns(e){e=e|0;var n=St;return n=w(C[e+48>>2]),Me(n)|0&&(n=p[(t[e+976>>2]|0)+2>>0]|0?w(1):w(0)),w(n)}function Ma(e,n){e=e|0,n=n|0,t[e+980>>2]=n}function Ls(e){return e=e|0,t[e+980>>2]|0}function h0(e,n){e=e|0,n=n|0;var r=0;r=e+4|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function Fs(e){return e=e|0,t[e+4>>2]|0}function Ni(e,n){e=e|0,n=n|0;var r=0;r=e+8|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function B(e){return e=e|0,t[e+8>>2]|0}function z(e,n){e=e|0,n=n|0;var r=0;r=e+12|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function G(e){return e=e|0,t[e+12>>2]|0}function $(e,n){e=e|0,n=n|0;var r=0;r=e+16|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function De(e){return e=e|0,t[e+16>>2]|0}function me(e,n){e=e|0,n=n|0;var r=0;r=e+20|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function xe(e){return e=e|0,t[e+20>>2]|0}function Z(e,n){e=e|0,n=n|0;var r=0;r=e+24|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function ke(e){return e=e|0,t[e+24>>2]|0}function Xe(e,n){e=e|0,n=n|0;var r=0;r=e+28|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function ht(e){return e=e|0,t[e+28>>2]|0}function ie(e,n){e=e|0,n=n|0;var r=0;r=e+32|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function qe(e){return e=e|0,t[e+32>>2]|0}function tt(e,n){e=e|0,n=n|0;var r=0;r=e+36|0,(t[r>>2]|0)!=(n|0)&&(t[r>>2]=n,Gn(e))}function Tt(e){return e=e|0,t[e+36>>2]|0}function kt(e,n){e=e|0,n=w(n);var r=0;r=e+40|0,w(C[r>>2])!=n&&(C[r>>2]=n,Gn(e))}function bt(e,n){e=e|0,n=w(n);var r=0;r=e+44|0,w(C[r>>2])!=n&&(C[r>>2]=n,Gn(e))}function on(e,n){e=e|0,n=w(n);var r=0;r=e+48|0,w(C[r>>2])!=n&&(C[r>>2]=n,Gn(e))}function tn(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+52|0,l=e+56|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function Lt(e,n){e=e|0,n=w(n);var r=0,u=0;u=e+52|0,r=e+56|0,(w(C[u>>2])==n?(t[r>>2]|0)==2:0)||(C[u>>2]=n,u=Me(n)|0,t[r>>2]=u?3:2,Gn(e))}function gn(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+52|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function lr(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=(s^1)&1,l=e+132+(n<<3)|0,n=e+132+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function Qn(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=s?0:2,l=e+132+(n<<3)|0,n=e+132+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function _r(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=n+132+(r<<3)|0,n=t[u+4>>2]|0,r=e,t[r>>2]=t[u>>2],t[r+4>>2]=n}function Cn(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=(s^1)&1,l=e+60+(n<<3)|0,n=e+60+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function Ar(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=s?0:2,l=e+60+(n<<3)|0,n=e+60+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function v0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=n+60+(r<<3)|0,n=t[u+4>>2]|0,r=e,t[r>>2]=t[u>>2],t[r+4>>2]=n}function Rr(e,n){e=e|0,n=n|0;var r=0;r=e+60+(n<<3)+4|0,(t[r>>2]|0)!=3&&(C[e+60+(n<<3)>>2]=w(le),t[r>>2]=3,Gn(e))}function nt(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=(s^1)&1,l=e+204+(n<<3)|0,n=e+204+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function _t(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=s?0:2,l=e+204+(n<<3)|0,n=e+204+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function Ze(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=n+204+(r<<3)|0,n=t[u+4>>2]|0,r=e,t[r>>2]=t[u>>2],t[r+4>>2]=n}function Ft(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0,s=0;s=Me(r)|0,u=(s^1)&1,l=e+276+(n<<3)|0,n=e+276+(n<<3)+4|0,(s|w(C[l>>2])==r?(t[n>>2]|0)==(u|0):0)||(C[l>>2]=r,t[n>>2]=u,Gn(e))}function nn(e,n){return e=e|0,n=n|0,w(C[e+276+(n<<3)>>2])}function sn(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+348|0,l=e+352|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function Yn(e,n){e=e|0,n=w(n);var r=0,u=0;u=e+348|0,r=e+352|0,(w(C[u>>2])==n?(t[r>>2]|0)==2:0)||(C[u>>2]=n,u=Me(n)|0,t[r>>2]=u?3:2,Gn(e))}function yr(e){e=e|0;var n=0;n=e+352|0,(t[n>>2]|0)!=3&&(C[e+348>>2]=w(le),t[n>>2]=3,Gn(e))}function nu(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+348|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function Cu(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+356|0,l=e+360|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function S0(e,n){e=e|0,n=w(n);var r=0,u=0;u=e+356|0,r=e+360|0,(w(C[u>>2])==n?(t[r>>2]|0)==2:0)||(C[u>>2]=n,u=Me(n)|0,t[r>>2]=u?3:2,Gn(e))}function X0(e){e=e|0;var n=0;n=e+360|0,(t[n>>2]|0)!=3&&(C[e+356>>2]=w(le),t[n>>2]=3,Gn(e))}function xu(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+356|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function di(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+364|0,l=e+368|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function ko(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=s?0:2,u=e+364|0,l=e+368|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function Zo(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+364|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function sf(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+372|0,l=e+376|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function gl(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=s?0:2,u=e+372|0,l=e+376|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function af(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+372|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function Mo(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+380|0,l=e+384|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function ds(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=s?0:2,u=e+380|0,l=e+384|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function bs(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+380|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function No(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=(s^1)&1,u=e+388|0,l=e+392|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function Lo(e,n){e=e|0,n=w(n);var r=0,u=0,l=0,s=0;s=Me(n)|0,r=s?0:2,u=e+388|0,l=e+392|0,(s|w(C[u>>2])==n?(t[l>>2]|0)==(r|0):0)||(C[u>>2]=n,t[l>>2]=r,Gn(e))}function ps(e,n){e=e|0,n=n|0;var r=0,u=0;u=n+388|0,r=t[u+4>>2]|0,n=e,t[n>>2]=t[u>>2],t[n+4>>2]=r}function Vu(e,n){e=e|0,n=w(n);var r=0;r=e+396|0,w(C[r>>2])!=n&&(C[r>>2]=n,Gn(e))}function yu(e){return e=e|0,w(C[e+396>>2])}function pi(e){return e=e|0,w(C[e+400>>2])}function T0(e){return e=e|0,w(C[e+404>>2])}function Q0(e){return e=e|0,w(C[e+408>>2])}function Fo(e){return e=e|0,w(C[e+412>>2])}function ta(e){return e=e|0,w(C[e+416>>2])}function Kl(e){return e=e|0,w(C[e+420>>2])}function Ki(e,n){switch(e=e|0,n=n|0,Iu(e,(n|0)<6,2918),n|0){case 0:{n=(t[e+496>>2]|0)==2?5:4;break}case 2:{n=(t[e+496>>2]|0)==2?4:5;break}default:}return w(C[e+424+(n<<2)>>2])}function Yr(e,n){switch(e=e|0,n=n|0,Iu(e,(n|0)<6,2918),n|0){case 0:{n=(t[e+496>>2]|0)==2?5:4;break}case 2:{n=(t[e+496>>2]|0)==2?4:5;break}default:}return w(C[e+448+(n<<2)>>2])}function fo(e,n){switch(e=e|0,n=n|0,Iu(e,(n|0)<6,2918),n|0){case 0:{n=(t[e+496>>2]|0)==2?5:4;break}case 2:{n=(t[e+496>>2]|0)==2?4:5;break}default:}return w(C[e+472+(n<<2)>>2])}function Oi(e,n){e=e|0,n=n|0;var r=0,u=St;return r=t[e+4>>2]|0,(r|0)==(t[n+4>>2]|0)?r?(u=w(C[e>>2]),e=w(jt(w(u-w(C[n>>2]))))>2]=0,t[u+4>>2]=0,t[u+8>>2]=0,Y0(u|0,e|0,n|0,0),sr(e,3,(p[u+11>>0]|0)<0?t[u>>2]|0:u,r),eB(u),m=r}function J0(e,n,r,u){e=w(e),n=w(n),r=r|0,u=u|0;var l=St;e=w(e*n),l=w(YE(e,w(1)));do if(gi(l,w(0))|0)e=w(e-l);else{if(e=w(e-l),gi(l,w(1))|0){e=w(e+w(1));break}if(r){e=w(e+w(1));break}u||(l>w(.5)?l=w(1):(u=gi(l,w(.5))|0,l=w(u?1:0)),e=w(e+l))}while(0);return w(e/n)}function Z0(e,n,r,u,l,s,h,D,S,M,O,P,K){e=e|0,n=w(n),r=r|0,u=w(u),l=l|0,s=w(s),h=h|0,D=w(D),S=w(S),M=w(M),O=w(O),P=w(P),K=K|0;var Pe=0,Ee=St,ve=St,Qe=St,We=St,st=St,Re=St;return S>2]),Ee!=w(0)):0)?(Qe=w(J0(n,Ee,0,0)),We=w(J0(u,Ee,0,0)),ve=w(J0(s,Ee,0,0)),Ee=w(J0(D,Ee,0,0))):(ve=s,Qe=n,Ee=D,We=u),(l|0)==(e|0)?Pe=gi(ve,Qe)|0:Pe=0,(h|0)==(r|0)?K=gi(Ee,We)|0:K=0,((Pe?0:(st=w(n-O),!(Te(e,st,S)|0)))?!(et(e,st,l,S)|0):0)?Pe=Ve(e,st,l,s,S)|0:Pe=1,((K?0:(Re=w(u-P),!(Te(r,Re,M)|0)))?!(et(r,Re,h,M)|0):0)?K=Ve(r,Re,h,D,M)|0:K=1,K=Pe&K),K|0}function Te(e,n,r){return e=e|0,n=w(n),r=w(r),(e|0)==1?e=gi(n,r)|0:e=0,e|0}function et(e,n,r,u){return e=e|0,n=w(n),r=r|0,u=w(u),(e|0)==2&(r|0)==0?n>=u?e=1:e=gi(n,u)|0:e=0,e|0}function Ve(e,n,r,u,l){return e=e|0,n=w(n),r=r|0,u=w(u),l=w(l),(e|0)==2&(r|0)==2&u>n?l<=n?e=1:e=gi(n,l)|0:e=0,e|0}function Gt(e,n,r,u,l,s,h,D,S,M,O){e=e|0,n=w(n),r=w(r),u=u|0,l=l|0,s=s|0,h=w(h),D=w(D),S=S|0,M=M|0,O=O|0;var P=0,K=0,Pe=0,Ee=0,ve=St,Qe=St,We=0,st=0,Re=0,Fe=0,Qt=0,Lr=0,Nn=0,mn=0,hr=0,kr=0,On=0,Zi=St,ts=St,ns=St,rs=0,Xs=0;On=m,m=m+160|0,mn=On+152|0,Nn=On+120|0,Lr=On+104|0,Re=On+72|0,Ee=On+56|0,Qt=On+8|0,st=On,Fe=(t[2279]|0)+1|0,t[2279]=Fe,hr=e+984|0,((p[hr>>0]|0)!=0?(t[e+512>>2]|0)!=(t[2278]|0):0)?We=4:(t[e+516>>2]|0)==(u|0)?kr=0:We=4,(We|0)==4&&(t[e+520>>2]=0,t[e+924>>2]=-1,t[e+928>>2]=-1,C[e+932>>2]=w(-1),C[e+936>>2]=w(-1),kr=1);e:do if(t[e+964>>2]|0)if(ve=w(Yt(e,2,h)),Qe=w(Yt(e,0,h)),P=e+916|0,ns=w(C[P>>2]),ts=w(C[e+920>>2]),Zi=w(C[e+932>>2]),Z0(l,n,s,r,t[e+924>>2]|0,ns,t[e+928>>2]|0,ts,Zi,w(C[e+936>>2]),ve,Qe,O)|0)We=22;else if(Pe=t[e+520>>2]|0,!Pe)We=21;else for(K=0;;){if(P=e+524+(K*24|0)|0,Zi=w(C[P>>2]),ts=w(C[e+524+(K*24|0)+4>>2]),ns=w(C[e+524+(K*24|0)+16>>2]),Z0(l,n,s,r,t[e+524+(K*24|0)+8>>2]|0,Zi,t[e+524+(K*24|0)+12>>2]|0,ts,ns,w(C[e+524+(K*24|0)+20>>2]),ve,Qe,O)|0){We=22;break e}if(K=K+1|0,K>>>0>=Pe>>>0){We=21;break}}else{if(S){if(P=e+916|0,!(gi(w(C[P>>2]),n)|0)){We=21;break}if(!(gi(w(C[e+920>>2]),r)|0)){We=21;break}if((t[e+924>>2]|0)!=(l|0)){We=21;break}P=(t[e+928>>2]|0)==(s|0)?P:0,We=22;break}if(Pe=t[e+520>>2]|0,!Pe)We=21;else for(K=0;;){if(P=e+524+(K*24|0)|0,((gi(w(C[P>>2]),n)|0?gi(w(C[e+524+(K*24|0)+4>>2]),r)|0:0)?(t[e+524+(K*24|0)+8>>2]|0)==(l|0):0)?(t[e+524+(K*24|0)+12>>2]|0)==(s|0):0){We=22;break e}if(K=K+1|0,K>>>0>=Pe>>>0){We=21;break}}}while(0);do if((We|0)==21)p[11697]|0?(P=0,We=28):(P=0,We=31);else if((We|0)==22){if(K=(p[11697]|0)!=0,!((P|0)!=0&(kr^1)))if(K){We=28;break}else{We=31;break}Ee=P+16|0,t[e+908>>2]=t[Ee>>2],Pe=P+20|0,t[e+912>>2]=t[Pe>>2],(p[11698]|0)==0|K^1||(t[st>>2]=Br(Fe)|0,t[st+4>>2]=Fe,sr(e,4,2972,st),K=t[e+972>>2]|0,K|0&&M1[K&127](e),l=wn(l,S)|0,s=wn(s,S)|0,Xs=+w(C[Ee>>2]),rs=+w(C[Pe>>2]),t[Qt>>2]=l,t[Qt+4>>2]=s,U[Qt+8>>3]=+n,U[Qt+16>>3]=+r,U[Qt+24>>3]=Xs,U[Qt+32>>3]=rs,t[Qt+40>>2]=M,sr(e,4,2989,Qt))}while(0);return(We|0)==28&&(K=Br(Fe)|0,t[Ee>>2]=K,t[Ee+4>>2]=Fe,t[Ee+8>>2]=kr?3047:11699,sr(e,4,3038,Ee),K=t[e+972>>2]|0,K|0&&M1[K&127](e),Qt=wn(l,S)|0,We=wn(s,S)|0,t[Re>>2]=Qt,t[Re+4>>2]=We,U[Re+8>>3]=+n,U[Re+16>>3]=+r,t[Re+24>>2]=M,sr(e,4,3049,Re),We=31),(We|0)==31&&(fu(e,n,r,u,l,s,h,D,S,O),p[11697]|0&&(K=t[2279]|0,Qt=Br(K)|0,t[Lr>>2]=Qt,t[Lr+4>>2]=K,t[Lr+8>>2]=kr?3047:11699,sr(e,4,3083,Lr),K=t[e+972>>2]|0,K|0&&M1[K&127](e),Qt=wn(l,S)|0,Lr=wn(s,S)|0,rs=+w(C[e+908>>2]),Xs=+w(C[e+912>>2]),t[Nn>>2]=Qt,t[Nn+4>>2]=Lr,U[Nn+8>>3]=rs,U[Nn+16>>3]=Xs,t[Nn+24>>2]=M,sr(e,4,3092,Nn)),t[e+516>>2]=u,P||(K=e+520|0,P=t[K>>2]|0,(P|0)==16&&(p[11697]|0&&sr(e,4,3124,mn),t[K>>2]=0,P=0),S?P=e+916|0:(t[K>>2]=P+1,P=e+524+(P*24|0)|0),C[P>>2]=n,C[P+4>>2]=r,t[P+8>>2]=l,t[P+12>>2]=s,t[P+16>>2]=t[e+908>>2],t[P+20>>2]=t[e+912>>2],P=0)),S&&(t[e+416>>2]=t[e+908>>2],t[e+420>>2]=t[e+912>>2],p[e+985>>0]=1,p[hr>>0]=0),t[2279]=(t[2279]|0)+-1,t[e+512>>2]=t[2278],m=On,kr|(P|0)==0|0}function Yt(e,n,r){e=e|0,n=n|0,r=w(r);var u=St;return u=w(Li(e,n,r)),w(u+w(A0(e,n,r)))}function sr(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=m,m=m+16|0,l=s,t[l>>2]=u,e?u=t[e+976>>2]|0:u=0,Ps(u,e,n,r,l),m=s}function Br(e){return e=e|0,(e>>>0>60?3201:3201+(60-e)|0)|0}function wn(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;return l=m,m=m+32|0,r=l+12|0,u=l,t[r>>2]=t[254],t[r+4>>2]=t[255],t[r+8>>2]=t[256],t[u>>2]=t[257],t[u+4>>2]=t[258],t[u+8>>2]=t[259],(e|0)>2?e=11699:e=t[(n?u:r)+(e<<2)>>2]|0,m=l,e|0}function fu(e,n,r,u,l,s,h,D,S,M){e=e|0,n=w(n),r=w(r),u=u|0,l=l|0,s=s|0,h=w(h),D=w(D),S=S|0,M=M|0;var O=0,P=0,K=0,Pe=0,Ee=St,ve=St,Qe=St,We=St,st=St,Re=St,Fe=St,Qt=0,Lr=0,Nn=0,mn=St,hr=St,kr=0,On=St,Zi=0,ts=0,ns=0,rs=0,Xs=0,$2=0,ed=0,Za=0,td=0,Oc=0,kc=0,nd=0,rd=0,id=0,si=0,$a=0,ud=0,zf=0,od=St,ld=St,Mc=St,Nc=St,qf=St,Il=0,Aa=0,As=0,ef=0,L1=0,F1=St,Lc=St,b1=St,P1=St,Bl=St,vl=St,tf=0,lu=St,I1=St,is=St,Hf=St,us=St,Wf=St,B1=0,U1=0,Vf=St,Ul=St,nf=0,j1=0,z1=0,q1=0,gr=St,Mu=0,ml=0,os=0,jl=0,Tr=0,Fn=0,rf=0,hn=St,H1=0,u0=0;rf=m,m=m+16|0,Il=rf+12|0,Aa=rf+8|0,As=rf+4|0,ef=rf,Iu(e,(l|0)==0|(Me(n)|0)^1,3326),Iu(e,(s|0)==0|(Me(r)|0)^1,3406),ml=El(e,u)|0,t[e+496>>2]=ml,Tr=I0(2,ml)|0,Fn=I0(0,ml)|0,C[e+440>>2]=w(Li(e,Tr,h)),C[e+444>>2]=w(A0(e,Tr,h)),C[e+428>>2]=w(Li(e,Fn,h)),C[e+436>>2]=w(A0(e,Fn,h)),C[e+464>>2]=w(R0(e,Tr)),C[e+468>>2]=w(co(e,Tr)),C[e+452>>2]=w(R0(e,Fn)),C[e+460>>2]=w(co(e,Fn)),C[e+488>>2]=w(Ru(e,Tr,h)),C[e+492>>2]=w(Yu(e,Tr,h)),C[e+476>>2]=w(Ru(e,Fn,h)),C[e+484>>2]=w(Yu(e,Fn,h));do if(t[e+964>>2]|0)Xl(e,n,r,l,s,h,D);else{if(os=e+948|0,jl=(t[e+952>>2]|0)-(t[os>>2]|0)>>2,!jl){hs(e,n,r,l,s,h,D);break}if(S?0:ra(e,n,r,l,s,h,D)|0)break;cs(e),$a=e+508|0,p[$a>>0]=0,Tr=I0(t[e+4>>2]|0,ml)|0,Fn=df(Tr,ml)|0,Mu=Fi(Tr)|0,ud=t[e+8>>2]|0,j1=e+28|0,zf=(t[j1>>2]|0)!=0,us=Mu?h:D,Vf=Mu?D:h,od=w(Ku(e,Tr,h)),ld=w(vs(e,Tr,h)),Ee=w(Ku(e,Fn,h)),Wf=w(wr(e,Tr,h)),Ul=w(wr(e,Fn,h)),Nn=Mu?l:s,nf=Mu?s:l,gr=Mu?Wf:Ul,st=Mu?Ul:Wf,Hf=w(Yt(e,2,h)),We=w(Yt(e,0,h)),ve=w(w(Sn(e+364|0,h))-gr),Qe=w(w(Sn(e+380|0,h))-gr),Re=w(w(Sn(e+372|0,D))-st),Fe=w(w(Sn(e+388|0,D))-st),Mc=Mu?ve:Re,Nc=Mu?Qe:Fe,Hf=w(n-Hf),n=w(Hf-gr),Me(n)|0?gr=n:gr=w(Eu(w(Yp(n,Qe)),ve)),I1=w(r-We),n=w(I1-st),Me(n)|0?is=n:is=w(Eu(w(Yp(n,Fe)),Re)),ve=Mu?gr:is,lu=Mu?is:gr;e:do if((Nn|0)==1)for(u=0,P=0;;){if(O=yi(e,P)|0,!u)(w(Xi(O))>w(0)?w(ru(O))>w(0):0)?u=O:u=0;else if($0(O)|0){Pe=0;break e}if(P=P+1|0,P>>>0>=jl>>>0){Pe=u;break}}else Pe=0;while(0);Qt=Pe+500|0,Lr=Pe+504|0,u=0,O=0,n=w(0),K=0;do{if(P=t[(t[os>>2]|0)+(K<<2)>>2]|0,(t[P+36>>2]|0)==1)Ci(P),p[P+985>>0]=1,p[P+984>>0]=0;else{Vr(P),S&&C0(P,El(P,ml)|0,ve,lu,gr);do if((t[P+24>>2]|0)!=1)if((P|0)==(Pe|0)){t[Qt>>2]=t[2278],C[Lr>>2]=w(0);break}else{Xr(e,P,gr,l,is,gr,is,s,ml,M);break}else O|0&&(t[O+960>>2]=P),t[P+960>>2]=0,O=P,u=(u|0)==0?P:u;while(0);vl=w(C[P+504>>2]),n=w(n+w(vl+w(Yt(P,Tr,gr))))}K=K+1|0}while((K|0)!=(jl|0));for(ns=n>ve,tf=zf&((Nn|0)==2&ns)?1:Nn,Zi=(nf|0)==1,Xs=Zi&(S^1),$2=(tf|0)==1,ed=(tf|0)==2,Za=976+(Tr<<2)|0,td=(nf|2|0)==2,id=Zi&(zf^1),Oc=1040+(Fn<<2)|0,kc=1040+(Tr<<2)|0,nd=976+(Fn<<2)|0,rd=(nf|0)!=1,ns=zf&((Nn|0)!=0&ns),ts=e+976|0,Zi=Zi^1,n=ve,kr=0,rs=0,vl=w(0),qf=w(0);;){e:do if(kr>>>0>>0)for(Lr=t[os>>2]|0,K=0,Fe=w(0),Re=w(0),Qe=w(0),ve=w(0),P=0,O=0,Pe=kr;;){if(Qt=t[Lr+(Pe<<2)>>2]|0,(t[Qt+36>>2]|0)!=1?(t[Qt+940>>2]=rs,(t[Qt+24>>2]|0)!=1):0){if(We=w(Yt(Qt,Tr,gr)),si=t[Za>>2]|0,r=w(Sn(Qt+380+(si<<3)|0,us)),st=w(C[Qt+504>>2]),r=w(Yp(r,st)),r=w(Eu(w(Sn(Qt+364+(si<<3)|0,us)),r)),zf&(K|0)!=0&w(We+w(Re+r))>n){s=K,We=Fe,Nn=Pe;break e}We=w(We+r),r=w(Re+We),We=w(Fe+We),$0(Qt)|0&&(Qe=w(Qe+w(Xi(Qt))),ve=w(ve-w(st*w(ru(Qt))))),O|0&&(t[O+960>>2]=Qt),t[Qt+960>>2]=0,K=K+1|0,O=Qt,P=(P|0)==0?Qt:P}else We=Fe,r=Re;if(Pe=Pe+1|0,Pe>>>0>>0)Fe=We,Re=r;else{s=K,Nn=Pe;break}}else s=0,We=w(0),Qe=w(0),ve=w(0),P=0,Nn=kr;while(0);si=Qe>w(0)&Qew(0)&veNc&((Me(Nc)|0)^1))n=Nc,si=51;else if(p[(t[ts>>2]|0)+3>>0]|0)si=51;else{if(mn!=w(0)?w(Xi(e))!=w(0):0){si=53;break}n=We,si=53}while(0);if((si|0)==51&&(si=0,Me(n)|0?si=53:(hr=w(n-We),On=n)),(si|0)==53&&(si=0,We>2]|0,Pe=hrw(0),Re=w(hr/mn),Qe=w(0),We=w(0),n=w(0),O=P;do r=w(Sn(O+380+(K<<3)|0,us)),ve=w(Sn(O+364+(K<<3)|0,us)),ve=w(Yp(r,w(Eu(ve,w(C[O+504>>2]))))),Pe?(r=w(ve*w(ru(O))),(r!=w(-0)?(hn=w(ve-w(st*r)),F1=w(Wn(O,Tr,hn,On,gr)),hn!=F1):0)&&(Qe=w(Qe-w(F1-ve)),n=w(n+r))):((Qt?(Lc=w(Xi(O)),Lc!=w(0)):0)?(hn=w(ve+w(Re*Lc)),b1=w(Wn(O,Tr,hn,On,gr)),hn!=b1):0)&&(Qe=w(Qe-w(b1-ve)),We=w(We-Lc)),O=t[O+960>>2]|0;while((O|0)!=0);if(n=w(Fe+n),ve=w(hr+Qe),L1)n=w(0);else{st=w(mn+We),Pe=t[Za>>2]|0,Qt=vew(0),st=w(ve/st),n=w(0);do{hn=w(Sn(P+380+(Pe<<3)|0,us)),Qe=w(Sn(P+364+(Pe<<3)|0,us)),Qe=w(Yp(hn,w(Eu(Qe,w(C[P+504>>2]))))),Qt?(hn=w(Qe*w(ru(P))),ve=w(-hn),hn!=w(-0)?(hn=w(Re*ve),ve=w(Wn(P,Tr,w(Qe+(Lr?ve:hn)),On,gr))):ve=Qe):(K?(P1=w(Xi(P)),P1!=w(0)):0)?ve=w(Wn(P,Tr,w(Qe+w(st*P1)),On,gr)):ve=Qe,n=w(n-w(ve-Qe)),We=w(Yt(P,Tr,gr)),r=w(Yt(P,Fn,gr)),ve=w(ve+We),C[Aa>>2]=ve,t[ef>>2]=1,Qe=w(C[P+396>>2]);e:do if(Me(Qe)|0){O=Me(lu)|0;do if(!O){if(ns|(Bu(P,Fn,lu)|0|Zi)||(Xu(e,P)|0)!=4||(t[(m0(P,Fn)|0)+4>>2]|0)==3||(t[(y0(P,Fn)|0)+4>>2]|0)==3)break;C[Il>>2]=lu,t[As>>2]=1;break e}while(0);if(Bu(P,Fn,lu)|0){O=t[P+992+(t[nd>>2]<<2)>>2]|0,hn=w(r+w(Sn(O,lu))),C[Il>>2]=hn,O=rd&(t[O+4>>2]|0)==2,t[As>>2]=((Me(hn)|0|O)^1)&1;break}else{C[Il>>2]=lu,t[As>>2]=O?0:2;break}}else hn=w(ve-We),mn=w(hn/Qe),hn=w(Qe*hn),t[As>>2]=1,C[Il>>2]=w(r+(Mu?mn:hn));while(0);kn(P,Tr,On,gr,ef,Aa),kn(P,Fn,lu,gr,As,Il);do if(Bu(P,Fn,lu)|0?0:(Xu(e,P)|0)==4){if((t[(m0(P,Fn)|0)+4>>2]|0)==3){O=0;break}O=(t[(y0(P,Fn)|0)+4>>2]|0)!=3}else O=0;while(0);hn=w(C[Aa>>2]),mn=w(C[Il>>2]),H1=t[ef>>2]|0,u0=t[As>>2]|0,Gt(P,Mu?hn:mn,Mu?mn:hn,ml,Mu?H1:u0,Mu?u0:H1,gr,is,S&(O^1),3488,M)|0,p[$a>>0]=p[$a>>0]|p[P+508>>0],P=t[P+960>>2]|0}while((P|0)!=0)}}else n=w(0);if(n=w(hr+n),u0=n>0]=u0|k[$a>>0],ed&n>w(0)?(O=t[Za>>2]|0,((t[e+364+(O<<3)+4>>2]|0)!=0?(Bl=w(Sn(e+364+(O<<3)|0,us)),Bl>=w(0)):0)?ve=w(Eu(w(0),w(Bl-w(On-n)))):ve=w(0)):ve=n,Qt=kr>>>0>>0,Qt){Pe=t[os>>2]|0,K=kr,O=0;do P=t[Pe+(K<<2)>>2]|0,t[P+24>>2]|0||(O=((t[(m0(P,Tr)|0)+4>>2]|0)==3&1)+O|0,O=O+((t[(y0(P,Tr)|0)+4>>2]|0)==3&1)|0),K=K+1|0;while((K|0)!=(Nn|0));O?(We=w(0),r=w(0)):si=101}else si=101;e:do if((si|0)==101)switch(si=0,ud|0){case 1:{O=0,We=w(ve*w(.5)),r=w(0);break e}case 2:{O=0,We=ve,r=w(0);break e}case 3:{if(s>>>0<=1){O=0,We=w(0),r=w(0);break e}r=w((s+-1|0)>>>0),O=0,We=w(0),r=w(w(Eu(ve,w(0)))/r);break e}case 5:{r=w(ve/w((s+1|0)>>>0)),O=0,We=r;break e}case 4:{r=w(ve/w(s>>>0)),O=0,We=w(r*w(.5));break e}default:{O=0,We=w(0),r=w(0);break e}}while(0);if(n=w(od+We),Qt){Qe=w(ve/w(O|0)),K=t[os>>2]|0,P=kr,ve=w(0);do{O=t[K+(P<<2)>>2]|0;e:do if((t[O+36>>2]|0)!=1){switch(t[O+24>>2]|0){case 1:{if(se(O,Tr)|0){if(!S)break e;hn=w(re(O,Tr,On)),hn=w(hn+w(R0(e,Tr))),hn=w(hn+w(Li(O,Tr,gr))),C[O+400+(t[kc>>2]<<2)>>2]=hn;break e}break}case 0:if(u0=(t[(m0(O,Tr)|0)+4>>2]|0)==3,hn=w(Qe+n),n=u0?hn:n,S&&(u0=O+400+(t[kc>>2]<<2)|0,C[u0>>2]=w(n+w(C[u0>>2]))),u0=(t[(y0(O,Tr)|0)+4>>2]|0)==3,hn=w(Qe+n),n=u0?hn:n,Xs){hn=w(r+w(Yt(O,Tr,gr))),ve=lu,n=w(n+w(hn+w(C[O+504>>2])));break e}else{n=w(n+w(r+w(Le(O,Tr,gr)))),ve=w(Eu(ve,w(Le(O,Fn,gr))));break e}default:}S&&(hn=w(We+w(R0(e,Tr))),u0=O+400+(t[kc>>2]<<2)|0,C[u0>>2]=w(hn+w(C[u0>>2])))}while(0);P=P+1|0}while((P|0)!=(Nn|0))}else ve=w(0);if(r=w(ld+n),td?We=w(w(Wn(e,Fn,w(Ul+ve),Vf,h))-Ul):We=lu,Qe=w(w(Wn(e,Fn,w(Ul+(id?lu:ve)),Vf,h))-Ul),Qt&S){P=kr;do{K=t[(t[os>>2]|0)+(P<<2)>>2]|0;do if((t[K+36>>2]|0)!=1){if((t[K+24>>2]|0)==1){if(se(K,Fn)|0){if(hn=w(re(K,Fn,lu)),hn=w(hn+w(R0(e,Fn))),hn=w(hn+w(Li(K,Fn,gr))),O=t[Oc>>2]|0,C[K+400+(O<<2)>>2]=hn,!(Me(hn)|0))break}else O=t[Oc>>2]|0;hn=w(R0(e,Fn)),C[K+400+(O<<2)>>2]=w(hn+w(Li(K,Fn,gr)));break}O=Xu(e,K)|0;do if((O|0)==4){if((t[(m0(K,Fn)|0)+4>>2]|0)==3){si=139;break}if((t[(y0(K,Fn)|0)+4>>2]|0)==3){si=139;break}if(Bu(K,Fn,lu)|0){n=Ee;break}H1=t[K+908+(t[Za>>2]<<2)>>2]|0,t[Il>>2]=H1,n=w(C[K+396>>2]),u0=Me(n)|0,ve=(t[W>>2]=H1,w(C[W>>2])),u0?n=Qe:(hr=w(Yt(K,Fn,gr)),hn=w(ve/n),n=w(n*ve),n=w(hr+(Mu?hn:n))),C[Aa>>2]=n,C[Il>>2]=w(w(Yt(K,Tr,gr))+ve),t[As>>2]=1,t[ef>>2]=1,kn(K,Tr,On,gr,As,Il),kn(K,Fn,lu,gr,ef,Aa),n=w(C[Il>>2]),hr=w(C[Aa>>2]),hn=Mu?n:hr,n=Mu?hr:n,u0=((Me(hn)|0)^1)&1,Gt(K,hn,n,ml,u0,((Me(n)|0)^1)&1,gr,is,1,3493,M)|0,n=Ee}else si=139;while(0);e:do if((si|0)==139){si=0,n=w(We-w(Le(K,Fn,gr)));do if((t[(m0(K,Fn)|0)+4>>2]|0)==3){if((t[(y0(K,Fn)|0)+4>>2]|0)!=3)break;n=w(Ee+w(Eu(w(0),w(n*w(.5)))));break e}while(0);if((t[(y0(K,Fn)|0)+4>>2]|0)==3){n=Ee;break}if((t[(m0(K,Fn)|0)+4>>2]|0)==3){n=w(Ee+w(Eu(w(0),n)));break}switch(O|0){case 1:{n=Ee;break e}case 2:{n=w(Ee+w(n*w(.5)));break e}default:{n=w(Ee+n);break e}}}while(0);hn=w(vl+n),u0=K+400+(t[Oc>>2]<<2)|0,C[u0>>2]=w(hn+w(C[u0>>2]))}while(0);P=P+1|0}while((P|0)!=(Nn|0))}if(vl=w(vl+Qe),qf=w(Eu(qf,r)),s=rs+1|0,Nn>>>0>=jl>>>0)break;n=On,kr=Nn,rs=s}do if(S){if(O=s>>>0>1,O?0:!(Ae(e)|0))break;if(!(Me(lu)|0)){n=w(lu-vl);e:do switch(t[e+12>>2]|0){case 3:{Ee=w(Ee+n),Re=w(0);break}case 2:{Ee=w(Ee+w(n*w(.5))),Re=w(0);break}case 4:{lu>vl?Re=w(n/w(s>>>0)):Re=w(0);break}case 7:if(lu>vl){Ee=w(Ee+w(n/w(s<<1>>>0))),Re=w(n/w(s>>>0)),Re=O?Re:w(0);break e}else{Ee=w(Ee+w(n*w(.5))),Re=w(0);break e}case 6:{Re=w(n/w(rs>>>0)),Re=lu>vl&O?Re:w(0);break}default:Re=w(0)}while(0);if(s|0)for(Qt=1040+(Fn<<2)|0,Lr=976+(Fn<<2)|0,Pe=0,P=0;;){e:do if(P>>>0>>0)for(ve=w(0),Qe=w(0),n=w(0),K=P;;){O=t[(t[os>>2]|0)+(K<<2)>>2]|0;do if((t[O+36>>2]|0)!=1?(t[O+24>>2]|0)==0:0){if((t[O+940>>2]|0)!=(Pe|0))break e;if(ot(O,Fn)|0&&(hn=w(C[O+908+(t[Lr>>2]<<2)>>2]),n=w(Eu(n,w(hn+w(Yt(O,Fn,gr)))))),(Xu(e,O)|0)!=5)break;Bl=w(vt(O)),Bl=w(Bl+w(Li(O,0,gr))),hn=w(C[O+912>>2]),hn=w(w(hn+w(Yt(O,0,gr)))-Bl),Bl=w(Eu(Qe,Bl)),hn=w(Eu(ve,hn)),ve=hn,Qe=Bl,n=w(Eu(n,w(Bl+hn)))}while(0);if(O=K+1|0,O>>>0>>0)K=O;else{K=O;break}}else Qe=w(0),n=w(0),K=P;while(0);if(st=w(Re+n),r=Ee,Ee=w(Ee+st),P>>>0>>0){We=w(r+Qe),O=P;do{P=t[(t[os>>2]|0)+(O<<2)>>2]|0;e:do if((t[P+36>>2]|0)!=1?(t[P+24>>2]|0)==0:0)switch(Xu(e,P)|0){case 1:{hn=w(r+w(Li(P,Fn,gr))),C[P+400+(t[Qt>>2]<<2)>>2]=hn;break e}case 3:{hn=w(w(Ee-w(A0(P,Fn,gr)))-w(C[P+908+(t[Lr>>2]<<2)>>2])),C[P+400+(t[Qt>>2]<<2)>>2]=hn;break e}case 2:{hn=w(r+w(w(st-w(C[P+908+(t[Lr>>2]<<2)>>2]))*w(.5))),C[P+400+(t[Qt>>2]<<2)>>2]=hn;break e}case 4:{if(hn=w(r+w(Li(P,Fn,gr))),C[P+400+(t[Qt>>2]<<2)>>2]=hn,Bu(P,Fn,lu)|0||(Mu?(ve=w(C[P+908>>2]),n=w(ve+w(Yt(P,Tr,gr))),Qe=st):(Qe=w(C[P+912>>2]),Qe=w(Qe+w(Yt(P,Fn,gr))),n=st,ve=w(C[P+908>>2])),gi(n,ve)|0?gi(Qe,w(C[P+912>>2]))|0:0))break e;Gt(P,n,Qe,ml,1,1,gr,is,1,3501,M)|0;break e}case 5:{C[P+404>>2]=w(w(We-w(vt(P)))+w(re(P,0,lu)));break e}default:break e}while(0);O=O+1|0}while((O|0)!=(K|0))}if(Pe=Pe+1|0,(Pe|0)==(s|0))break;P=K}}}while(0);if(C[e+908>>2]=w(Wn(e,2,Hf,h,h)),C[e+912>>2]=w(Wn(e,0,I1,D,h)),((tf|0)!=0?(B1=t[e+32>>2]|0,U1=(tf|0)==2,!(U1&(B1|0)!=2)):0)?U1&(B1|0)==2&&(n=w(Wf+On),n=w(Eu(w(Yp(n,w(Xt(e,Tr,qf,us)))),Wf)),si=198):(n=w(Wn(e,Tr,qf,us,h)),si=198),(si|0)==198&&(C[e+908+(t[976+(Tr<<2)>>2]<<2)>>2]=n),((nf|0)!=0?(z1=t[e+32>>2]|0,q1=(nf|0)==2,!(q1&(z1|0)!=2)):0)?q1&(z1|0)==2&&(n=w(Ul+lu),n=w(Eu(w(Yp(n,w(Xt(e,Fn,w(Ul+vl),Vf)))),Ul)),si=204):(n=w(Wn(e,Fn,w(Ul+vl),Vf,h)),si=204),(si|0)==204&&(C[e+908+(t[976+(Fn<<2)>>2]<<2)>>2]=n),S){if((t[j1>>2]|0)==2){P=976+(Fn<<2)|0,K=1040+(Fn<<2)|0,O=0;do Pe=yi(e,O)|0,t[Pe+24>>2]|0||(H1=t[P>>2]|0,hn=w(C[e+908+(H1<<2)>>2]),u0=Pe+400+(t[K>>2]<<2)|0,hn=w(hn-w(C[u0>>2])),C[u0>>2]=w(hn-w(C[Pe+908+(H1<<2)>>2]))),O=O+1|0;while((O|0)!=(jl|0))}if(u|0){O=Mu?tf:l;do xn(e,u,gr,O,is,ml,M),u=t[u+960>>2]|0;while((u|0)!=0)}if(O=(Tr|2|0)==3,P=(Fn|2|0)==3,O|P){u=0;do K=t[(t[os>>2]|0)+(u<<2)>>2]|0,(t[K+36>>2]|0)!=1&&(O&&_n(e,K,Tr),P&&_n(e,K,Fn)),u=u+1|0;while((u|0)!=(jl|0))}}}while(0);m=rf}function Gu(e,n){e=e|0,n=w(n);var r=0;Dn(e,n>=w(0),3147),r=n==w(0),C[e+4>>2]=r?w(0):n}function Kr(e,n,r,u){e=e|0,n=w(n),r=w(r),u=u|0;var l=St,s=St,h=0,D=0,S=0;t[2278]=(t[2278]|0)+1,Vr(e),Bu(e,2,n)|0?(l=w(Sn(t[e+992>>2]|0,n)),S=1,l=w(l+w(Yt(e,2,n)))):(l=w(Sn(e+380|0,n)),l>=w(0)?S=2:(S=((Me(n)|0)^1)&1,l=n)),Bu(e,0,r)|0?(s=w(Sn(t[e+996>>2]|0,r)),D=1,s=w(s+w(Yt(e,0,n)))):(s=w(Sn(e+388|0,r)),s>=w(0)?D=2:(D=((Me(r)|0)^1)&1,s=r)),h=e+976|0,(Gt(e,l,s,u,S,D,n,r,1,3189,t[h>>2]|0)|0?(C0(e,t[e+496>>2]|0,n,r,n),Au(e,w(C[(t[h>>2]|0)+4>>2]),w(0),w(0)),p[11696]|0):0)&&ff(e,7)}function Vr(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;D=m,m=m+32|0,h=D+24|0,s=D+16|0,u=D+8|0,l=D,r=0;do n=e+380+(r<<3)|0,((t[e+380+(r<<3)+4>>2]|0)!=0?(S=n,M=t[S+4>>2]|0,O=u,t[O>>2]=t[S>>2],t[O+4>>2]=M,O=e+364+(r<<3)|0,M=t[O+4>>2]|0,S=l,t[S>>2]=t[O>>2],t[S+4>>2]=M,t[s>>2]=t[u>>2],t[s+4>>2]=t[u+4>>2],t[h>>2]=t[l>>2],t[h+4>>2]=t[l+4>>2],Oi(s,h)|0):0)||(n=e+348+(r<<3)|0),t[e+992+(r<<2)>>2]=n,r=r+1|0;while((r|0)!=2);m=D}function Bu(e,n,r){e=e|0,n=n|0,r=w(r);var u=0;switch(e=t[e+992+(t[976+(n<<2)>>2]<<2)>>2]|0,t[e+4>>2]|0){case 0:case 3:{e=0;break}case 1:{w(C[e>>2])>2])>2]|0){case 2:{n=w(w(w(C[e>>2])*n)/w(100));break}case 1:{n=w(C[e>>2]);break}default:n=w(le)}return w(n)}function C0(e,n,r,u,l){e=e|0,n=n|0,r=w(r),u=w(u),l=w(l);var s=0,h=St;n=t[e+944>>2]|0?n:1,s=I0(t[e+4>>2]|0,n)|0,n=df(s,n)|0,r=w(Sr(e,s,r)),u=w(Sr(e,n,u)),h=w(r+w(Li(e,s,l))),C[e+400+(t[1040+(s<<2)>>2]<<2)>>2]=h,r=w(r+w(A0(e,s,l))),C[e+400+(t[1e3+(s<<2)>>2]<<2)>>2]=r,r=w(u+w(Li(e,n,l))),C[e+400+(t[1040+(n<<2)>>2]<<2)>>2]=r,l=w(u+w(A0(e,n,l))),C[e+400+(t[1e3+(n<<2)>>2]<<2)>>2]=l}function Au(e,n,r,u){e=e|0,n=w(n),r=w(r),u=w(u);var l=0,s=0,h=St,D=St,S=0,M=0,O=St,P=0,K=St,Pe=St,Ee=St,ve=St;if(n!=w(0)&&(l=e+400|0,ve=w(C[l>>2]),s=e+404|0,Ee=w(C[s>>2]),P=e+416|0,Pe=w(C[P>>2]),M=e+420|0,h=w(C[M>>2]),K=w(ve+r),O=w(Ee+u),u=w(K+Pe),D=w(O+h),S=(t[e+988>>2]|0)==1,C[l>>2]=w(J0(ve,n,0,S)),C[s>>2]=w(J0(Ee,n,0,S)),r=w(YE(w(Pe*n),w(1))),gi(r,w(0))|0?s=0:s=(gi(r,w(1))|0)^1,r=w(YE(w(h*n),w(1))),gi(r,w(0))|0?l=0:l=(gi(r,w(1))|0)^1,ve=w(J0(u,n,S&s,S&(s^1))),C[P>>2]=w(ve-w(J0(K,n,0,S))),ve=w(J0(D,n,S&l,S&(l^1))),C[M>>2]=w(ve-w(J0(O,n,0,S))),s=(t[e+952>>2]|0)-(t[e+948>>2]|0)>>2,s|0)){l=0;do Au(yi(e,l)|0,n,K,O),l=l+1|0;while((l|0)!=(s|0))}}function ei(e,n,r,u,l){switch(e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,r|0){case 5:case 0:{e=F8(t[489]|0,u,l)|0;break}default:e=QI(u,l)|0}return e|0}function _l(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;l=m,m=m+16|0,s=l,t[s>>2]=u,Ps(e,0,n,r,s),m=l}function Ps(e,n,r,u,l){if(e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,e=e|0?e:956,tS[t[e+8>>2]&1](e,n,r,u,l)|0,(r|0)==5)Xn();else return}function Uu(e,n,r){e=e|0,n=n|0,r=r|0,p[e+n>>0]=r&1}function na(e,n){e=e|0,n=n|0;var r=0,u=0;t[e>>2]=0,t[e+4>>2]=0,t[e+8>>2]=0,r=n+4|0,u=(t[r>>2]|0)-(t[n>>2]|0)>>2,u|0&&(zi(e,u),Is(e,t[n>>2]|0,t[r>>2]|0,u))}function zi(e,n){e=e|0,n=n|0;var r=0;if((x0(e)|0)>>>0>>0&&li(e),n>>>0>1073741823)Xn();else{r=cn(n<<2)|0,t[e+4>>2]=r,t[e>>2]=r,t[e+8>>2]=r+(n<<2);return}}function Is(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,u=e+4|0,e=r-n|0,(e|0)>0&&(pr(t[u>>2]|0,n|0,e|0)|0,t[u>>2]=(t[u>>2]|0)+(e>>>2<<2))}function x0(e){return e=e|0,1073741823}function Li(e,n,r){return e=e|0,n=n|0,r=w(r),(Fi(n)|0?(t[e+96>>2]|0)!=0:0)?e=e+92|0:e=dt(e+60|0,t[1040+(n<<2)>>2]|0,992)|0,w($o(e,r))}function A0(e,n,r){return e=e|0,n=n|0,r=w(r),(Fi(n)|0?(t[e+104>>2]|0)!=0:0)?e=e+100|0:e=dt(e+60|0,t[1e3+(n<<2)>>2]|0,992)|0,w($o(e,r))}function Fi(e){return e=e|0,(e|1|0)==3|0}function $o(e,n){return e=e|0,n=w(n),(t[e+4>>2]|0)==3?n=w(0):n=w(Sn(e,n)),w(n)}function El(e,n){return e=e|0,n=n|0,e=t[e>>2]|0,((e|0)==0?(n|0)>1?n:1:e)|0}function I0(e,n){e=e|0,n=n|0;var r=0;e:do if((n|0)==2){switch(e|0){case 2:{e=3;break e}case 3:break;default:{r=4;break e}}e=2}else r=4;while(0);return e|0}function R0(e,n){e=e|0,n=n|0;var r=St;return((Fi(n)|0?(t[e+312>>2]|0)!=0:0)?(r=w(C[e+308>>2]),r>=w(0)):0)||(r=w(Eu(w(C[(dt(e+276|0,t[1040+(n<<2)>>2]|0,992)|0)>>2]),w(0)))),w(r)}function co(e,n){e=e|0,n=n|0;var r=St;return((Fi(n)|0?(t[e+320>>2]|0)!=0:0)?(r=w(C[e+316>>2]),r>=w(0)):0)||(r=w(Eu(w(C[(dt(e+276|0,t[1e3+(n<<2)>>2]|0,992)|0)>>2]),w(0)))),w(r)}function Ru(e,n,r){e=e|0,n=n|0,r=w(r);var u=St;return((Fi(n)|0?(t[e+240>>2]|0)!=0:0)?(u=w(Sn(e+236|0,r)),u>=w(0)):0)||(u=w(Eu(w(Sn(dt(e+204|0,t[1040+(n<<2)>>2]|0,992)|0,r)),w(0)))),w(u)}function Yu(e,n,r){e=e|0,n=n|0,r=w(r);var u=St;return((Fi(n)|0?(t[e+248>>2]|0)!=0:0)?(u=w(Sn(e+244|0,r)),u>=w(0)):0)||(u=w(Eu(w(Sn(dt(e+204|0,t[1e3+(n<<2)>>2]|0,992)|0,r)),w(0)))),w(u)}function Xl(e,n,r,u,l,s,h){e=e|0,n=w(n),r=w(r),u=u|0,l=l|0,s=w(s),h=w(h);var D=St,S=St,M=St,O=St,P=St,K=St,Pe=0,Ee=0,ve=0;ve=m,m=m+16|0,Pe=ve,Ee=e+964|0,Iu(e,(t[Ee>>2]|0)!=0,3519),D=w(wr(e,2,n)),S=w(wr(e,0,n)),M=w(Yt(e,2,n)),O=w(Yt(e,0,n)),Me(n)|0?P=n:P=w(Eu(w(0),w(w(n-M)-D))),Me(r)|0?K=r:K=w(Eu(w(0),w(w(r-O)-S))),(u|0)==1&(l|0)==1?(C[e+908>>2]=w(Wn(e,2,w(n-M),s,s)),n=w(Wn(e,0,w(r-O),h,s))):(nS[t[Ee>>2]&1](Pe,e,P,u,K,l),P=w(D+w(C[Pe>>2])),K=w(n-M),C[e+908>>2]=w(Wn(e,2,(u|2|0)==2?P:K,s,s)),K=w(S+w(C[Pe+4>>2])),n=w(r-O),n=w(Wn(e,0,(l|2|0)==2?K:n,h,s))),C[e+912>>2]=n,m=ve}function hs(e,n,r,u,l,s,h){e=e|0,n=w(n),r=w(r),u=u|0,l=l|0,s=w(s),h=w(h);var D=St,S=St,M=St,O=St;M=w(wr(e,2,s)),D=w(wr(e,0,s)),O=w(Yt(e,2,s)),S=w(Yt(e,0,s)),n=w(n-O),C[e+908>>2]=w(Wn(e,2,(u|2|0)==2?M:n,s,s)),r=w(r-S),C[e+912>>2]=w(Wn(e,0,(l|2|0)==2?D:r,h,s))}function ra(e,n,r,u,l,s,h){e=e|0,n=w(n),r=w(r),u=u|0,l=l|0,s=w(s),h=w(h);var D=0,S=St,M=St;return D=(u|0)==2,((n<=w(0)&D?0:!(r<=w(0)&(l|0)==2))?!((u|0)==1&(l|0)==1):0)?e=0:(S=w(Yt(e,0,s)),M=w(Yt(e,2,s)),D=n>2]=w(Wn(e,2,D?w(0):n,s,s)),n=w(r-S),D=r>2]=w(Wn(e,0,D?w(0):n,h,s)),e=1),e|0}function df(e,n){return e=e|0,n=n|0,yn(e)|0?e=I0(2,n)|0:e=0,e|0}function Ku(e,n,r){return e=e|0,n=n|0,r=w(r),r=w(Ru(e,n,r)),w(r+w(R0(e,n)))}function vs(e,n,r){return e=e|0,n=n|0,r=w(r),r=w(Yu(e,n,r)),w(r+w(co(e,n)))}function wr(e,n,r){e=e|0,n=n|0,r=w(r);var u=St;return u=w(Ku(e,n,r)),w(u+w(vs(e,n,r)))}function $0(e){return e=e|0,t[e+24>>2]|0?e=0:w(Xi(e))!=w(0)?e=1:e=w(ru(e))!=w(0),e|0}function Xi(e){e=e|0;var n=St;if(t[e+944>>2]|0){if(n=w(C[e+44>>2]),Me(n)|0)return n=w(C[e+40>>2]),e=n>w(0)&((Me(n)|0)^1),w(e?n:w(0))}else n=w(0);return w(n)}function ru(e){e=e|0;var n=St,r=0,u=St;do if(t[e+944>>2]|0){if(n=w(C[e+48>>2]),Me(n)|0){if(r=p[(t[e+976>>2]|0)+2>>0]|0,r<<24>>24==0?(u=w(C[e+40>>2]),u>24?w(1):w(0)}}else n=w(0);while(0);return w(n)}function Ci(e){e=e|0;var n=0,r=0;if(Iv(e+400|0,0,540)|0,p[e+985>>0]=1,cs(e),r=mu(e)|0,r|0){n=e+948|0,e=0;do Ci(t[(t[n>>2]|0)+(e<<2)>>2]|0),e=e+1|0;while((e|0)!=(r|0))}}function Xr(e,n,r,u,l,s,h,D,S,M){e=e|0,n=n|0,r=w(r),u=u|0,l=w(l),s=w(s),h=w(h),D=D|0,S=S|0,M=M|0;var O=0,P=St,K=0,Pe=0,Ee=St,ve=St,Qe=0,We=St,st=0,Re=St,Fe=0,Qt=0,Lr=0,Nn=0,mn=0,hr=0,kr=0,On=0,Zi=0,ts=0;Zi=m,m=m+16|0,Lr=Zi+12|0,Nn=Zi+8|0,mn=Zi+4|0,hr=Zi,On=I0(t[e+4>>2]|0,S)|0,Fe=Fi(On)|0,P=w(Sn(En(n)|0,Fe?s:h)),Qt=Bu(n,2,s)|0,kr=Bu(n,0,h)|0;do if(Me(P)|0?0:!(Me(Fe?r:l)|0)){if(O=n+504|0,!(Me(w(C[O>>2]))|0)&&(!(er(t[n+976>>2]|0,0)|0)||(t[n+500>>2]|0)==(t[2278]|0)))break;C[O>>2]=w(Eu(P,w(wr(n,On,s))))}else K=7;while(0);do if((K|0)==7){if(st=Fe^1,!(st|Qt^1)){h=w(Sn(t[n+992>>2]|0,s)),C[n+504>>2]=w(Eu(h,w(wr(n,2,s))));break}if(!(Fe|kr^1)){h=w(Sn(t[n+996>>2]|0,h)),C[n+504>>2]=w(Eu(h,w(wr(n,0,s))));break}C[Lr>>2]=w(le),C[Nn>>2]=w(le),t[mn>>2]=0,t[hr>>2]=0,We=w(Yt(n,2,s)),Re=w(Yt(n,0,s)),Qt?(Ee=w(We+w(Sn(t[n+992>>2]|0,s))),C[Lr>>2]=Ee,t[mn>>2]=1,Pe=1):(Pe=0,Ee=w(le)),kr?(P=w(Re+w(Sn(t[n+996>>2]|0,h))),C[Nn>>2]=P,t[hr>>2]=1,O=1):(O=0,P=w(le)),K=t[e+32>>2]|0,Fe&(K|0)==2?K=2:(Me(Ee)|0?!(Me(r)|0):0)&&(C[Lr>>2]=r,t[mn>>2]=2,Pe=2,Ee=r),(((K|0)==2&st?0:Me(P)|0)?!(Me(l)|0):0)&&(C[Nn>>2]=l,t[hr>>2]=2,O=2,P=l),ve=w(C[n+396>>2]),Qe=Me(ve)|0;do if(Qe)K=Pe;else{if((Pe|0)==1&st){C[Nn>>2]=w(w(Ee-We)/ve),t[hr>>2]=1,O=1,K=1;break}Fe&(O|0)==1?(C[Lr>>2]=w(ve*w(P-Re)),t[mn>>2]=1,O=1,K=1):K=Pe}while(0);ts=Me(r)|0,Pe=(Xu(e,n)|0)!=4,(Fe|Qt|((u|0)!=1|ts)|(Pe|(K|0)==1)?0:(C[Lr>>2]=r,t[mn>>2]=1,!Qe))&&(C[Nn>>2]=w(w(r-We)/ve),t[hr>>2]=1,O=1),(kr|st|((D|0)!=1|(Me(l)|0))|(Pe|(O|0)==1)?0:(C[Nn>>2]=l,t[hr>>2]=1,!Qe))&&(C[Lr>>2]=w(ve*w(l-Re)),t[mn>>2]=1),kn(n,2,s,s,mn,Lr),kn(n,0,h,s,hr,Nn),r=w(C[Lr>>2]),l=w(C[Nn>>2]),Gt(n,r,l,S,t[mn>>2]|0,t[hr>>2]|0,s,h,0,3565,M)|0,h=w(C[n+908+(t[976+(On<<2)>>2]<<2)>>2]),C[n+504>>2]=w(Eu(h,w(wr(n,On,s))))}while(0);t[n+500>>2]=t[2278],m=Zi}function Wn(e,n,r,u,l){return e=e|0,n=n|0,r=w(r),u=w(u),l=w(l),u=w(Xt(e,n,r,u)),w(Eu(u,w(wr(e,n,l))))}function Xu(e,n){return e=e|0,n=n|0,n=n+20|0,n=t[((t[n>>2]|0)==0?e+16|0:n)>>2]|0,((n|0)==5?yn(t[e+4>>2]|0)|0:0)&&(n=1),n|0}function m0(e,n){return e=e|0,n=n|0,(Fi(n)|0?(t[e+96>>2]|0)!=0:0)?n=4:n=t[1040+(n<<2)>>2]|0,e+60+(n<<3)|0}function y0(e,n){return e=e|0,n=n|0,(Fi(n)|0?(t[e+104>>2]|0)!=0:0)?n=5:n=t[1e3+(n<<2)>>2]|0,e+60+(n<<3)|0}function kn(e,n,r,u,l,s){switch(e=e|0,n=n|0,r=w(r),u=w(u),l=l|0,s=s|0,r=w(Sn(e+380+(t[976+(n<<2)>>2]<<3)|0,r)),r=w(r+w(Yt(e,n,u))),t[l>>2]|0){case 2:case 1:{l=Me(r)|0,u=w(C[s>>2]),C[s>>2]=l|u>2]=2,C[s>>2]=r);break}default:}}function se(e,n){return e=e|0,n=n|0,e=e+132|0,(Fi(n)|0?(t[(dt(e,4,948)|0)+4>>2]|0)!=0:0)?e=1:e=(t[(dt(e,t[1040+(n<<2)>>2]|0,948)|0)+4>>2]|0)!=0,e|0}function re(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0;return e=e+132|0,(Fi(n)|0?(u=dt(e,4,948)|0,(t[u+4>>2]|0)!=0):0)?l=4:(u=dt(e,t[1040+(n<<2)>>2]|0,948)|0,t[u+4>>2]|0?l=4:r=w(0)),(l|0)==4&&(r=w(Sn(u,r))),w(r)}function Le(e,n,r){e=e|0,n=n|0,r=w(r);var u=St;return u=w(C[e+908+(t[976+(n<<2)>>2]<<2)>>2]),u=w(u+w(Li(e,n,r))),w(u+w(A0(e,n,r)))}function Ae(e){e=e|0;var n=0,r=0,u=0;e:do if(yn(t[e+4>>2]|0)|0)n=0;else if((t[e+16>>2]|0)!=5)if(r=mu(e)|0,!r)n=0;else for(n=0;;){if(u=yi(e,n)|0,(t[u+24>>2]|0)==0?(t[u+20>>2]|0)==5:0){n=1;break e}if(n=n+1|0,n>>>0>=r>>>0){n=0;break}}else n=1;while(0);return n|0}function ot(e,n){e=e|0,n=n|0;var r=St;return r=w(C[e+908+(t[976+(n<<2)>>2]<<2)>>2]),r>=w(0)&((Me(r)|0)^1)|0}function vt(e){e=e|0;var n=St,r=0,u=0,l=0,s=0,h=0,D=0,S=St;if(r=t[e+968>>2]|0,r)S=w(C[e+908>>2]),n=w(C[e+912>>2]),n=w(J8[r&0](e,S,n)),Iu(e,(Me(n)|0)^1,3573);else{s=mu(e)|0;do if(s|0){for(r=0,l=0;;){if(u=yi(e,l)|0,t[u+940>>2]|0){h=8;break}if((t[u+24>>2]|0)!=1)if(D=(Xu(e,u)|0)==5,D){r=u;break}else r=(r|0)==0?u:r;if(l=l+1|0,l>>>0>=s>>>0){h=8;break}}if((h|0)==8&&!r)break;return n=w(vt(r)),w(n+w(C[r+404>>2]))}while(0);n=w(C[e+912>>2])}return w(n)}function Xt(e,n,r,u){e=e|0,n=n|0,r=w(r),u=w(u);var l=St,s=0;return yn(n)|0?(n=1,s=3):Fi(n)|0?(n=0,s=3):(u=w(le),l=w(le)),(s|0)==3&&(l=w(Sn(e+364+(n<<3)|0,u)),u=w(Sn(e+380+(n<<3)|0,u))),s=u=w(0)&((Me(u)|0)^1)),r=s?u:r,s=l>=w(0)&((Me(l)|0)^1)&r>2]|0,s)|0,Ee=df(Qe,s)|0,ve=Fi(Qe)|0,P=w(Yt(n,2,r)),K=w(Yt(n,0,r)),Bu(n,2,r)|0?D=w(P+w(Sn(t[n+992>>2]|0,r))):(se(n,2)|0?It(n,2)|0:0)?(D=w(C[e+908>>2]),S=w(R0(e,2)),S=w(D-w(S+w(co(e,2)))),D=w(re(n,2,r)),D=w(Wn(n,2,w(S-w(D+w(xi(n,2,r)))),r,r))):D=w(le),Bu(n,0,l)|0?S=w(K+w(Sn(t[n+996>>2]|0,l))):(se(n,0)|0?It(n,0)|0:0)?(S=w(C[e+912>>2]),st=w(R0(e,0)),st=w(S-w(st+w(co(e,0)))),S=w(re(n,0,l)),S=w(Wn(n,0,w(st-w(S+w(xi(n,0,l)))),l,r))):S=w(le),M=Me(D)|0,O=Me(S)|0;do if(M^O?(Pe=w(C[n+396>>2]),!(Me(Pe)|0)):0)if(M){D=w(P+w(w(S-K)*Pe));break}else{st=w(K+w(w(D-P)/Pe)),S=O?st:S;break}while(0);O=Me(D)|0,M=Me(S)|0,O|M&&(Re=(O^1)&1,u=r>w(0)&((u|0)!=0&O),D=ve?D:u?r:D,Gt(n,D,S,s,ve?Re:u?2:Re,O&(M^1)&1,D,S,0,3623,h)|0,D=w(C[n+908>>2]),D=w(D+w(Yt(n,2,r))),S=w(C[n+912>>2]),S=w(S+w(Yt(n,0,r)))),Gt(n,D,S,s,1,1,D,S,1,3635,h)|0,(It(n,Qe)|0?!(se(n,Qe)|0):0)?(Re=t[976+(Qe<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(st-w(C[n+908+(Re<<2)>>2])),st=w(st-w(co(e,Qe))),st=w(st-w(A0(n,Qe,r))),st=w(st-w(xi(n,Qe,ve?r:l))),C[n+400+(t[1040+(Qe<<2)>>2]<<2)>>2]=st):We=21;do if((We|0)==21){if(se(n,Qe)|0?0:(t[e+8>>2]|0)==1){Re=t[976+(Qe<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(w(st-w(C[n+908+(Re<<2)>>2]))*w(.5)),C[n+400+(t[1040+(Qe<<2)>>2]<<2)>>2]=st;break}(se(n,Qe)|0?0:(t[e+8>>2]|0)==2)&&(Re=t[976+(Qe<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(st-w(C[n+908+(Re<<2)>>2])),C[n+400+(t[1040+(Qe<<2)>>2]<<2)>>2]=st)}while(0);(It(n,Ee)|0?!(se(n,Ee)|0):0)?(Re=t[976+(Ee<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(st-w(C[n+908+(Re<<2)>>2])),st=w(st-w(co(e,Ee))),st=w(st-w(A0(n,Ee,r))),st=w(st-w(xi(n,Ee,ve?l:r))),C[n+400+(t[1040+(Ee<<2)>>2]<<2)>>2]=st):We=30;do if((We|0)==30?!(se(n,Ee)|0):0){if((Xu(e,n)|0)==2){Re=t[976+(Ee<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(w(st-w(C[n+908+(Re<<2)>>2]))*w(.5)),C[n+400+(t[1040+(Ee<<2)>>2]<<2)>>2]=st;break}Re=(Xu(e,n)|0)==3,Re^(t[e+28>>2]|0)==2&&(Re=t[976+(Ee<<2)>>2]|0,st=w(C[e+908+(Re<<2)>>2]),st=w(st-w(C[n+908+(Re<<2)>>2])),C[n+400+(t[1040+(Ee<<2)>>2]<<2)>>2]=st)}while(0)}function _n(e,n,r){e=e|0,n=n|0,r=r|0;var u=St,l=0;l=t[976+(r<<2)>>2]|0,u=w(C[n+908+(l<<2)>>2]),u=w(w(C[e+908+(l<<2)>>2])-u),u=w(u-w(C[n+400+(t[1040+(r<<2)>>2]<<2)>>2])),C[n+400+(t[1e3+(r<<2)>>2]<<2)>>2]=u}function yn(e){return e=e|0,(e|1|0)==1|0}function En(e){e=e|0;var n=St;switch(t[e+56>>2]|0){case 0:case 3:{n=w(C[e+40>>2]),n>w(0)&((Me(n)|0)^1)?e=p[(t[e+976>>2]|0)+2>>0]|0?1056:992:e=1056;break}default:e=e+52|0}return e|0}function er(e,n){return e=e|0,n=n|0,(p[e+n>>0]|0)!=0|0}function It(e,n){return e=e|0,n=n|0,e=e+132|0,(Fi(n)|0?(t[(dt(e,5,948)|0)+4>>2]|0)!=0:0)?e=1:e=(t[(dt(e,t[1e3+(n<<2)>>2]|0,948)|0)+4>>2]|0)!=0,e|0}function xi(e,n,r){e=e|0,n=n|0,r=w(r);var u=0,l=0;return e=e+132|0,(Fi(n)|0?(u=dt(e,5,948)|0,(t[u+4>>2]|0)!=0):0)?l=4:(u=dt(e,t[1e3+(n<<2)>>2]|0,948)|0,t[u+4>>2]|0?l=4:r=w(0)),(l|0)==4&&(r=w(Sn(u,r))),w(r)}function Sr(e,n,r){return e=e|0,n=n|0,r=w(r),se(e,n)|0?r=w(re(e,n,r)):r=w(-w(xi(e,n,r))),w(r)}function cr(e){return e=w(e),C[W>>2]=e,t[W>>2]|0|0}function Y(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>1073741823)Xn();else{l=cn(n<<2)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<2)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<2)}function Qr(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>2)<<2)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Jr(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-4-n|0)>>>2)<<2)),e=t[e>>2]|0,e|0&&yt(e)}function Ur(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;if(h=e+4|0,D=t[h>>2]|0,l=D-u|0,s=l>>2,e=n+(s<<2)|0,e>>>0>>0){u=D;do t[u>>2]=t[e>>2],e=e+4|0,u=(t[h>>2]|0)+4|0,t[h>>2]=u;while(e>>>0>>0)}s|0&&Iy(D+(0-s<<2)|0,n|0,l|0)|0}function lt(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0;return D=n+4|0,S=t[D>>2]|0,l=t[e>>2]|0,h=r,s=h-l|0,u=S+(0-(s>>2)<<2)|0,t[D>>2]=u,(s|0)>0&&pr(u|0,l|0,s|0)|0,l=e+4|0,s=n+8|0,u=(t[l>>2]|0)-h|0,(u|0)>0&&(pr(t[s>>2]|0,r|0,u|0)|0,t[s>>2]=(t[s>>2]|0)+(u>>>2<<2)),h=t[e>>2]|0,t[e>>2]=t[D>>2],t[D>>2]=h,h=t[l>>2]|0,t[l>>2]=t[s>>2],t[s>>2]=h,h=e+8|0,r=n+12|0,e=t[h>>2]|0,t[h>>2]=t[r>>2],t[r>>2]=e,t[n>>2]=t[D>>2],S|0}function hi(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;if(h=t[n>>2]|0,s=t[r>>2]|0,(h|0)!=(s|0)){l=e+8|0,r=((s+-4-h|0)>>>2)+1|0,e=h,u=t[l>>2]|0;do t[u>>2]=t[e>>2],u=(t[l>>2]|0)+4|0,t[l>>2]=u,e=e+4|0;while((e|0)!=(s|0));t[n>>2]=h+(r<<2)}}function Qi(){_e()}function g0(){var e=0;return e=cn(4)|0,bn(e),e|0}function bn(e){e=e|0,t[e>>2]=p0()|0}function Qu(e){e=e|0,e|0&&(eo(e),yt(e))}function eo(e){e=e|0,K0(t[e>>2]|0)}function po(e,n,r){e=e|0,n=n|0,r=r|0,Uu(t[e>>2]|0,n,r)}function Ju(e,n){e=e|0,n=w(n),Gu(t[e>>2]|0,n)}function bo(e,n){return e=e|0,n=n|0,er(t[e>>2]|0,n)|0}function to(){var e=0;return e=cn(8)|0,Na(e,0),e|0}function Na(e,n){e=e|0,n=n|0,n?n=Hn(t[n>>2]|0)|0:n=or()|0,t[e>>2]=n,t[e+4>>2]=0,Ma(n,e)}function pf(e){e=e|0;var n=0;return n=cn(8)|0,Na(n,e),n|0}function uc(e){e=e|0,e|0&&(ms(e),yt(e))}function ms(e){e=e|0;var n=0;bu(t[e>>2]|0),n=e+4|0,e=t[n>>2]|0,t[n>>2]=0,e|0&&(ia(e),yt(e))}function ia(e){e=e|0,B0(e)}function B0(e){e=e|0,e=t[e>>2]|0,e|0&&Ir(e|0)}function oc(e){return e=e|0,Ls(e)|0}function La(e){e=e|0;var n=0,r=0;r=e+4|0,n=t[r>>2]|0,t[r>>2]=0,n|0&&(ia(n),yt(n)),ao(t[e>>2]|0)}function gd(e,n){e=e|0,n=n|0,ea(t[e>>2]|0,t[n>>2]|0)}function $1(e,n){e=e|0,n=n|0,Z(t[e>>2]|0,n)}function e2(e,n,r){e=e|0,n=n|0,r=+r,lr(t[e>>2]|0,n,w(r))}function ho(e,n,r){e=e|0,n=n|0,r=+r,Qn(t[e>>2]|0,n,w(r))}function Uc(e,n){e=e|0,n=n|0,z(t[e>>2]|0,n)}function Dl(e,n){e=e|0,n=n|0,$(t[e>>2]|0,n)}function el(e,n){e=e|0,n=n|0,me(t[e>>2]|0,n)}function _d(e,n){e=e|0,n=n|0,h0(t[e>>2]|0,n)}function Bs(e,n){e=e|0,n=n|0,Xe(t[e>>2]|0,n)}function wl(e,n){e=e|0,n=n|0,Ni(t[e>>2]|0,n)}function t2(e,n,r){e=e|0,n=n|0,r=+r,Cn(t[e>>2]|0,n,w(r))}function Po(e,n,r){e=e|0,n=n|0,r=+r,Ar(t[e>>2]|0,n,w(r))}function Fa(e,n){e=e|0,n=n|0,Rr(t[e>>2]|0,n)}function ba(e,n){e=e|0,n=n|0,ie(t[e>>2]|0,n)}function Pa(e,n){e=e|0,n=n|0,tt(t[e>>2]|0,n)}function ua(e,n){e=e|0,n=+n,kt(t[e>>2]|0,w(n))}function ys(e,n){e=e|0,n=+n,tn(t[e>>2]|0,w(n))}function gs(e,n){e=e|0,n=+n,Lt(t[e>>2]|0,w(n))}function Ql(e,n){e=e|0,n=+n,bt(t[e>>2]|0,w(n))}function Io(e,n){e=e|0,n=+n,on(t[e>>2]|0,w(n))}function hf(e,n){e=e|0,n=+n,sn(t[e>>2]|0,w(n))}function tl(e,n){e=e|0,n=+n,Yn(t[e>>2]|0,w(n))}function ju(e){e=e|0,yr(t[e>>2]|0)}function Ia(e,n){e=e|0,n=+n,Cu(t[e>>2]|0,w(n))}function Zu(e,n){e=e|0,n=+n,S0(t[e>>2]|0,w(n))}function U0(e){e=e|0,X0(t[e>>2]|0)}function vf(e,n){e=e|0,n=+n,di(t[e>>2]|0,w(n))}function jc(e,n){e=e|0,n=+n,ko(t[e>>2]|0,w(n))}function lc(e,n){e=e|0,n=+n,sf(t[e>>2]|0,w(n))}function Sl(e,n){e=e|0,n=+n,gl(t[e>>2]|0,w(n))}function _s(e,n){e=e|0,n=+n,Mo(t[e>>2]|0,w(n))}function oa(e,n){e=e|0,n=+n,ds(t[e>>2]|0,w(n))}function n2(e,n){e=e|0,n=+n,No(t[e>>2]|0,w(n))}function la(e,n){e=e|0,n=+n,Lo(t[e>>2]|0,w(n))}function sc(e,n){e=e|0,n=+n,Vu(t[e>>2]|0,w(n))}function zc(e,n,r){e=e|0,n=n|0,r=+r,Ft(t[e>>2]|0,n,w(r))}function bi(e,n,r){e=e|0,n=n|0,r=+r,nt(t[e>>2]|0,n,w(r))}function g(e,n,r){e=e|0,n=n|0,r=+r,_t(t[e>>2]|0,n,w(r))}function y(e){return e=e|0,ke(t[e>>2]|0)|0}function A(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;u=m,m=m+16|0,l=u,_r(l,t[n>>2]|0,r),F(e,l),m=u}function F(e,n){e=e|0,n=n|0,I(e,t[n+4>>2]|0,+w(C[n>>2]))}function I(e,n,r){e=e|0,n=n|0,r=+r,t[e>>2]=n,U[e+8>>3]=r}function J(e){return e=e|0,G(t[e>>2]|0)|0}function fe(e){return e=e|0,De(t[e>>2]|0)|0}function mt(e){return e=e|0,xe(t[e>>2]|0)|0}function Ct(e){return e=e|0,Fs(t[e>>2]|0)|0}function Mt(e){return e=e|0,ht(t[e>>2]|0)|0}function Er(e){return e=e|0,B(t[e>>2]|0)|0}function $u(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;u=m,m=m+16|0,l=u,v0(l,t[n>>2]|0,r),F(e,l),m=u}function iu(e){return e=e|0,qe(t[e>>2]|0)|0}function j0(e){return e=e|0,Tt(t[e>>2]|0)|0}function Tl(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,gn(u,t[n>>2]|0),F(e,u),m=r}function e0(e){return e=e|0,+ +w(lf(t[e>>2]|0))}function He(e){return e=e|0,+ +w(Ns(t[e>>2]|0))}function Be(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,nu(u,t[n>>2]|0),F(e,u),m=r}function ut(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,xu(u,t[n>>2]|0),F(e,u),m=r}function Jt(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,Zo(u,t[n>>2]|0),F(e,u),m=r}function jn(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,af(u,t[n>>2]|0),F(e,u),m=r}function ti(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,bs(u,t[n>>2]|0),F(e,u),m=r}function tr(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,ps(u,t[n>>2]|0),F(e,u),m=r}function ii(e){return e=e|0,+ +w(yu(t[e>>2]|0))}function qi(e,n){return e=e|0,n=n|0,+ +w(nn(t[e>>2]|0,n))}function jr(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;u=m,m=m+16|0,l=u,Ze(l,t[n>>2]|0,r),F(e,l),m=u}function gu(e,n,r){e=e|0,n=n|0,r=r|0,ka(t[e>>2]|0,t[n>>2]|0,r)}function Ba(e,n){e=e|0,n=n|0,Tu(t[e>>2]|0,t[n>>2]|0)}function Ua(e){return e=e|0,mu(t[e>>2]|0)|0}function r2(e){return e=e|0,e=ri(t[e>>2]|0)|0,e?e=oc(e)|0:e=0,e|0}function Ed(e,n){return e=e|0,n=n|0,e=yi(t[e>>2]|0,n)|0,e?e=oc(e)|0:e=0,e|0}function Dd(e,n){e=e|0,n=n|0;var r=0,u=0;u=cn(4)|0,mf(u,n),r=e+4|0,n=t[r>>2]|0,t[r>>2]=u,n|0&&(ia(n),yt(n)),$s(t[e>>2]|0,1)}function mf(e,n){e=e|0,n=n|0,rl(e,n)}function i2(e,n,r,u,l,s){e=e|0,n=n|0,r=w(r),u=u|0,l=w(l),s=s|0;var h=0,D=0;h=m,m=m+16|0,D=h,ch(D,Ls(n)|0,+r,u,+l,s),C[e>>2]=w(+U[D>>3]),C[e+4>>2]=w(+U[D+8>>3]),m=h}function ch(e,n,r,u,l,s){e=e|0,n=n|0,r=+r,u=u|0,l=+l,s=s|0;var h=0,D=0,S=0,M=0,O=0;h=m,m=m+32|0,O=h+8|0,M=h+20|0,S=h,D=h+16|0,U[O>>3]=r,t[M>>2]=u,U[S>>3]=l,t[D>>2]=s,qc(e,t[n+4>>2]|0,O,M,S,D),m=h}function qc(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0;h=m,m=m+16|0,D=h,Ta(D),n=vo(n)|0,dh(e,n,+U[r>>3],t[u>>2]|0,+U[l>>3],t[s>>2]|0),Ca(D),m=h}function vo(e){return e=e|0,t[e>>2]|0}function dh(e,n,r,u,l,s){e=e|0,n=n|0,r=+r,u=u|0,l=+l,s=s|0;var h=0;h=mo(ph()|0)|0,r=+Cl(r),u=u2(u)|0,l=+Cl(l),o2(e,Wr(0,h|0,n|0,+r,u|0,+l,u2(s)|0)|0)}function ph(){var e=0;return p[7608]|0||(Wc(9120),e=7608,t[e>>2]=1,t[e+4>>2]=0),9120}function mo(e){return e=e|0,t[e+8>>2]|0}function Cl(e){return e=+e,+ +ja(e)}function u2(e){return e=e|0,s2(e)|0}function o2(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;l=m,m=m+32|0,r=l,u=n,u&1?(wd(r,0),Yi(u|0,r|0)|0,Hc(e,r),Mr(r)):(t[e>>2]=t[n>>2],t[e+4>>2]=t[n+4>>2],t[e+8>>2]=t[n+8>>2],t[e+12>>2]=t[n+12>>2]),m=l}function wd(e,n){e=e|0,n=n|0,l2(e,n),t[e+8>>2]=0,p[e+24>>0]=0}function Hc(e,n){e=e|0,n=n|0,n=n+8|0,t[e>>2]=t[n>>2],t[e+4>>2]=t[n+4>>2],t[e+8>>2]=t[n+8>>2],t[e+12>>2]=t[n+12>>2]}function Mr(e){e=e|0,p[e+24>>0]=0}function l2(e,n){e=e|0,n=n|0,t[e>>2]=n}function s2(e){return e=e|0,e|0}function ja(e){return e=+e,+e}function Wc(e){e=e|0,nl(e,Sd()|0,4)}function Sd(){return 1064}function nl(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r,t[e+8>>2]=Pt(n|0,r+1|0)|0}function rl(e,n){e=e|0,n=n|0,n=t[n>>2]|0,t[e>>2]=n,Ei(n|0)}function hh(e){e=e|0;var n=0,r=0;r=e+4|0,n=t[r>>2]|0,t[r>>2]=0,n|0&&(ia(n),yt(n)),$s(t[e>>2]|0,0)}function yf(e){e=e|0,Gr(t[e>>2]|0)}function Vc(e){return e=e|0,Yl(t[e>>2]|0)|0}function Td(e,n,r,u){e=e|0,n=+n,r=+r,u=u|0,Kr(t[e>>2]|0,w(n),w(r),u)}function vh(e){return e=e|0,+ +w(pi(t[e>>2]|0))}function il(e){return e=e|0,+ +w(Q0(t[e>>2]|0))}function sa(e){return e=e|0,+ +w(T0(t[e>>2]|0))}function Cd(e){return e=e|0,+ +w(Fo(t[e>>2]|0))}function xd(e){return e=e|0,+ +w(ta(t[e>>2]|0))}function ac(e){return e=e|0,+ +w(Kl(t[e>>2]|0))}function mh(e,n){e=e|0,n=n|0,U[e>>3]=+w(pi(t[n>>2]|0)),U[e+8>>3]=+w(Q0(t[n>>2]|0)),U[e+16>>3]=+w(T0(t[n>>2]|0)),U[e+24>>3]=+w(Fo(t[n>>2]|0)),U[e+32>>3]=+w(ta(t[n>>2]|0)),U[e+40>>3]=+w(Kl(t[n>>2]|0))}function Ad(e,n){return e=e|0,n=n|0,+ +w(Ki(t[e>>2]|0,n))}function a2(e,n){return e=e|0,n=n|0,+ +w(Yr(t[e>>2]|0,n))}function Gc(e,n){return e=e|0,n=n|0,+ +w(fo(t[e>>2]|0,n))}function Yc(){return Oa()|0}function Us(){Rd(),aa(),Kc(),fc(),cc(),f2()}function Rd(){bN(11713,4938,1)}function aa(){eN(10448)}function Kc(){bM(10408)}function fc(){iM(10324)}function cc(){yE(10096)}function f2(){yh(9132)}function yh(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0,st=0,Re=0,Fe=0,Qt=0,Lr=0,Nn=0,mn=0,hr=0,kr=0,On=0,Zi=0,ts=0,ns=0,rs=0,Xs=0,$2=0,ed=0,Za=0,td=0,Oc=0,kc=0,nd=0,rd=0,id=0,si=0,$a=0,ud=0,zf=0,od=0,ld=0,Mc=0,Nc=0,qf=0,Il=0,Aa=0,As=0,ef=0,L1=0,F1=0,Lc=0,b1=0,P1=0,Bl=0,vl=0,tf=0,lu=0,I1=0,is=0,Hf=0,us=0,Wf=0,B1=0,U1=0,Vf=0,Ul=0,nf=0,j1=0,z1=0,q1=0,gr=0,Mu=0,ml=0,os=0,jl=0,Tr=0,Fn=0,rf=0;n=m,m=m+672|0,r=n+656|0,rf=n+648|0,Fn=n+640|0,Tr=n+632|0,jl=n+624|0,os=n+616|0,ml=n+608|0,Mu=n+600|0,gr=n+592|0,q1=n+584|0,z1=n+576|0,j1=n+568|0,nf=n+560|0,Ul=n+552|0,Vf=n+544|0,U1=n+536|0,B1=n+528|0,Wf=n+520|0,us=n+512|0,Hf=n+504|0,is=n+496|0,I1=n+488|0,lu=n+480|0,tf=n+472|0,vl=n+464|0,Bl=n+456|0,P1=n+448|0,b1=n+440|0,Lc=n+432|0,F1=n+424|0,L1=n+416|0,ef=n+408|0,As=n+400|0,Aa=n+392|0,Il=n+384|0,qf=n+376|0,Nc=n+368|0,Mc=n+360|0,ld=n+352|0,od=n+344|0,zf=n+336|0,ud=n+328|0,$a=n+320|0,si=n+312|0,id=n+304|0,rd=n+296|0,nd=n+288|0,kc=n+280|0,Oc=n+272|0,td=n+264|0,Za=n+256|0,ed=n+248|0,$2=n+240|0,Xs=n+232|0,rs=n+224|0,ns=n+216|0,ts=n+208|0,Zi=n+200|0,On=n+192|0,kr=n+184|0,hr=n+176|0,mn=n+168|0,Nn=n+160|0,Lr=n+152|0,Qt=n+144|0,Fe=n+136|0,Re=n+128|0,st=n+120|0,We=n+112|0,Qe=n+104|0,ve=n+96|0,Ee=n+88|0,Pe=n+80|0,K=n+72|0,P=n+64|0,O=n+56|0,M=n+48|0,S=n+40|0,D=n+32|0,h=n+24|0,s=n+16|0,l=n+8|0,u=n,gf(e,3646),Xc(e,3651,2)|0,gh(e,3665,2)|0,vm(e,3682,18)|0,t[rf>>2]=19,t[rf+4>>2]=0,t[r>>2]=t[rf>>2],t[r+4>>2]=t[rf+4>>2],js(e,3690,r)|0,t[Fn>>2]=1,t[Fn+4>>2]=0,t[r>>2]=t[Fn>>2],t[r+4>>2]=t[Fn+4>>2],fa(e,3696,r)|0,t[Tr>>2]=2,t[Tr+4>>2]=0,t[r>>2]=t[Tr>>2],t[r+4>>2]=t[Tr+4>>2],Ji(e,3706,r)|0,t[jl>>2]=1,t[jl+4>>2]=0,t[r>>2]=t[jl>>2],t[r+4>>2]=t[jl+4>>2],O0(e,3722,r)|0,t[os>>2]=2,t[os+4>>2]=0,t[r>>2]=t[os>>2],t[r+4>>2]=t[os+4>>2],O0(e,3734,r)|0,t[ml>>2]=3,t[ml+4>>2]=0,t[r>>2]=t[ml>>2],t[r+4>>2]=t[ml+4>>2],Ji(e,3753,r)|0,t[Mu>>2]=4,t[Mu+4>>2]=0,t[r>>2]=t[Mu>>2],t[r+4>>2]=t[Mu+4>>2],Ji(e,3769,r)|0,t[gr>>2]=5,t[gr+4>>2]=0,t[r>>2]=t[gr>>2],t[r+4>>2]=t[gr+4>>2],Ji(e,3783,r)|0,t[q1>>2]=6,t[q1+4>>2]=0,t[r>>2]=t[q1>>2],t[r+4>>2]=t[q1+4>>2],Ji(e,3796,r)|0,t[z1>>2]=7,t[z1+4>>2]=0,t[r>>2]=t[z1>>2],t[r+4>>2]=t[z1+4>>2],Ji(e,3813,r)|0,t[j1>>2]=8,t[j1+4>>2]=0,t[r>>2]=t[j1>>2],t[r+4>>2]=t[j1+4>>2],Ji(e,3825,r)|0,t[nf>>2]=3,t[nf+4>>2]=0,t[r>>2]=t[nf>>2],t[r+4>>2]=t[nf+4>>2],O0(e,3843,r)|0,t[Ul>>2]=4,t[Ul+4>>2]=0,t[r>>2]=t[Ul>>2],t[r+4>>2]=t[Ul+4>>2],O0(e,3853,r)|0,t[Vf>>2]=9,t[Vf+4>>2]=0,t[r>>2]=t[Vf>>2],t[r+4>>2]=t[Vf+4>>2],Ji(e,3870,r)|0,t[U1>>2]=10,t[U1+4>>2]=0,t[r>>2]=t[U1>>2],t[r+4>>2]=t[U1+4>>2],Ji(e,3884,r)|0,t[B1>>2]=11,t[B1+4>>2]=0,t[r>>2]=t[B1>>2],t[r+4>>2]=t[B1+4>>2],Ji(e,3896,r)|0,t[Wf>>2]=1,t[Wf+4>>2]=0,t[r>>2]=t[Wf>>2],t[r+4>>2]=t[Wf+4>>2],t0(e,3907,r)|0,t[us>>2]=2,t[us+4>>2]=0,t[r>>2]=t[us>>2],t[r+4>>2]=t[us+4>>2],t0(e,3915,r)|0,t[Hf>>2]=3,t[Hf+4>>2]=0,t[r>>2]=t[Hf>>2],t[r+4>>2]=t[Hf+4>>2],t0(e,3928,r)|0,t[is>>2]=4,t[is+4>>2]=0,t[r>>2]=t[is>>2],t[r+4>>2]=t[is+4>>2],t0(e,3948,r)|0,t[I1>>2]=5,t[I1+4>>2]=0,t[r>>2]=t[I1>>2],t[r+4>>2]=t[I1+4>>2],t0(e,3960,r)|0,t[lu>>2]=6,t[lu+4>>2]=0,t[r>>2]=t[lu>>2],t[r+4>>2]=t[lu+4>>2],t0(e,3974,r)|0,t[tf>>2]=7,t[tf+4>>2]=0,t[r>>2]=t[tf>>2],t[r+4>>2]=t[tf+4>>2],t0(e,3983,r)|0,t[vl>>2]=20,t[vl+4>>2]=0,t[r>>2]=t[vl>>2],t[r+4>>2]=t[vl+4>>2],js(e,3999,r)|0,t[Bl>>2]=8,t[Bl+4>>2]=0,t[r>>2]=t[Bl>>2],t[r+4>>2]=t[Bl+4>>2],t0(e,4012,r)|0,t[P1>>2]=9,t[P1+4>>2]=0,t[r>>2]=t[P1>>2],t[r+4>>2]=t[P1+4>>2],t0(e,4022,r)|0,t[b1>>2]=21,t[b1+4>>2]=0,t[r>>2]=t[b1>>2],t[r+4>>2]=t[b1+4>>2],js(e,4039,r)|0,t[Lc>>2]=10,t[Lc+4>>2]=0,t[r>>2]=t[Lc>>2],t[r+4>>2]=t[Lc+4>>2],t0(e,4053,r)|0,t[F1>>2]=11,t[F1+4>>2]=0,t[r>>2]=t[F1>>2],t[r+4>>2]=t[F1+4>>2],t0(e,4065,r)|0,t[L1>>2]=12,t[L1+4>>2]=0,t[r>>2]=t[L1>>2],t[r+4>>2]=t[L1+4>>2],t0(e,4084,r)|0,t[ef>>2]=13,t[ef+4>>2]=0,t[r>>2]=t[ef>>2],t[r+4>>2]=t[ef+4>>2],t0(e,4097,r)|0,t[As>>2]=14,t[As+4>>2]=0,t[r>>2]=t[As>>2],t[r+4>>2]=t[As+4>>2],t0(e,4117,r)|0,t[Aa>>2]=15,t[Aa+4>>2]=0,t[r>>2]=t[Aa>>2],t[r+4>>2]=t[Aa+4>>2],t0(e,4129,r)|0,t[Il>>2]=16,t[Il+4>>2]=0,t[r>>2]=t[Il>>2],t[r+4>>2]=t[Il+4>>2],t0(e,4148,r)|0,t[qf>>2]=17,t[qf+4>>2]=0,t[r>>2]=t[qf>>2],t[r+4>>2]=t[qf+4>>2],t0(e,4161,r)|0,t[Nc>>2]=18,t[Nc+4>>2]=0,t[r>>2]=t[Nc>>2],t[r+4>>2]=t[Nc+4>>2],t0(e,4181,r)|0,t[Mc>>2]=5,t[Mc+4>>2]=0,t[r>>2]=t[Mc>>2],t[r+4>>2]=t[Mc+4>>2],O0(e,4196,r)|0,t[ld>>2]=6,t[ld+4>>2]=0,t[r>>2]=t[ld>>2],t[r+4>>2]=t[ld+4>>2],O0(e,4206,r)|0,t[od>>2]=7,t[od+4>>2]=0,t[r>>2]=t[od>>2],t[r+4>>2]=t[od+4>>2],O0(e,4217,r)|0,t[zf>>2]=3,t[zf+4>>2]=0,t[r>>2]=t[zf>>2],t[r+4>>2]=t[zf+4>>2],Jl(e,4235,r)|0,t[ud>>2]=1,t[ud+4>>2]=0,t[r>>2]=t[ud>>2],t[r+4>>2]=t[ud+4>>2],za(e,4251,r)|0,t[$a>>2]=4,t[$a+4>>2]=0,t[r>>2]=t[$a>>2],t[r+4>>2]=t[$a+4>>2],Jl(e,4263,r)|0,t[si>>2]=5,t[si+4>>2]=0,t[r>>2]=t[si>>2],t[r+4>>2]=t[si+4>>2],Jl(e,4279,r)|0,t[id>>2]=6,t[id+4>>2]=0,t[r>>2]=t[id>>2],t[r+4>>2]=t[id+4>>2],Jl(e,4293,r)|0,t[rd>>2]=7,t[rd+4>>2]=0,t[r>>2]=t[rd>>2],t[r+4>>2]=t[rd+4>>2],Jl(e,4306,r)|0,t[nd>>2]=8,t[nd+4>>2]=0,t[r>>2]=t[nd>>2],t[r+4>>2]=t[nd+4>>2],Jl(e,4323,r)|0,t[kc>>2]=9,t[kc+4>>2]=0,t[r>>2]=t[kc>>2],t[r+4>>2]=t[kc+4>>2],Jl(e,4335,r)|0,t[Oc>>2]=2,t[Oc+4>>2]=0,t[r>>2]=t[Oc>>2],t[r+4>>2]=t[Oc+4>>2],za(e,4353,r)|0,t[td>>2]=12,t[td+4>>2]=0,t[r>>2]=t[td>>2],t[r+4>>2]=t[td+4>>2],no(e,4363,r)|0,t[Za>>2]=1,t[Za+4>>2]=0,t[r>>2]=t[Za>>2],t[r+4>>2]=t[Za+4>>2],ul(e,4376,r)|0,t[ed>>2]=2,t[ed+4>>2]=0,t[r>>2]=t[ed>>2],t[r+4>>2]=t[ed+4>>2],ul(e,4388,r)|0,t[$2>>2]=13,t[$2+4>>2]=0,t[r>>2]=t[$2>>2],t[r+4>>2]=t[$2+4>>2],no(e,4402,r)|0,t[Xs>>2]=14,t[Xs+4>>2]=0,t[r>>2]=t[Xs>>2],t[r+4>>2]=t[Xs+4>>2],no(e,4411,r)|0,t[rs>>2]=15,t[rs+4>>2]=0,t[r>>2]=t[rs>>2],t[r+4>>2]=t[rs+4>>2],no(e,4421,r)|0,t[ns>>2]=16,t[ns+4>>2]=0,t[r>>2]=t[ns>>2],t[r+4>>2]=t[ns+4>>2],no(e,4433,r)|0,t[ts>>2]=17,t[ts+4>>2]=0,t[r>>2]=t[ts>>2],t[r+4>>2]=t[ts+4>>2],no(e,4446,r)|0,t[Zi>>2]=18,t[Zi+4>>2]=0,t[r>>2]=t[Zi>>2],t[r+4>>2]=t[Zi+4>>2],no(e,4458,r)|0,t[On>>2]=3,t[On+4>>2]=0,t[r>>2]=t[On>>2],t[r+4>>2]=t[On+4>>2],ul(e,4471,r)|0,t[kr>>2]=1,t[kr+4>>2]=0,t[r>>2]=t[kr>>2],t[r+4>>2]=t[kr+4>>2],dc(e,4486,r)|0,t[hr>>2]=10,t[hr+4>>2]=0,t[r>>2]=t[hr>>2],t[r+4>>2]=t[hr+4>>2],Jl(e,4496,r)|0,t[mn>>2]=11,t[mn+4>>2]=0,t[r>>2]=t[mn>>2],t[r+4>>2]=t[mn+4>>2],Jl(e,4508,r)|0,t[Nn>>2]=3,t[Nn+4>>2]=0,t[r>>2]=t[Nn>>2],t[r+4>>2]=t[Nn+4>>2],za(e,4519,r)|0,t[Lr>>2]=4,t[Lr+4>>2]=0,t[r>>2]=t[Lr>>2],t[r+4>>2]=t[Lr+4>>2],Od(e,4530,r)|0,t[Qt>>2]=19,t[Qt+4>>2]=0,t[r>>2]=t[Qt>>2],t[r+4>>2]=t[Qt+4>>2],_h(e,4542,r)|0,t[Fe>>2]=12,t[Fe+4>>2]=0,t[r>>2]=t[Fe>>2],t[r+4>>2]=t[Fe+4>>2],_f(e,4554,r)|0,t[Re>>2]=13,t[Re+4>>2]=0,t[r>>2]=t[Re>>2],t[r+4>>2]=t[Re+4>>2],Ef(e,4568,r)|0,t[st>>2]=2,t[st+4>>2]=0,t[r>>2]=t[st>>2],t[r+4>>2]=t[st+4>>2],Qc(e,4578,r)|0,t[We>>2]=20,t[We+4>>2]=0,t[r>>2]=t[We>>2],t[r+4>>2]=t[We+4>>2],xl(e,4587,r)|0,t[Qe>>2]=22,t[Qe+4>>2]=0,t[r>>2]=t[Qe>>2],t[r+4>>2]=t[Qe+4>>2],js(e,4602,r)|0,t[ve>>2]=23,t[ve+4>>2]=0,t[r>>2]=t[ve>>2],t[r+4>>2]=t[ve+4>>2],js(e,4619,r)|0,t[Ee>>2]=14,t[Ee+4>>2]=0,t[r>>2]=t[Ee>>2],t[r+4>>2]=t[Ee+4>>2],Jc(e,4629,r)|0,t[Pe>>2]=1,t[Pe+4>>2]=0,t[r>>2]=t[Pe>>2],t[r+4>>2]=t[Pe+4>>2],ca(e,4637,r)|0,t[K>>2]=4,t[K+4>>2]=0,t[r>>2]=t[K>>2],t[r+4>>2]=t[K+4>>2],ul(e,4653,r)|0,t[P>>2]=5,t[P+4>>2]=0,t[r>>2]=t[P>>2],t[r+4>>2]=t[P+4>>2],ul(e,4669,r)|0,t[O>>2]=6,t[O+4>>2]=0,t[r>>2]=t[O>>2],t[r+4>>2]=t[O+4>>2],ul(e,4686,r)|0,t[M>>2]=7,t[M+4>>2]=0,t[r>>2]=t[M>>2],t[r+4>>2]=t[M+4>>2],ul(e,4701,r)|0,t[S>>2]=8,t[S+4>>2]=0,t[r>>2]=t[S>>2],t[r+4>>2]=t[S+4>>2],ul(e,4719,r)|0,t[D>>2]=9,t[D+4>>2]=0,t[r>>2]=t[D>>2],t[r+4>>2]=t[D+4>>2],ul(e,4736,r)|0,t[h>>2]=21,t[h+4>>2]=0,t[r>>2]=t[h>>2],t[r+4>>2]=t[h+4>>2],c2(e,4754,r)|0,t[s>>2]=2,t[s+4>>2]=0,t[r>>2]=t[s>>2],t[r+4>>2]=t[s+4>>2],dc(e,4772,r)|0,t[l>>2]=3,t[l+4>>2]=0,t[r>>2]=t[l>>2],t[r+4>>2]=t[l+4>>2],dc(e,4790,r)|0,t[u>>2]=4,t[u+4>>2]=0,t[r>>2]=t[u>>2],t[r+4>>2]=t[u+4>>2],dc(e,4808,r)|0,m=n}function gf(e,n){e=e|0,n=n|0;var r=0;r=Ja()|0,t[e>>2]=r,jo(r,n),Q2(t[e>>2]|0)}function Xc(e,n,r){return e=e|0,n=n|0,r=r|0,Ot(e,Or(n)|0,r,0),e|0}function gh(e,n,r){return e=e|0,n=n|0,r=r|0,c(e,Or(n)|0,r,0),e|0}function vm(e,n,r){return e=e|0,n=n|0,r=r|0,cE(e,Or(n)|0,r,0),e|0}function js(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],rE(e,n,l),m=u,e|0}function fa(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],bl(e,n,l),m=u,e|0}function Ji(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],d(e,n,l),m=u,e|0}function O0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Tv(e,n,l),m=u,e|0}function t0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],sy(e,n,l),m=u,e|0}function Jl(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],W2(e,n,l),m=u,e|0}function za(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],H2(e,n,l),m=u,e|0}function no(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],k0(e,n,l),m=u,e|0}function ul(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Ep(e,n,l),m=u,e|0}function dc(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Hm(e,n,l),m=u,e|0}function Od(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],n0(e,n,l),m=u,e|0}function _h(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],x2(e,n,l),m=u,e|0}function _f(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Fm(e,n,l),m=u,e|0}function Ef(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Zd(e,n,l),m=u,e|0}function Qc(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],p1(e,n,l),m=u,e|0}function xl(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Ga(e,n,l),m=u,e|0}function Jc(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Id(e,n,l),m=u,e|0}function ca(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Nd(e,n,l),m=u,e|0}function c2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],d2(e,n,l),m=u,e|0}function d2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],da(e,r,l,1),m=u}function Or(e){return e=e|0,e|0}function da(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=kd()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Zc(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,p2(s,u)|0,u),m=l}function kd(){var e=0,n=0;if(p[7616]|0||(ol(9136),Ht(24,9136,he|0)|0,n=7616,t[n>>2]=1,t[n+4>>2]=0),!(rr(9136)|0)){e=9136,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));ol(9136)}return 9136}function Zc(e){return e=e|0,0}function p2(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=kd()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Df(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(wf(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function vi(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0;h=m,m=m+32|0,K=h+24|0,P=h+20|0,S=h+16|0,O=h+12|0,M=h+8|0,D=h+4|0,Pe=h,t[P>>2]=n,t[S>>2]=r,t[O>>2]=u,t[M>>2]=l,t[D>>2]=s,s=e+28|0,t[Pe>>2]=t[s>>2],t[K>>2]=t[Pe>>2],Md(e+24|0,K,P,O,M,S,D)|0,t[s>>2]=t[t[s>>2]>>2],m=h}function Md(e,n,r,u,l,s,h){return e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,h=h|0,e=mm(n)|0,n=cn(24)|0,h2(n+4|0,t[r>>2]|0,t[u>>2]|0,t[l>>2]|0,t[s>>2]|0,t[h>>2]|0),t[n>>2]=t[e>>2],t[e>>2]=n,n|0}function mm(e){return e=e|0,t[e>>2]|0}function h2(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,t[e>>2]=n,t[e+4>>2]=r,t[e+8>>2]=u,t[e+12>>2]=l,t[e+16>>2]=s}function dn(e,n){return e=e|0,n=n|0,n|e|0}function Df(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function wf(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=ym(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Sf(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Df(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Eh(e,D),gm(D),m=M;return}}function ym(e){return e=e|0,357913941}function Sf(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Eh(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function gm(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function ol(e){e=e|0,Bo(e)}function $c(e){e=e|0,Un(e+24|0)}function rr(e){return e=e|0,t[e>>2]|0}function Un(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Bo(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,3,n,zn()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function dr(){return 9228}function zn(){return 1140}function ll(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=yo(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=pc(n,u)|0,m=r,n|0}function Pn(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,t[e>>2]=n,t[e+4>>2]=r,t[e+8>>2]=u,t[e+12>>2]=l,t[e+16>>2]=s}function yo(e){return e=e|0,(t[(kd()|0)+24>>2]|0)+(e*12|0)|0}function pc(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;return l=m,m=m+48|0,u=l,r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),N1[r&31](u,e),u=ro(u)|0,m=l,u|0}function ro(e){e=e|0;var n=0,r=0,u=0,l=0;return l=m,m=m+32|0,n=l+12|0,r=l,u=Ou(qa()|0)|0,u?(Zl(n,u),Tf(r,n),hc(e,r),e=Es(n)|0):e=vc(e)|0,m=l,e|0}function qa(){var e=0;return p[7632]|0||(xf(9184),Ht(25,9184,he|0)|0,e=7632,t[e>>2]=1,t[e+4>>2]=0),9184}function Ou(e){return e=e|0,t[e+36>>2]|0}function Zl(e,n){e=e|0,n=n|0,t[e>>2]=n,t[e+4>>2]=e,t[e+8>>2]=0}function Tf(e,n){e=e|0,n=n|0,t[e>>2]=t[n>>2],t[e+4>>2]=t[n+4>>2],t[e+8>>2]=0}function hc(e,n){e=e|0,n=n|0,io(n,e,e+8|0,e+16|0,e+24|0,e+32|0,e+40|0)|0}function Es(e){return e=e|0,t[(t[e+4>>2]|0)+8>>2]|0}function vc(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0;S=m,m=m+16|0,r=S+4|0,u=S,l=Sa(8)|0,s=l,h=cn(48)|0,D=h,n=D+48|0;do t[D>>2]=t[e>>2],D=D+4|0,e=e+4|0;while((D|0)<(n|0));return n=s+4|0,t[n>>2]=h,D=cn(8)|0,h=t[n>>2]|0,t[u>>2]=0,t[r>>2]=t[u>>2],Dh(D,h,r),t[l>>2]=D,m=S,s|0}function Dh(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,r=cn(16)|0,t[r+4>>2]=0,t[r+8>>2]=0,t[r>>2]=1092,t[r+12>>2]=n,t[e+4>>2]=r}function an(e){e=e|0,Pv(e),yt(e)}function $l(e){e=e|0,e=t[e+12>>2]|0,e|0&&yt(e)}function go(e){e=e|0,yt(e)}function io(e,n,r,u,l,s,h){return e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,h=h|0,s=Hi(t[e>>2]|0,n,r,u,l,s,h)|0,h=e+4|0,t[(t[h>>2]|0)+8>>2]=s,t[(t[h>>2]|0)+8>>2]|0}function Hi(e,n,r,u,l,s,h){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,h=h|0;var D=0,S=0;return D=m,m=m+16|0,S=D,Ta(S),e=vo(e)|0,h=zr(e,+U[n>>3],+U[r>>3],+U[u>>3],+U[l>>3],+U[s>>3],+U[h>>3])|0,Ca(S),m=D,h|0}function zr(e,n,r,u,l,s,h){e=e|0,n=+n,r=+r,u=+u,l=+l,s=+s,h=+h;var D=0;return D=mo(Cf()|0)|0,n=+Cl(n),r=+Cl(r),u=+Cl(u),l=+Cl(l),s=+Cl(s),f0(0,D|0,e|0,+n,+r,+u,+l,+s,+ +Cl(h))|0}function Cf(){var e=0;return p[7624]|0||(_m(9172),e=7624,t[e>>2]=1,t[e+4>>2]=0),9172}function _m(e){e=e|0,nl(e,Al()|0,6)}function Al(){return 1112}function xf(e){e=e|0,Ha(e)}function Af(e){e=e|0,v2(e+24|0),m2(e+16|0)}function v2(e){e=e|0,e1(e)}function m2(e){e=e|0,mc(e)}function mc(e){e=e|0;var n=0,r=0;if(n=t[e>>2]|0,n|0)do r=n,n=t[n>>2]|0,yt(r);while((n|0)!=0);t[e>>2]=0}function e1(e){e=e|0;var n=0,r=0;if(n=t[e>>2]|0,n|0)do r=n,n=t[n>>2]|0,yt(r);while((n|0)!=0);t[e>>2]=0}function Ha(e){e=e|0;var n=0;t[e+16>>2]=0,t[e+20>>2]=0,n=e+24|0,t[n>>2]=0,t[e+28>>2]=n,t[e+36>>2]=0,p[e+40>>0]=0,p[e+41>>0]=0}function Nd(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],y2(e,r,l,0),m=u}function y2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=t1()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Rf(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,n1(s,u)|0,u),m=l}function t1(){var e=0,n=0;if(p[7640]|0||(Rl(9232),Ht(26,9232,he|0)|0,n=7640,t[n>>2]=1,t[n+4>>2]=0),!(rr(9232)|0)){e=9232,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Rl(9232)}return 9232}function Rf(e){return e=e|0,0}function n1(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=t1()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Wa(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(r1(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Wa(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function r1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Ld(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,g2(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Wa(s,u,r),t[S>>2]=(t[S>>2]|0)+12,yc(e,D),i1(D),m=M;return}}function Ld(e){return e=e|0,357913941}function g2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function yc(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function i1(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Rl(e){e=e|0,Fd(e)}function pa(e){e=e|0,wh(e+24|0)}function wh(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Fd(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,1,n,bd()|0,3),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function bd(){return 1144}function Sh(e,n,r,u,l){e=e|0,n=n|0,r=+r,u=+u,l=l|0;var s=0,h=0,D=0,S=0;s=m,m=m+16|0,h=s+8|0,D=s,S=_2(e)|0,e=t[S+4>>2]|0,t[D>>2]=t[S>>2],t[D+4>>2]=e,t[h>>2]=t[D>>2],t[h+4>>2]=t[D+4>>2],Th(n,h,r,u,l),m=s}function _2(e){return e=e|0,(t[(t1()|0)+24>>2]|0)+(e*12|0)|0}function Th(e,n,r,u,l){e=e|0,n=n|0,r=+r,u=+u,l=l|0;var s=0,h=0,D=0,S=0,M=0;M=m,m=m+16|0,h=M+2|0,D=M+1|0,S=M,s=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(s=t[(t[e>>2]|0)+s>>2]|0),Ol(h,r),r=+es(h,r),Ol(D,u),u=+es(D,u),Ds(S,l),S=zs(S,l)|0,Z8[s&1](e,r,u,S),m=M}function Ol(e,n){e=e|0,n=+n}function es(e,n){return e=e|0,n=+n,+ +Ch(n)}function Ds(e,n){e=e|0,n=n|0}function zs(e,n){return e=e|0,n=n|0,Pd(n)|0}function Pd(e){return e=e|0,e|0}function Ch(e){return e=+e,+e}function Id(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Bd(e,r,l,1),m=u}function Bd(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=u1()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=o1(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,xh(s,u)|0,u),m=l}function u1(){var e=0,n=0;if(p[7648]|0||(l1(9268),Ht(27,9268,he|0)|0,n=7648,t[n>>2]=1,t[n+4>>2]=0),!(rr(9268)|0)){e=9268,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));l1(9268)}return 9268}function o1(e){return e=e|0,0}function xh(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=u1()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Ud(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(jd(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Ud(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function jd(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=ws(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Va(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Ud(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Ah(e,D),uu(D),m=M;return}}function ws(e){return e=e|0,357913941}function Va(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Ah(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function uu(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function l1(e){e=e|0,kl(e)}function Rh(e){e=e|0,s1(e+24|0)}function s1(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function kl(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,4,n,Oh()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Oh(){return 1160}function zd(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=kh(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=a1(n,u)|0,m=r,n|0}function kh(e){return e=e|0,(t[(u1()|0)+24>>2]|0)+(e*12|0)|0}function a1(e,n){e=e|0,n=n|0;var r=0;return r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),Ml(Xp[r&31](e)|0)|0}function Ml(e){return e=e|0,e&1|0}function Ga(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ha(e,r,l,0),m=u}function ha(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=qd()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Hd(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Em(s,u)|0,u),m=l}function qd(){var e=0,n=0;if(p[7656]|0||(Lh(9304),Ht(28,9304,he|0)|0,n=7656,t[n>>2]=1,t[n+4>>2]=0),!(rr(9304)|0)){e=9304,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Lh(9304)}return 9304}function Hd(e){return e=e|0,0}function Em(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=qd()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Wd(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(Mh(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Wd(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function Mh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Nh(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Vd(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Wd(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Dm(e,D),wm(D),m=M;return}}function Nh(e){return e=e|0,357913941}function Vd(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Dm(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function wm(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Lh(e){e=e|0,f1(e)}function Sm(e){e=e|0,Gd(e+24|0)}function Gd(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function f1(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,5,n,c1()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function c1(){return 1164}function d1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=va(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Yd(n,l,r),m=u}function va(e){return e=e|0,(t[(qd()|0)+24>>2]|0)+(e*12|0)|0}function Yd(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),qs(l,r),r=Hs(l,r)|0,N1[u&31](e,r),Ws(l),m=s}function qs(e,n){e=e|0,n=n|0,Kd(e,n)}function Hs(e,n){return e=e|0,n=n|0,e|0}function Ws(e){e=e|0,ia(e)}function Kd(e,n){e=e|0,n=n|0,ma(e,n)}function ma(e,n){e=e|0,n=n|0,t[e>>2]=n}function p1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],E2(e,r,l,0),m=u}function E2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=gc()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Xd(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,_o(s,u)|0,u),m=l}function gc(){var e=0,n=0;if(p[7664]|0||(Uh(9340),Ht(29,9340,he|0)|0,n=7664,t[n>>2]=1,t[n+4>>2]=0),!(rr(9340)|0)){e=9340,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Uh(9340)}return 9340}function Xd(e){return e=e|0,0}function _o(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=gc()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Fh(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(bh(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Fh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function bh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Ph(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Ih(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Fh(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Tm(e,D),Bh(D),m=M;return}}function Ph(e){return e=e|0,357913941}function Ih(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Tm(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Bh(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Uh(e){e=e|0,jh(e)}function h1(e){e=e|0,Qd(e+24|0)}function Qd(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function jh(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,4,n,Jd()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Jd(){return 1180}function zh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=Cm(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],r=xm(n,l,r)|0,m=u,r|0}function Cm(e){return e=e|0,(t[(gc()|0)+24>>2]|0)+(e*12|0)|0}function xm(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;return s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Of(l,r),l=kf(l,r)|0,l=D2(ZE[u&15](e,l)|0)|0,m=s,l|0}function Of(e,n){e=e|0,n=n|0}function kf(e,n){return e=e|0,n=n|0,Am(n)|0}function D2(e){return e=e|0,e|0}function Am(e){return e=e|0,e|0}function Zd(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],w2(e,r,l,0),m=u}function w2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=$d()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=qh(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,ep(s,u)|0,u),m=l}function $d(){var e=0,n=0;if(p[7672]|0||(Vh(9376),Ht(30,9376,he|0)|0,n=7672,t[n>>2]=1,t[n+4>>2]=0),!(rr(9376)|0)){e=9376,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Vh(9376)}return 9376}function qh(e){return e=e|0,0}function ep(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=$d()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Hh(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(Wh(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Hh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function Wh(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=tp(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Rm(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Hh(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Om(e,D),km(D),m=M;return}}function tp(e){return e=e|0,357913941}function Rm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Om(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function km(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Vh(e){e=e|0,np(e)}function v1(e){e=e|0,Mm(e+24|0)}function Mm(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function np(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,5,n,rp()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function rp(){return 1196}function Nm(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=Lm(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=Gh(n,u)|0,m=r,n|0}function Lm(e){return e=e|0,(t[($d()|0)+24>>2]|0)+(e*12|0)|0}function Gh(e,n){e=e|0,n=n|0;var r=0;return r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),D2(Xp[r&31](e)|0)|0}function Fm(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],bm(e,r,l,1),m=u}function bm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=ip()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=up(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,ya(s,u)|0,u),m=l}function ip(){var e=0,n=0;if(p[7680]|0||(lp(9412),Ht(31,9412,he|0)|0,n=7680,t[n>>2]=1,t[n+4>>2]=0),!(rr(9412)|0)){e=9412,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));lp(9412)}return 9412}function up(e){return e=e|0,0}function ya(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=ip()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],m1(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(op(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function m1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function op(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Yh(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,S2(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],m1(s,u,r),t[S>>2]=(t[S>>2]|0)+12,y1(e,D),Kh(D),m=M;return}}function Yh(e){return e=e|0,357913941}function S2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function y1(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Kh(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function lp(e){e=e|0,Qh(e)}function Xh(e){e=e|0,sp(e+24|0)}function sp(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Qh(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,6,n,Jh()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Jh(){return 1200}function ap(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=T2(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=C2(n,u)|0,m=r,n|0}function T2(e){return e=e|0,(t[(ip()|0)+24>>2]|0)+(e*12|0)|0}function C2(e,n){e=e|0,n=n|0;var r=0;return r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),z0(Xp[r&31](e)|0)|0}function z0(e){return e=e|0,e|0}function x2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ga(e,r,l,0),m=u}function ga(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Ya()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=A2(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,R2(s,u)|0,u),m=l}function Ya(){var e=0,n=0;if(p[7688]|0||(dp(9448),Ht(32,9448,he|0)|0,n=7688,t[n>>2]=1,t[n+4>>2]=0),!(rr(9448)|0)){e=9448,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));dp(9448)}return 9448}function A2(e){return e=e|0,0}function R2(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Ya()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],fp(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(O2(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function fp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function O2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Zh(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Pm(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],fp(s,u,r),t[S>>2]=(t[S>>2]|0)+12,$h(e,D),cp(D),m=M;return}}function Zh(e){return e=e|0,357913941}function Pm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function $h(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function cp(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function dp(e){e=e|0,Bm(e)}function pp(e){e=e|0,Im(e+24|0)}function Im(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Bm(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,6,n,Eo()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Eo(){return 1204}function k2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=Um(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],sl(n,l,r),m=u}function Um(e){return e=e|0,(t[(Ya()|0)+24>>2]|0)+(e*12|0)|0}function sl(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Jn(l,r),l=Vs(l,r)|0,N1[u&31](e,l),m=s}function Jn(e,n){e=e|0,n=n|0}function Vs(e,n){return e=e|0,n=n|0,al(n)|0}function al(e){return e=e|0,e|0}function n0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ev(e,r,l,0),m=u}function ev(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Gs()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=hp(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,jm(s,u)|0,u),m=l}function Gs(){var e=0,n=0;if(p[7696]|0||(yp(9484),Ht(33,9484,he|0)|0,n=7696,t[n>>2]=1,t[n+4>>2]=0),!(rr(9484)|0)){e=9484,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));yp(9484)}return 9484}function hp(e){return e=e|0,0}function jm(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Gs()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],tv(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(vp(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function tv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function vp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=zm(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,mp(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],tv(s,u,r),t[S>>2]=(t[S>>2]|0)+12,_c(e,D),Ea(D),m=M;return}}function zm(e){return e=e|0,357913941}function mp(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function _c(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Ea(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function yp(e){e=e|0,zu(e)}function M2(e){e=e|0,ku(e+24|0)}function ku(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function zu(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,1,n,gp()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function gp(){return 1212}function _p(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+16|0,s=l+8|0,h=l,D=nv(e)|0,e=t[D+4>>2]|0,t[h>>2]=t[D>>2],t[h+4>>2]=e,t[s>>2]=t[h>>2],t[s+4>>2]=t[h+4>>2],qm(n,s,r,u),m=l}function nv(e){return e=e|0,(t[(Gs()|0)+24>>2]|0)+(e*12|0)|0}function qm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;D=m,m=m+16|0,s=D+1|0,h=D,l=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(l=t[(t[e>>2]|0)+l>>2]|0),Jn(s,r),s=Vs(s,r)|0,Of(h,u),h=kf(h,u)|0,jy[l&15](e,s,h),m=D}function Hm(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Wm(e,r,l,1),m=u}function Wm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=N2()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=rv(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Ec(s,u)|0,u),m=l}function N2(){var e=0,n=0;if(p[7704]|0||(iv(9520),Ht(34,9520,he|0)|0,n=7704,t[n>>2]=1,t[n+4>>2]=0),!(rr(9520)|0)){e=9520,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));iv(9520)}return 9520}function rv(e){return e=e|0,0}function Ec(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=N2()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],g1(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(Vm(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function g1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function Vm(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=L2(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,_1(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],g1(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Nl(e,D),Da(D),m=M;return}}function L2(e){return e=e|0,357913941}function _1(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Nl(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Da(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function iv(e){e=e|0,ov(e)}function Gm(e){e=e|0,uv(e+24|0)}function uv(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function ov(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,1,n,Ym()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Ym(){return 1224}function lv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;return l=m,m=m+16|0,s=l+8|0,h=l,D=wa(e)|0,e=t[D+4>>2]|0,t[h>>2]=t[D>>2],t[h+4>>2]=e,t[s>>2]=t[h>>2],t[s+4>>2]=t[h+4>>2],u=+Cr(n,s,r),m=l,+u}function wa(e){return e=e|0,(t[(N2()|0)+24>>2]|0)+(e*12|0)|0}function Cr(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Ds(l,r),l=zs(l,r)|0,h=+ja(+eS[u&7](e,l)),m=s,+h}function Ep(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],fl(e,r,l,1),m=u}function fl(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=cu()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=E1(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,ki(s,u)|0,u),m=l}function cu(){var e=0,n=0;if(p[7712]|0||(wp(9556),Ht(35,9556,he|0)|0,n=7712,t[n>>2]=1,t[n+4>>2]=0),!(rr(9556)|0)){e=9556,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));wp(9556)}return 9556}function E1(e){return e=e|0,0}function ki(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=cu()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Dp(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(F2(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Dp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function F2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Do(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Ss(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Dp(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Mf(e,D),b2(D),m=M;return}}function Do(e){return e=e|0,357913941}function Ss(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Mf(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function b2(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function wp(e){e=e|0,Sp(e)}function D1(e){e=e|0,w1(e+24|0)}function w1(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Sp(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,5,n,Zn()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Zn(){return 1232}function cl(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=qn(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],r=+q0(n,l),m=u,+r}function qn(e){return e=e|0,(t[(cu()|0)+24>>2]|0)+(e*12|0)|0}function q0(e,n){e=e|0,n=n|0;var r=0;return r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),+ +ja(+$8[r&15](e))}function k0(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],P2(e,r,l,1),m=u}function P2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Ll()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=S1(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Dc(s,u)|0,u),m=l}function Ll(){var e=0,n=0;if(p[7720]|0||(U2(9592),Ht(36,9592,he|0)|0,n=7720,t[n>>2]=1,t[n+4>>2]=0),!(rr(9592)|0)){e=9592,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));U2(9592)}return 9592}function S1(e){return e=e|0,0}function Dc(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Ll()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],wc(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(I2(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function wc(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function I2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Tp(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,M0(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],wc(s,u,r),t[S>>2]=(t[S>>2]|0)+12,fn(e,D),B2(D),m=M;return}}function Tp(e){return e=e|0,357913941}function M0(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function fn(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function B2(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function U2(e){e=e|0,Cc(e)}function Sc(e){e=e|0,Tc(e+24|0)}function Tc(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Cc(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,7,n,T1()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function T1(){return 1276}function Cp(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=Ka(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=Km(n,u)|0,m=r,n|0}function Ka(e){return e=e|0,(t[(Ll()|0)+24>>2]|0)+(e*12|0)|0}function Km(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;return l=m,m=m+16|0,u=l,r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),N1[r&31](u,e),u=xc(u)|0,m=l,u|0}function xc(e){e=e|0;var n=0,r=0,u=0,l=0;return l=m,m=m+32|0,n=l+12|0,r=l,u=Ou(j2()|0)|0,u?(Zl(n,u),Tf(r,n),sv(e,r),e=Es(n)|0):e=C1(e)|0,m=l,e|0}function j2(){var e=0;return p[7736]|0||(Uo(9640),Ht(25,9640,he|0)|0,e=7736,t[e>>2]=1,t[e+4>>2]=0),9640}function sv(e,n){e=e|0,n=n|0,Ac(n,e,e+8|0)|0}function C1(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0;return r=m,m=m+16|0,l=r+4|0,h=r,u=Sa(8)|0,n=u,D=cn(16)|0,t[D>>2]=t[e>>2],t[D+4>>2]=t[e+4>>2],t[D+8>>2]=t[e+8>>2],t[D+12>>2]=t[e+12>>2],s=n+4|0,t[s>>2]=D,e=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],Nf(e,s,l),t[u>>2]=e,m=r,n|0}function Nf(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,r=cn(16)|0,t[r+4>>2]=0,t[r+8>>2]=0,t[r>>2]=1244,t[r+12>>2]=n,t[e+4>>2]=r}function Lf(e){e=e|0,Pv(e),yt(e)}function x1(e){e=e|0,e=t[e+12>>2]|0,e|0&&yt(e)}function Fl(e){e=e|0,yt(e)}function Ac(e,n,r){return e=e|0,n=n|0,r=r|0,n=Ff(t[e>>2]|0,n,r)|0,r=e+4|0,t[(t[r>>2]|0)+8>>2]=n,t[(t[r>>2]|0)+8>>2]|0}function Ff(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;return u=m,m=m+16|0,l=u,Ta(l),e=vo(e)|0,r=Xm(e,t[n>>2]|0,+U[r>>3])|0,Ca(l),m=u,r|0}function Xm(e,n,r){e=e|0,n=n|0,r=+r;var u=0;return u=mo(dl()|0)|0,n=u2(n)|0,Pr(0,u|0,e|0,n|0,+ +Cl(r))|0}function dl(){var e=0;return p[7728]|0||(z2(9628),e=7728,t[e>>2]=1,t[e+4>>2]=0),9628}function z2(e){e=e|0,nl(e,q2()|0,2)}function q2(){return 1264}function Uo(e){e=e|0,Ha(e)}function H2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Qm(e,r,l,1),m=u}function Qm(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=A1()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Jm(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Zm(s,u)|0,u),m=l}function A1(){var e=0,n=0;if(p[7744]|0||(cv(9684),Ht(37,9684,he|0)|0,n=7744,t[n>>2]=1,t[n+4>>2]=0),!(rr(9684)|0)){e=9684,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));cv(9684)}return 9684}function Jm(e){return e=e|0,0}function Zm(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=A1()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],av(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):($m(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function av(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function $m(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=fv(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,ey(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],av(s,u,r),t[S>>2]=(t[S>>2]|0)+12,ty(e,D),ny(D),m=M;return}}function fv(e){return e=e|0,357913941}function ey(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function ty(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function ny(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function cv(e){e=e|0,iy(e)}function ry(e){e=e|0,xp(e+24|0)}function xp(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function iy(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,5,n,bf()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function bf(){return 1280}function dv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=pv(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],r=hv(n,l,r)|0,m=u,r|0}function pv(e){return e=e|0,(t[(A1()|0)+24>>2]|0)+(e*12|0)|0}function hv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return h=m,m=m+32|0,l=h,s=h+16|0,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Ds(s,r),s=zs(s,r)|0,jy[u&15](l,e,s),s=xc(l)|0,m=h,s|0}function W2(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],V2(e,r,l,1),m=u}function V2(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Ap()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=vv(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,G2(s,u)|0,u),m=l}function Ap(){var e=0,n=0;if(p[7752]|0||(Ev(9720),Ht(38,9720,he|0)|0,n=7752,t[n>>2]=1,t[n+4>>2]=0),!(rr(9720)|0)){e=9720,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Ev(9720)}return 9720}function vv(e){return e=e|0,0}function G2(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Ap()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],mv(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(yv(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function mv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function yv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Rp(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,gv(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],mv(s,u,r),t[S>>2]=(t[S>>2]|0)+12,_v(e,D),uy(D),m=M;return}}function Rp(e){return e=e|0,357913941}function gv(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function _v(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function uy(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Ev(e){e=e|0,Dv(e)}function oy(e){e=e|0,Y2(e+24|0)}function Y2(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Dv(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,8,n,Op()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Op(){return 1288}function ly(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;return r=m,m=m+16|0,u=r+8|0,l=r,s=r0(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],n=kp(n,u)|0,m=r,n|0}function r0(e){return e=e|0,(t[(Ap()|0)+24>>2]|0)+(e*12|0)|0}function kp(e,n){e=e|0,n=n|0;var r=0;return r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),s2(Xp[r&31](e)|0)|0}function sy(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ay(e,r,l,0),m=u}function ay(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Mp()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=Xa(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Np(s,u)|0,u),m=l}function Mp(){var e=0,n=0;if(p[7760]|0||(bp(9756),Ht(39,9756,he|0)|0,n=7760,t[n>>2]=1,t[n+4>>2]=0),!(rr(9756)|0)){e=9756,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));bp(9756)}return 9756}function Xa(e){return e=e|0,0}function Np(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Mp()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],Lp(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(Fp(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function Lp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function Fp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=fy(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,cy(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],Lp(s,u,r),t[S>>2]=(t[S>>2]|0)+12,wv(e,D),Pf(D),m=M;return}}function fy(e){return e=e|0,357913941}function cy(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function wv(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Pf(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function bp(e){e=e|0,py(e)}function Sv(e){e=e|0,dy(e+24|0)}function dy(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function py(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,8,n,Pp()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Pp(){return 1292}function Ip(e,n,r){e=e|0,n=n|0,r=+r;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=hy(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],vy(n,l,r),m=u}function hy(e){return e=e|0,(t[(Mp()|0)+24>>2]|0)+(e*12|0)|0}function vy(e,n,r){e=e|0,n=n|0,r=+r;var u=0,l=0,s=0;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Ol(l,r),r=+es(l,r),Q8[u&31](e,r),m=s}function Tv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Bp(e,r,l,0),m=u}function Bp(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Up()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=K2(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,my(s,u)|0,u),m=l}function Up(){var e=0,n=0;if(p[7768]|0||(jp(9792),Ht(40,9792,he|0)|0,n=7768,t[n>>2]=1,t[n+4>>2]=0),!(rr(9792)|0)){e=9792,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));jp(9792)}return 9792}function K2(e){return e=e|0,0}function my(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Up()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],R1(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(yy(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function R1(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function yy(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Cv(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,xv(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],R1(s,u,r),t[S>>2]=(t[S>>2]|0)+12,gy(e,D),If(D),m=M;return}}function Cv(e){return e=e|0,357913941}function xv(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function gy(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function If(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function jp(e){e=e|0,Ey(e)}function Av(e){e=e|0,_y(e+24|0)}function _y(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Ey(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,1,n,zp()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function zp(){return 1300}function Dy(e,n,r,u){e=e|0,n=n|0,r=r|0,u=+u;var l=0,s=0,h=0,D=0;l=m,m=m+16|0,s=l+8|0,h=l,D=Ys(e)|0,e=t[D+4>>2]|0,t[h>>2]=t[D>>2],t[h+4>>2]=e,t[s>>2]=t[h>>2],t[s+4>>2]=t[h+4>>2],wy(n,s,r,u),m=l}function Ys(e){return e=e|0,(t[(Up()|0)+24>>2]|0)+(e*12|0)|0}function wy(e,n,r,u){e=e|0,n=n|0,r=r|0,u=+u;var l=0,s=0,h=0,D=0;D=m,m=m+16|0,s=D+1|0,h=D,l=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(l=t[(t[e>>2]|0)+l>>2]|0),Ds(s,r),s=zs(s,r)|0,Ol(h,u),u=+es(h,u),iS[l&15](e,s,u),m=D}function d(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],v(e,r,l,0),m=u}function v(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=x()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=b(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,H(s,u)|0,u),m=l}function x(){var e=0,n=0;if(p[7776]|0||(Rt(9828),Ht(41,9828,he|0)|0,n=7776,t[n>>2]=1,t[n+4>>2]=0),!(rr(9828)|0)){e=9828,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Rt(9828)}return 9828}function b(e){return e=e|0,0}function H(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=x()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],ee(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(de(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function ee(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function de(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=ye(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,be(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],ee(s,u,r),t[S>>2]=(t[S>>2]|0)+12,gt(e,D),Dt(D),m=M;return}}function ye(e){return e=e|0,357913941}function be(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function gt(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Dt(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Rt(e){e=e|0,$n(e)}function rn(e){e=e|0,Rn(e+24|0)}function Rn(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function $n(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,7,n,Nr()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Nr(){return 1312}function ir(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=Zr(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ui(n,l,r),m=u}function Zr(e){return e=e|0,(t[(x()|0)+24>>2]|0)+(e*12|0)|0}function ui(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Ds(l,r),l=zs(l,r)|0,N1[u&31](e,l),m=s}function bl(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],Wi(e,r,l,0),m=u}function Wi(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=uo()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=i0(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,Ts(s,u)|0,u),m=l}function uo(){var e=0,n=0;if(p[7784]|0||(r_(9864),Ht(42,9864,he|0)|0,n=7784,t[n>>2]=1,t[n+4>>2]=0),!(rr(9864)|0)){e=9864,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));r_(9864)}return 9864}function i0(e){return e=e|0,0}function Ts(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=uo()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],wo(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(Rv(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function wo(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function Rv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=X4(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Sy(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],wo(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Ty(e,D),Qa(D),m=M;return}}function X4(e){return e=e|0,357913941}function Sy(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Ty(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Qa(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function r_(e){e=e|0,Z4(e)}function Q4(e){e=e|0,J4(e+24|0)}function J4(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function Z4(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,8,n,$4()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function $4(){return 1320}function Cy(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=eE(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],tE(n,l,r),m=u}function eE(e){return e=e|0,(t[(uo()|0)+24>>2]|0)+(e*12|0)|0}function tE(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),xy(l,r),l=i_(l,r)|0,N1[u&31](e,l),m=s}function xy(e,n){e=e|0,n=n|0}function i_(e,n){return e=e|0,n=n|0,nE(n)|0}function nE(e){return e=e|0,e|0}function rE(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],u_(e,r,l,0),m=u}function u_(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=Bf()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=o_(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,iE(s,u)|0,u),m=l}function Bf(){var e=0,n=0;if(p[7792]|0||(Oy(9900),Ht(43,9900,he|0)|0,n=7792,t[n>>2]=1,t[n+4>>2]=0),!(rr(9900)|0)){e=9900,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Oy(9900)}return 9900}function o_(e){return e=e|0,0}function iE(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=Bf()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],qp(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(uE(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function qp(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function uE(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=Ov(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,Ay(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],qp(s,u,r),t[S>>2]=(t[S>>2]|0)+12,Ry(e,D),oE(D),m=M;return}}function Ov(e){return e=e|0,357913941}function Ay(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function Ry(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function oE(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function Oy(e){e=e|0,l_(e)}function lE(e){e=e|0,sE(e+24|0)}function sE(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function l_(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,22,n,aE()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function aE(){return 1344}function fE(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0;r=m,m=m+16|0,u=r+8|0,l=r,s=s_(e)|0,e=t[s+4>>2]|0,t[l>>2]=t[s>>2],t[l+4>>2]=e,t[u>>2]=t[l>>2],t[u+4>>2]=t[l+4>>2],kv(n,u),m=r}function s_(e){return e=e|0,(t[(Bf()|0)+24>>2]|0)+(e*12|0)|0}function kv(e,n){e=e|0,n=n|0;var r=0;r=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(r=t[(t[e>>2]|0)+r>>2]|0),M1[r&127](e)}function cE(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=ky()|0,e=dE(r)|0,vi(s,n,l,e,pE(r,u)|0,u)}function ky(){var e=0,n=0;if(p[7800]|0||(Ny(9936),Ht(44,9936,he|0)|0,n=7800,t[n>>2]=1,t[n+4>>2]=0),!(rr(9936)|0)){e=9936,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));Ny(9936)}return 9936}function dE(e){return e=e|0,e|0}function pE(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=ky()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(My(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(a_(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function My(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function a_(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=f_(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,c_(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,My(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,d_(e,l),p_(l),m=D;return}}function f_(e){return e=e|0,536870911}function c_(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function d_(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function p_(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function Ny(e){e=e|0,v_(e)}function h_(e){e=e|0,hE(e+24|0)}function hE(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function v_(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,23,n,Eo()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function vE(e,n){e=e|0,n=n|0,a(t[(mE(e)|0)>>2]|0,n)}function mE(e){return e=e|0,(t[(ky()|0)+24>>2]|0)+(e<<3)|0}function a(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,Jn(u,n),n=Vs(u,n)|0,M1[e&127](n),m=r}function c(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=_()|0,e=T(r)|0,vi(s,n,l,e,R(r,u)|0,u)}function _(){var e=0,n=0;if(p[7808]|0||(pt(9972),Ht(45,9972,he|0)|0,n=7808,t[n>>2]=1,t[n+4>>2]=0),!(rr(9972)|0)){e=9972,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));pt(9972)}return 9972}function T(e){return e=e|0,e|0}function R(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=_()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(j(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(V(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function j(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function V(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=te(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,oe(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,j(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,Ie(e,l),Ye(l),m=D;return}}function te(e){return e=e|0,536870911}function oe(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function Ie(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Ye(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function pt(e){e=e|0,zt(e)}function Nt(e){e=e|0,Vt(e+24|0)}function Vt(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function zt(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,9,n,vn()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function vn(){return 1348}function xr(e,n){return e=e|0,n=n|0,wi(t[($r(e)|0)>>2]|0,n)|0}function $r(e){return e=e|0,(t[(_()|0)+24>>2]|0)+(e<<3)|0}function wi(e,n){e=e|0,n=n|0;var r=0,u=0;return r=m,m=m+16|0,u=r,N0(u,n),n=Vi(u,n)|0,n=D2(Xp[e&31](n)|0)|0,m=r,n|0}function N0(e,n){e=e|0,n=n|0}function Vi(e,n){return e=e|0,n=n|0,it(n)|0}function it(e){return e=e|0,e|0}function Ot(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=Je()|0,e=Bt(r)|0,vi(s,n,l,e,Mn(r,u)|0,u)}function Je(){var e=0,n=0;if(p[7816]|0||(qr(10008),Ht(46,10008,he|0)|0,n=7816,t[n>>2]=1,t[n+4>>2]=0),!(rr(10008)|0)){e=10008,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));qr(10008)}return 10008}function Bt(e){return e=e|0,e|0}function Mn(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=Je()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(pn(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(Pi(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function pn(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function Pi(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=oi(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,qu(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,pn(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,ar(e,l),ou(l),m=D;return}}function oi(e){return e=e|0,536870911}function qu(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function ar(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function ou(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function qr(e){e=e|0,H0(e)}function _u(e){e=e|0,_0(e+24|0)}function _0(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function H0(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,15,n,rp()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Cs(e){return e=e|0,pl(t[(Hu(e)|0)>>2]|0)|0}function Hu(e){return e=e|0,(t[(Je()|0)+24>>2]|0)+(e<<3)|0}function pl(e){return e=e|0,D2(N_[e&7]()|0)|0}function Ja(){var e=0;return p[7832]|0||(y_(10052),Ht(25,10052,he|0)|0,e=7832,t[e>>2]=1,t[e+4>>2]=0),10052}function jo(e,n){e=e|0,n=n|0,t[e>>2]=xs()|0,t[e+4>>2]=X2()|0,t[e+12>>2]=n,t[e+8>>2]=Uf()|0,t[e+32>>2]=2}function xs(){return 11709}function X2(){return 1188}function Uf(){return O1()|0}function Rc(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(zo(r),yt(r)):n|0&&(ms(n),yt(n))}function Pl(e,n){return e=e|0,n=n|0,n&e|0}function zo(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function O1(){var e=0;return p[7824]|0||(t[2511]=m_()|0,t[2512]=0,e=7824,t[e>>2]=1,t[e+4>>2]=0),10044}function m_(){return 0}function y_(e){e=e|0,Ha(e)}function yE(e){e=e|0;var n=0,r=0,u=0,l=0,s=0;n=m,m=m+32|0,r=n+24|0,s=n+16|0,l=n+8|0,u=n,g_(e,4827),gE(e,4834,3)|0,_E(e,3682,47)|0,t[s>>2]=9,t[s+4>>2]=0,t[r>>2]=t[s>>2],t[r+4>>2]=t[s+4>>2],Ly(e,4841,r)|0,t[l>>2]=1,t[l+4>>2]=0,t[r>>2]=t[l>>2],t[r+4>>2]=t[l+4>>2],__(e,4871,r)|0,t[u>>2]=10,t[u+4>>2]=0,t[r>>2]=t[u>>2],t[r+4>>2]=t[u+4>>2],EE(e,4891,r)|0,m=n}function g_(e,n){e=e|0,n=n|0;var r=0;r=Qk()|0,t[e>>2]=r,Jk(r,n),Q2(t[e>>2]|0)}function gE(e,n,r){return e=e|0,n=n|0,r=r|0,Fk(e,Or(n)|0,r,0),e|0}function _E(e,n,r){return e=e|0,n=n|0,r=r|0,_k(e,Or(n)|0,r,0),e|0}function Ly(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],ek(e,n,l),m=u,e|0}function __(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],bO(e,n,l),m=u,e|0}function EE(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=t[r+4>>2]|0,t[s>>2]=t[r>>2],t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],DE(e,n,l),m=u,e|0}function DE(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],wE(e,r,l,1),m=u}function wE(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=SE()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=DO(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,wO(s,u)|0,u),m=l}function SE(){var e=0,n=0;if(p[7840]|0||(L3(10100),Ht(48,10100,he|0)|0,n=7840,t[n>>2]=1,t[n+4>>2]=0),!(rr(10100)|0)){e=10100,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));L3(10100)}return 10100}function DO(e){return e=e|0,0}function wO(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=SE()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],N3(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(SO(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function N3(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function SO(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=TO(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,CO(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],N3(s,u,r),t[S>>2]=(t[S>>2]|0)+12,xO(e,D),AO(D),m=M;return}}function TO(e){return e=e|0,357913941}function CO(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function xO(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function AO(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function L3(e){e=e|0,kO(e)}function RO(e){e=e|0,OO(e+24|0)}function OO(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function kO(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,6,n,MO()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function MO(){return 1364}function NO(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;return u=m,m=m+16|0,l=u+8|0,s=u,h=LO(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],r=FO(n,l,r)|0,m=u,r|0}function LO(e){return e=e|0,(t[(SE()|0)+24>>2]|0)+(e*12|0)|0}function FO(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;return s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),Ds(l,r),l=zs(l,r)|0,l=Ml(ZE[u&15](e,l)|0)|0,m=s,l|0}function bO(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],PO(e,r,l,0),m=u}function PO(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=TE()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=IO(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,BO(s,u)|0,u),m=l}function TE(){var e=0,n=0;if(p[7848]|0||(b3(10136),Ht(49,10136,he|0)|0,n=7848,t[n>>2]=1,t[n+4>>2]=0),!(rr(10136)|0)){e=10136,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));b3(10136)}return 10136}function IO(e){return e=e|0,0}function BO(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=TE()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],F3(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(UO(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function F3(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function UO(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=jO(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,zO(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],F3(s,u,r),t[S>>2]=(t[S>>2]|0)+12,qO(e,D),HO(D),m=M;return}}function jO(e){return e=e|0,357913941}function zO(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function qO(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function HO(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function b3(e){e=e|0,GO(e)}function WO(e){e=e|0,VO(e+24|0)}function VO(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function GO(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,9,n,YO()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function YO(){return 1372}function KO(e,n,r){e=e|0,n=n|0,r=+r;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,l=u+8|0,s=u,h=XO(e)|0,e=t[h+4>>2]|0,t[s>>2]=t[h>>2],t[s+4>>2]=e,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],QO(n,l,r),m=u}function XO(e){return e=e|0,(t[(TE()|0)+24>>2]|0)+(e*12|0)|0}function QO(e,n,r){e=e|0,n=n|0,r=+r;var u=0,l=0,s=0,h=St;s=m,m=m+16|0,l=s,u=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(u=t[(t[e>>2]|0)+u>>2]|0),JO(l,r),h=w(ZO(l,r)),X8[u&1](e,h),m=s}function JO(e,n){e=e|0,n=+n}function ZO(e,n){return e=e|0,n=+n,w($O(n))}function $O(e){return e=+e,w(e)}function ek(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,l=u+8|0,s=u,D=t[r>>2]|0,h=t[r+4>>2]|0,r=Or(n)|0,t[s>>2]=D,t[s+4>>2]=h,t[l>>2]=t[s>>2],t[l+4>>2]=t[s+4>>2],tk(e,r,l,0),m=u}function tk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0,S=0,M=0,O=0;l=m,m=m+32|0,s=l+16|0,O=l+8|0,D=l,M=t[r>>2]|0,S=t[r+4>>2]|0,h=t[e>>2]|0,e=CE()|0,t[O>>2]=M,t[O+4>>2]=S,t[s>>2]=t[O>>2],t[s+4>>2]=t[O+4>>2],r=nk(s)|0,t[D>>2]=M,t[D+4>>2]=S,t[s>>2]=t[D>>2],t[s+4>>2]=t[D+4>>2],vi(h,n,e,r,rk(s,u)|0,u),m=l}function CE(){var e=0,n=0;if(p[7856]|0||(I3(10172),Ht(50,10172,he|0)|0,n=7856,t[n>>2]=1,t[n+4>>2]=0),!(rr(10172)|0)){e=10172,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));I3(10172)}return 10172}function nk(e){return e=e|0,0}function rk(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0;return O=m,m=m+32|0,l=O+24|0,h=O+16|0,D=O,S=O+8|0,s=t[e>>2]|0,u=t[e+4>>2]|0,t[D>>2]=s,t[D+4>>2]=u,P=CE()|0,M=P+24|0,e=dn(n,4)|0,t[S>>2]=e,n=P+28|0,r=t[n>>2]|0,r>>>0<(t[P+32>>2]|0)>>>0?(t[h>>2]=s,t[h+4>>2]=u,t[l>>2]=t[h>>2],t[l+4>>2]=t[h+4>>2],P3(r,l,e),e=(t[n>>2]|0)+12|0,t[n>>2]=e):(ik(M,D,S),e=t[n>>2]|0),m=O,((e-(t[M>>2]|0)|0)/12|0)+-1|0}function P3(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=t[n+4>>2]|0,t[e>>2]=t[n>>2],t[e+4>>2]=u,t[e+8>>2]=r}function ik(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;if(M=m,m=m+48|0,u=M+32|0,h=M+24|0,D=M,S=e+4|0,l=(((t[S>>2]|0)-(t[e>>2]|0)|0)/12|0)+1|0,s=uk(e)|0,s>>>0>>0)li(e);else{O=t[e>>2]|0,K=((t[e+8>>2]|0)-O|0)/12|0,P=K<<1,ok(D,K>>>0>>1>>>0?P>>>0>>0?l:P:s,((t[S>>2]|0)-O|0)/12|0,e+8|0),S=D+8|0,s=t[S>>2]|0,l=t[n+4>>2]|0,r=t[r>>2]|0,t[h>>2]=t[n>>2],t[h+4>>2]=l,t[u>>2]=t[h>>2],t[u+4>>2]=t[h+4>>2],P3(s,u,r),t[S>>2]=(t[S>>2]|0)+12,lk(e,D),sk(D),m=M;return}}function uk(e){return e=e|0,357913941}function ok(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>357913941)Xn();else{l=cn(n*12|0)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r*12|0)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n*12|0)}function lk(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(((l|0)/-12|0)*12|0)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function sk(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~(((u+-12-n|0)>>>0)/12|0)*12|0)),e=t[e>>2]|0,e|0&&yt(e)}function I3(e){e=e|0,ck(e)}function ak(e){e=e|0,fk(e+24|0)}function fk(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~(((n+-12-u|0)>>>0)/12|0)*12|0)),yt(r))}function ck(e){e=e|0;var n=0;n=dr()|0,Pn(e,2,3,n,dk()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function dk(){return 1380}function pk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+16|0,s=l+8|0,h=l,D=hk(e)|0,e=t[D+4>>2]|0,t[h>>2]=t[D>>2],t[h+4>>2]=e,t[s>>2]=t[h>>2],t[s+4>>2]=t[h+4>>2],vk(n,s,r,u),m=l}function hk(e){return e=e|0,(t[(CE()|0)+24>>2]|0)+(e*12|0)|0}function vk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;D=m,m=m+16|0,s=D+1|0,h=D,l=t[n>>2]|0,n=t[n+4>>2]|0,e=e+(n>>1)|0,n&1&&(l=t[(t[e>>2]|0)+l>>2]|0),Ds(s,r),s=zs(s,r)|0,mk(h,u),h=yk(h,u)|0,jy[l&15](e,s,h),m=D}function mk(e,n){e=e|0,n=n|0}function yk(e,n){return e=e|0,n=n|0,gk(n)|0}function gk(e){return e=e|0,(e|0)!=0|0}function _k(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=xE()|0,e=Ek(r)|0,vi(s,n,l,e,Dk(r,u)|0,u)}function xE(){var e=0,n=0;if(p[7864]|0||(U3(10208),Ht(51,10208,he|0)|0,n=7864,t[n>>2]=1,t[n+4>>2]=0),!(rr(10208)|0)){e=10208,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));U3(10208)}return 10208}function Ek(e){return e=e|0,e|0}function Dk(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=xE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(B3(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(wk(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function B3(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function wk(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=Sk(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,Tk(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,B3(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,Ck(e,l),xk(l),m=D;return}}function Sk(e){return e=e|0,536870911}function Tk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function Ck(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function xk(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function U3(e){e=e|0,Ok(e)}function Ak(e){e=e|0,Rk(e+24|0)}function Rk(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function Ok(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,24,n,kk()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function kk(){return 1392}function Mk(e,n){e=e|0,n=n|0,Lk(t[(Nk(e)|0)>>2]|0,n)}function Nk(e){return e=e|0,(t[(xE()|0)+24>>2]|0)+(e<<3)|0}function Lk(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,N0(u,n),n=Vi(u,n)|0,M1[e&127](n),m=r}function Fk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=AE()|0,e=bk(r)|0,vi(s,n,l,e,Pk(r,u)|0,u)}function AE(){var e=0,n=0;if(p[7872]|0||(z3(10244),Ht(52,10244,he|0)|0,n=7872,t[n>>2]=1,t[n+4>>2]=0),!(rr(10244)|0)){e=10244,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));z3(10244)}return 10244}function bk(e){return e=e|0,e|0}function Pk(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=AE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(j3(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(Ik(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function j3(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function Ik(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=Bk(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,Uk(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,j3(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,jk(e,l),zk(l),m=D;return}}function Bk(e){return e=e|0,536870911}function Uk(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function jk(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function zk(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function z3(e){e=e|0,Wk(e)}function qk(e){e=e|0,Hk(e+24|0)}function Hk(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function Wk(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,16,n,Vk()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Vk(){return 1400}function Gk(e){return e=e|0,Kk(t[(Yk(e)|0)>>2]|0)|0}function Yk(e){return e=e|0,(t[(AE()|0)+24>>2]|0)+(e<<3)|0}function Kk(e){return e=e|0,Xk(N_[e&7]()|0)|0}function Xk(e){return e=e|0,e|0}function Qk(){var e=0;return p[7880]|0||(rM(10280),Ht(25,10280,he|0)|0,e=7880,t[e>>2]=1,t[e+4>>2]=0),10280}function Jk(e,n){e=e|0,n=n|0,t[e>>2]=Zk()|0,t[e+4>>2]=$k()|0,t[e+12>>2]=n,t[e+8>>2]=eM()|0,t[e+32>>2]=4}function Zk(){return 11711}function $k(){return 1356}function eM(){return O1()|0}function tM(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(nM(r),yt(r)):n|0&&(eo(n),yt(n))}function nM(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function rM(e){e=e|0,Ha(e)}function iM(e){e=e|0,uM(e,4920),oM(e)|0,lM(e)|0}function uM(e,n){e=e|0,n=n|0;var r=0;r=j2()|0,t[e>>2]=r,RM(r,n),Q2(t[e>>2]|0)}function oM(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,gM()|0),e|0}function lM(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,sM()|0),e|0}function sM(){var e=0;return p[7888]|0||(q3(10328),Ht(53,10328,he|0)|0,e=7888,t[e>>2]=1,t[e+4>>2]=0),rr(10328)|0||q3(10328),10328}function Hp(e,n){e=e|0,n=n|0,vi(e,0,n,0,0,0)}function q3(e){e=e|0,cM(e),Wp(e,10)}function aM(e){e=e|0,fM(e+24|0)}function fM(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function cM(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,1,n,vM()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function dM(e,n,r){e=e|0,n=n|0,r=+r,pM(e,n,r)}function Wp(e,n){e=e|0,n=n|0,t[e+20>>2]=n}function pM(e,n,r){e=e|0,n=n|0,r=+r;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+16|0,s=u+8|0,D=u+13|0,l=u,h=u+12|0,Ds(D,n),t[s>>2]=zs(D,n)|0,Ol(h,r),U[l>>3]=+es(h,r),hM(e,s,l),m=u}function hM(e,n,r){e=e|0,n=n|0,r=r|0,I(e+8|0,t[n>>2]|0,+U[r>>3]),p[e+24>>0]=1}function vM(){return 1404}function mM(e,n){return e=e|0,n=+n,yM(e,n)|0}function yM(e,n){e=e|0,n=+n;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return u=m,m=m+16|0,s=u+4|0,h=u+8|0,D=u,l=Sa(8)|0,r=l,S=cn(16)|0,Ds(s,e),e=zs(s,e)|0,Ol(h,n),I(S,e,+es(h,n)),h=r+4|0,t[h>>2]=S,e=cn(8)|0,h=t[h>>2]|0,t[D>>2]=0,t[s>>2]=t[D>>2],Nf(e,h,s),t[l>>2]=e,m=u,r|0}function gM(){var e=0;return p[7896]|0||(H3(10364),Ht(54,10364,he|0)|0,e=7896,t[e>>2]=1,t[e+4>>2]=0),rr(10364)|0||H3(10364),10364}function H3(e){e=e|0,DM(e),Wp(e,55)}function _M(e){e=e|0,EM(e+24|0)}function EM(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function DM(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,4,n,CM()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function wM(e){e=e|0,SM(e)}function SM(e){e=e|0,TM(e)}function TM(e){e=e|0,W3(e+8|0),p[e+24>>0]=1}function W3(e){e=e|0,t[e>>2]=0,U[e+8>>3]=0}function CM(){return 1424}function xM(){return AM()|0}function AM(){var e=0,n=0,r=0,u=0,l=0,s=0,h=0;return n=m,m=m+16|0,l=n+4|0,h=n,r=Sa(8)|0,e=r,u=cn(16)|0,W3(u),s=e+4|0,t[s>>2]=u,u=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],Nf(u,s,l),t[r>>2]=u,m=n,e|0}function RM(e,n){e=e|0,n=n|0,t[e>>2]=OM()|0,t[e+4>>2]=kM()|0,t[e+12>>2]=n,t[e+8>>2]=MM()|0,t[e+32>>2]=5}function OM(){return 11710}function kM(){return 1416}function MM(){return E_()|0}function NM(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(LM(r),yt(r)):n|0&&yt(n)}function LM(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function E_(){var e=0;return p[7904]|0||(t[2600]=FM()|0,t[2601]=0,e=7904,t[e>>2]=1,t[e+4>>2]=0),10400}function FM(){return t[357]|0}function bM(e){e=e|0,PM(e,4926),IM(e)|0}function PM(e,n){e=e|0,n=n|0;var r=0;r=qa()|0,t[e>>2]=r,KM(r,n),Q2(t[e>>2]|0)}function IM(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,BM()|0),e|0}function BM(){var e=0;return p[7912]|0||(V3(10412),Ht(56,10412,he|0)|0,e=7912,t[e>>2]=1,t[e+4>>2]=0),rr(10412)|0||V3(10412),10412}function V3(e){e=e|0,zM(e),Wp(e,57)}function UM(e){e=e|0,jM(e+24|0)}function jM(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function zM(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,5,n,VM()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function qM(e){e=e|0,HM(e)}function HM(e){e=e|0,WM(e)}function WM(e){e=e|0;var n=0,r=0;n=e+8|0,r=n+48|0;do t[n>>2]=0,n=n+4|0;while((n|0)<(r|0));p[e+56>>0]=1}function VM(){return 1432}function GM(){return YM()|0}function YM(){var e=0,n=0,r=0,u=0,l=0,s=0,h=0,D=0;h=m,m=m+16|0,e=h+4|0,n=h,r=Sa(8)|0,u=r,l=cn(48)|0,s=l,D=s+48|0;do t[s>>2]=0,s=s+4|0;while((s|0)<(D|0));return s=u+4|0,t[s>>2]=l,D=cn(8)|0,s=t[s>>2]|0,t[n>>2]=0,t[e>>2]=t[n>>2],Dh(D,s,e),t[r>>2]=D,m=h,u|0}function KM(e,n){e=e|0,n=n|0,t[e>>2]=XM()|0,t[e+4>>2]=QM()|0,t[e+12>>2]=n,t[e+8>>2]=JM()|0,t[e+32>>2]=6}function XM(){return 11704}function QM(){return 1436}function JM(){return E_()|0}function ZM(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&($M(r),yt(r)):n|0&&yt(n)}function $M(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function eN(e){e=e|0,tN(e,4933),nN(e)|0,rN(e)|0}function tN(e,n){e=e|0,n=n|0;var r=0;r=AN()|0,t[e>>2]=r,RN(r,n),Q2(t[e>>2]|0)}function nN(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,yN()|0),e|0}function rN(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,iN()|0),e|0}function iN(){var e=0;return p[7920]|0||(G3(10452),Ht(58,10452,he|0)|0,e=7920,t[e>>2]=1,t[e+4>>2]=0),rr(10452)|0||G3(10452),10452}function G3(e){e=e|0,lN(e),Wp(e,1)}function uN(e){e=e|0,oN(e+24|0)}function oN(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function lN(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,1,n,cN()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function sN(e,n,r){e=e|0,n=+n,r=+r,aN(e,n,r)}function aN(e,n,r){e=e|0,n=+n,r=+r;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+32|0,s=u+8|0,D=u+17|0,l=u,h=u+16|0,Ol(D,n),U[s>>3]=+es(D,n),Ol(h,r),U[l>>3]=+es(h,r),fN(e,s,l),m=u}function fN(e,n,r){e=e|0,n=n|0,r=r|0,Y3(e+8|0,+U[n>>3],+U[r>>3]),p[e+24>>0]=1}function Y3(e,n,r){e=e|0,n=+n,r=+r,U[e>>3]=n,U[e+8>>3]=r}function cN(){return 1472}function dN(e,n){return e=+e,n=+n,pN(e,n)|0}function pN(e,n){e=+e,n=+n;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return u=m,m=m+16|0,h=u+4|0,D=u+8|0,S=u,l=Sa(8)|0,r=l,s=cn(16)|0,Ol(h,e),e=+es(h,e),Ol(D,n),Y3(s,e,+es(D,n)),D=r+4|0,t[D>>2]=s,s=cn(8)|0,D=t[D>>2]|0,t[S>>2]=0,t[h>>2]=t[S>>2],K3(s,D,h),t[l>>2]=s,m=u,r|0}function K3(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,r=cn(16)|0,t[r+4>>2]=0,t[r+8>>2]=0,t[r>>2]=1452,t[r+12>>2]=n,t[e+4>>2]=r}function hN(e){e=e|0,Pv(e),yt(e)}function vN(e){e=e|0,e=t[e+12>>2]|0,e|0&&yt(e)}function mN(e){e=e|0,yt(e)}function yN(){var e=0;return p[7928]|0||(X3(10488),Ht(59,10488,he|0)|0,e=7928,t[e>>2]=1,t[e+4>>2]=0),rr(10488)|0||X3(10488),10488}function X3(e){e=e|0,EN(e),Wp(e,60)}function gN(e){e=e|0,_N(e+24|0)}function _N(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function EN(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,6,n,TN()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function DN(e){e=e|0,wN(e)}function wN(e){e=e|0,SN(e)}function SN(e){e=e|0,Q3(e+8|0),p[e+24>>0]=1}function Q3(e){e=e|0,t[e>>2]=0,t[e+4>>2]=0,t[e+8>>2]=0,t[e+12>>2]=0}function TN(){return 1492}function CN(){return xN()|0}function xN(){var e=0,n=0,r=0,u=0,l=0,s=0,h=0;return n=m,m=m+16|0,l=n+4|0,h=n,r=Sa(8)|0,e=r,u=cn(16)|0,Q3(u),s=e+4|0,t[s>>2]=u,u=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],K3(u,s,l),t[r>>2]=u,m=n,e|0}function AN(){var e=0;return p[7936]|0||(FN(10524),Ht(25,10524,he|0)|0,e=7936,t[e>>2]=1,t[e+4>>2]=0),10524}function RN(e,n){e=e|0,n=n|0,t[e>>2]=ON()|0,t[e+4>>2]=kN()|0,t[e+12>>2]=n,t[e+8>>2]=MN()|0,t[e+32>>2]=7}function ON(){return 11700}function kN(){return 1484}function MN(){return E_()|0}function NN(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(LN(r),yt(r)):n|0&&yt(n)}function LN(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function FN(e){e=e|0,Ha(e)}function bN(e,n,r){e=e|0,n=n|0,r=r|0,e=Or(n)|0,n=PN(r)|0,r=IN(r,0)|0,pL(e,n,r,RE()|0,0)}function PN(e){return e=e|0,e|0}function IN(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=RE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(Z3(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(WN(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function RE(){var e=0,n=0;if(p[7944]|0||(J3(10568),Ht(61,10568,he|0)|0,n=7944,t[n>>2]=1,t[n+4>>2]=0),!(rr(10568)|0)){e=10568,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));J3(10568)}return 10568}function J3(e){e=e|0,jN(e)}function BN(e){e=e|0,UN(e+24|0)}function UN(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function jN(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,17,n,Jh()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function zN(e){return e=e|0,HN(t[(qN(e)|0)>>2]|0)|0}function qN(e){return e=e|0,(t[(RE()|0)+24>>2]|0)+(e<<3)|0}function HN(e){return e=e|0,z0(N_[e&7]()|0)|0}function Z3(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function WN(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=VN(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,GN(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,Z3(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,YN(e,l),KN(l),m=D;return}}function VN(e){return e=e|0,536870911}function GN(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function YN(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function KN(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function XN(){QN()}function QN(){JN(10604)}function JN(e){e=e|0,ZN(e,4955)}function ZN(e,n){e=e|0,n=n|0;var r=0;r=$N()|0,t[e>>2]=r,eL(r,n),Q2(t[e>>2]|0)}function $N(){var e=0;return p[7952]|0||(aL(10612),Ht(25,10612,he|0)|0,e=7952,t[e>>2]=1,t[e+4>>2]=0),10612}function eL(e,n){e=e|0,n=n|0,t[e>>2]=iL()|0,t[e+4>>2]=uL()|0,t[e+12>>2]=n,t[e+8>>2]=oL()|0,t[e+32>>2]=8}function Q2(e){e=e|0;var n=0,r=0;n=m,m=m+16|0,r=n,Mv()|0,t[r>>2]=e,tL(10608,r),m=n}function Mv(){return p[11714]|0||(t[2652]=0,Ht(62,10608,he|0)|0,p[11714]=1),10608}function tL(e,n){e=e|0,n=n|0;var r=0;r=cn(8)|0,t[r+4>>2]=t[n>>2],t[r>>2]=t[e>>2],t[e>>2]=r}function nL(e){e=e|0,rL(e)}function rL(e){e=e|0;var n=0,r=0;if(n=t[e>>2]|0,n|0)do r=n,n=t[n>>2]|0,yt(r);while((n|0)!=0);t[e>>2]=0}function iL(){return 11715}function uL(){return 1496}function oL(){return O1()|0}function lL(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(sL(r),yt(r)):n|0&&yt(n)}function sL(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function aL(e){e=e|0,Ha(e)}function fL(e,n){e=e|0,n=n|0;var r=0,u=0;Mv()|0,r=t[2652]|0;e:do if(r|0){for(;u=t[r+4>>2]|0,!(u|0?(L8(OE(u)|0,e)|0)==0:0);)if(r=t[r>>2]|0,!r)break e;cL(u,n)}while(0)}function OE(e){return e=e|0,t[e+12>>2]|0}function cL(e,n){e=e|0,n=n|0;var r=0;e=e+36|0,r=t[e>>2]|0,r|0&&(ia(r),yt(r)),r=cn(4)|0,mf(r,n),t[e>>2]=r}function kE(){return p[11716]|0||(t[2664]=0,Ht(63,10656,he|0)|0,p[11716]=1),10656}function $3(){var e=0;return p[11717]|0?e=t[2665]|0:(dL(),t[2665]=1504,p[11717]=1,e=1504),e|0}function dL(){p[11740]|0||(p[11718]=dn(dn(8,0)|0,0)|0,p[11719]=dn(dn(0,0)|0,0)|0,p[11720]=dn(dn(0,16)|0,0)|0,p[11721]=dn(dn(8,0)|0,0)|0,p[11722]=dn(dn(0,0)|0,0)|0,p[11723]=dn(dn(8,0)|0,0)|0,p[11724]=dn(dn(0,0)|0,0)|0,p[11725]=dn(dn(8,0)|0,0)|0,p[11726]=dn(dn(0,0)|0,0)|0,p[11727]=dn(dn(8,0)|0,0)|0,p[11728]=dn(dn(0,0)|0,0)|0,p[11729]=dn(dn(0,0)|0,32)|0,p[11730]=dn(dn(0,0)|0,32)|0,p[11740]=1)}function e8(){return 1572}function pL(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0,O=0;s=m,m=m+32|0,O=s+16|0,M=s+12|0,S=s+8|0,D=s+4|0,h=s,t[O>>2]=e,t[M>>2]=n,t[S>>2]=r,t[D>>2]=u,t[h>>2]=l,kE()|0,hL(10656,O,M,S,D,h),m=s}function hL(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0;h=cn(24)|0,h2(h+4|0,t[n>>2]|0,t[r>>2]|0,t[u>>2]|0,t[l>>2]|0,t[s>>2]|0),t[h>>2]=t[e>>2],t[e>>2]=h}function t8(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0,st=0;if(st=m,m=m+32|0,Ee=st+20|0,ve=st+8|0,Qe=st+4|0,We=st,n=t[n>>2]|0,n|0){Pe=Ee+4|0,S=Ee+8|0,M=ve+4|0,O=ve+8|0,P=ve+8|0,K=Ee+8|0;do{if(h=n+4|0,D=ME(h)|0,D|0){if(l=Fy(D)|0,t[Ee>>2]=0,t[Pe>>2]=0,t[S>>2]=0,u=(by(D)|0)+1|0,vL(Ee,u),u|0)for(;u=u+-1|0,jf(ve,t[l>>2]|0),s=t[Pe>>2]|0,s>>>0<(t[K>>2]|0)>>>0?(t[s>>2]=t[ve>>2],t[Pe>>2]=(t[Pe>>2]|0)+4):NE(Ee,ve),u;)l=l+4|0;u=Py(D)|0,t[ve>>2]=0,t[M>>2]=0,t[O>>2]=0;e:do if(t[u>>2]|0)for(l=0,s=0;;){if((l|0)==(s|0)?mL(ve,u):(t[l>>2]=t[u>>2],t[M>>2]=(t[M>>2]|0)+4),u=u+4|0,!(t[u>>2]|0))break e;l=t[M>>2]|0,s=t[P>>2]|0}while(0);t[Qe>>2]=D_(h)|0,t[We>>2]=rr(D)|0,yL(r,e,Qe,We,Ee,ve),LE(ve),k1(Ee)}n=t[n>>2]|0}while((n|0)!=0)}m=st}function ME(e){return e=e|0,t[e+12>>2]|0}function Fy(e){return e=e|0,t[e+12>>2]|0}function by(e){return e=e|0,t[e+16>>2]|0}function vL(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;l=m,m=m+32|0,r=l,u=t[e>>2]|0,(t[e+8>>2]|0)-u>>2>>>0>>0&&(a8(r,n,(t[e+4>>2]|0)-u>>2,e+8|0),f8(e,r),c8(r)),m=l}function NE(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0;if(h=m,m=m+32|0,r=h,u=e+4|0,l=((t[u>>2]|0)-(t[e>>2]|0)>>2)+1|0,s=s8(e)|0,s>>>0>>0)li(e);else{D=t[e>>2]|0,M=(t[e+8>>2]|0)-D|0,S=M>>1,a8(r,M>>2>>>0>>1>>>0?S>>>0>>0?l:S:s,(t[u>>2]|0)-D>>2,e+8|0),s=r+8|0,t[t[s>>2]>>2]=t[n>>2],t[s>>2]=(t[s>>2]|0)+4,f8(e,r),c8(r),m=h;return}}function Py(e){return e=e|0,t[e+8>>2]|0}function mL(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0;if(h=m,m=m+32|0,r=h,u=e+4|0,l=((t[u>>2]|0)-(t[e>>2]|0)>>2)+1|0,s=l8(e)|0,s>>>0>>0)li(e);else{D=t[e>>2]|0,M=(t[e+8>>2]|0)-D|0,S=M>>1,PL(r,M>>2>>>0>>1>>>0?S>>>0>>0?l:S:s,(t[u>>2]|0)-D>>2,e+8|0),s=r+8|0,t[t[s>>2]>>2]=t[n>>2],t[s>>2]=(t[s>>2]|0)+4,IL(e,r),BL(r),m=h;return}}function D_(e){return e=e|0,t[e>>2]|0}function yL(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,gL(e,n,r,u,l,s)}function LE(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-4-u|0)>>>2)<<2)),yt(r))}function k1(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-4-u|0)>>>2)<<2)),yt(r))}function gL(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0,S=0,M=0,O=0,P=0;h=m,m=m+48|0,O=h+40|0,D=h+32|0,P=h+24|0,S=h+12|0,M=h,Ta(D),e=vo(e)|0,t[P>>2]=t[n>>2],r=t[r>>2]|0,u=t[u>>2]|0,FE(S,l),_L(M,s),t[O>>2]=t[P>>2],EL(e,O,r,u,S,M),LE(M),k1(S),Ca(D),m=h}function FE(e,n){e=e|0,n=n|0;var r=0,u=0;t[e>>2]=0,t[e+4>>2]=0,t[e+8>>2]=0,r=n+4|0,u=(t[r>>2]|0)-(t[n>>2]|0)>>2,u|0&&(FL(e,u),bL(e,t[n>>2]|0,t[r>>2]|0,u))}function _L(e,n){e=e|0,n=n|0;var r=0,u=0;t[e>>2]=0,t[e+4>>2]=0,t[e+8>>2]=0,r=n+4|0,u=(t[r>>2]|0)-(t[n>>2]|0)>>2,u|0&&(NL(e,u),LL(e,t[n>>2]|0,t[r>>2]|0,u))}function EL(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0,S=0,M=0,O=0,P=0;h=m,m=m+32|0,O=h+28|0,P=h+24|0,D=h+12|0,S=h,M=mo(DL()|0)|0,t[P>>2]=t[n>>2],t[O>>2]=t[P>>2],n=Vp(O)|0,r=n8(r)|0,u=bE(u)|0,t[D>>2]=t[l>>2],O=l+4|0,t[D+4>>2]=t[O>>2],P=l+8|0,t[D+8>>2]=t[P>>2],t[P>>2]=0,t[O>>2]=0,t[l>>2]=0,l=PE(D)|0,t[S>>2]=t[s>>2],O=s+4|0,t[S+4>>2]=t[O>>2],P=s+8|0,t[S+8>>2]=t[P>>2],t[P>>2]=0,t[O>>2]=0,t[s>>2]=0,G0(0,M|0,e|0,n|0,r|0,u|0,l|0,wL(S)|0)|0,LE(S),k1(D),m=h}function DL(){var e=0;return p[7968]|0||(kL(10708),e=7968,t[e>>2]=1,t[e+4>>2]=0),10708}function Vp(e){return e=e|0,i8(e)|0}function n8(e){return e=e|0,r8(e)|0}function bE(e){return e=e|0,z0(e)|0}function PE(e){return e=e|0,TL(e)|0}function wL(e){return e=e|0,SL(e)|0}function SL(e){e=e|0;var n=0,r=0,u=0;if(u=(t[e+4>>2]|0)-(t[e>>2]|0)|0,r=u>>2,u=Sa(u+4|0)|0,t[u>>2]=r,r|0){n=0;do t[u+4+(n<<2)>>2]=r8(t[(t[e>>2]|0)+(n<<2)>>2]|0)|0,n=n+1|0;while((n|0)!=(r|0))}return u|0}function r8(e){return e=e|0,e|0}function TL(e){e=e|0;var n=0,r=0,u=0;if(u=(t[e+4>>2]|0)-(t[e>>2]|0)|0,r=u>>2,u=Sa(u+4|0)|0,t[u>>2]=r,r|0){n=0;do t[u+4+(n<<2)>>2]=i8((t[e>>2]|0)+(n<<2)|0)|0,n=n+1|0;while((n|0)!=(r|0))}return u|0}function i8(e){e=e|0;var n=0,r=0,u=0,l=0;return l=m,m=m+32|0,n=l+12|0,r=l,u=Ou(u8()|0)|0,u?(Zl(n,u),Tf(r,n),lI(e,r),e=Es(n)|0):e=CL(e)|0,m=l,e|0}function u8(){var e=0;return p[7960]|0||(OL(10664),Ht(25,10664,he|0)|0,e=7960,t[e>>2]=1,t[e+4>>2]=0),10664}function CL(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0;return r=m,m=m+16|0,l=r+4|0,h=r,u=Sa(8)|0,n=u,D=cn(4)|0,t[D>>2]=t[e>>2],s=n+4|0,t[s>>2]=D,e=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],o8(e,s,l),t[u>>2]=e,m=r,n|0}function o8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,r=cn(16)|0,t[r+4>>2]=0,t[r+8>>2]=0,t[r>>2]=1656,t[r+12>>2]=n,t[e+4>>2]=r}function xL(e){e=e|0,Pv(e),yt(e)}function AL(e){e=e|0,e=t[e+12>>2]|0,e|0&&yt(e)}function RL(e){e=e|0,yt(e)}function OL(e){e=e|0,Ha(e)}function kL(e){e=e|0,nl(e,ML()|0,5)}function ML(){return 1676}function NL(e,n){e=e|0,n=n|0;var r=0;if((l8(e)|0)>>>0>>0&&li(e),n>>>0>1073741823)Xn();else{r=cn(n<<2)|0,t[e+4>>2]=r,t[e>>2]=r,t[e+8>>2]=r+(n<<2);return}}function LL(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,u=e+4|0,e=r-n|0,(e|0)>0&&(pr(t[u>>2]|0,n|0,e|0)|0,t[u>>2]=(t[u>>2]|0)+(e>>>2<<2))}function l8(e){return e=e|0,1073741823}function FL(e,n){e=e|0,n=n|0;var r=0;if((s8(e)|0)>>>0>>0&&li(e),n>>>0>1073741823)Xn();else{r=cn(n<<2)|0,t[e+4>>2]=r,t[e>>2]=r,t[e+8>>2]=r+(n<<2);return}}function bL(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,u=e+4|0,e=r-n|0,(e|0)>0&&(pr(t[u>>2]|0,n|0,e|0)|0,t[u>>2]=(t[u>>2]|0)+(e>>>2<<2))}function s8(e){return e=e|0,1073741823}function PL(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>1073741823)Xn();else{l=cn(n<<2)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<2)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<2)}function IL(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>2)<<2)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function BL(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-4-n|0)>>>2)<<2)),e=t[e>>2]|0,e|0&&yt(e)}function a8(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>1073741823)Xn();else{l=cn(n<<2)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<2)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<2)}function f8(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>2)<<2)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function c8(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-4-n|0)>>>2)<<2)),e=t[e>>2]|0,e|0&&yt(e)}function UL(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0;if(ve=m,m=m+32|0,O=ve+20|0,P=ve+12|0,M=ve+16|0,K=ve+4|0,Pe=ve,Ee=ve+8|0,D=$3()|0,s=t[D>>2]|0,h=t[s>>2]|0,h|0)for(S=t[D+8>>2]|0,D=t[D+4>>2]|0;jf(O,h),jL(e,O,D,S),s=s+4|0,h=t[s>>2]|0,h;)S=S+1|0,D=D+1|0;if(s=e8()|0,h=t[s>>2]|0,h|0)do jf(O,h),t[P>>2]=t[s+4>>2],zL(n,O,P),s=s+8|0,h=t[s>>2]|0;while((h|0)!=0);if(s=t[(Mv()|0)>>2]|0,s|0)do n=t[s+4>>2]|0,jf(O,t[(Nv(n)|0)>>2]|0),t[P>>2]=OE(n)|0,qL(r,O,P),s=t[s>>2]|0;while((s|0)!=0);if(jf(M,0),s=kE()|0,t[O>>2]=t[M>>2],t8(O,s,l),s=t[(Mv()|0)>>2]|0,s|0){e=O+4|0,n=O+8|0,r=O+8|0;do{if(S=t[s+4>>2]|0,jf(P,t[(Nv(S)|0)>>2]|0),HL(K,d8(S)|0),h=t[K>>2]|0,h|0){t[O>>2]=0,t[e>>2]=0,t[n>>2]=0;do jf(Pe,t[(Nv(t[h+4>>2]|0)|0)>>2]|0),D=t[e>>2]|0,D>>>0<(t[r>>2]|0)>>>0?(t[D>>2]=t[Pe>>2],t[e>>2]=(t[e>>2]|0)+4):NE(O,Pe),h=t[h>>2]|0;while((h|0)!=0);WL(u,P,O),k1(O)}t[Ee>>2]=t[P>>2],M=p8(S)|0,t[O>>2]=t[Ee>>2],t8(O,M,l),m2(K),s=t[s>>2]|0}while((s|0)!=0)}m=ve}function jL(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,rF(e,n,r,u)}function zL(e,n,r){e=e|0,n=n|0,r=r|0,nF(e,n,r)}function Nv(e){return e=e|0,e|0}function qL(e,n,r){e=e|0,n=n|0,r=r|0,ZL(e,n,r)}function d8(e){return e=e|0,e+16|0}function HL(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;if(s=m,m=m+16|0,l=s+8|0,r=s,t[e>>2]=0,u=t[n>>2]|0,t[l>>2]=u,t[r>>2]=e,r=JL(r)|0,u|0){if(u=cn(12)|0,h=(h8(l)|0)+4|0,e=t[h+4>>2]|0,n=u+4|0,t[n>>2]=t[h>>2],t[n+4>>2]=e,n=t[t[l>>2]>>2]|0,t[l>>2]=n,!n)e=u;else for(n=u;e=cn(12)|0,S=(h8(l)|0)+4|0,D=t[S+4>>2]|0,h=e+4|0,t[h>>2]=t[S>>2],t[h+4>>2]=D,t[n>>2]=e,h=t[t[l>>2]>>2]|0,t[l>>2]=h,h;)n=e;t[e>>2]=t[r>>2],t[r>>2]=u}m=s}function WL(e,n,r){e=e|0,n=n|0,r=r|0,VL(e,n,r)}function p8(e){return e=e|0,e+24|0}function VL(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+32|0,h=u+24|0,l=u+16|0,D=u+12|0,s=u,Ta(l),e=vo(e)|0,t[D>>2]=t[n>>2],FE(s,r),t[h>>2]=t[D>>2],YL(e,h,s),k1(s),Ca(l),m=u}function YL(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=m,m=m+32|0,h=u+16|0,D=u+12|0,l=u,s=mo(KL()|0)|0,t[D>>2]=t[n>>2],t[h>>2]=t[D>>2],n=Vp(h)|0,t[l>>2]=t[r>>2],h=r+4|0,t[l+4>>2]=t[h>>2],D=r+8|0,t[l+8>>2]=t[D>>2],t[D>>2]=0,t[h>>2]=0,t[r>>2]=0,F0(0,s|0,e|0,n|0,PE(l)|0)|0,k1(l),m=u}function KL(){var e=0;return p[7976]|0||(XL(10720),e=7976,t[e>>2]=1,t[e+4>>2]=0),10720}function XL(e){e=e|0,nl(e,QL()|0,2)}function QL(){return 1732}function JL(e){return e=e|0,t[e>>2]|0}function h8(e){return e=e|0,t[e>>2]|0}function ZL(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+32|0,s=u+16|0,l=u+8|0,h=u,Ta(l),e=vo(e)|0,t[h>>2]=t[n>>2],r=t[r>>2]|0,t[s>>2]=t[h>>2],v8(e,s,r),Ca(l),m=u}function v8(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+16|0,s=u+4|0,h=u,l=mo($L()|0)|0,t[h>>2]=t[n>>2],t[s>>2]=t[h>>2],n=Vp(s)|0,F0(0,l|0,e|0,n|0,n8(r)|0)|0,m=u}function $L(){var e=0;return p[7984]|0||(eF(10732),e=7984,t[e>>2]=1,t[e+4>>2]=0),10732}function eF(e){e=e|0,nl(e,tF()|0,2)}function tF(){return 1744}function nF(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;u=m,m=m+32|0,s=u+16|0,l=u+8|0,h=u,Ta(l),e=vo(e)|0,t[h>>2]=t[n>>2],r=t[r>>2]|0,t[s>>2]=t[h>>2],v8(e,s,r),Ca(l),m=u}function rF(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+32|0,h=l+16|0,s=l+8|0,D=l,Ta(s),e=vo(e)|0,t[D>>2]=t[n>>2],r=p[r>>0]|0,u=p[u>>0]|0,t[h>>2]=t[D>>2],iF(e,h,r,u),Ca(s),m=l}function iF(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+16|0,h=l+4|0,D=l,s=mo(uF()|0)|0,t[D>>2]=t[n>>2],t[h>>2]=t[D>>2],n=Vp(h)|0,r=Lv(r)|0,Bn(0,s|0,e|0,n|0,r|0,Lv(u)|0)|0,m=l}function uF(){var e=0;return p[7992]|0||(lF(10744),e=7992,t[e>>2]=1,t[e+4>>2]=0),10744}function Lv(e){return e=e|0,oF(e)|0}function oF(e){return e=e|0,e&255|0}function lF(e){e=e|0,nl(e,sF()|0,3)}function sF(){return 1756}function aF(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;switch(K=m,m=m+32|0,D=K+8|0,S=K+4|0,M=K+20|0,O=K,ma(e,0),u=oI(n)|0,t[D>>2]=0,P=D+4|0,t[P>>2]=0,t[D+8>>2]=0,u<<24>>24){case 0:{p[M>>0]=0,fF(S,r,M),w_(e,S)|0,B0(S);break}case 8:{P=qE(n)|0,p[M>>0]=8,jf(O,t[P+4>>2]|0),cF(S,r,M,O,P+8|0),w_(e,S)|0,B0(S);break}case 9:{if(s=qE(n)|0,n=t[s+4>>2]|0,n|0)for(h=D+8|0,l=s+12|0;n=n+-1|0,jf(S,t[l>>2]|0),u=t[P>>2]|0,u>>>0<(t[h>>2]|0)>>>0?(t[u>>2]=t[S>>2],t[P>>2]=(t[P>>2]|0)+4):NE(D,S),n;)l=l+4|0;p[M>>0]=9,jf(O,t[s+8>>2]|0),dF(S,r,M,O,D),w_(e,S)|0,B0(S);break}default:P=qE(n)|0,p[M>>0]=u,jf(O,t[P+4>>2]|0),pF(S,r,M,O),w_(e,S)|0,B0(S)}k1(D),m=K}function fF(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;u=m,m=m+16|0,l=u,Ta(l),n=vo(n)|0,xF(e,n,p[r>>0]|0),Ca(l),m=u}function w_(e,n){e=e|0,n=n|0;var r=0;return r=t[e>>2]|0,r|0&&Ir(r|0),t[e>>2]=t[n>>2],t[n>>2]=0,e|0}function cF(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0;s=m,m=m+32|0,D=s+16|0,h=s+8|0,S=s,Ta(h),n=vo(n)|0,r=p[r>>0]|0,t[S>>2]=t[u>>2],l=t[l>>2]|0,t[D>>2]=t[S>>2],wF(e,n,r,D,l),Ca(h),m=s}function dF(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0;s=m,m=m+32|0,S=s+24|0,h=s+16|0,M=s+12|0,D=s,Ta(h),n=vo(n)|0,r=p[r>>0]|0,t[M>>2]=t[u>>2],FE(D,l),t[S>>2]=t[M>>2],gF(e,n,r,S,D),k1(D),Ca(h),m=s}function pF(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+32|0,h=l+16|0,s=l+8|0,D=l,Ta(s),n=vo(n)|0,r=p[r>>0]|0,t[D>>2]=t[u>>2],t[h>>2]=t[D>>2],hF(e,n,r,h),Ca(s),m=l}function hF(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0,h=0,D=0;l=m,m=m+16|0,s=l+4|0,D=l,h=mo(vF()|0)|0,r=Lv(r)|0,t[D>>2]=t[u>>2],t[s>>2]=t[D>>2],S_(e,F0(0,h|0,n|0,r|0,Vp(s)|0)|0),m=l}function vF(){var e=0;return p[8e3]|0||(mF(10756),e=8e3,t[e>>2]=1,t[e+4>>2]=0),10756}function S_(e,n){e=e|0,n=n|0,ma(e,n)}function mF(e){e=e|0,nl(e,yF()|0,2)}function yF(){return 1772}function gF(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0;s=m,m=m+32|0,S=s+16|0,M=s+12|0,h=s,D=mo(_F()|0)|0,r=Lv(r)|0,t[M>>2]=t[u>>2],t[S>>2]=t[M>>2],u=Vp(S)|0,t[h>>2]=t[l>>2],S=l+4|0,t[h+4>>2]=t[S>>2],M=l+8|0,t[h+8>>2]=t[M>>2],t[M>>2]=0,t[S>>2]=0,t[l>>2]=0,S_(e,Bn(0,D|0,n|0,r|0,u|0,PE(h)|0)|0),k1(h),m=s}function _F(){var e=0;return p[8008]|0||(EF(10768),e=8008,t[e>>2]=1,t[e+4>>2]=0),10768}function EF(e){e=e|0,nl(e,DF()|0,3)}function DF(){return 1784}function wF(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0;s=m,m=m+16|0,D=s+4|0,S=s,h=mo(SF()|0)|0,r=Lv(r)|0,t[S>>2]=t[u>>2],t[D>>2]=t[S>>2],u=Vp(D)|0,S_(e,Bn(0,h|0,n|0,r|0,u|0,bE(l)|0)|0),m=s}function SF(){var e=0;return p[8016]|0||(TF(10780),e=8016,t[e>>2]=1,t[e+4>>2]=0),10780}function TF(e){e=e|0,nl(e,CF()|0,3)}function CF(){return 1800}function xF(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;u=mo(AF()|0)|0,S_(e,ji(0,u|0,n|0,Lv(r)|0)|0)}function AF(){var e=0;return p[8024]|0||(RF(10792),e=8024,t[e>>2]=1,t[e+4>>2]=0),10792}function RF(e){e=e|0,nl(e,OF()|0,1)}function OF(){return 1816}function kF(){MF(),NF(),LF()}function MF(){t[2702]=H8(65536)|0}function NF(){$F(10856)}function LF(){FF(10816)}function FF(e){e=e|0,bF(e,5044),PF(e)|0}function bF(e,n){e=e|0,n=n|0;var r=0;r=u8()|0,t[e>>2]=r,YF(r,n),Q2(t[e>>2]|0)}function PF(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,IF()|0),e|0}function IF(){var e=0;return p[8032]|0||(m8(10820),Ht(64,10820,he|0)|0,e=8032,t[e>>2]=1,t[e+4>>2]=0),rr(10820)|0||m8(10820),10820}function m8(e){e=e|0,jF(e),Wp(e,25)}function BF(e){e=e|0,UF(e+24|0)}function UF(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function jF(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,18,n,WF()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function zF(e,n){e=e|0,n=n|0,qF(e,n)}function qF(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;r=m,m=m+16|0,u=r,l=r+4|0,Of(l,n),t[u>>2]=kf(l,n)|0,HF(e,u),m=r}function HF(e,n){e=e|0,n=n|0,y8(e+4|0,t[n>>2]|0),p[e+8>>0]=1}function y8(e,n){e=e|0,n=n|0,t[e>>2]=n}function WF(){return 1824}function VF(e){return e=e|0,GF(e)|0}function GF(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0;return r=m,m=m+16|0,l=r+4|0,h=r,u=Sa(8)|0,n=u,D=cn(4)|0,Of(l,e),y8(D,kf(l,e)|0),s=n+4|0,t[s>>2]=D,e=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],o8(e,s,l),t[u>>2]=e,m=r,n|0}function Sa(e){e=e|0;var n=0,r=0;return e=e+7&-8,(e>>>0<=32768?(n=t[2701]|0,e>>>0<=(65536-n|0)>>>0):0)?(r=(t[2702]|0)+n|0,t[2701]=n+e,e=r):(e=H8(e+8|0)|0,t[e>>2]=t[2703],t[2703]=e,e=e+8|0),e|0}function YF(e,n){e=e|0,n=n|0,t[e>>2]=KF()|0,t[e+4>>2]=XF()|0,t[e+12>>2]=n,t[e+8>>2]=QF()|0,t[e+32>>2]=9}function KF(){return 11744}function XF(){return 1832}function QF(){return E_()|0}function JF(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(ZF(r),yt(r)):n|0&&yt(n)}function ZF(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function $F(e){e=e|0,eb(e,5052),tb(e)|0,nb(e,5058,26)|0,rb(e,5069,1)|0,ib(e,5077,10)|0,ub(e,5087,19)|0,ob(e,5094,27)|0}function eb(e,n){e=e|0,n=n|0;var r=0;r=ZP()|0,t[e>>2]=r,$P(r,n),Q2(t[e>>2]|0)}function tb(e){e=e|0;var n=0;return n=t[e>>2]|0,Hp(n,BP()|0),e|0}function nb(e,n,r){return e=e|0,n=n|0,r=r|0,EP(e,Or(n)|0,r,0),e|0}function rb(e,n,r){return e=e|0,n=n|0,r=r|0,uP(e,Or(n)|0,r,0),e|0}function ib(e,n,r){return e=e|0,n=n|0,r=r|0,Ib(e,Or(n)|0,r,0),e|0}function ub(e,n,r){return e=e|0,n=n|0,r=r|0,wb(e,Or(n)|0,r,0),e|0}function g8(e,n){e=e|0,n=n|0;var r=0,u=0;e:for(;;){for(r=t[2703]|0;;){if((r|0)==(n|0))break e;if(u=t[r>>2]|0,t[2703]=u,!r)r=u;else break}yt(r)}t[2701]=e}function ob(e,n,r){return e=e|0,n=n|0,r=r|0,lb(e,Or(n)|0,r,0),e|0}function lb(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=IE()|0,e=sb(r)|0,vi(s,n,l,e,ab(r,u)|0,u)}function IE(){var e=0,n=0;if(p[8040]|0||(E8(10860),Ht(65,10860,he|0)|0,n=8040,t[n>>2]=1,t[n+4>>2]=0),!(rr(10860)|0)){e=10860,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));E8(10860)}return 10860}function sb(e){return e=e|0,e|0}function ab(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=IE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(_8(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(fb(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function _8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function fb(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=cb(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,db(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,_8(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,pb(e,l),hb(l),m=D;return}}function cb(e){return e=e|0,536870911}function db(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function pb(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function hb(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function E8(e){e=e|0,yb(e)}function vb(e){e=e|0,mb(e+24|0)}function mb(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function yb(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,11,n,gb()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function gb(){return 1840}function _b(e,n,r){e=e|0,n=n|0,r=r|0,Db(t[(Eb(e)|0)>>2]|0,n,r)}function Eb(e){return e=e|0,(t[(IE()|0)+24>>2]|0)+(e<<3)|0}function Db(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;u=m,m=m+16|0,s=u+1|0,l=u,Of(s,n),n=kf(s,n)|0,Of(l,r),r=kf(l,r)|0,N1[e&31](n,r),m=u}function wb(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=BE()|0,e=Sb(r)|0,vi(s,n,l,e,Tb(r,u)|0,u)}function BE(){var e=0,n=0;if(p[8048]|0||(w8(10896),Ht(66,10896,he|0)|0,n=8048,t[n>>2]=1,t[n+4>>2]=0),!(rr(10896)|0)){e=10896,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));w8(10896)}return 10896}function Sb(e){return e=e|0,e|0}function Tb(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=BE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(D8(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(Cb(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function D8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function Cb(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=xb(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,Ab(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,D8(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,Rb(e,l),Ob(l),m=D;return}}function xb(e){return e=e|0,536870911}function Ab(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function Rb(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Ob(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function w8(e){e=e|0,Nb(e)}function kb(e){e=e|0,Mb(e+24|0)}function Mb(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function Nb(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,11,n,Lb()|0,1),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Lb(){return 1852}function Fb(e,n){return e=e|0,n=n|0,Pb(t[(bb(e)|0)>>2]|0,n)|0}function bb(e){return e=e|0,(t[(BE()|0)+24>>2]|0)+(e<<3)|0}function Pb(e,n){e=e|0,n=n|0;var r=0,u=0;return r=m,m=m+16|0,u=r,Of(u,n),n=kf(u,n)|0,n=z0(Xp[e&31](n)|0)|0,m=r,n|0}function Ib(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=UE()|0,e=Bb(r)|0,vi(s,n,l,e,Ub(r,u)|0,u)}function UE(){var e=0,n=0;if(p[8056]|0||(T8(10932),Ht(67,10932,he|0)|0,n=8056,t[n>>2]=1,t[n+4>>2]=0),!(rr(10932)|0)){e=10932,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));T8(10932)}return 10932}function Bb(e){return e=e|0,e|0}function Ub(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=UE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(S8(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(jb(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function S8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function jb(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=zb(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,qb(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,S8(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,Hb(e,l),Wb(l),m=D;return}}function zb(e){return e=e|0,536870911}function qb(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function Hb(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function Wb(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function T8(e){e=e|0,Yb(e)}function Vb(e){e=e|0,Gb(e+24|0)}function Gb(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function Yb(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,7,n,Kb()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function Kb(){return 1860}function Xb(e,n,r){return e=e|0,n=n|0,r=r|0,Jb(t[(Qb(e)|0)>>2]|0,n,r)|0}function Qb(e){return e=e|0,(t[(UE()|0)+24>>2]|0)+(e<<3)|0}function Jb(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0;return u=m,m=m+32|0,h=u+12|0,s=u+8|0,D=u,S=u+16|0,l=u+4|0,Zb(S,n),$b(D,S,n),qs(l,r),r=Hs(l,r)|0,t[h>>2]=t[D>>2],jy[e&15](s,h,r),r=eP(s)|0,B0(s),Ws(l),m=u,r|0}function Zb(e,n){e=e|0,n=n|0}function $b(e,n,r){e=e|0,n=n|0,r=r|0,tP(e,r)}function eP(e){return e=e|0,vo(e)|0}function tP(e,n){e=e|0,n=n|0;var r=0,u=0,l=0;l=m,m=m+16|0,r=l,u=n,u&1?(nP(r,0),Yi(u|0,r|0)|0,rP(e,r),iP(r)):t[e>>2]=t[n>>2],m=l}function nP(e,n){e=e|0,n=n|0,l2(e,n),t[e+4>>2]=0,p[e+8>>0]=0}function rP(e,n){e=e|0,n=n|0,t[e>>2]=t[n+4>>2]}function iP(e){e=e|0,p[e+8>>0]=0}function uP(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=jE()|0,e=oP(r)|0,vi(s,n,l,e,lP(r,u)|0,u)}function jE(){var e=0,n=0;if(p[8064]|0||(x8(10968),Ht(68,10968,he|0)|0,n=8064,t[n>>2]=1,t[n+4>>2]=0),!(rr(10968)|0)){e=10968,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));x8(10968)}return 10968}function oP(e){return e=e|0,e|0}function lP(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=jE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(C8(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(sP(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function C8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function sP(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=aP(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,fP(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,C8(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,cP(e,l),dP(l),m=D;return}}function aP(e){return e=e|0,536870911}function fP(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function cP(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function dP(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function x8(e){e=e|0,vP(e)}function pP(e){e=e|0,hP(e+24|0)}function hP(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function vP(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,1,n,mP()|0,5),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function mP(){return 1872}function yP(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,_P(t[(gP(e)|0)>>2]|0,n,r,u,l,s)}function gP(e){return e=e|0,(t[(jE()|0)+24>>2]|0)+(e<<3)|0}function _P(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0,S=0,M=0,O=0,P=0;h=m,m=m+32|0,D=h+16|0,S=h+12|0,M=h+8|0,O=h+4|0,P=h,qs(D,n),n=Hs(D,n)|0,qs(S,r),r=Hs(S,r)|0,qs(M,u),u=Hs(M,u)|0,qs(O,l),l=Hs(O,l)|0,qs(P,s),s=Hs(P,s)|0,K8[e&1](n,r,u,l,s),Ws(P),Ws(O),Ws(M),Ws(S),Ws(D),m=h}function EP(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;s=t[e>>2]|0,l=zE()|0,e=DP(r)|0,vi(s,n,l,e,wP(r,u)|0,u)}function zE(){var e=0,n=0;if(p[8072]|0||(R8(11004),Ht(69,11004,he|0)|0,n=8072,t[n>>2]=1,t[n+4>>2]=0),!(rr(11004)|0)){e=11004,n=e+36|0;do t[e>>2]=0,e=e+4|0;while((e|0)<(n|0));R8(11004)}return 11004}function DP(e){return e=e|0,e|0}function wP(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0,D=0,S=0;return D=m,m=m+16|0,l=D,s=D+4|0,t[l>>2]=e,S=zE()|0,h=S+24|0,n=dn(n,4)|0,t[s>>2]=n,r=S+28|0,u=t[r>>2]|0,u>>>0<(t[S+32>>2]|0)>>>0?(A8(u,e,n),n=(t[r>>2]|0)+8|0,t[r>>2]=n):(SP(h,l,s),n=t[r>>2]|0),m=D,(n-(t[h>>2]|0)>>3)+-1|0}function A8(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,t[e+4>>2]=r}function SP(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0;if(D=m,m=m+32|0,l=D,s=e+4|0,h=((t[s>>2]|0)-(t[e>>2]|0)>>3)+1|0,u=TP(e)|0,u>>>0>>0)li(e);else{S=t[e>>2]|0,O=(t[e+8>>2]|0)-S|0,M=O>>2,CP(l,O>>3>>>0>>1>>>0?M>>>0>>0?h:M:u,(t[s>>2]|0)-S>>3,e+8|0),h=l+8|0,A8(t[h>>2]|0,t[n>>2]|0,t[r>>2]|0),t[h>>2]=(t[h>>2]|0)+8,xP(e,l),AP(l),m=D;return}}function TP(e){return e=e|0,536870911}function CP(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0;t[e+12>>2]=0,t[e+16>>2]=u;do if(n)if(n>>>0>536870911)Xn();else{l=cn(n<<3)|0;break}else l=0;while(0);t[e>>2]=l,u=l+(r<<3)|0,t[e+8>>2]=u,t[e+4>>2]=u,t[e+12>>2]=l+(n<<3)}function xP(e,n){e=e|0,n=n|0;var r=0,u=0,l=0,s=0,h=0;u=t[e>>2]|0,h=e+4|0,s=n+4|0,l=(t[h>>2]|0)-u|0,r=(t[s>>2]|0)+(0-(l>>3)<<3)|0,t[s>>2]=r,(l|0)>0?(pr(r|0,u|0,l|0)|0,u=s,r=t[s>>2]|0):u=s,s=t[e>>2]|0,t[e>>2]=r,t[u>>2]=s,s=n+8|0,l=t[h>>2]|0,t[h>>2]=t[s>>2],t[s>>2]=l,s=e+8|0,h=n+12|0,e=t[s>>2]|0,t[s>>2]=t[h>>2],t[h>>2]=e,t[n>>2]=t[u>>2]}function AP(e){e=e|0;var n=0,r=0,u=0;n=t[e+4>>2]|0,r=e+8|0,u=t[r>>2]|0,(u|0)!=(n|0)&&(t[r>>2]=u+(~((u+-8-n|0)>>>3)<<3)),e=t[e>>2]|0,e|0&&yt(e)}function R8(e){e=e|0,kP(e)}function RP(e){e=e|0,OP(e+24|0)}function OP(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function kP(e){e=e|0;var n=0;n=dr()|0,Pn(e,1,12,n,MP()|0,2),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function MP(){return 1896}function NP(e,n,r){e=e|0,n=n|0,r=r|0,FP(t[(LP(e)|0)>>2]|0,n,r)}function LP(e){return e=e|0,(t[(zE()|0)+24>>2]|0)+(e<<3)|0}function FP(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;u=m,m=m+16|0,s=u+4|0,l=u,bP(s,n),n=PP(s,n)|0,qs(l,r),r=Hs(l,r)|0,N1[e&31](n,r),Ws(l),m=u}function bP(e,n){e=e|0,n=n|0}function PP(e,n){return e=e|0,n=n|0,IP(n)|0}function IP(e){return e=e|0,e|0}function BP(){var e=0;return p[8080]|0||(O8(11040),Ht(70,11040,he|0)|0,e=8080,t[e>>2]=1,t[e+4>>2]=0),rr(11040)|0||O8(11040),11040}function O8(e){e=e|0,zP(e),Wp(e,71)}function UP(e){e=e|0,jP(e+24|0)}function jP(e){e=e|0;var n=0,r=0,u=0;r=t[e>>2]|0,u=r,r|0&&(e=e+4|0,n=t[e>>2]|0,(n|0)!=(r|0)&&(t[e>>2]=n+(~((n+-8-u|0)>>>3)<<3)),yt(r))}function zP(e){e=e|0;var n=0;n=dr()|0,Pn(e,5,7,n,VP()|0,0),t[e+24>>2]=0,t[e+28>>2]=0,t[e+32>>2]=0}function qP(e){e=e|0,HP(e)}function HP(e){e=e|0,WP(e)}function WP(e){e=e|0,p[e+8>>0]=1}function VP(){return 1936}function GP(){return YP()|0}function YP(){var e=0,n=0,r=0,u=0,l=0,s=0,h=0;return n=m,m=m+16|0,l=n+4|0,h=n,r=Sa(8)|0,e=r,s=e+4|0,t[s>>2]=cn(1)|0,u=cn(8)|0,s=t[s>>2]|0,t[h>>2]=0,t[l>>2]=t[h>>2],KP(u,s,l),t[r>>2]=u,m=n,e|0}function KP(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]=n,r=cn(16)|0,t[r+4>>2]=0,t[r+8>>2]=0,t[r>>2]=1916,t[r+12>>2]=n,t[e+4>>2]=r}function XP(e){e=e|0,Pv(e),yt(e)}function QP(e){e=e|0,e=t[e+12>>2]|0,e|0&&yt(e)}function JP(e){e=e|0,yt(e)}function ZP(){var e=0;return p[8088]|0||(uI(11076),Ht(25,11076,he|0)|0,e=8088,t[e>>2]=1,t[e+4>>2]=0),11076}function $P(e,n){e=e|0,n=n|0,t[e>>2]=eI()|0,t[e+4>>2]=tI()|0,t[e+12>>2]=n,t[e+8>>2]=nI()|0,t[e+32>>2]=10}function eI(){return 11745}function tI(){return 1940}function nI(){return O1()|0}function rI(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,(Pl(u,896)|0)==512?r|0&&(iI(r),yt(r)):n|0&&yt(n)}function iI(e){e=e|0,e=t[e+4>>2]|0,e|0&&J2(e)}function uI(e){e=e|0,Ha(e)}function jf(e,n){e=e|0,n=n|0,t[e>>2]=n}function qE(e){return e=e|0,t[e>>2]|0}function oI(e){return e=e|0,p[t[e>>2]>>0]|0}function lI(e,n){e=e|0,n=n|0;var r=0,u=0;r=m,m=m+16|0,u=r,t[u>>2]=t[e>>2],sI(n,u)|0,m=r}function sI(e,n){e=e|0,n=n|0;var r=0;return r=aI(t[e>>2]|0,n)|0,n=e+4|0,t[(t[n>>2]|0)+8>>2]=r,t[(t[n>>2]|0)+8>>2]|0}function aI(e,n){e=e|0,n=n|0;var r=0,u=0;return r=m,m=m+16|0,u=r,Ta(u),e=vo(e)|0,n=fI(e,t[n>>2]|0)|0,Ca(u),m=r,n|0}function Ta(e){e=e|0,t[e>>2]=t[2701],t[e+4>>2]=t[2703]}function fI(e,n){e=e|0,n=n|0;var r=0;return r=mo(cI()|0)|0,ji(0,r|0,e|0,bE(n)|0)|0}function Ca(e){e=e|0,g8(t[e>>2]|0,t[e+4>>2]|0)}function cI(){var e=0;return p[8096]|0||(dI(11120),e=8096,t[e>>2]=1,t[e+4>>2]=0),11120}function dI(e){e=e|0,nl(e,pI()|0,1)}function pI(){return 1948}function hI(){vI()}function vI(){var e=0,n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0;if(Ee=m,m=m+16|0,O=Ee+4|0,P=Ee,Ln(65536,10804,t[2702]|0,10812),r=$3()|0,n=t[r>>2]|0,e=t[n>>2]|0,e|0)for(u=t[r+8>>2]|0,r=t[r+4>>2]|0;Wl(e|0,k[r>>0]|0|0,p[u>>0]|0),n=n+4|0,e=t[n>>2]|0,e;)u=u+1|0,r=r+1|0;if(e=e8()|0,n=t[e>>2]|0,n|0)do xo(n|0,t[e+4>>2]|0),e=e+8|0,n=t[e>>2]|0;while((n|0)!=0);xo(mI()|0,5167),M=Mv()|0,e=t[M>>2]|0;e:do if(e|0){do yI(t[e+4>>2]|0),e=t[e>>2]|0;while((e|0)!=0);if(e=t[M>>2]|0,e|0){S=M;do{for(;l=e,e=t[e>>2]|0,l=t[l+4>>2]|0,!!(gI(l)|0);)if(t[P>>2]=S,t[O>>2]=t[P>>2],_I(M,O)|0,!e)break e;if(EI(l),S=t[S>>2]|0,n=k8(l)|0,s=lo()|0,h=m,m=m+((1*(n<<2)|0)+15&-16)|0,D=m,m=m+((1*(n<<2)|0)+15&-16)|0,n=t[(d8(l)|0)>>2]|0,n|0)for(r=h,u=D;t[r>>2]=t[(Nv(t[n+4>>2]|0)|0)>>2],t[u>>2]=t[n+8>>2],n=t[n>>2]|0,n;)r=r+4|0,u=u+4|0;ve=Nv(l)|0,n=DI(l)|0,r=k8(l)|0,u=wI(l)|0,Ao(ve|0,n|0,h|0,D|0,r|0,u|0,OE(l)|0),ci(s|0)}while((e|0)!=0)}}while(0);if(e=t[(kE()|0)>>2]|0,e|0)do ve=e+4|0,M=ME(ve)|0,l=Py(M)|0,s=Fy(M)|0,h=(by(M)|0)+1|0,D=T_(M)|0,S=M8(ve)|0,M=rr(M)|0,O=D_(ve)|0,P=HE(ve)|0,oo(0,l|0,s|0,h|0,D|0,S|0,M|0,O|0,P|0,WE(ve)|0),e=t[e>>2]|0;while((e|0)!=0);e=t[(Mv()|0)>>2]|0;e:do if(e|0){t:for(;;){if(n=t[e+4>>2]|0,n|0?(K=t[(Nv(n)|0)>>2]|0,Pe=t[(p8(n)|0)>>2]|0,Pe|0):0){r=Pe;do{n=r+4|0,u=ME(n)|0;n:do if(u|0)switch(rr(u)|0){case 0:break t;case 4:case 3:case 2:{D=Py(u)|0,S=Fy(u)|0,M=(by(u)|0)+1|0,O=T_(u)|0,P=rr(u)|0,ve=D_(n)|0,oo(K|0,D|0,S|0,M|0,O|0,0,P|0,ve|0,HE(n)|0,WE(n)|0);break n}case 1:{h=Py(u)|0,D=Fy(u)|0,S=(by(u)|0)+1|0,M=T_(u)|0,O=M8(n)|0,P=rr(u)|0,ve=D_(n)|0,oo(K|0,h|0,D|0,S|0,M|0,O|0,P|0,ve|0,HE(n)|0,WE(n)|0);break n}case 5:{M=Py(u)|0,O=Fy(u)|0,P=(by(u)|0)+1|0,ve=T_(u)|0,oo(K|0,M|0,O|0,P|0,ve|0,SI(u)|0,rr(u)|0,0,0,0);break n}default:break n}while(0);r=t[r>>2]|0}while((r|0)!=0)}if(e=t[e>>2]|0,!e)break e}Xn()}while(0);Ms(),m=Ee}function mI(){return 11703}function yI(e){e=e|0,p[e+40>>0]=0}function gI(e){return e=e|0,(p[e+40>>0]|0)!=0|0}function _I(e,n){return e=e|0,n=n|0,n=TI(n)|0,e=t[n>>2]|0,t[n>>2]=t[e>>2],yt(e),t[n>>2]|0}function EI(e){e=e|0,p[e+40>>0]=1}function k8(e){return e=e|0,t[e+20>>2]|0}function DI(e){return e=e|0,t[e+8>>2]|0}function wI(e){return e=e|0,t[e+32>>2]|0}function T_(e){return e=e|0,t[e+4>>2]|0}function M8(e){return e=e|0,t[e+4>>2]|0}function HE(e){return e=e|0,t[e+8>>2]|0}function WE(e){return e=e|0,t[e+16>>2]|0}function SI(e){return e=e|0,t[e+20>>2]|0}function TI(e){return e=e|0,t[e>>2]|0}function C_(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0,st=0,Re=0,Fe=0,Qt=0;Qt=m,m=m+16|0,K=Qt;do if(e>>>0<245){if(M=e>>>0<11?16:e+11&-8,e=M>>>3,P=t[2783]|0,r=P>>>e,r&3|0)return n=(r&1^1)+e|0,e=11172+(n<<1<<2)|0,r=e+8|0,u=t[r>>2]|0,l=u+8|0,s=t[l>>2]|0,(e|0)==(s|0)?t[2783]=P&~(1<>2]=e,t[r>>2]=s),Fe=n<<3,t[u+4>>2]=Fe|3,Fe=u+Fe+4|0,t[Fe>>2]=t[Fe>>2]|1,Fe=l,m=Qt,Fe|0;if(O=t[2785]|0,M>>>0>O>>>0){if(r|0)return n=2<>>12&16,n=n>>>h,r=n>>>5&8,n=n>>>r,l=n>>>2&4,n=n>>>l,e=n>>>1&2,n=n>>>e,u=n>>>1&1,u=(r|h|l|e|u)+(n>>>u)|0,n=11172+(u<<1<<2)|0,e=n+8|0,l=t[e>>2]|0,h=l+8|0,r=t[h>>2]|0,(n|0)==(r|0)?(e=P&~(1<>2]=n,t[e>>2]=r,e=P),s=(u<<3)-M|0,t[l+4>>2]=M|3,u=l+M|0,t[u+4>>2]=s|1,t[u+s>>2]=s,O|0&&(l=t[2788]|0,n=O>>>3,r=11172+(n<<1<<2)|0,n=1<>2]|0):(t[2783]=e|n,n=r,e=r+8|0),t[e>>2]=l,t[n+12>>2]=l,t[l+8>>2]=n,t[l+12>>2]=r),t[2785]=s,t[2788]=u,Fe=h,m=Qt,Fe|0;if(D=t[2784]|0,D){if(r=(D&0-D)+-1|0,h=r>>>12&16,r=r>>>h,s=r>>>5&8,r=r>>>s,S=r>>>2&4,r=r>>>S,u=r>>>1&2,r=r>>>u,e=r>>>1&1,e=t[11436+((s|h|S|u|e)+(r>>>e)<<2)>>2]|0,r=(t[e+4>>2]&-8)-M|0,u=t[e+16+(((t[e+16>>2]|0)==0&1)<<2)>>2]|0,!u)S=e,s=r;else{do h=(t[u+4>>2]&-8)-M|0,S=h>>>0>>0,r=S?h:r,e=S?u:e,u=t[u+16+(((t[u+16>>2]|0)==0&1)<<2)>>2]|0;while((u|0)!=0);S=e,s=r}if(h=S+M|0,S>>>0>>0){l=t[S+24>>2]|0,n=t[S+12>>2]|0;do if((n|0)==(S|0)){if(e=S+20|0,n=t[e>>2]|0,!n&&(e=S+16|0,n=t[e>>2]|0,!n)){r=0;break}for(;;){if(r=n+20|0,u=t[r>>2]|0,u|0){n=u,e=r;continue}if(r=n+16|0,u=t[r>>2]|0,u)n=u,e=r;else break}t[e>>2]=0,r=n}else r=t[S+8>>2]|0,t[r+12>>2]=n,t[n+8>>2]=r,r=n;while(0);do if(l|0){if(n=t[S+28>>2]|0,e=11436+(n<<2)|0,(S|0)==(t[e>>2]|0)){if(t[e>>2]=r,!r){t[2784]=D&~(1<>2]|0)!=(S|0)&1)<<2)>>2]=r,!r)break;t[r+24>>2]=l,n=t[S+16>>2]|0,n|0&&(t[r+16>>2]=n,t[n+24>>2]=r),n=t[S+20>>2]|0,n|0&&(t[r+20>>2]=n,t[n+24>>2]=r)}while(0);return s>>>0<16?(Fe=s+M|0,t[S+4>>2]=Fe|3,Fe=S+Fe+4|0,t[Fe>>2]=t[Fe>>2]|1):(t[S+4>>2]=M|3,t[h+4>>2]=s|1,t[h+s>>2]=s,O|0&&(u=t[2788]|0,n=O>>>3,r=11172+(n<<1<<2)|0,n=1<>2]|0):(t[2783]=P|n,n=r,e=r+8|0),t[e>>2]=u,t[n+12>>2]=u,t[u+8>>2]=n,t[u+12>>2]=r),t[2785]=s,t[2788]=h),Fe=S+8|0,m=Qt,Fe|0}else P=M}else P=M}else P=M}else if(e>>>0<=4294967231)if(e=e+11|0,M=e&-8,S=t[2784]|0,S){u=0-M|0,e=e>>>8,e?M>>>0>16777215?D=31:(P=(e+1048320|0)>>>16&8,Re=e<>>16&4,Re=Re<>>16&2,D=14-(O|P|D)+(Re<>>15)|0,D=M>>>(D+7|0)&1|D<<1):D=0,r=t[11436+(D<<2)>>2]|0;e:do if(!r)r=0,e=0,Re=57;else for(e=0,h=M<<((D|0)==31?0:25-(D>>>1)|0),s=0;;){if(l=(t[r+4>>2]&-8)-M|0,l>>>0>>0)if(l)e=r,u=l;else{e=r,u=0,l=r,Re=61;break e}if(l=t[r+20>>2]|0,r=t[r+16+(h>>>31<<2)>>2]|0,s=(l|0)==0|(l|0)==(r|0)?s:l,l=(r|0)==0,l){r=s,Re=57;break}else h=h<<((l^1)&1)}while(0);if((Re|0)==57){if((r|0)==0&(e|0)==0){if(e=2<>>12&16,P=P>>>h,s=P>>>5&8,P=P>>>s,D=P>>>2&4,P=P>>>D,O=P>>>1&2,P=P>>>O,r=P>>>1&1,e=0,r=t[11436+((s|h|D|O|r)+(P>>>r)<<2)>>2]|0}r?(l=r,Re=61):(D=e,h=u)}if((Re|0)==61)for(;;)if(Re=0,r=(t[l+4>>2]&-8)-M|0,P=r>>>0>>0,r=P?r:u,e=P?l:e,l=t[l+16+(((t[l+16>>2]|0)==0&1)<<2)>>2]|0,l)u=r,Re=61;else{D=e,h=r;break}if((D|0)!=0?h>>>0<((t[2785]|0)-M|0)>>>0:0){if(s=D+M|0,D>>>0>=s>>>0)return Fe=0,m=Qt,Fe|0;l=t[D+24>>2]|0,n=t[D+12>>2]|0;do if((n|0)==(D|0)){if(e=D+20|0,n=t[e>>2]|0,!n&&(e=D+16|0,n=t[e>>2]|0,!n)){n=0;break}for(;;){if(r=n+20|0,u=t[r>>2]|0,u|0){n=u,e=r;continue}if(r=n+16|0,u=t[r>>2]|0,u)n=u,e=r;else break}t[e>>2]=0}else Fe=t[D+8>>2]|0,t[Fe+12>>2]=n,t[n+8>>2]=Fe;while(0);do if(l){if(e=t[D+28>>2]|0,r=11436+(e<<2)|0,(D|0)==(t[r>>2]|0)){if(t[r>>2]=n,!n){u=S&~(1<>2]|0)!=(D|0)&1)<<2)>>2]=n,!n){u=S;break}t[n+24>>2]=l,e=t[D+16>>2]|0,e|0&&(t[n+16>>2]=e,t[e+24>>2]=n),e=t[D+20>>2]|0,e&&(t[n+20>>2]=e,t[e+24>>2]=n),u=S}else u=S;while(0);do if(h>>>0>=16){if(t[D+4>>2]=M|3,t[s+4>>2]=h|1,t[s+h>>2]=h,n=h>>>3,h>>>0<256){r=11172+(n<<1<<2)|0,e=t[2783]|0,n=1<>2]|0):(t[2783]=e|n,n=r,e=r+8|0),t[e>>2]=s,t[n+12>>2]=s,t[s+8>>2]=n,t[s+12>>2]=r;break}if(n=h>>>8,n?h>>>0>16777215?n=31:(Re=(n+1048320|0)>>>16&8,Fe=n<>>16&4,Fe=Fe<>>16&2,n=14-(st|Re|n)+(Fe<>>15)|0,n=h>>>(n+7|0)&1|n<<1):n=0,r=11436+(n<<2)|0,t[s+28>>2]=n,e=s+16|0,t[e+4>>2]=0,t[e>>2]=0,e=1<>2]=s,t[s+24>>2]=r,t[s+12>>2]=s,t[s+8>>2]=s;break}for(e=h<<((n|0)==31?0:25-(n>>>1)|0),r=t[r>>2]|0;;){if((t[r+4>>2]&-8|0)==(h|0)){Re=97;break}if(u=r+16+(e>>>31<<2)|0,n=t[u>>2]|0,n)e=e<<1,r=n;else{Re=96;break}}if((Re|0)==96){t[u>>2]=s,t[s+24>>2]=r,t[s+12>>2]=s,t[s+8>>2]=s;break}else if((Re|0)==97){Re=r+8|0,Fe=t[Re>>2]|0,t[Fe+12>>2]=s,t[Re>>2]=s,t[s+8>>2]=Fe,t[s+12>>2]=r,t[s+24>>2]=0;break}}else Fe=h+M|0,t[D+4>>2]=Fe|3,Fe=D+Fe+4|0,t[Fe>>2]=t[Fe>>2]|1;while(0);return Fe=D+8|0,m=Qt,Fe|0}else P=M}else P=M;else P=-1;while(0);if(r=t[2785]|0,r>>>0>=P>>>0)return n=r-P|0,e=t[2788]|0,n>>>0>15?(Fe=e+P|0,t[2788]=Fe,t[2785]=n,t[Fe+4>>2]=n|1,t[Fe+n>>2]=n,t[e+4>>2]=P|3):(t[2785]=0,t[2788]=0,t[e+4>>2]=r|3,Fe=e+r+4|0,t[Fe>>2]=t[Fe>>2]|1),Fe=e+8|0,m=Qt,Fe|0;if(h=t[2786]|0,h>>>0>P>>>0)return st=h-P|0,t[2786]=st,Fe=t[2789]|0,Re=Fe+P|0,t[2789]=Re,t[Re+4>>2]=st|1,t[Fe+4>>2]=P|3,Fe=Fe+8|0,m=Qt,Fe|0;if(t[2901]|0?e=t[2903]|0:(t[2903]=4096,t[2902]=4096,t[2904]=-1,t[2905]=-1,t[2906]=0,t[2894]=0,e=K&-16^1431655768,t[K>>2]=e,t[2901]=e,e=4096),D=P+48|0,S=P+47|0,s=e+S|0,l=0-e|0,M=s&l,M>>>0<=P>>>0||(e=t[2893]|0,e|0?(O=t[2891]|0,K=O+M|0,K>>>0<=O>>>0|K>>>0>e>>>0):0))return Fe=0,m=Qt,Fe|0;e:do if(t[2894]&4)n=0,Re=133;else{r=t[2789]|0;t:do if(r){for(u=11580;e=t[u>>2]|0,!(e>>>0<=r>>>0?(ve=u+4|0,(e+(t[ve>>2]|0)|0)>>>0>r>>>0):0);)if(e=t[u+8>>2]|0,e)u=e;else{Re=118;break t}if(n=s-h&l,n>>>0<2147483647)if(e=Z2(n|0)|0,(e|0)==((t[u>>2]|0)+(t[ve>>2]|0)|0)){if((e|0)!=(-1|0)){h=n,s=e,Re=135;break e}}else u=e,Re=126;else n=0}else Re=118;while(0);do if((Re|0)==118)if(r=Z2(0)|0,(r|0)!=(-1|0)?(n=r,Pe=t[2902]|0,Ee=Pe+-1|0,n=((Ee&n|0)==0?0:(Ee+n&0-Pe)-n|0)+M|0,Pe=t[2891]|0,Ee=n+Pe|0,n>>>0>P>>>0&n>>>0<2147483647):0){if(ve=t[2893]|0,ve|0?Ee>>>0<=Pe>>>0|Ee>>>0>ve>>>0:0){n=0;break}if(e=Z2(n|0)|0,(e|0)==(r|0)){h=n,s=r,Re=135;break e}else u=e,Re=126}else n=0;while(0);do if((Re|0)==126){if(r=0-n|0,!(D>>>0>n>>>0&(n>>>0<2147483647&(u|0)!=(-1|0))))if((u|0)==(-1|0)){n=0;break}else{h=n,s=u,Re=135;break e}if(e=t[2903]|0,e=S-n+e&0-e,e>>>0>=2147483647){h=n,s=u,Re=135;break e}if((Z2(e|0)|0)==(-1|0)){Z2(r|0)|0,n=0;break}else{h=e+n|0,s=u,Re=135;break e}}while(0);t[2894]=t[2894]|4,Re=133}while(0);if((((Re|0)==133?M>>>0<2147483647:0)?(st=Z2(M|0)|0,ve=Z2(0)|0,Qe=ve-st|0,We=Qe>>>0>(P+40|0)>>>0,!((st|0)==(-1|0)|We^1|st>>>0>>0&((st|0)!=(-1|0)&(ve|0)!=(-1|0))^1)):0)&&(h=We?Qe:n,s=st,Re=135),(Re|0)==135){n=(t[2891]|0)+h|0,t[2891]=n,n>>>0>(t[2892]|0)>>>0&&(t[2892]=n),S=t[2789]|0;do if(S){for(n=11580;;){if(e=t[n>>2]|0,r=n+4|0,u=t[r>>2]|0,(s|0)==(e+u|0)){Re=145;break}if(l=t[n+8>>2]|0,l)n=l;else break}if(((Re|0)==145?(t[n+12>>2]&8|0)==0:0)?S>>>0>>0&S>>>0>=e>>>0:0){t[r>>2]=u+h,Fe=S+8|0,Fe=(Fe&7|0)==0?0:0-Fe&7,Re=S+Fe|0,Fe=(t[2786]|0)+(h-Fe)|0,t[2789]=Re,t[2786]=Fe,t[Re+4>>2]=Fe|1,t[Re+Fe+4>>2]=40,t[2790]=t[2905];break}for(s>>>0<(t[2787]|0)>>>0&&(t[2787]=s),r=s+h|0,n=11580;;){if((t[n>>2]|0)==(r|0)){Re=153;break}if(e=t[n+8>>2]|0,e)n=e;else break}if((Re|0)==153?(t[n+12>>2]&8|0)==0:0){t[n>>2]=s,O=n+4|0,t[O>>2]=(t[O>>2]|0)+h,O=s+8|0,O=s+((O&7|0)==0?0:0-O&7)|0,n=r+8|0,n=r+((n&7|0)==0?0:0-n&7)|0,M=O+P|0,D=n-O-P|0,t[O+4>>2]=P|3;do if((n|0)!=(S|0)){if((n|0)==(t[2788]|0)){Fe=(t[2785]|0)+D|0,t[2785]=Fe,t[2788]=M,t[M+4>>2]=Fe|1,t[M+Fe>>2]=Fe;break}if(e=t[n+4>>2]|0,(e&3|0)==1){h=e&-8,u=e>>>3;e:do if(e>>>0<256)if(e=t[n+8>>2]|0,r=t[n+12>>2]|0,(r|0)==(e|0)){t[2783]=t[2783]&~(1<>2]=r,t[r+8>>2]=e;break}else{s=t[n+24>>2]|0,e=t[n+12>>2]|0;do if((e|0)==(n|0)){if(u=n+16|0,r=u+4|0,e=t[r>>2]|0,!e)if(e=t[u>>2]|0,e)r=u;else{e=0;break}for(;;){if(u=e+20|0,l=t[u>>2]|0,l|0){e=l,r=u;continue}if(u=e+16|0,l=t[u>>2]|0,l)e=l,r=u;else break}t[r>>2]=0}else Fe=t[n+8>>2]|0,t[Fe+12>>2]=e,t[e+8>>2]=Fe;while(0);if(!s)break;r=t[n+28>>2]|0,u=11436+(r<<2)|0;do if((n|0)!=(t[u>>2]|0)){if(t[s+16+(((t[s+16>>2]|0)!=(n|0)&1)<<2)>>2]=e,!e)break e}else{if(t[u>>2]=e,e|0)break;t[2784]=t[2784]&~(1<>2]=s,r=n+16|0,u=t[r>>2]|0,u|0&&(t[e+16>>2]=u,t[u+24>>2]=e),r=t[r+4>>2]|0,!r)break;t[e+20>>2]=r,t[r+24>>2]=e}while(0);n=n+h|0,l=h+D|0}else l=D;if(n=n+4|0,t[n>>2]=t[n>>2]&-2,t[M+4>>2]=l|1,t[M+l>>2]=l,n=l>>>3,l>>>0<256){r=11172+(n<<1<<2)|0,e=t[2783]|0,n=1<>2]|0):(t[2783]=e|n,n=r,e=r+8|0),t[e>>2]=M,t[n+12>>2]=M,t[M+8>>2]=n,t[M+12>>2]=r;break}n=l>>>8;do if(!n)n=0;else{if(l>>>0>16777215){n=31;break}Re=(n+1048320|0)>>>16&8,Fe=n<>>16&4,Fe=Fe<>>16&2,n=14-(st|Re|n)+(Fe<>>15)|0,n=l>>>(n+7|0)&1|n<<1}while(0);if(u=11436+(n<<2)|0,t[M+28>>2]=n,e=M+16|0,t[e+4>>2]=0,t[e>>2]=0,e=t[2784]|0,r=1<>2]=M,t[M+24>>2]=u,t[M+12>>2]=M,t[M+8>>2]=M;break}for(e=l<<((n|0)==31?0:25-(n>>>1)|0),r=t[u>>2]|0;;){if((t[r+4>>2]&-8|0)==(l|0)){Re=194;break}if(u=r+16+(e>>>31<<2)|0,n=t[u>>2]|0,n)e=e<<1,r=n;else{Re=193;break}}if((Re|0)==193){t[u>>2]=M,t[M+24>>2]=r,t[M+12>>2]=M,t[M+8>>2]=M;break}else if((Re|0)==194){Re=r+8|0,Fe=t[Re>>2]|0,t[Fe+12>>2]=M,t[Re>>2]=M,t[M+8>>2]=Fe,t[M+12>>2]=r,t[M+24>>2]=0;break}}else Fe=(t[2786]|0)+D|0,t[2786]=Fe,t[2789]=M,t[M+4>>2]=Fe|1;while(0);return Fe=O+8|0,m=Qt,Fe|0}for(n=11580;e=t[n>>2]|0,!(e>>>0<=S>>>0?(Fe=e+(t[n+4>>2]|0)|0,Fe>>>0>S>>>0):0);)n=t[n+8>>2]|0;l=Fe+-47|0,e=l+8|0,e=l+((e&7|0)==0?0:0-e&7)|0,l=S+16|0,e=e>>>0>>0?S:e,n=e+8|0,r=s+8|0,r=(r&7|0)==0?0:0-r&7,Re=s+r|0,r=h+-40-r|0,t[2789]=Re,t[2786]=r,t[Re+4>>2]=r|1,t[Re+r+4>>2]=40,t[2790]=t[2905],r=e+4|0,t[r>>2]=27,t[n>>2]=t[2895],t[n+4>>2]=t[2896],t[n+8>>2]=t[2897],t[n+12>>2]=t[2898],t[2895]=s,t[2896]=h,t[2898]=0,t[2897]=n,n=e+24|0;do Re=n,n=n+4|0,t[n>>2]=7;while((Re+8|0)>>>0>>0);if((e|0)!=(S|0)){if(s=e-S|0,t[r>>2]=t[r>>2]&-2,t[S+4>>2]=s|1,t[e>>2]=s,n=s>>>3,s>>>0<256){r=11172+(n<<1<<2)|0,e=t[2783]|0,n=1<>2]|0):(t[2783]=e|n,n=r,e=r+8|0),t[e>>2]=S,t[n+12>>2]=S,t[S+8>>2]=n,t[S+12>>2]=r;break}if(n=s>>>8,n?s>>>0>16777215?r=31:(Re=(n+1048320|0)>>>16&8,Fe=n<>>16&4,Fe=Fe<>>16&2,r=14-(st|Re|r)+(Fe<>>15)|0,r=s>>>(r+7|0)&1|r<<1):r=0,u=11436+(r<<2)|0,t[S+28>>2]=r,t[S+20>>2]=0,t[l>>2]=0,n=t[2784]|0,e=1<>2]=S,t[S+24>>2]=u,t[S+12>>2]=S,t[S+8>>2]=S;break}for(e=s<<((r|0)==31?0:25-(r>>>1)|0),r=t[u>>2]|0;;){if((t[r+4>>2]&-8|0)==(s|0)){Re=216;break}if(u=r+16+(e>>>31<<2)|0,n=t[u>>2]|0,n)e=e<<1,r=n;else{Re=215;break}}if((Re|0)==215){t[u>>2]=S,t[S+24>>2]=r,t[S+12>>2]=S,t[S+8>>2]=S;break}else if((Re|0)==216){Re=r+8|0,Fe=t[Re>>2]|0,t[Fe+12>>2]=S,t[Re>>2]=S,t[S+8>>2]=Fe,t[S+12>>2]=r,t[S+24>>2]=0;break}}}else{Fe=t[2787]|0,(Fe|0)==0|s>>>0>>0&&(t[2787]=s),t[2895]=s,t[2896]=h,t[2898]=0,t[2792]=t[2901],t[2791]=-1,n=0;do Fe=11172+(n<<1<<2)|0,t[Fe+12>>2]=Fe,t[Fe+8>>2]=Fe,n=n+1|0;while((n|0)!=32);Fe=s+8|0,Fe=(Fe&7|0)==0?0:0-Fe&7,Re=s+Fe|0,Fe=h+-40-Fe|0,t[2789]=Re,t[2786]=Fe,t[Re+4>>2]=Fe|1,t[Re+Fe+4>>2]=40,t[2790]=t[2905]}while(0);if(n=t[2786]|0,n>>>0>P>>>0)return st=n-P|0,t[2786]=st,Fe=t[2789]|0,Re=Fe+P|0,t[2789]=Re,t[Re+4>>2]=st|1,t[Fe+4>>2]=P|3,Fe=Fe+8|0,m=Qt,Fe|0}return t[(Fv()|0)>>2]=12,Fe=0,m=Qt,Fe|0}function x_(e){e=e|0;var n=0,r=0,u=0,l=0,s=0,h=0,D=0,S=0;if(!!e){r=e+-8|0,l=t[2787]|0,e=t[e+-4>>2]|0,n=e&-8,S=r+n|0;do if(e&1)D=r,h=r;else{if(u=t[r>>2]|0,!(e&3)||(h=r+(0-u)|0,s=u+n|0,h>>>0>>0))return;if((h|0)==(t[2788]|0)){if(e=S+4|0,n=t[e>>2]|0,(n&3|0)!=3){D=h,n=s;break}t[2785]=s,t[e>>2]=n&-2,t[h+4>>2]=s|1,t[h+s>>2]=s;return}if(r=u>>>3,u>>>0<256)if(e=t[h+8>>2]|0,n=t[h+12>>2]|0,(n|0)==(e|0)){t[2783]=t[2783]&~(1<>2]=n,t[n+8>>2]=e,D=h,n=s;break}l=t[h+24>>2]|0,e=t[h+12>>2]|0;do if((e|0)==(h|0)){if(r=h+16|0,n=r+4|0,e=t[n>>2]|0,!e)if(e=t[r>>2]|0,e)n=r;else{e=0;break}for(;;){if(r=e+20|0,u=t[r>>2]|0,u|0){e=u,n=r;continue}if(r=e+16|0,u=t[r>>2]|0,u)e=u,n=r;else break}t[n>>2]=0}else D=t[h+8>>2]|0,t[D+12>>2]=e,t[e+8>>2]=D;while(0);if(l){if(n=t[h+28>>2]|0,r=11436+(n<<2)|0,(h|0)==(t[r>>2]|0)){if(t[r>>2]=e,!e){t[2784]=t[2784]&~(1<>2]|0)!=(h|0)&1)<<2)>>2]=e,!e){D=h,n=s;break}t[e+24>>2]=l,n=h+16|0,r=t[n>>2]|0,r|0&&(t[e+16>>2]=r,t[r+24>>2]=e),n=t[n+4>>2]|0,n?(t[e+20>>2]=n,t[n+24>>2]=e,D=h,n=s):(D=h,n=s)}else D=h,n=s}while(0);if(!(h>>>0>=S>>>0)&&(e=S+4|0,u=t[e>>2]|0,!!(u&1))){if(u&2)t[e>>2]=u&-2,t[D+4>>2]=n|1,t[h+n>>2]=n,l=n;else{if(e=t[2788]|0,(S|0)==(t[2789]|0)){if(S=(t[2786]|0)+n|0,t[2786]=S,t[2789]=D,t[D+4>>2]=S|1,(D|0)!=(e|0))return;t[2788]=0,t[2785]=0;return}if((S|0)==(e|0)){S=(t[2785]|0)+n|0,t[2785]=S,t[2788]=h,t[D+4>>2]=S|1,t[h+S>>2]=S;return}l=(u&-8)+n|0,r=u>>>3;do if(u>>>0<256)if(n=t[S+8>>2]|0,e=t[S+12>>2]|0,(e|0)==(n|0)){t[2783]=t[2783]&~(1<>2]=e,t[e+8>>2]=n;break}else{s=t[S+24>>2]|0,e=t[S+12>>2]|0;do if((e|0)==(S|0)){if(r=S+16|0,n=r+4|0,e=t[n>>2]|0,!e)if(e=t[r>>2]|0,e)n=r;else{r=0;break}for(;;){if(r=e+20|0,u=t[r>>2]|0,u|0){e=u,n=r;continue}if(r=e+16|0,u=t[r>>2]|0,u)e=u,n=r;else break}t[n>>2]=0,r=e}else r=t[S+8>>2]|0,t[r+12>>2]=e,t[e+8>>2]=r,r=e;while(0);if(s|0){if(e=t[S+28>>2]|0,n=11436+(e<<2)|0,(S|0)==(t[n>>2]|0)){if(t[n>>2]=r,!r){t[2784]=t[2784]&~(1<>2]|0)!=(S|0)&1)<<2)>>2]=r,!r)break;t[r+24>>2]=s,e=S+16|0,n=t[e>>2]|0,n|0&&(t[r+16>>2]=n,t[n+24>>2]=r),e=t[e+4>>2]|0,e|0&&(t[r+20>>2]=e,t[e+24>>2]=r)}}while(0);if(t[D+4>>2]=l|1,t[h+l>>2]=l,(D|0)==(t[2788]|0)){t[2785]=l;return}}if(e=l>>>3,l>>>0<256){r=11172+(e<<1<<2)|0,n=t[2783]|0,e=1<>2]|0):(t[2783]=n|e,e=r,n=r+8|0),t[n>>2]=D,t[e+12>>2]=D,t[D+8>>2]=e,t[D+12>>2]=r;return}e=l>>>8,e?l>>>0>16777215?e=31:(h=(e+1048320|0)>>>16&8,S=e<>>16&4,S=S<>>16&2,e=14-(s|h|e)+(S<>>15)|0,e=l>>>(e+7|0)&1|e<<1):e=0,u=11436+(e<<2)|0,t[D+28>>2]=e,t[D+20>>2]=0,t[D+16>>2]=0,n=t[2784]|0,r=1<>>1)|0),r=t[u>>2]|0;;){if((t[r+4>>2]&-8|0)==(l|0)){e=73;break}if(u=r+16+(n>>>31<<2)|0,e=t[u>>2]|0,e)n=n<<1,r=e;else{e=72;break}}if((e|0)==72){t[u>>2]=D,t[D+24>>2]=r,t[D+12>>2]=D,t[D+8>>2]=D;break}else if((e|0)==73){h=r+8|0,S=t[h>>2]|0,t[S+12>>2]=D,t[h>>2]=D,t[D+8>>2]=S,t[D+12>>2]=r,t[D+24>>2]=0;break}}else t[2784]=n|r,t[u>>2]=D,t[D+24>>2]=u,t[D+12>>2]=D,t[D+8>>2]=D;while(0);if(S=(t[2791]|0)+-1|0,t[2791]=S,!S)e=11588;else return;for(;e=t[e>>2]|0,e;)e=e+8|0;t[2791]=-1}}}function CI(){return 11628}function xI(e){e=e|0;var n=0,r=0;return n=m,m=m+16|0,r=n,t[r>>2]=OI(t[e+60>>2]|0)|0,e=A_(wu(6,r|0)|0)|0,m=n,e|0}function N8(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0;P=m,m=m+48|0,M=P+16|0,s=P,l=P+32|0,D=e+28|0,u=t[D>>2]|0,t[l>>2]=u,S=e+20|0,u=(t[S>>2]|0)-u|0,t[l+4>>2]=u,t[l+8>>2]=n,t[l+12>>2]=r,u=u+r|0,h=e+60|0,t[s>>2]=t[h>>2],t[s+4>>2]=l,t[s+8>>2]=2,s=A_(d0(146,s|0)|0)|0;e:do if((u|0)!=(s|0)){for(n=2;!((s|0)<0);)if(u=u-s|0,Pe=t[l+4>>2]|0,K=s>>>0>Pe>>>0,l=K?l+8|0:l,n=(K<<31>>31)+n|0,Pe=s-(K?Pe:0)|0,t[l>>2]=(t[l>>2]|0)+Pe,K=l+4|0,t[K>>2]=(t[K>>2]|0)-Pe,t[M>>2]=t[h>>2],t[M+4>>2]=l,t[M+8>>2]=n,s=A_(d0(146,M|0)|0)|0,(u|0)==(s|0)){O=3;break e}t[e+16>>2]=0,t[D>>2]=0,t[S>>2]=0,t[e>>2]=t[e>>2]|32,(n|0)==2?r=0:r=r-(t[l+4>>2]|0)|0}else O=3;while(0);return(O|0)==3&&(Pe=t[e+44>>2]|0,t[e+16>>2]=Pe+(t[e+48>>2]|0),t[D>>2]=Pe,t[S>>2]=Pe),m=P,r|0}function AI(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;return l=m,m=m+32|0,s=l,u=l+20|0,t[s>>2]=t[e+60>>2],t[s+4>>2]=0,t[s+8>>2]=n,t[s+12>>2]=u,t[s+16>>2]=r,(A_(Ti(140,s|0)|0)|0)<0?(t[u>>2]=-1,e=-1):e=t[u>>2]|0,m=l,e|0}function A_(e){return e=e|0,e>>>0>4294963200&&(t[(Fv()|0)>>2]=0-e,e=-1),e|0}function Fv(){return(RI()|0)+64|0}function RI(){return VE()|0}function VE(){return 2084}function OI(e){return e=e|0,e|0}function kI(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;return l=m,m=m+32|0,u=l,t[e+36>>2]=1,((t[e>>2]&64|0)==0?(t[u>>2]=t[e+60>>2],t[u+4>>2]=21523,t[u+8>>2]=l+16,b0(54,u|0)|0):0)&&(p[e+75>>0]=-1),u=N8(e,n,r)|0,m=l,u|0}function L8(e,n){e=e|0,n=n|0;var r=0,u=0;if(r=p[e>>0]|0,u=p[n>>0]|0,r<<24>>24==0?1:r<<24>>24!=u<<24>>24)e=u;else{do e=e+1|0,n=n+1|0,r=p[e>>0]|0,u=p[n>>0]|0;while(!(r<<24>>24==0?1:r<<24>>24!=u<<24>>24));e=u}return(r&255)-(e&255)|0}function MI(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0;e:do if(!r)e=0;else{for(;u=p[e>>0]|0,l=p[n>>0]|0,u<<24>>24==l<<24>>24;)if(r=r+-1|0,r)e=e+1|0,n=n+1|0;else{e=0;break e}e=(u&255)-(l&255)|0}while(0);return e|0}function F8(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0;ve=m,m=m+224|0,O=ve+120|0,P=ve+80|0,Pe=ve,Ee=ve+136|0,u=P,l=u+40|0;do t[u>>2]=0,u=u+4|0;while((u|0)<(l|0));return t[O>>2]=t[r>>2],(GE(0,n,O,Pe,P)|0)<0?r=-1:((t[e+76>>2]|0)>-1?K=NI(e)|0:K=0,r=t[e>>2]|0,M=r&32,(p[e+74>>0]|0)<1&&(t[e>>2]=r&-33),u=e+48|0,t[u>>2]|0?r=GE(e,n,O,Pe,P)|0:(l=e+44|0,s=t[l>>2]|0,t[l>>2]=Ee,h=e+28|0,t[h>>2]=Ee,D=e+20|0,t[D>>2]=Ee,t[u>>2]=80,S=e+16|0,t[S>>2]=Ee+80,r=GE(e,n,O,Pe,P)|0,s&&(M_[t[e+36>>2]&7](e,0,0)|0,r=(t[D>>2]|0)==0?-1:r,t[l>>2]=s,t[u>>2]=0,t[S>>2]=0,t[h>>2]=0,t[D>>2]=0)),u=t[e>>2]|0,t[e>>2]=u|M,K|0&&LI(e),r=(u&32|0)==0?r:-1),m=ve,r|0}function GE(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0,st=0,Re=0,Fe=0,Qt=0,Lr=0,Nn=0,mn=0,hr=0,kr=0,On=0;On=m,m=m+64|0,Nn=On+16|0,mn=On,Qt=On+24|0,hr=On+8|0,kr=On+20|0,t[Nn>>2]=n,st=(e|0)!=0,Re=Qt+40|0,Fe=Re,Qt=Qt+39|0,Lr=hr+4|0,h=0,s=0,O=0;e:for(;;){do if((s|0)>-1)if((h|0)>(2147483647-s|0)){t[(Fv()|0)>>2]=75,s=-1;break}else{s=h+s|0;break}while(0);if(h=p[n>>0]|0,h<<24>>24)D=n;else{We=87;break}t:for(;;){switch(h<<24>>24){case 37:{h=D,We=9;break t}case 0:{h=D;break t}default:}Qe=D+1|0,t[Nn>>2]=Qe,h=p[Qe>>0]|0,D=Qe}t:do if((We|0)==9)for(;;){if(We=0,(p[D+1>>0]|0)!=37)break t;if(h=h+1|0,D=D+2|0,t[Nn>>2]=D,(p[D>>0]|0)==37)We=9;else break}while(0);if(h=h-n|0,st&&qo(e,n,h),h|0){n=D;continue}S=D+1|0,h=(p[S>>0]|0)+-48|0,h>>>0<10?(Qe=(p[D+2>>0]|0)==36,ve=Qe?h:-1,O=Qe?1:O,S=Qe?D+3|0:S):ve=-1,t[Nn>>2]=S,h=p[S>>0]|0,D=(h<<24>>24)+-32|0;t:do if(D>>>0<32)for(M=0,P=h;;){if(h=1<>2]=S,h=p[S>>0]|0,D=(h<<24>>24)+-32|0,D>>>0>=32)break;P=h}else M=0;while(0);if(h<<24>>24==42){if(D=S+1|0,h=(p[D>>0]|0)+-48|0,h>>>0<10?(p[S+2>>0]|0)==36:0)t[l+(h<<2)>>2]=10,h=t[u+((p[D>>0]|0)+-48<<3)>>2]|0,O=1,S=S+3|0;else{if(O|0){s=-1;break}st?(O=(t[r>>2]|0)+(4-1)&~(4-1),h=t[O>>2]|0,t[r>>2]=O+4,O=0,S=D):(h=0,O=0,S=D)}t[Nn>>2]=S,Qe=(h|0)<0,h=Qe?0-h|0:h,M=Qe?M|8192:M}else{if(h=b8(Nn)|0,(h|0)<0){s=-1;break}S=t[Nn>>2]|0}do if((p[S>>0]|0)==46){if((p[S+1>>0]|0)!=42){t[Nn>>2]=S+1,D=b8(Nn)|0,S=t[Nn>>2]|0;break}if(P=S+2|0,D=(p[P>>0]|0)+-48|0,D>>>0<10?(p[S+3>>0]|0)==36:0){t[l+(D<<2)>>2]=10,D=t[u+((p[P>>0]|0)+-48<<3)>>2]|0,S=S+4|0,t[Nn>>2]=S;break}if(O|0){s=-1;break e}st?(Qe=(t[r>>2]|0)+(4-1)&~(4-1),D=t[Qe>>2]|0,t[r>>2]=Qe+4):D=0,t[Nn>>2]=P,S=P}else D=-1;while(0);for(Ee=0;;){if(((p[S>>0]|0)+-65|0)>>>0>57){s=-1;break e}if(Qe=S+1|0,t[Nn>>2]=Qe,P=p[(p[S>>0]|0)+-65+(5178+(Ee*58|0))>>0]|0,K=P&255,(K+-1|0)>>>0<8)Ee=K,S=Qe;else break}if(!(P<<24>>24)){s=-1;break}Pe=(ve|0)>-1;do if(P<<24>>24==19)if(Pe){s=-1;break e}else We=49;else{if(Pe){t[l+(ve<<2)>>2]=K,Pe=u+(ve<<3)|0,ve=t[Pe+4>>2]|0,We=mn,t[We>>2]=t[Pe>>2],t[We+4>>2]=ve,We=49;break}if(!st){s=0;break e}P8(mn,K,r)}while(0);if((We|0)==49?(We=0,!st):0){h=0,n=Qe;continue}S=p[S>>0]|0,S=(Ee|0)!=0&(S&15|0)==3?S&-33:S,Pe=M&-65537,ve=(M&8192|0)==0?M:Pe;t:do switch(S|0){case 110:switch((Ee&255)<<24>>24){case 0:{t[t[mn>>2]>>2]=s,h=0,n=Qe;continue e}case 1:{t[t[mn>>2]>>2]=s,h=0,n=Qe;continue e}case 2:{h=t[mn>>2]|0,t[h>>2]=s,t[h+4>>2]=((s|0)<0)<<31>>31,h=0,n=Qe;continue e}case 3:{E[t[mn>>2]>>1]=s,h=0,n=Qe;continue e}case 4:{p[t[mn>>2]>>0]=s,h=0,n=Qe;continue e}case 6:{t[t[mn>>2]>>2]=s,h=0,n=Qe;continue e}case 7:{h=t[mn>>2]|0,t[h>>2]=s,t[h+4>>2]=((s|0)<0)<<31>>31,h=0,n=Qe;continue e}default:{h=0,n=Qe;continue e}}case 112:{S=120,D=D>>>0>8?D:8,n=ve|8,We=61;break}case 88:case 120:{n=ve,We=61;break}case 111:{S=mn,n=t[S>>2]|0,S=t[S+4>>2]|0,K=bI(n,S,Re)|0,Pe=Fe-K|0,M=0,P=5642,D=(ve&8|0)==0|(D|0)>(Pe|0)?D:Pe+1|0,Pe=ve,We=67;break}case 105:case 100:if(S=mn,n=t[S>>2]|0,S=t[S+4>>2]|0,(S|0)<0){n=R_(0,0,n|0,S|0)|0,S=ft,M=mn,t[M>>2]=n,t[M+4>>2]=S,M=1,P=5642,We=66;break t}else{M=(ve&2049|0)!=0&1,P=(ve&2048|0)==0?(ve&1|0)==0?5642:5644:5643,We=66;break t}case 117:{S=mn,M=0,P=5642,n=t[S>>2]|0,S=t[S+4>>2]|0,We=66;break}case 99:{p[Qt>>0]=t[mn>>2],n=Qt,M=0,P=5642,K=Re,S=1,D=Pe;break}case 109:{S=PI(t[(Fv()|0)>>2]|0)|0,We=71;break}case 115:{S=t[mn>>2]|0,S=S|0?S:5652,We=71;break}case 67:{t[hr>>2]=t[mn>>2],t[Lr>>2]=0,t[mn>>2]=hr,K=-1,S=hr,We=75;break}case 83:{n=t[mn>>2]|0,D?(K=D,S=n,We=75):(hl(e,32,h,0,ve),n=0,We=84);break}case 65:case 71:case 70:case 69:case 97:case 103:case 102:case 101:{h=BI(e,+U[mn>>3],h,D,ve,S)|0,n=Qe;continue e}default:M=0,P=5642,K=Re,S=D,D=ve}while(0);t:do if((We|0)==61)ve=mn,Ee=t[ve>>2]|0,ve=t[ve+4>>2]|0,K=FI(Ee,ve,Re,S&32)|0,P=(n&8|0)==0|(Ee|0)==0&(ve|0)==0,M=P?0:2,P=P?5642:5642+(S>>4)|0,Pe=n,n=Ee,S=ve,We=67;else if((We|0)==66)K=bv(n,S,Re)|0,Pe=ve,We=67;else if((We|0)==71)We=0,ve=II(S,0,D)|0,Ee=(ve|0)==0,n=S,M=0,P=5642,K=Ee?S+D|0:ve,S=Ee?D:ve-S|0,D=Pe;else if((We|0)==75){for(We=0,P=S,n=0,D=0;M=t[P>>2]|0,!(!M||(D=I8(kr,M)|0,(D|0)<0|D>>>0>(K-n|0)>>>0));)if(n=D+n|0,K>>>0>n>>>0)P=P+4|0;else break;if((D|0)<0){s=-1;break e}if(hl(e,32,h,n,ve),!n)n=0,We=84;else for(M=0;;){if(D=t[S>>2]|0,!D){We=84;break t}if(D=I8(kr,D)|0,M=D+M|0,(M|0)>(n|0)){We=84;break t}if(qo(e,kr,D),M>>>0>=n>>>0){We=84;break}else S=S+4|0}}while(0);if((We|0)==67)We=0,S=(n|0)!=0|(S|0)!=0,ve=(D|0)!=0|S,S=((S^1)&1)+(Fe-K)|0,n=ve?K:Re,K=Re,S=ve?(D|0)>(S|0)?D:S:D,D=(D|0)>-1?Pe&-65537:Pe;else if((We|0)==84){We=0,hl(e,32,h,n,ve^8192),h=(h|0)>(n|0)?h:n,n=Qe;continue}Ee=K-n|0,Pe=(S|0)<(Ee|0)?Ee:S,ve=Pe+M|0,h=(h|0)<(ve|0)?ve:h,hl(e,32,h,ve,D),qo(e,P,M),hl(e,48,h,ve,D^65536),hl(e,48,Pe,Ee,0),qo(e,n,Ee),hl(e,32,h,ve,D^8192),n=Qe}e:do if((We|0)==87&&!e)if(!O)s=0;else{for(s=1;n=t[l+(s<<2)>>2]|0,!!n;)if(P8(u+(s<<3)|0,n,r),s=s+1|0,(s|0)>=10){s=1;break e}for(;;){if(t[l+(s<<2)>>2]|0){s=-1;break e}if(s=s+1|0,(s|0)>=10){s=1;break}}}while(0);return m=On,s|0}function NI(e){return e=e|0,0}function LI(e){e=e|0}function qo(e,n,r){e=e|0,n=n|0,r=r|0,t[e>>2]&32||YI(n,r,e)|0}function b8(e){e=e|0;var n=0,r=0,u=0;if(r=t[e>>2]|0,u=(p[r>>0]|0)+-48|0,u>>>0<10){n=0;do n=u+(n*10|0)|0,r=r+1|0,t[e>>2]=r,u=(p[r>>0]|0)+-48|0;while(u>>>0<10)}else n=0;return n|0}function P8(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;e:do if(n>>>0<=20)do switch(n|0){case 9:{u=(t[r>>2]|0)+(4-1)&~(4-1),n=t[u>>2]|0,t[r>>2]=u+4,t[e>>2]=n;break e}case 10:{u=(t[r>>2]|0)+(4-1)&~(4-1),n=t[u>>2]|0,t[r>>2]=u+4,u=e,t[u>>2]=n,t[u+4>>2]=((n|0)<0)<<31>>31;break e}case 11:{u=(t[r>>2]|0)+(4-1)&~(4-1),n=t[u>>2]|0,t[r>>2]=u+4,u=e,t[u>>2]=n,t[u+4>>2]=0;break e}case 12:{u=(t[r>>2]|0)+(8-1)&~(8-1),n=u,l=t[n>>2]|0,n=t[n+4>>2]|0,t[r>>2]=u+8,u=e,t[u>>2]=l,t[u+4>>2]=n;break e}case 13:{l=(t[r>>2]|0)+(4-1)&~(4-1),u=t[l>>2]|0,t[r>>2]=l+4,u=(u&65535)<<16>>16,l=e,t[l>>2]=u,t[l+4>>2]=((u|0)<0)<<31>>31;break e}case 14:{l=(t[r>>2]|0)+(4-1)&~(4-1),u=t[l>>2]|0,t[r>>2]=l+4,l=e,t[l>>2]=u&65535,t[l+4>>2]=0;break e}case 15:{l=(t[r>>2]|0)+(4-1)&~(4-1),u=t[l>>2]|0,t[r>>2]=l+4,u=(u&255)<<24>>24,l=e,t[l>>2]=u,t[l+4>>2]=((u|0)<0)<<31>>31;break e}case 16:{l=(t[r>>2]|0)+(4-1)&~(4-1),u=t[l>>2]|0,t[r>>2]=l+4,l=e,t[l>>2]=u&255,t[l+4>>2]=0;break e}case 17:{l=(t[r>>2]|0)+(8-1)&~(8-1),s=+U[l>>3],t[r>>2]=l+8,U[e>>3]=s;break e}case 18:{l=(t[r>>2]|0)+(8-1)&~(8-1),s=+U[l>>3],t[r>>2]=l+8,U[e>>3]=s;break e}default:break e}while(0);while(0)}function FI(e,n,r,u){if(e=e|0,n=n|0,r=r|0,u=u|0,!((e|0)==0&(n|0)==0))do r=r+-1|0,p[r>>0]=k[5694+(e&15)>>0]|0|u,e=O_(e|0,n|0,4)|0,n=ft;while(!((e|0)==0&(n|0)==0));return r|0}function bI(e,n,r){if(e=e|0,n=n|0,r=r|0,!((e|0)==0&(n|0)==0))do r=r+-1|0,p[r>>0]=e&7|48,e=O_(e|0,n|0,3)|0,n=ft;while(!((e|0)==0&(n|0)==0));return r|0}function bv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;if(n>>>0>0|(n|0)==0&e>>>0>4294967295){for(;u=QE(e|0,n|0,10,0)|0,r=r+-1|0,p[r>>0]=u&255|48,u=e,e=XE(e|0,n|0,10,0)|0,n>>>0>9|(n|0)==9&u>>>0>4294967295;)n=ft;n=e}else n=e;if(n)for(;r=r+-1|0,p[r>>0]=(n>>>0)%10|0|48,!(n>>>0<10);)n=(n>>>0)/10|0;return r|0}function PI(e){return e=e|0,HI(e,t[(qI()|0)+188>>2]|0)|0}function II(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;s=n&255,u=(r|0)!=0;e:do if(u&(e&3|0)!=0)for(l=n&255;;){if((p[e>>0]|0)==l<<24>>24){h=6;break e}if(e=e+1|0,r=r+-1|0,u=(r|0)!=0,!(u&(e&3|0)!=0)){h=5;break}}else h=5;while(0);(h|0)==5&&(u?h=6:r=0);e:do if((h|0)==6&&(l=n&255,(p[e>>0]|0)!=l<<24>>24)){u=nr(s,16843009)|0;t:do if(r>>>0>3){for(;s=t[e>>2]^u,!((s&-2139062144^-2139062144)&s+-16843009|0);)if(e=e+4|0,r=r+-4|0,r>>>0<=3){h=11;break t}}else h=11;while(0);if((h|0)==11&&!r){r=0;break}for(;;){if((p[e>>0]|0)==l<<24>>24)break e;if(e=e+1|0,r=r+-1|0,!r){r=0;break}}}while(0);return(r|0?e:0)|0}function hl(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0;if(h=m,m=m+256|0,s=h,(r|0)>(u|0)&(l&73728|0)==0){if(l=r-u|0,Iv(s|0,n|0,(l>>>0<256?l:256)|0)|0,l>>>0>255){n=r-u|0;do qo(e,s,256),l=l+-256|0;while(l>>>0>255);l=n&255}qo(e,s,l)}m=h}function I8(e,n){return e=e|0,n=n|0,e?e=jI(e,n,0)|0:e=0,e|0}function BI(e,n,r,u,l,s){e=e|0,n=+n,r=r|0,u=u|0,l=l|0,s=s|0;var h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0,ve=0,Qe=0,We=0,st=0,Re=0,Fe=0,Qt=0,Lr=0,Nn=0,mn=0,hr=0,kr=0,On=0,Zi=0;Zi=m,m=m+560|0,S=Zi+8|0,Qe=Zi,On=Zi+524|0,kr=On,M=Zi+512|0,t[Qe>>2]=0,hr=M+12|0,B8(n)|0,(ft|0)<0?(n=-n,Nn=1,Lr=5659):(Nn=(l&2049|0)!=0&1,Lr=(l&2048|0)==0?(l&1|0)==0?5660:5665:5662),B8(n)|0,mn=ft&2146435072;do if(mn>>>0<2146435072|(mn|0)==2146435072&0<0){if(Pe=+UI(n,Qe)*2,h=Pe!=0,h&&(t[Qe>>2]=(t[Qe>>2]|0)+-1),st=s|32,(st|0)==97){Ee=s&32,K=(Ee|0)==0?Lr:Lr+9|0,P=Nn|2,h=12-u|0;do if(u>>>0>11|(h|0)==0)n=Pe;else{n=8;do h=h+-1|0,n=n*16;while((h|0)!=0);if((p[K>>0]|0)==45){n=-(n+(-Pe-n));break}else{n=Pe+n-n;break}}while(0);D=t[Qe>>2]|0,h=(D|0)<0?0-D|0:D,h=bv(h,((h|0)<0)<<31>>31,hr)|0,(h|0)==(hr|0)&&(h=M+11|0,p[h>>0]=48),p[h+-1>>0]=(D>>31&2)+43,O=h+-2|0,p[O>>0]=s+15,M=(u|0)<1,S=(l&8|0)==0,h=On;do mn=~~n,D=h+1|0,p[h>>0]=k[5694+mn>>0]|Ee,n=(n-+(mn|0))*16,((D-kr|0)==1?!(S&(M&n==0)):0)?(p[D>>0]=46,h=h+2|0):h=D;while(n!=0);mn=h-kr|0,kr=hr-O|0,hr=(u|0)!=0&(mn+-2|0)<(u|0)?u+2|0:mn,h=kr+P+hr|0,hl(e,32,r,h,l),qo(e,K,P),hl(e,48,r,h,l^65536),qo(e,On,mn),hl(e,48,hr-mn|0,0,0),qo(e,O,kr),hl(e,32,r,h,l^8192);break}D=(u|0)<0?6:u,h?(h=(t[Qe>>2]|0)+-28|0,t[Qe>>2]=h,n=Pe*268435456):(n=Pe,h=t[Qe>>2]|0),mn=(h|0)<0?S:S+288|0,S=mn;do Fe=~~n>>>0,t[S>>2]=Fe,S=S+4|0,n=(n-+(Fe>>>0))*1e9;while(n!=0);if((h|0)>0)for(M=mn,P=S;;){if(O=(h|0)<29?h:29,h=P+-4|0,h>>>0>=M>>>0){S=0;do Re=W8(t[h>>2]|0,0,O|0)|0,Re=KE(Re|0,ft|0,S|0,0)|0,Fe=ft,We=QE(Re|0,Fe|0,1e9,0)|0,t[h>>2]=We,S=XE(Re|0,Fe|0,1e9,0)|0,h=h+-4|0;while(h>>>0>=M>>>0);S&&(M=M+-4|0,t[M>>2]=S)}for(S=P;!(S>>>0<=M>>>0);)if(h=S+-4|0,!(t[h>>2]|0))S=h;else break;if(h=(t[Qe>>2]|0)-O|0,t[Qe>>2]=h,(h|0)>0)P=S;else break}else M=mn;if((h|0)<0){u=((D+25|0)/9|0)+1|0,ve=(st|0)==102;do{if(Ee=0-h|0,Ee=(Ee|0)<9?Ee:9,M>>>0>>0){O=(1<>>Ee,K=0,h=M;do Fe=t[h>>2]|0,t[h>>2]=(Fe>>>Ee)+K,K=nr(Fe&O,P)|0,h=h+4|0;while(h>>>0>>0);h=(t[M>>2]|0)==0?M+4|0:M,K?(t[S>>2]=K,M=h,h=S+4|0):(M=h,h=S)}else M=(t[M>>2]|0)==0?M+4|0:M,h=S;S=ve?mn:M,S=(h-S>>2|0)>(u|0)?S+(u<<2)|0:h,h=(t[Qe>>2]|0)+Ee|0,t[Qe>>2]=h}while((h|0)<0);h=M,u=S}else h=M,u=S;if(Fe=mn,h>>>0>>0){if(S=(Fe-h>>2)*9|0,O=t[h>>2]|0,O>>>0>=10){M=10;do M=M*10|0,S=S+1|0;while(O>>>0>=M>>>0)}}else S=0;if(ve=(st|0)==103,We=(D|0)!=0,M=D-((st|0)!=102?S:0)+((We&ve)<<31>>31)|0,(M|0)<(((u-Fe>>2)*9|0)+-9|0)){if(M=M+9216|0,Ee=mn+4+(((M|0)/9|0)+-1024<<2)|0,M=((M|0)%9|0)+1|0,(M|0)<9){O=10;do O=O*10|0,M=M+1|0;while((M|0)!=9)}else O=10;if(P=t[Ee>>2]|0,K=(P>>>0)%(O>>>0)|0,M=(Ee+4|0)==(u|0),M&(K|0)==0)M=Ee;else if(Pe=(((P>>>0)/(O>>>0)|0)&1|0)==0?9007199254740992:9007199254740994,Re=(O|0)/2|0,n=K>>>0>>0?.5:M&(K|0)==(Re|0)?1:1.5,Nn&&(Re=(p[Lr>>0]|0)==45,n=Re?-n:n,Pe=Re?-Pe:Pe),M=P-K|0,t[Ee>>2]=M,Pe+n!=Pe){if(Re=M+O|0,t[Ee>>2]=Re,Re>>>0>999999999)for(S=Ee;M=S+-4|0,t[S>>2]=0,M>>>0>>0&&(h=h+-4|0,t[h>>2]=0),Re=(t[M>>2]|0)+1|0,t[M>>2]=Re,Re>>>0>999999999;)S=M;else M=Ee;if(S=(Fe-h>>2)*9|0,P=t[h>>2]|0,P>>>0>=10){O=10;do O=O*10|0,S=S+1|0;while(P>>>0>=O>>>0)}}else M=Ee;M=M+4|0,M=u>>>0>M>>>0?M:u,Re=h}else M=u,Re=h;for(st=M;;){if(st>>>0<=Re>>>0){Qe=0;break}if(h=st+-4|0,!(t[h>>2]|0))st=h;else{Qe=1;break}}u=0-S|0;do if(ve)if(h=((We^1)&1)+D|0,(h|0)>(S|0)&(S|0)>-5?(O=s+-1|0,D=h+-1-S|0):(O=s+-2|0,D=h+-1|0),h=l&8,h)Ee=h;else{if(Qe?(Qt=t[st+-4>>2]|0,(Qt|0)!=0):0)if((Qt>>>0)%10|0)M=0;else{M=0,h=10;do h=h*10|0,M=M+1|0;while(!((Qt>>>0)%(h>>>0)|0|0))}else M=9;if(h=((st-Fe>>2)*9|0)+-9|0,(O|32|0)==102){Ee=h-M|0,Ee=(Ee|0)>0?Ee:0,D=(D|0)<(Ee|0)?D:Ee,Ee=0;break}else{Ee=h+S-M|0,Ee=(Ee|0)>0?Ee:0,D=(D|0)<(Ee|0)?D:Ee,Ee=0;break}}else O=s,Ee=l&8;while(0);if(ve=D|Ee,P=(ve|0)!=0&1,K=(O|32|0)==102,K)We=0,h=(S|0)>0?S:0;else{if(h=(S|0)<0?u:S,h=bv(h,((h|0)<0)<<31>>31,hr)|0,M=hr,(M-h|0)<2)do h=h+-1|0,p[h>>0]=48;while((M-h|0)<2);p[h+-1>>0]=(S>>31&2)+43,h=h+-2|0,p[h>>0]=O,We=h,h=M-h|0}if(h=Nn+1+D+P+h|0,hl(e,32,r,h,l),qo(e,Lr,Nn),hl(e,48,r,h,l^65536),K){O=Re>>>0>mn>>>0?mn:Re,Ee=On+9|0,P=Ee,K=On+8|0,M=O;do{if(S=bv(t[M>>2]|0,0,Ee)|0,(M|0)==(O|0))(S|0)==(Ee|0)&&(p[K>>0]=48,S=K);else if(S>>>0>On>>>0){Iv(On|0,48,S-kr|0)|0;do S=S+-1|0;while(S>>>0>On>>>0)}qo(e,S,P-S|0),M=M+4|0}while(M>>>0<=mn>>>0);if(ve|0&&qo(e,5710,1),M>>>0>>0&(D|0)>0)for(;;){if(S=bv(t[M>>2]|0,0,Ee)|0,S>>>0>On>>>0){Iv(On|0,48,S-kr|0)|0;do S=S+-1|0;while(S>>>0>On>>>0)}if(qo(e,S,(D|0)<9?D:9),M=M+4|0,S=D+-9|0,M>>>0>>0&(D|0)>9)D=S;else{D=S;break}}hl(e,48,D+9|0,9,0)}else{if(ve=Qe?st:Re+4|0,(D|0)>-1){Qe=On+9|0,Ee=(Ee|0)==0,u=Qe,P=0-kr|0,K=On+8|0,O=Re;do{S=bv(t[O>>2]|0,0,Qe)|0,(S|0)==(Qe|0)&&(p[K>>0]=48,S=K);do if((O|0)==(Re|0)){if(M=S+1|0,qo(e,S,1),Ee&(D|0)<1){S=M;break}qo(e,5710,1),S=M}else{if(S>>>0<=On>>>0)break;Iv(On|0,48,S+P|0)|0;do S=S+-1|0;while(S>>>0>On>>>0)}while(0);kr=u-S|0,qo(e,S,(D|0)>(kr|0)?kr:D),D=D-kr|0,O=O+4|0}while(O>>>0>>0&(D|0)>-1)}hl(e,48,D+18|0,18,0),qo(e,We,hr-We|0)}hl(e,32,r,h,l^8192)}else On=(s&32|0)!=0,h=Nn+3|0,hl(e,32,r,h,l&-65537),qo(e,Lr,Nn),qo(e,n!=n|!1?On?5686:5690:On?5678:5682,3),hl(e,32,r,h,l^8192);while(0);return m=Zi,((h|0)<(r|0)?r:h)|0}function B8(e){e=+e;var n=0;return U[W>>3]=e,n=t[W>>2]|0,ft=t[W+4>>2]|0,n|0}function UI(e,n){return e=+e,n=n|0,+ +U8(e,n)}function U8(e,n){e=+e,n=n|0;var r=0,u=0,l=0;switch(U[W>>3]=e,r=t[W>>2]|0,u=t[W+4>>2]|0,l=O_(r|0,u|0,52)|0,l&2047){case 0:{e!=0?(e=+U8(e*18446744073709552e3,n),r=(t[n>>2]|0)+-64|0):r=0,t[n>>2]=r;break}case 2047:break;default:t[n>>2]=(l&2047)+-1022,t[W>>2]=r,t[W+4>>2]=u&-2146435073|1071644672,e=+U[W>>3]}return+e}function jI(e,n,r){e=e|0,n=n|0,r=r|0;do if(e){if(n>>>0<128){p[e>>0]=n,e=1;break}if(!(t[t[(zI()|0)+188>>2]>>2]|0))if((n&-128|0)==57216){p[e>>0]=n,e=1;break}else{t[(Fv()|0)>>2]=84,e=-1;break}if(n>>>0<2048){p[e>>0]=n>>>6|192,p[e+1>>0]=n&63|128,e=2;break}if(n>>>0<55296|(n&-8192|0)==57344){p[e>>0]=n>>>12|224,p[e+1>>0]=n>>>6&63|128,p[e+2>>0]=n&63|128,e=3;break}if((n+-65536|0)>>>0<1048576){p[e>>0]=n>>>18|240,p[e+1>>0]=n>>>12&63|128,p[e+2>>0]=n>>>6&63|128,p[e+3>>0]=n&63|128,e=4;break}else{t[(Fv()|0)>>2]=84,e=-1;break}}else e=1;while(0);return e|0}function zI(){return VE()|0}function qI(){return VE()|0}function HI(e,n){e=e|0,n=n|0;var r=0,u=0;for(u=0;;){if((k[5712+u>>0]|0)==(e|0)){e=2;break}if(r=u+1|0,(r|0)==87){r=5800,u=87,e=5;break}else u=r}if((e|0)==2&&(u?(r=5800,e=5):r=5800),(e|0)==5)for(;;){do e=r,r=r+1|0;while((p[e>>0]|0)!=0);if(u=u+-1|0,u)e=5;else break}return WI(r,t[n+20>>2]|0)|0}function WI(e,n){return e=e|0,n=n|0,VI(e,n)|0}function VI(e,n){return e=e|0,n=n|0,n?n=GI(t[n>>2]|0,t[n+4>>2]|0,e)|0:n=0,(n|0?n:e)|0}function GI(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0;K=(t[e>>2]|0)+1794895138|0,s=Gp(t[e+8>>2]|0,K)|0,u=Gp(t[e+12>>2]|0,K)|0,l=Gp(t[e+16>>2]|0,K)|0;e:do if((s>>>0>>2>>>0?(P=n-(s<<2)|0,u>>>0

>>0&l>>>0

>>0):0)?((l|u)&3|0)==0:0){for(P=u>>>2,O=l>>>2,M=0;;){if(D=s>>>1,S=M+D|0,h=S<<1,l=h+P|0,u=Gp(t[e+(l<<2)>>2]|0,K)|0,l=Gp(t[e+(l+1<<2)>>2]|0,K)|0,!(l>>>0>>0&u>>>0<(n-l|0)>>>0)){u=0;break e}if(p[e+(l+u)>>0]|0){u=0;break e}if(u=L8(r,e+l|0)|0,!u)break;if(u=(u|0)<0,(s|0)==1){u=0;break e}else M=u?M:S,s=u?D:s-D|0}u=h+O|0,l=Gp(t[e+(u<<2)>>2]|0,K)|0,u=Gp(t[e+(u+1<<2)>>2]|0,K)|0,u>>>0>>0&l>>>0<(n-u|0)>>>0?u=(p[e+(u+l)>>0]|0)==0?e+u|0:0:u=0}else u=0;while(0);return u|0}function Gp(e,n){e=e|0,n=n|0;var r=0;return r=Y8(e|0)|0,((n|0)==0?e:r)|0}function YI(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0,D=0;u=r+16|0,l=t[u>>2]|0,l?s=5:KI(r)|0?u=0:(l=t[u>>2]|0,s=5);e:do if((s|0)==5){if(D=r+20|0,h=t[D>>2]|0,u=h,(l-h|0)>>>0>>0){u=M_[t[r+36>>2]&7](r,e,n)|0;break}t:do if((p[r+75>>0]|0)>-1){for(h=n;;){if(!h){s=0,l=e;break t}if(l=h+-1|0,(p[e+l>>0]|0)==10)break;h=l}if(u=M_[t[r+36>>2]&7](r,e,h)|0,u>>>0>>0)break e;s=h,l=e+h|0,n=n-h|0,u=t[D>>2]|0}else s=0,l=e;while(0);pr(u|0,l|0,n|0)|0,t[D>>2]=(t[D>>2]|0)+n,u=s+n|0}while(0);return u|0}function KI(e){e=e|0;var n=0,r=0;return n=e+74|0,r=p[n>>0]|0,p[n>>0]=r+255|r,n=t[e>>2]|0,n&8?(t[e>>2]=n|32,e=-1):(t[e+8>>2]=0,t[e+4>>2]=0,r=t[e+44>>2]|0,t[e+28>>2]=r,t[e+20>>2]=r,t[e+16>>2]=r+(t[e+48>>2]|0),e=0),e|0}function Eu(e,n){e=w(e),n=w(n);var r=0,u=0;r=j8(e)|0;do if((r&2147483647)>>>0<=2139095040){if(u=j8(n)|0,(u&2147483647)>>>0<=2139095040)if((u^r|0)<0){e=(r|0)<0?n:e;break}else{e=e>2]=e,t[W>>2]|0|0}function Yp(e,n){e=w(e),n=w(n);var r=0,u=0;r=z8(e)|0;do if((r&2147483647)>>>0<=2139095040){if(u=z8(n)|0,(u&2147483647)>>>0<=2139095040)if((u^r|0)<0){e=(r|0)<0?e:n;break}else{e=e>2]=e,t[W>>2]|0|0}function YE(e,n){e=w(e),n=w(n);var r=0,u=0,l=0,s=0,h=0,D=0,S=0,M=0;s=(C[W>>2]=e,t[W>>2]|0),D=(C[W>>2]=n,t[W>>2]|0),r=s>>>23&255,h=D>>>23&255,S=s&-2147483648,l=D<<1;e:do if((l|0)!=0?!((r|0)==255|((XI(n)|0)&2147483647)>>>0>2139095040):0){if(u=s<<1,u>>>0<=l>>>0)return n=w(e*w(0)),w((u|0)==(l|0)?n:e);if(r)u=s&8388607|8388608;else{if(r=s<<9,(r|0)>-1){u=r,r=0;do r=r+-1|0,u=u<<1;while((u|0)>-1)}else r=0;u=s<<1-r}if(h)D=D&8388607|8388608;else{if(s=D<<9,(s|0)>-1){l=0;do l=l+-1|0,s=s<<1;while((s|0)>-1)}else l=0;h=l,D=D<<1-l}l=u-D|0,s=(l|0)>-1;t:do if((r|0)>(h|0)){for(;;){if(s)if(l)u=l;else break;if(u=u<<1,r=r+-1|0,l=u-D|0,s=(l|0)>-1,(r|0)<=(h|0))break t}n=w(e*w(0));break e}while(0);if(s)if(l)u=l;else{n=w(e*w(0));break}if(u>>>0<8388608)do u=u<<1,r=r+-1|0;while(u>>>0<8388608);(r|0)>0?r=u+-8388608|r<<23:r=u>>>(1-r|0),n=(t[W>>2]=r|S,w(C[W>>2]))}else M=3;while(0);return(M|0)==3&&(n=w(e*n),n=w(n/n)),w(n)}function XI(e){return e=w(e),C[W>>2]=e,t[W>>2]|0|0}function QI(e,n){return e=e|0,n=n|0,F8(t[582]|0,e,n)|0}function li(e){e=e|0,Xn()}function Pv(e){e=e|0}function JI(e,n){return e=e|0,n=n|0,0}function ZI(e){return e=e|0,(q8(e+4|0)|0)==-1?(M1[t[(t[e>>2]|0)+8>>2]&127](e),e=1):e=0,e|0}function q8(e){e=e|0;var n=0;return n=t[e>>2]|0,t[e>>2]=n+-1,n+-1|0}function J2(e){e=e|0,ZI(e)|0&&$I(e)}function $I(e){e=e|0;var n=0;n=e+8|0,((t[n>>2]|0)!=0?(q8(n)|0)!=-1:0)||M1[t[(t[e>>2]|0)+16>>2]&127](e)}function cn(e){e=e|0;var n=0;for(n=(e|0)==0?1:e;e=C_(n)|0,!(e|0);){if(e=tB()|0,!e){e=0;break}rS[e&0]()}return e|0}function H8(e){return e=e|0,cn(e)|0}function yt(e){e=e|0,x_(e)}function eB(e){e=e|0,(p[e+11>>0]|0)<0&&yt(t[e>>2]|0)}function tB(){var e=0;return e=t[2923]|0,t[2923]=e+0,e|0}function nB(){}function R_(e,n,r,u){return e=e|0,n=n|0,r=r|0,u=u|0,u=n-u-(r>>>0>e>>>0|0)>>>0,ft=u,e-r>>>0|0|0}function KE(e,n,r,u){return e=e|0,n=n|0,r=r|0,u=u|0,r=e+r>>>0,ft=n+u+(r>>>0>>0|0)>>>0,r|0|0}function Iv(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0,h=0;if(s=e+r|0,n=n&255,(r|0)>=67){for(;e&3;)p[e>>0]=n,e=e+1|0;for(u=s&-4|0,l=u-64|0,h=n|n<<8|n<<16|n<<24;(e|0)<=(l|0);)t[e>>2]=h,t[e+4>>2]=h,t[e+8>>2]=h,t[e+12>>2]=h,t[e+16>>2]=h,t[e+20>>2]=h,t[e+24>>2]=h,t[e+28>>2]=h,t[e+32>>2]=h,t[e+36>>2]=h,t[e+40>>2]=h,t[e+44>>2]=h,t[e+48>>2]=h,t[e+52>>2]=h,t[e+56>>2]=h,t[e+60>>2]=h,e=e+64|0;for(;(e|0)<(u|0);)t[e>>2]=h,e=e+4|0}for(;(e|0)<(s|0);)p[e>>0]=n,e=e+1|0;return s-r|0}function W8(e,n,r){return e=e|0,n=n|0,r=r|0,(r|0)<32?(ft=n<>>32-r,e<>>r,e>>>r|(n&(1<>>r-32|0)}function pr(e,n,r){e=e|0,n=n|0,r=r|0;var u=0,l=0,s=0;if((r|0)>=8192)return ni(e|0,n|0,r|0)|0;if(s=e|0,l=e+r|0,(e&3)==(n&3)){for(;e&3;){if(!r)return s|0;p[e>>0]=p[n>>0]|0,e=e+1|0,n=n+1|0,r=r-1|0}for(r=l&-4|0,u=r-64|0;(e|0)<=(u|0);)t[e>>2]=t[n>>2],t[e+4>>2]=t[n+4>>2],t[e+8>>2]=t[n+8>>2],t[e+12>>2]=t[n+12>>2],t[e+16>>2]=t[n+16>>2],t[e+20>>2]=t[n+20>>2],t[e+24>>2]=t[n+24>>2],t[e+28>>2]=t[n+28>>2],t[e+32>>2]=t[n+32>>2],t[e+36>>2]=t[n+36>>2],t[e+40>>2]=t[n+40>>2],t[e+44>>2]=t[n+44>>2],t[e+48>>2]=t[n+48>>2],t[e+52>>2]=t[n+52>>2],t[e+56>>2]=t[n+56>>2],t[e+60>>2]=t[n+60>>2],e=e+64|0,n=n+64|0;for(;(e|0)<(r|0);)t[e>>2]=t[n>>2],e=e+4|0,n=n+4|0}else for(r=l-4|0;(e|0)<(r|0);)p[e>>0]=p[n>>0]|0,p[e+1>>0]=p[n+1>>0]|0,p[e+2>>0]=p[n+2>>0]|0,p[e+3>>0]=p[n+3>>0]|0,e=e+4|0,n=n+4|0;for(;(e|0)<(l|0);)p[e>>0]=p[n>>0]|0,e=e+1|0,n=n+1|0;return s|0}function V8(e){e=e|0;var n=0;return n=p[Se+(e&255)>>0]|0,(n|0)<8?n|0:(n=p[Se+(e>>8&255)>>0]|0,(n|0)<8?n+8|0:(n=p[Se+(e>>16&255)>>0]|0,(n|0)<8?n+16|0:(p[Se+(e>>>24)>>0]|0)+24|0))}function G8(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0;var s=0,h=0,D=0,S=0,M=0,O=0,P=0,K=0,Pe=0,Ee=0;if(O=e,S=n,M=S,h=r,K=u,D=K,!M)return s=(l|0)!=0,D?s?(t[l>>2]=e|0,t[l+4>>2]=n&0,K=0,l=0,ft=K,l|0):(K=0,l=0,ft=K,l|0):(s&&(t[l>>2]=(O>>>0)%(h>>>0),t[l+4>>2]=0),K=0,l=(O>>>0)/(h>>>0)>>>0,ft=K,l|0);s=(D|0)==0;do if(h){if(!s){if(s=(vr(D|0)|0)-(vr(M|0)|0)|0,s>>>0<=31){P=s+1|0,D=31-s|0,n=s-31>>31,h=P,e=O>>>(P>>>0)&n|M<>>(P>>>0)&n,s=0,D=O<>2]=e|0,t[l+4>>2]=S|n&0,K=0,l=0,ft=K,l|0):(K=0,l=0,ft=K,l|0)}if(s=h-1|0,s&h|0){D=(vr(h|0)|0)+33-(vr(M|0)|0)|0,Ee=64-D|0,P=32-D|0,S=P>>31,Pe=D-32|0,n=Pe>>31,h=D,e=P-1>>31&M>>>(Pe>>>0)|(M<>>(D>>>0))&n,n=n&M>>>(D>>>0),s=O<>>(Pe>>>0))&S|O<>31;break}return l|0&&(t[l>>2]=s&O,t[l+4>>2]=0),(h|0)==1?(Pe=S|n&0,Ee=e|0|0,ft=Pe,Ee|0):(Ee=V8(h|0)|0,Pe=M>>>(Ee>>>0)|0,Ee=M<<32-Ee|O>>>(Ee>>>0)|0,ft=Pe,Ee|0)}else{if(s)return l|0&&(t[l>>2]=(M>>>0)%(h>>>0),t[l+4>>2]=0),Pe=0,Ee=(M>>>0)/(h>>>0)>>>0,ft=Pe,Ee|0;if(!O)return l|0&&(t[l>>2]=0,t[l+4>>2]=(M>>>0)%(D>>>0)),Pe=0,Ee=(M>>>0)/(D>>>0)>>>0,ft=Pe,Ee|0;if(s=D-1|0,!(s&D))return l|0&&(t[l>>2]=e|0,t[l+4>>2]=s&M|n&0),Pe=0,Ee=M>>>((V8(D|0)|0)>>>0),ft=Pe,Ee|0;if(s=(vr(D|0)|0)-(vr(M|0)|0)|0,s>>>0<=30){n=s+1|0,D=31-s|0,h=n,e=M<>>(n>>>0),n=M>>>(n>>>0),s=0,D=O<>2]=e|0,t[l+4>>2]=S|n&0,Pe=0,Ee=0,ft=Pe,Ee|0):(Pe=0,Ee=0,ft=Pe,Ee|0)}while(0);if(!h)M=D,S=0,D=0;else{P=r|0|0,O=K|u&0,M=KE(P|0,O|0,-1,-1)|0,r=ft,S=D,D=0;do u=S,S=s>>>31|S<<1,s=D|s<<1,u=e<<1|u>>>31|0,K=e>>>31|n<<1|0,R_(M|0,r|0,u|0,K|0)|0,Ee=ft,Pe=Ee>>31|((Ee|0)<0?-1:0)<<1,D=Pe&1,e=R_(u|0,K|0,Pe&P|0,(((Ee|0)<0?-1:0)>>31|((Ee|0)<0?-1:0)<<1)&O|0)|0,n=ft,h=h-1|0;while((h|0)!=0);M=S,S=0}return h=0,l|0&&(t[l>>2]=e,t[l+4>>2]=n),Pe=(s|0)>>>31|(M|h)<<1|(h<<1|s>>>31)&0|S,Ee=(s<<1|0>>>31)&-2|D,ft=Pe,Ee|0}function XE(e,n,r,u){return e=e|0,n=n|0,r=r|0,u=u|0,G8(e,n,r,u,0)|0}function Z2(e){e=e|0;var n=0,r=0;return r=e+15&-16|0,n=t[q>>2]|0,e=n+r|0,(r|0)>0&(e|0)<(n|0)|(e|0)<0?(ur()|0,Vl(12),-1):(t[q>>2]=e,((e|0)>(Fr()|0)?(fr()|0)==0:0)?(t[q>>2]=n,Vl(12),-1):n|0)}function Iy(e,n,r){e=e|0,n=n|0,r=r|0;var u=0;if((n|0)<(e|0)&(e|0)<(n+r|0)){for(u=e,n=n+r|0,e=e+r|0;(r|0)>0;)e=e-1|0,n=n-1|0,r=r-1|0,p[e>>0]=p[n>>0]|0;e=u}else pr(e,n,r)|0;return e|0}function QE(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0;var l=0,s=0;return s=m,m=m+16|0,l=s|0,G8(e,n,r,u,l)|0,m=s,ft=t[l+4>>2]|0,t[l>>2]|0|0}function Y8(e){return e=e|0,(e&255)<<24|(e>>8&255)<<16|(e>>16&255)<<8|e>>>24|0}function rB(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,K8[e&1](n|0,r|0,u|0,l|0,s|0)}function iB(e,n,r){e=e|0,n=n|0,r=w(r),X8[e&1](n|0,w(r))}function uB(e,n,r){e=e|0,n=n|0,r=+r,Q8[e&31](n|0,+r)}function oB(e,n,r,u){return e=e|0,n=n|0,r=w(r),u=w(u),w(J8[e&0](n|0,w(r),w(u)))}function lB(e,n){e=e|0,n=n|0,M1[e&127](n|0)}function sB(e,n,r){e=e|0,n=n|0,r=r|0,N1[e&31](n|0,r|0)}function aB(e,n){return e=e|0,n=n|0,Xp[e&31](n|0)|0}function fB(e,n,r,u,l){e=e|0,n=n|0,r=+r,u=+u,l=l|0,Z8[e&1](n|0,+r,+u,l|0)}function cB(e,n,r,u){e=e|0,n=n|0,r=+r,u=+u,VB[e&1](n|0,+r,+u)}function dB(e,n,r,u){return e=e|0,n=n|0,r=r|0,u=u|0,M_[e&7](n|0,r|0,u|0)|0}function pB(e,n,r,u){return e=e|0,n=n|0,r=r|0,u=u|0,+GB[e&1](n|0,r|0,u|0)}function hB(e,n){return e=e|0,n=n|0,+$8[e&15](n|0)}function vB(e,n,r){return e=e|0,n=n|0,r=+r,YB[e&1](n|0,+r)|0}function mB(e,n,r){return e=e|0,n=n|0,r=r|0,ZE[e&15](n|0,r|0)|0}function yB(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=+u,l=+l,s=s|0,KB[e&1](n|0,r|0,+u,+l,s|0)}function gB(e,n,r,u,l,s,h){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,h=h|0,XB[e&1](n|0,r|0,u|0,l|0,s|0,h|0)}function _B(e,n,r){return e=e|0,n=n|0,r=r|0,+eS[e&7](n|0,r|0)}function EB(e){return e=e|0,N_[e&7]()|0}function DB(e,n,r,u,l,s){return e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,tS[e&1](n|0,r|0,u|0,l|0,s|0)|0}function wB(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=+l,QB[e&1](n|0,r|0,u|0,+l)}function SB(e,n,r,u,l,s,h){e=e|0,n=n|0,r=r|0,u=w(u),l=l|0,s=w(s),h=h|0,nS[e&1](n|0,r|0,w(u),l|0,w(s),h|0)}function TB(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,jy[e&15](n|0,r|0,u|0)}function CB(e){e=e|0,rS[e&0]()}function xB(e,n,r,u){e=e|0,n=n|0,r=r|0,u=+u,iS[e&15](n|0,r|0,+u)}function AB(e,n,r){return e=e|0,n=+n,r=+r,JB[e&1](+n,+r)|0}function RB(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,$E[e&15](n|0,r|0,u|0,l|0)}function OB(e,n,r,u,l){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,Ut(0)}function kB(e,n){e=e|0,n=w(n),Ut(1)}function Ks(e,n){e=e|0,n=+n,Ut(2)}function MB(e,n,r){return e=e|0,n=w(n),r=w(r),Ut(3),St}function Kn(e){e=e|0,Ut(4)}function By(e,n){e=e|0,n=n|0,Ut(5)}function xa(e){return e=e|0,Ut(6),0}function NB(e,n,r,u){e=e|0,n=+n,r=+r,u=u|0,Ut(7)}function LB(e,n,r){e=e|0,n=+n,r=+r,Ut(8)}function FB(e,n,r){return e=e|0,n=n|0,r=r|0,Ut(9),0}function bB(e,n,r){return e=e|0,n=n|0,r=r|0,Ut(10),0}function Kp(e){return e=e|0,Ut(11),0}function PB(e,n){return e=e|0,n=+n,Ut(12),0}function Uy(e,n){return e=e|0,n=n|0,Ut(13),0}function IB(e,n,r,u,l){e=e|0,n=n|0,r=+r,u=+u,l=l|0,Ut(14)}function BB(e,n,r,u,l,s){e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,s=s|0,Ut(15)}function JE(e,n){return e=e|0,n=n|0,Ut(16),0}function UB(){return Ut(17),0}function jB(e,n,r,u,l){return e=e|0,n=n|0,r=r|0,u=u|0,l=l|0,Ut(18),0}function zB(e,n,r,u){e=e|0,n=n|0,r=r|0,u=+u,Ut(19)}function qB(e,n,r,u,l,s){e=e|0,n=n|0,r=w(r),u=u|0,l=w(l),s=s|0,Ut(20)}function k_(e,n,r){e=e|0,n=n|0,r=r|0,Ut(21)}function HB(){Ut(22)}function Bv(e,n,r){e=e|0,n=n|0,r=+r,Ut(23)}function WB(e,n){return e=+e,n=+n,Ut(24),0}function Uv(e,n,r,u){e=e|0,n=n|0,r=r|0,u=u|0,Ut(25)}var K8=[OB,UL],X8=[kB,Ju],Q8=[Ks,ua,ys,gs,Ql,Io,hf,tl,Ia,Zu,vf,jc,lc,Sl,_s,oa,n2,la,sc,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks,Ks],J8=[MB],M1=[Kn,Pv,an,$l,go,Lf,x1,Fl,hN,vN,mN,xL,AL,RL,XP,QP,JP,Ne,uc,La,ju,U0,hh,yf,$c,Af,pa,Rh,Sm,h1,v1,Xh,pp,M2,Gm,D1,Sc,ry,oy,Sv,Av,rn,Q4,lE,h_,Nt,_u,Qu,RO,WO,ak,Ak,qk,aM,_M,wM,UM,qM,uN,gN,DN,BN,nL,v2,BF,vb,kb,Vb,pP,RP,UP,qP,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn,Kn],N1=[By,gd,$1,Uc,Dl,el,_d,Bs,wl,Fa,ba,Pa,Tl,Be,ut,Jt,jn,ti,tr,Ba,Dd,mh,fE,vE,Mk,zF,fL,g8,By,By,By,By],Xp=[xa,xI,pf,y,J,fe,mt,Ct,Mt,Er,iu,j0,Ua,r2,Vc,Cs,Gk,zN,VF,Sa,xa,xa,xa,xa,xa,xa,xa,xa,xa,xa,xa,xa],Z8=[NB,Td],VB=[LB,sN],M_=[FB,N8,AI,kI,zh,dv,NO,Xb],GB=[bB,lv],$8=[Kp,e0,He,ii,vh,il,sa,Cd,xd,ac,Kp,Kp,Kp,Kp,Kp,Kp],YB=[PB,mM],ZE=[Uy,JI,Ed,ll,zd,Nm,ap,Cp,ly,xr,bo,Fb,Uy,Uy,Uy,Uy],KB=[IB,Sh],XB=[BB,yP],eS=[JE,qi,Ad,a2,Gc,cl,JE,JE],N_=[UB,Yc,to,g0,xM,GM,CN,GP],tS=[jB,ei],QB=[zB,Dy],nS=[qB,i2],jy=[k_,A,$u,jr,gu,d1,k2,ir,Cy,po,aF,_b,NP,k_,k_,k_],rS=[HB],iS=[Bv,e2,ho,t2,Po,zc,bi,g,Ip,KO,dM,Bv,Bv,Bv,Bv,Bv],JB=[WB,dN],$E=[Uv,_p,Rc,pk,tM,NM,ZM,NN,lL,JF,rI,Uv,Uv,Uv,Uv,Uv];return{_llvm_bswap_i32:Y8,dynCall_idd:AB,dynCall_i:EB,_i64Subtract:R_,___udivdi3:XE,dynCall_vif:iB,setThrew:fs,dynCall_viii:TB,_bitshift64Lshr:O_,_bitshift64Shl:W8,dynCall_vi:lB,dynCall_viiddi:yB,dynCall_diii:pB,dynCall_iii:mB,_memset:Iv,_sbrk:Z2,_memcpy:pr,__GLOBAL__sub_I_Yoga_cpp:Qi,dynCall_vii:sB,___uremdi3:QE,dynCall_vid:uB,stackAlloc:so,_nbind_init:hI,getTempRet0:X,dynCall_di:hB,dynCall_iid:vB,setTempRet0:P0,_i64Add:KE,dynCall_fiff:oB,dynCall_iiii:dB,_emscripten_get_global_libc:CI,dynCall_viid:xB,dynCall_viiid:wB,dynCall_viififi:SB,dynCall_ii:aB,__GLOBAL__sub_I_Binding_cc:kF,dynCall_viiii:RB,dynCall_iiiiii:DB,stackSave:Jo,dynCall_viiiii:rB,__GLOBAL__sub_I_nbind_cc:Us,dynCall_vidd:cB,_free:x_,runPostSets:nB,dynCall_viiiiii:gB,establishStackSpace:Fu,_memmove:Iy,stackRestore:Gl,_malloc:C_,__GLOBAL__sub_I_common_cc:XN,dynCall_viddi:fB,dynCall_dii:_B,dynCall_v:CB}}(Module.asmGlobalArg,Module.asmLibraryArg,buffer),_llvm_bswap_i32=Module._llvm_bswap_i32=asm._llvm_bswap_i32,getTempRet0=Module.getTempRet0=asm.getTempRet0,___udivdi3=Module.___udivdi3=asm.___udivdi3,setThrew=Module.setThrew=asm.setThrew,_bitshift64Lshr=Module._bitshift64Lshr=asm._bitshift64Lshr,_bitshift64Shl=Module._bitshift64Shl=asm._bitshift64Shl,_memset=Module._memset=asm._memset,_sbrk=Module._sbrk=asm._sbrk,_memcpy=Module._memcpy=asm._memcpy,stackAlloc=Module.stackAlloc=asm.stackAlloc,___uremdi3=Module.___uremdi3=asm.___uremdi3,_nbind_init=Module._nbind_init=asm._nbind_init,_i64Subtract=Module._i64Subtract=asm._i64Subtract,setTempRet0=Module.setTempRet0=asm.setTempRet0,_i64Add=Module._i64Add=asm._i64Add,_emscripten_get_global_libc=Module._emscripten_get_global_libc=asm._emscripten_get_global_libc,__GLOBAL__sub_I_Yoga_cpp=Module.__GLOBAL__sub_I_Yoga_cpp=asm.__GLOBAL__sub_I_Yoga_cpp,__GLOBAL__sub_I_Binding_cc=Module.__GLOBAL__sub_I_Binding_cc=asm.__GLOBAL__sub_I_Binding_cc,stackSave=Module.stackSave=asm.stackSave,__GLOBAL__sub_I_nbind_cc=Module.__GLOBAL__sub_I_nbind_cc=asm.__GLOBAL__sub_I_nbind_cc,_free=Module._free=asm._free,runPostSets=Module.runPostSets=asm.runPostSets,establishStackSpace=Module.establishStackSpace=asm.establishStackSpace,_memmove=Module._memmove=asm._memmove,stackRestore=Module.stackRestore=asm.stackRestore,_malloc=Module._malloc=asm._malloc,__GLOBAL__sub_I_common_cc=Module.__GLOBAL__sub_I_common_cc=asm.__GLOBAL__sub_I_common_cc,dynCall_viiiii=Module.dynCall_viiiii=asm.dynCall_viiiii,dynCall_vif=Module.dynCall_vif=asm.dynCall_vif,dynCall_vid=Module.dynCall_vid=asm.dynCall_vid,dynCall_fiff=Module.dynCall_fiff=asm.dynCall_fiff,dynCall_vi=Module.dynCall_vi=asm.dynCall_vi,dynCall_vii=Module.dynCall_vii=asm.dynCall_vii,dynCall_ii=Module.dynCall_ii=asm.dynCall_ii,dynCall_viddi=Module.dynCall_viddi=asm.dynCall_viddi,dynCall_vidd=Module.dynCall_vidd=asm.dynCall_vidd,dynCall_iiii=Module.dynCall_iiii=asm.dynCall_iiii,dynCall_diii=Module.dynCall_diii=asm.dynCall_diii,dynCall_di=Module.dynCall_di=asm.dynCall_di,dynCall_iid=Module.dynCall_iid=asm.dynCall_iid,dynCall_iii=Module.dynCall_iii=asm.dynCall_iii,dynCall_viiddi=Module.dynCall_viiddi=asm.dynCall_viiddi,dynCall_viiiiii=Module.dynCall_viiiiii=asm.dynCall_viiiiii,dynCall_dii=Module.dynCall_dii=asm.dynCall_dii,dynCall_i=Module.dynCall_i=asm.dynCall_i,dynCall_iiiiii=Module.dynCall_iiiiii=asm.dynCall_iiiiii,dynCall_viiid=Module.dynCall_viiid=asm.dynCall_viiid,dynCall_viififi=Module.dynCall_viififi=asm.dynCall_viififi,dynCall_viii=Module.dynCall_viii=asm.dynCall_viii,dynCall_v=Module.dynCall_v=asm.dynCall_v,dynCall_viid=Module.dynCall_viid=asm.dynCall_viid,dynCall_idd=Module.dynCall_idd=asm.dynCall_idd,dynCall_viiii=Module.dynCall_viiii=asm.dynCall_viiii;Runtime.stackAlloc=Module.stackAlloc,Runtime.stackSave=Module.stackSave,Runtime.stackRestore=Module.stackRestore,Runtime.establishStackSpace=Module.establishStackSpace,Runtime.setTempRet0=Module.setTempRet0,Runtime.getTempRet0=Module.getTempRet0,Module.asm=asm;function ExitStatus(i){this.name="ExitStatus",this.message="Program terminated with exit("+i+")",this.status=i}ExitStatus.prototype=new Error,ExitStatus.prototype.constructor=ExitStatus;var initialStackTop,preloadStartTime=null,calledMain=!1;dependenciesFulfilled=function i(){Module.calledRun||run(),Module.calledRun||(dependenciesFulfilled=i)},Module.callMain=Module.callMain=function(o){o=o||[],ensureInitRuntime();var f=o.length+1;function p(){for(var N=0;N<4-1;N++)E.push(0)}var E=[allocate(intArrayFromString(Module.thisProgram),"i8",ALLOC_NORMAL)];p();for(var t=0;t0||(preRun(),runDependencies>0)||Module.calledRun)return;function o(){Module.calledRun||(Module.calledRun=!0,!ABORT&&(ensureInitRuntime(),preMain(),Module.onRuntimeInitialized&&Module.onRuntimeInitialized(),Module._main&&shouldRunNow&&Module.callMain(i),postRun()))}Module.setStatus?(Module.setStatus("Running..."),setTimeout(function(){setTimeout(function(){Module.setStatus("")},1),o()},1)):o()}Module.run=Module.run=run;function exit(i,o){o&&Module.noExitRuntime||(Module.noExitRuntime||(ABORT=!0,EXITSTATUS=i,STACKTOP=initialStackTop,exitRuntime(),Module.onExit&&Module.onExit(i)),ENVIRONMENT_IS_NODE&&process.exit(i),Module.quit(i,new ExitStatus(i)))}Module.exit=Module.exit=exit;var abortDecorators=[];function abort(i){Module.onAbort&&Module.onAbort(i),i!==void 0?(Module.print(i),Module.printErr(i),i=JSON.stringify(i)):i="",ABORT=!0,EXITSTATUS=1;var o=` +If this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.`,f="abort("+i+") at "+stackTrace()+o;throw abortDecorators&&abortDecorators.forEach(function(p){f=p(f,i)}),f}if(Module.abort=Module.abort=abort,Module.preInit)for(typeof Module.preInit=="function"&&(Module.preInit=[Module.preInit]);Module.preInit.length>0;)Module.preInit.pop()();var shouldRunNow=!0;Module.noInitialRun&&(shouldRunNow=!1),run()})});var eh=ce((Wne,O9)=>{"use strict";var tX=A9(),nX=R9(),hw=!1,vw=null;nX({},function(i,o){if(!hw){if(hw=!0,i)throw i;vw=o}});if(!hw)throw new Error("Failed to load the yoga module - it needed to be loaded synchronously, but didn't");O9.exports=tX(vw.bind,vw.lib)});var M9=ce((Vne,k9)=>{"use strict";k9.exports=({onlyFirst:i=!1}={})=>{let o=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(o,i?void 0:"g")}});var mw=ce((Gne,N9)=>{"use strict";var rX=M9();N9.exports=i=>typeof i=="string"?i.replace(rX(),""):i});var gw=ce((Yne,yw)=>{"use strict";var L9=i=>Number.isNaN(i)?!1:i>=4352&&(i<=4447||i===9001||i===9002||11904<=i&&i<=12871&&i!==12351||12880<=i&&i<=19903||19968<=i&&i<=42182||43360<=i&&i<=43388||44032<=i&&i<=55203||63744<=i&&i<=64255||65040<=i&&i<=65049||65072<=i&&i<=65131||65281<=i&&i<=65376||65504<=i&&i<=65510||110592<=i&&i<=110593||127488<=i&&i<=127569||131072<=i&&i<=262141);yw.exports=L9;yw.exports.default=L9});var b9=ce((Kne,F9)=>{"use strict";F9.exports=function(){return/\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62(?:\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67|\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74|\uDB40\uDC77\uDB40\uDC6C\uDB40\uDC73)\uDB40\uDC7F|\uD83D\uDC68(?:\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68\uD83C\uDFFB|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFE])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D)?\uD83D\uDC68|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D[\uDC68\uDC69])\u200D(?:\uD83D[\uDC66\uDC67])|[\u2695\u2696\u2708]\uFE0F|\uD83D[\uDC66\uDC67]|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|(?:\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708])\uFE0F|\uD83C\uDFFB\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C[\uDFFB-\uDFFF])|(?:\uD83E\uDDD1\uD83C\uDFFB\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)\uD83C\uDFFB|\uD83E\uDDD1(?:\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1)|(?:\uD83E\uDDD1\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFF\u200D\uD83E\uDD1D\u200D(?:\uD83D[\uDC68\uDC69]))(?:\uD83C[\uDFFB-\uDFFE])|(?:\uD83E\uDDD1\uD83C\uDFFC\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB\uDFFC])|\uD83D\uDC69(?:\uD83C\uDFFE\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB-\uDFFD\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFC\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFD-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFB\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFC-\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFD\u200D(?:\uD83E\uDD1D\u200D\uD83D\uDC68(?:\uD83C[\uDFFB\uDFFC\uDFFE\uDFFF])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\u200D(?:\u2764\uFE0F\u200D(?:\uD83D\uDC8B\u200D(?:\uD83D[\uDC68\uDC69])|\uD83D[\uDC68\uDC69])|\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD])|\uD83C\uDFFF\u200D(?:\uD83C[\uDF3E\uDF73\uDF93\uDFA4\uDFA8\uDFEB\uDFED]|\uD83D[\uDCBB\uDCBC\uDD27\uDD2C\uDE80\uDE92]|\uD83E[\uDDAF-\uDDB3\uDDBC\uDDBD]))|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67]))|(?:\uD83E\uDDD1\uD83C\uDFFD\u200D\uD83E\uDD1D\u200D\uD83E\uDDD1|\uD83D\uDC69\uD83C\uDFFE\u200D\uD83E\uDD1D\u200D\uD83D\uDC69)(?:\uD83C[\uDFFB-\uDFFD])|\uD83D\uDC69\u200D\uD83D\uDC66\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC69\u200D(?:\uD83D[\uDC66\uDC67])|(?:\uD83D\uDC41\uFE0F\u200D\uD83D\uDDE8|\uD83D\uDC69(?:\uD83C\uDFFF\u200D[\u2695\u2696\u2708]|\uD83C\uDFFE\u200D[\u2695\u2696\u2708]|\uD83C\uDFFC\u200D[\u2695\u2696\u2708]|\uD83C\uDFFB\u200D[\u2695\u2696\u2708]|\uD83C\uDFFD\u200D[\u2695\u2696\u2708]|\u200D[\u2695\u2696\u2708])|(?:(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)\uFE0F|\uD83D\uDC6F|\uD83E[\uDD3C\uDDDE\uDDDF])\u200D[\u2640\u2642]|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:(?:\uD83C[\uDFFB-\uDFFF])\u200D[\u2640\u2642]|\u200D[\u2640\u2642])|\uD83C\uDFF4\u200D\u2620)\uFE0F|\uD83D\uDC69\u200D\uD83D\uDC67\u200D(?:\uD83D[\uDC66\uDC67])|\uD83C\uDFF3\uFE0F\u200D\uD83C\uDF08|\uD83D\uDC15\u200D\uD83E\uDDBA|\uD83D\uDC69\u200D\uD83D\uDC66|\uD83D\uDC69\u200D\uD83D\uDC67|\uD83C\uDDFD\uD83C\uDDF0|\uD83C\uDDF4\uD83C\uDDF2|\uD83C\uDDF6\uD83C\uDDE6|[#\*0-9]\uFE0F\u20E3|\uD83C\uDDE7(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEF\uDDF1-\uDDF4\uDDF6-\uDDF9\uDDFB\uDDFC\uDDFE\uDDFF])|\uD83C\uDDF9(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDED\uDDEF-\uDDF4\uDDF7\uDDF9\uDDFB\uDDFC\uDDFF])|\uD83C\uDDEA(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDED\uDDF7-\uDDFA])|\uD83E\uDDD1(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF7(?:\uD83C[\uDDEA\uDDF4\uDDF8\uDDFA\uDDFC])|\uD83D\uDC69(?:\uD83C[\uDFFB-\uDFFF])|\uD83C\uDDF2(?:\uD83C[\uDDE6\uDDE8-\uDDED\uDDF0-\uDDFF])|\uD83C\uDDE6(?:\uD83C[\uDDE8-\uDDEC\uDDEE\uDDF1\uDDF2\uDDF4\uDDF6-\uDDFA\uDDFC\uDDFD\uDDFF])|\uD83C\uDDF0(?:\uD83C[\uDDEA\uDDEC-\uDDEE\uDDF2\uDDF3\uDDF5\uDDF7\uDDFC\uDDFE\uDDFF])|\uD83C\uDDED(?:\uD83C[\uDDF0\uDDF2\uDDF3\uDDF7\uDDF9\uDDFA])|\uD83C\uDDE9(?:\uD83C[\uDDEA\uDDEC\uDDEF\uDDF0\uDDF2\uDDF4\uDDFF])|\uD83C\uDDFE(?:\uD83C[\uDDEA\uDDF9])|\uD83C\uDDEC(?:\uD83C[\uDDE6\uDDE7\uDDE9-\uDDEE\uDDF1-\uDDF3\uDDF5-\uDDFA\uDDFC\uDDFE])|\uD83C\uDDF8(?:\uD83C[\uDDE6-\uDDEA\uDDEC-\uDDF4\uDDF7-\uDDF9\uDDFB\uDDFD-\uDDFF])|\uD83C\uDDEB(?:\uD83C[\uDDEE-\uDDF0\uDDF2\uDDF4\uDDF7])|\uD83C\uDDF5(?:\uD83C[\uDDE6\uDDEA-\uDDED\uDDF0-\uDDF3\uDDF7-\uDDF9\uDDFC\uDDFE])|\uD83C\uDDFB(?:\uD83C[\uDDE6\uDDE8\uDDEA\uDDEC\uDDEE\uDDF3\uDDFA])|\uD83C\uDDF3(?:\uD83C[\uDDE6\uDDE8\uDDEA-\uDDEC\uDDEE\uDDF1\uDDF4\uDDF5\uDDF7\uDDFA\uDDFF])|\uD83C\uDDE8(?:\uD83C[\uDDE6\uDDE8\uDDE9\uDDEB-\uDDEE\uDDF0-\uDDF5\uDDF7\uDDFA-\uDDFF])|\uD83C\uDDF1(?:\uD83C[\uDDE6-\uDDE8\uDDEE\uDDF0\uDDF7-\uDDFB\uDDFE])|\uD83C\uDDFF(?:\uD83C[\uDDE6\uDDF2\uDDFC])|\uD83C\uDDFC(?:\uD83C[\uDDEB\uDDF8])|\uD83C\uDDFA(?:\uD83C[\uDDE6\uDDEC\uDDF2\uDDF3\uDDF8\uDDFE\uDDFF])|\uD83C\uDDEE(?:\uD83C[\uDDE8-\uDDEA\uDDF1-\uDDF4\uDDF6-\uDDF9])|\uD83C\uDDEF(?:\uD83C[\uDDEA\uDDF2\uDDF4\uDDF5])|(?:\uD83C[\uDFC3\uDFC4\uDFCA]|\uD83D[\uDC6E\uDC71\uDC73\uDC77\uDC81\uDC82\uDC86\uDC87\uDE45-\uDE47\uDE4B\uDE4D\uDE4E\uDEA3\uDEB4-\uDEB6]|\uD83E[\uDD26\uDD37-\uDD39\uDD3D\uDD3E\uDDB8\uDDB9\uDDCD-\uDDCF\uDDD6-\uDDDD])(?:\uD83C[\uDFFB-\uDFFF])|(?:\u26F9|\uD83C[\uDFCB\uDFCC]|\uD83D\uDD75)(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u261D\u270A-\u270D]|\uD83C[\uDF85\uDFC2\uDFC7]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66\uDC67\uDC6B-\uDC6D\uDC70\uDC72\uDC74-\uDC76\uDC78\uDC7C\uDC83\uDC85\uDCAA\uDD74\uDD7A\uDD90\uDD95\uDD96\uDE4C\uDE4F\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1C\uDD1E\uDD1F\uDD30-\uDD36\uDDB5\uDDB6\uDDBB\uDDD2-\uDDD5])(?:\uD83C[\uDFFB-\uDFFF])|(?:[\u231A\u231B\u23E9-\u23EC\u23F0\u23F3\u25FD\u25FE\u2614\u2615\u2648-\u2653\u267F\u2693\u26A1\u26AA\u26AB\u26BD\u26BE\u26C4\u26C5\u26CE\u26D4\u26EA\u26F2\u26F3\u26F5\u26FA\u26FD\u2705\u270A\u270B\u2728\u274C\u274E\u2753-\u2755\u2757\u2795-\u2797\u27B0\u27BF\u2B1B\u2B1C\u2B50\u2B55]|\uD83C[\uDC04\uDCCF\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE1A\uDE2F\uDE32-\uDE36\uDE38-\uDE3A\uDE50\uDE51\uDF00-\uDF20\uDF2D-\uDF35\uDF37-\uDF7C\uDF7E-\uDF93\uDFA0-\uDFCA\uDFCF-\uDFD3\uDFE0-\uDFF0\uDFF4\uDFF8-\uDFFF]|\uD83D[\uDC00-\uDC3E\uDC40\uDC42-\uDCFC\uDCFF-\uDD3D\uDD4B-\uDD4E\uDD50-\uDD67\uDD7A\uDD95\uDD96\uDDA4\uDDFB-\uDE4F\uDE80-\uDEC5\uDECC\uDED0-\uDED2\uDED5\uDEEB\uDEEC\uDEF4-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])|(?:[#\*0-9\xA9\xAE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9\u21AA\u231A\u231B\u2328\u23CF\u23E9-\u23F3\u23F8-\u23FA\u24C2\u25AA\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2604\u260E\u2611\u2614\u2615\u2618\u261D\u2620\u2622\u2623\u2626\u262A\u262E\u262F\u2638-\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665\u2666\u2668\u267B\u267E\u267F\u2692-\u2697\u2699\u269B\u269C\u26A0\u26A1\u26AA\u26AB\u26B0\u26B1\u26BD\u26BE\u26C4\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F5\u26F7-\u26FA\u26FD\u2702\u2705\u2708-\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934\u2935\u2B05-\u2B07\u2B1B\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299]|\uD83C[\uDC04\uDCCF\uDD70\uDD71\uDD7E\uDD7F\uDD8E\uDD91-\uDD9A\uDDE6-\uDDFF\uDE01\uDE02\uDE1A\uDE2F\uDE32-\uDE3A\uDE50\uDE51\uDF00-\uDF21\uDF24-\uDF93\uDF96\uDF97\uDF99-\uDF9B\uDF9E-\uDFF0\uDFF3-\uDFF5\uDFF7-\uDFFF]|\uD83D[\uDC00-\uDCFD\uDCFF-\uDD3D\uDD49-\uDD4E\uDD50-\uDD67\uDD6F\uDD70\uDD73-\uDD7A\uDD87\uDD8A-\uDD8D\uDD90\uDD95\uDD96\uDDA4\uDDA5\uDDA8\uDDB1\uDDB2\uDDBC\uDDC2-\uDDC4\uDDD1-\uDDD3\uDDDC-\uDDDE\uDDE1\uDDE3\uDDE8\uDDEF\uDDF3\uDDFA-\uDE4F\uDE80-\uDEC5\uDECB-\uDED2\uDED5\uDEE0-\uDEE5\uDEE9\uDEEB\uDEEC\uDEF0\uDEF3-\uDEFA\uDFE0-\uDFEB]|\uD83E[\uDD0D-\uDD3A\uDD3C-\uDD45\uDD47-\uDD71\uDD73-\uDD76\uDD7A-\uDDA2\uDDA5-\uDDAA\uDDAE-\uDDCA\uDDCD-\uDDFF\uDE70-\uDE73\uDE78-\uDE7A\uDE80-\uDE82\uDE90-\uDE95])\uFE0F|(?:[\u261D\u26F9\u270A-\u270D]|\uD83C[\uDF85\uDFC2-\uDFC4\uDFC7\uDFCA-\uDFCC]|\uD83D[\uDC42\uDC43\uDC46-\uDC50\uDC66-\uDC78\uDC7C\uDC81-\uDC83\uDC85-\uDC87\uDC8F\uDC91\uDCAA\uDD74\uDD75\uDD7A\uDD90\uDD95\uDD96\uDE45-\uDE47\uDE4B-\uDE4F\uDEA3\uDEB4-\uDEB6\uDEC0\uDECC]|\uD83E[\uDD0F\uDD18-\uDD1F\uDD26\uDD30-\uDD39\uDD3C-\uDD3E\uDDB5\uDDB6\uDDB8\uDDB9\uDDBB\uDDCD-\uDDCF\uDDD1-\uDDDD])/g}});var m4=ce((Xne,_w)=>{"use strict";var iX=mw(),uX=gw(),oX=b9(),P9=i=>{if(i=i.replace(oX()," "),typeof i!="string"||i.length===0)return 0;i=iX(i);let o=0;for(let f=0;f=127&&p<=159||p>=768&&p<=879||(p>65535&&f++,o+=uX(p)?2:1)}return o};_w.exports=P9;_w.exports.default=P9});var Dw=ce((Qne,Ew)=>{"use strict";var lX=m4(),I9=i=>{let o=0;for(let f of i.split(` +`))o=Math.max(o,lX(f));return o};Ew.exports=I9;Ew.exports.default=I9});var B9=ce(vg=>{"use strict";var sX=vg&&vg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(vg,"__esModule",{value:!0});var aX=sX(Dw()),ww={};vg.default=i=>{if(i.length===0)return{width:0,height:0};if(ww[i])return ww[i];let o=aX.default(i),f=i.split(` +`).length;return ww[i]={width:o,height:f},{width:o,height:f}}});var U9=ce(mg=>{"use strict";var fX=mg&&mg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(mg,"__esModule",{value:!0});var Ii=fX(eh()),cX=(i,o)=>{"position"in o&&i.setPositionType(o.position==="absolute"?Ii.default.POSITION_TYPE_ABSOLUTE:Ii.default.POSITION_TYPE_RELATIVE)},dX=(i,o)=>{"marginLeft"in o&&i.setMargin(Ii.default.EDGE_START,o.marginLeft||0),"marginRight"in o&&i.setMargin(Ii.default.EDGE_END,o.marginRight||0),"marginTop"in o&&i.setMargin(Ii.default.EDGE_TOP,o.marginTop||0),"marginBottom"in o&&i.setMargin(Ii.default.EDGE_BOTTOM,o.marginBottom||0)},pX=(i,o)=>{"paddingLeft"in o&&i.setPadding(Ii.default.EDGE_LEFT,o.paddingLeft||0),"paddingRight"in o&&i.setPadding(Ii.default.EDGE_RIGHT,o.paddingRight||0),"paddingTop"in o&&i.setPadding(Ii.default.EDGE_TOP,o.paddingTop||0),"paddingBottom"in o&&i.setPadding(Ii.default.EDGE_BOTTOM,o.paddingBottom||0)},hX=(i,o)=>{var f;"flexGrow"in o&&i.setFlexGrow((f=o.flexGrow)!==null&&f!==void 0?f:0),"flexShrink"in o&&i.setFlexShrink(typeof o.flexShrink=="number"?o.flexShrink:1),"flexDirection"in o&&(o.flexDirection==="row"&&i.setFlexDirection(Ii.default.FLEX_DIRECTION_ROW),o.flexDirection==="row-reverse"&&i.setFlexDirection(Ii.default.FLEX_DIRECTION_ROW_REVERSE),o.flexDirection==="column"&&i.setFlexDirection(Ii.default.FLEX_DIRECTION_COLUMN),o.flexDirection==="column-reverse"&&i.setFlexDirection(Ii.default.FLEX_DIRECTION_COLUMN_REVERSE)),"flexBasis"in o&&(typeof o.flexBasis=="number"?i.setFlexBasis(o.flexBasis):typeof o.flexBasis=="string"?i.setFlexBasisPercent(Number.parseInt(o.flexBasis,10)):i.setFlexBasis(NaN)),"alignItems"in o&&((o.alignItems==="stretch"||!o.alignItems)&&i.setAlignItems(Ii.default.ALIGN_STRETCH),o.alignItems==="flex-start"&&i.setAlignItems(Ii.default.ALIGN_FLEX_START),o.alignItems==="center"&&i.setAlignItems(Ii.default.ALIGN_CENTER),o.alignItems==="flex-end"&&i.setAlignItems(Ii.default.ALIGN_FLEX_END)),"alignSelf"in o&&((o.alignSelf==="auto"||!o.alignSelf)&&i.setAlignSelf(Ii.default.ALIGN_AUTO),o.alignSelf==="flex-start"&&i.setAlignSelf(Ii.default.ALIGN_FLEX_START),o.alignSelf==="center"&&i.setAlignSelf(Ii.default.ALIGN_CENTER),o.alignSelf==="flex-end"&&i.setAlignSelf(Ii.default.ALIGN_FLEX_END)),"justifyContent"in o&&((o.justifyContent==="flex-start"||!o.justifyContent)&&i.setJustifyContent(Ii.default.JUSTIFY_FLEX_START),o.justifyContent==="center"&&i.setJustifyContent(Ii.default.JUSTIFY_CENTER),o.justifyContent==="flex-end"&&i.setJustifyContent(Ii.default.JUSTIFY_FLEX_END),o.justifyContent==="space-between"&&i.setJustifyContent(Ii.default.JUSTIFY_SPACE_BETWEEN),o.justifyContent==="space-around"&&i.setJustifyContent(Ii.default.JUSTIFY_SPACE_AROUND))},vX=(i,o)=>{var f,p;"width"in o&&(typeof o.width=="number"?i.setWidth(o.width):typeof o.width=="string"?i.setWidthPercent(Number.parseInt(o.width,10)):i.setWidthAuto()),"height"in o&&(typeof o.height=="number"?i.setHeight(o.height):typeof o.height=="string"?i.setHeightPercent(Number.parseInt(o.height,10)):i.setHeightAuto()),"minWidth"in o&&(typeof o.minWidth=="string"?i.setMinWidthPercent(Number.parseInt(o.minWidth,10)):i.setMinWidth((f=o.minWidth)!==null&&f!==void 0?f:0)),"minHeight"in o&&(typeof o.minHeight=="string"?i.setMinHeightPercent(Number.parseInt(o.minHeight,10)):i.setMinHeight((p=o.minHeight)!==null&&p!==void 0?p:0))},mX=(i,o)=>{"display"in o&&i.setDisplay(o.display==="flex"?Ii.default.DISPLAY_FLEX:Ii.default.DISPLAY_NONE)},yX=(i,o)=>{if("borderStyle"in o){let f=typeof o.borderStyle=="string"?1:0;i.setBorder(Ii.default.EDGE_TOP,f),i.setBorder(Ii.default.EDGE_BOTTOM,f),i.setBorder(Ii.default.EDGE_LEFT,f),i.setBorder(Ii.default.EDGE_RIGHT,f)}};mg.default=(i,o={})=>{cX(i,o),dX(i,o),pX(i,o),hX(i,o),vX(i,o),mX(i,o),yX(i,o)}});var z9=ce(($ne,j9)=>{"use strict";j9.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});var Sw=ce((ere,q9)=>{var yg=z9(),H9={};for(let i of Object.keys(yg))H9[yg[i]]=i;var In={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};q9.exports=In;for(let i of Object.keys(In)){if(!("channels"in In[i]))throw new Error("missing channels property: "+i);if(!("labels"in In[i]))throw new Error("missing channel labels property: "+i);if(In[i].labels.length!==In[i].channels)throw new Error("channel and label counts mismatch: "+i);let{channels:o,labels:f}=In[i];delete In[i].channels,delete In[i].labels,Object.defineProperty(In[i],"channels",{value:o}),Object.defineProperty(In[i],"labels",{value:f})}In.rgb.hsl=function(i){let o=i[0]/255,f=i[1]/255,p=i[2]/255,E=Math.min(o,f,p),t=Math.max(o,f,p),k=t-E,L,N;t===E?L=0:o===t?L=(f-p)/k:f===t?L=2+(p-o)/k:p===t&&(L=4+(o-f)/k),L=Math.min(L*60,360),L<0&&(L+=360);let C=(E+t)/2;return t===E?N=0:C<=.5?N=k/(t+E):N=k/(2-t-E),[L,N*100,C*100]};In.rgb.hsv=function(i){let o,f,p,E,t,k=i[0]/255,L=i[1]/255,N=i[2]/255,C=Math.max(k,L,N),U=C-Math.min(k,L,N),q=function(W){return(C-W)/6/U+1/2};return U===0?(E=0,t=0):(t=U/C,o=q(k),f=q(L),p=q(N),k===C?E=p-f:L===C?E=1/3+o-p:N===C&&(E=2/3+f-o),E<0?E+=1:E>1&&(E-=1)),[E*360,t*100,C*100]};In.rgb.hwb=function(i){let o=i[0],f=i[1],p=i[2],E=In.rgb.hsl(i)[0],t=1/255*Math.min(o,Math.min(f,p));return p=1-1/255*Math.max(o,Math.max(f,p)),[E,t*100,p*100]};In.rgb.cmyk=function(i){let o=i[0]/255,f=i[1]/255,p=i[2]/255,E=Math.min(1-o,1-f,1-p),t=(1-o-E)/(1-E)||0,k=(1-f-E)/(1-E)||0,L=(1-p-E)/(1-E)||0;return[t*100,k*100,L*100,E*100]};function gX(i,o){return(i[0]-o[0])**2+(i[1]-o[1])**2+(i[2]-o[2])**2}In.rgb.keyword=function(i){let o=H9[i];if(o)return o;let f=Infinity,p;for(let E of Object.keys(yg)){let t=yg[E],k=gX(i,t);k.04045?((o+.055)/1.055)**2.4:o/12.92,f=f>.04045?((f+.055)/1.055)**2.4:f/12.92,p=p>.04045?((p+.055)/1.055)**2.4:p/12.92;let E=o*.4124+f*.3576+p*.1805,t=o*.2126+f*.7152+p*.0722,k=o*.0193+f*.1192+p*.9505;return[E*100,t*100,k*100]};In.rgb.lab=function(i){let o=In.rgb.xyz(i),f=o[0],p=o[1],E=o[2];f/=95.047,p/=100,E/=108.883,f=f>.008856?f**(1/3):7.787*f+16/116,p=p>.008856?p**(1/3):7.787*p+16/116,E=E>.008856?E**(1/3):7.787*E+16/116;let t=116*p-16,k=500*(f-p),L=200*(p-E);return[t,k,L]};In.hsl.rgb=function(i){let o=i[0]/360,f=i[1]/100,p=i[2]/100,E,t,k;if(f===0)return k=p*255,[k,k,k];p<.5?E=p*(1+f):E=p+f-p*f;let L=2*p-E,N=[0,0,0];for(let C=0;C<3;C++)t=o+1/3*-(C-1),t<0&&t++,t>1&&t--,6*t<1?k=L+(E-L)*6*t:2*t<1?k=E:3*t<2?k=L+(E-L)*(2/3-t)*6:k=L,N[C]=k*255;return N};In.hsl.hsv=function(i){let o=i[0],f=i[1]/100,p=i[2]/100,E=f,t=Math.max(p,.01);p*=2,f*=p<=1?p:2-p,E*=t<=1?t:2-t;let k=(p+f)/2,L=p===0?2*E/(t+E):2*f/(p+f);return[o,L*100,k*100]};In.hsv.rgb=function(i){let o=i[0]/60,f=i[1]/100,p=i[2]/100,E=Math.floor(o)%6,t=o-Math.floor(o),k=255*p*(1-f),L=255*p*(1-f*t),N=255*p*(1-f*(1-t));switch(p*=255,E){case 0:return[p,N,k];case 1:return[L,p,k];case 2:return[k,p,N];case 3:return[k,L,p];case 4:return[N,k,p];case 5:return[p,k,L]}};In.hsv.hsl=function(i){let o=i[0],f=i[1]/100,p=i[2]/100,E=Math.max(p,.01),t,k;k=(2-f)*p;let L=(2-f)*E;return t=f*E,t/=L<=1?L:2-L,t=t||0,k/=2,[o,t*100,k*100]};In.hwb.rgb=function(i){let o=i[0]/360,f=i[1]/100,p=i[2]/100,E=f+p,t;E>1&&(f/=E,p/=E);let k=Math.floor(6*o),L=1-p;t=6*o-k,(k&1)!=0&&(t=1-t);let N=f+t*(L-f),C,U,q;switch(k){default:case 6:case 0:C=L,U=N,q=f;break;case 1:C=N,U=L,q=f;break;case 2:C=f,U=L,q=N;break;case 3:C=f,U=N,q=L;break;case 4:C=N,U=f,q=L;break;case 5:C=L,U=f,q=N;break}return[C*255,U*255,q*255]};In.cmyk.rgb=function(i){let o=i[0]/100,f=i[1]/100,p=i[2]/100,E=i[3]/100,t=1-Math.min(1,o*(1-E)+E),k=1-Math.min(1,f*(1-E)+E),L=1-Math.min(1,p*(1-E)+E);return[t*255,k*255,L*255]};In.xyz.rgb=function(i){let o=i[0]/100,f=i[1]/100,p=i[2]/100,E,t,k;return E=o*3.2406+f*-1.5372+p*-.4986,t=o*-.9689+f*1.8758+p*.0415,k=o*.0557+f*-.204+p*1.057,E=E>.0031308?1.055*E**(1/2.4)-.055:E*12.92,t=t>.0031308?1.055*t**(1/2.4)-.055:t*12.92,k=k>.0031308?1.055*k**(1/2.4)-.055:k*12.92,E=Math.min(Math.max(0,E),1),t=Math.min(Math.max(0,t),1),k=Math.min(Math.max(0,k),1),[E*255,t*255,k*255]};In.xyz.lab=function(i){let o=i[0],f=i[1],p=i[2];o/=95.047,f/=100,p/=108.883,o=o>.008856?o**(1/3):7.787*o+16/116,f=f>.008856?f**(1/3):7.787*f+16/116,p=p>.008856?p**(1/3):7.787*p+16/116;let E=116*f-16,t=500*(o-f),k=200*(f-p);return[E,t,k]};In.lab.xyz=function(i){let o=i[0],f=i[1],p=i[2],E,t,k;t=(o+16)/116,E=f/500+t,k=t-p/200;let L=t**3,N=E**3,C=k**3;return t=L>.008856?L:(t-16/116)/7.787,E=N>.008856?N:(E-16/116)/7.787,k=C>.008856?C:(k-16/116)/7.787,E*=95.047,t*=100,k*=108.883,[E,t,k]};In.lab.lch=function(i){let o=i[0],f=i[1],p=i[2],E;E=Math.atan2(p,f)*360/2/Math.PI,E<0&&(E+=360);let k=Math.sqrt(f*f+p*p);return[o,k,E]};In.lch.lab=function(i){let o=i[0],f=i[1],E=i[2]/360*2*Math.PI,t=f*Math.cos(E),k=f*Math.sin(E);return[o,t,k]};In.rgb.ansi16=function(i,o=null){let[f,p,E]=i,t=o===null?In.rgb.hsv(i)[2]:o;if(t=Math.round(t/50),t===0)return 30;let k=30+(Math.round(E/255)<<2|Math.round(p/255)<<1|Math.round(f/255));return t===2&&(k+=60),k};In.hsv.ansi16=function(i){return In.rgb.ansi16(In.hsv.rgb(i),i[2])};In.rgb.ansi256=function(i){let o=i[0],f=i[1],p=i[2];return o===f&&f===p?o<8?16:o>248?231:Math.round((o-8)/247*24)+232:16+36*Math.round(o/255*5)+6*Math.round(f/255*5)+Math.round(p/255*5)};In.ansi16.rgb=function(i){let o=i%10;if(o===0||o===7)return i>50&&(o+=3.5),o=o/10.5*255,[o,o,o];let f=(~~(i>50)+1)*.5,p=(o&1)*f*255,E=(o>>1&1)*f*255,t=(o>>2&1)*f*255;return[p,E,t]};In.ansi256.rgb=function(i){if(i>=232){let t=(i-232)*10+8;return[t,t,t]}i-=16;let o,f=Math.floor(i/36)/5*255,p=Math.floor((o=i%36)/6)/5*255,E=o%6/5*255;return[f,p,E]};In.rgb.hex=function(i){let f=(((Math.round(i[0])&255)<<16)+((Math.round(i[1])&255)<<8)+(Math.round(i[2])&255)).toString(16).toUpperCase();return"000000".substring(f.length)+f};In.hex.rgb=function(i){let o=i.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!o)return[0,0,0];let f=o[0];o[0].length===3&&(f=f.split("").map(L=>L+L).join(""));let p=parseInt(f,16),E=p>>16&255,t=p>>8&255,k=p&255;return[E,t,k]};In.rgb.hcg=function(i){let o=i[0]/255,f=i[1]/255,p=i[2]/255,E=Math.max(Math.max(o,f),p),t=Math.min(Math.min(o,f),p),k=E-t,L,N;return k<1?L=t/(1-k):L=0,k<=0?N=0:E===o?N=(f-p)/k%6:E===f?N=2+(p-o)/k:N=4+(o-f)/k,N/=6,N%=1,[N*360,k*100,L*100]};In.hsl.hcg=function(i){let o=i[1]/100,f=i[2]/100,p=f<.5?2*o*f:2*o*(1-f),E=0;return p<1&&(E=(f-.5*p)/(1-p)),[i[0],p*100,E*100]};In.hsv.hcg=function(i){let o=i[1]/100,f=i[2]/100,p=o*f,E=0;return p<1&&(E=(f-p)/(1-p)),[i[0],p*100,E*100]};In.hcg.rgb=function(i){let o=i[0]/360,f=i[1]/100,p=i[2]/100;if(f===0)return[p*255,p*255,p*255];let E=[0,0,0],t=o%1*6,k=t%1,L=1-k,N=0;switch(Math.floor(t)){case 0:E[0]=1,E[1]=k,E[2]=0;break;case 1:E[0]=L,E[1]=1,E[2]=0;break;case 2:E[0]=0,E[1]=1,E[2]=k;break;case 3:E[0]=0,E[1]=L,E[2]=1;break;case 4:E[0]=k,E[1]=0,E[2]=1;break;default:E[0]=1,E[1]=0,E[2]=L}return N=(1-f)*p,[(f*E[0]+N)*255,(f*E[1]+N)*255,(f*E[2]+N)*255]};In.hcg.hsv=function(i){let o=i[1]/100,f=i[2]/100,p=o+f*(1-o),E=0;return p>0&&(E=o/p),[i[0],E*100,p*100]};In.hcg.hsl=function(i){let o=i[1]/100,p=i[2]/100*(1-o)+.5*o,E=0;return p>0&&p<.5?E=o/(2*p):p>=.5&&p<1&&(E=o/(2*(1-p))),[i[0],E*100,p*100]};In.hcg.hwb=function(i){let o=i[1]/100,f=i[2]/100,p=o+f*(1-o);return[i[0],(p-o)*100,(1-p)*100]};In.hwb.hcg=function(i){let o=i[1]/100,f=i[2]/100,p=1-f,E=p-o,t=0;return E<1&&(t=(p-E)/(1-E)),[i[0],E*100,t*100]};In.apple.rgb=function(i){return[i[0]/65535*255,i[1]/65535*255,i[2]/65535*255]};In.rgb.apple=function(i){return[i[0]/255*65535,i[1]/255*65535,i[2]/255*65535]};In.gray.rgb=function(i){return[i[0]/100*255,i[0]/100*255,i[0]/100*255]};In.gray.hsl=function(i){return[0,0,i[0]]};In.gray.hsv=In.gray.hsl;In.gray.hwb=function(i){return[0,100,i[0]]};In.gray.cmyk=function(i){return[0,0,0,i[0]]};In.gray.lab=function(i){return[i[0],0,0]};In.gray.hex=function(i){let o=Math.round(i[0]/100*255)&255,p=((o<<16)+(o<<8)+o).toString(16).toUpperCase();return"000000".substring(p.length)+p};In.rgb.gray=function(i){return[(i[0]+i[1]+i[2])/3/255*100]}});var V9=ce((tre,W9)=>{var y4=Sw();function _X(){let i={},o=Object.keys(y4);for(let f=o.length,p=0;p{var Tw=Sw(),SX=V9(),sm={},TX=Object.keys(Tw);function CX(i){let o=function(...f){let p=f[0];return p==null?p:(p.length>1&&(f=p),i(f))};return"conversion"in i&&(o.conversion=i.conversion),o}function xX(i){let o=function(...f){let p=f[0];if(p==null)return p;p.length>1&&(f=p);let E=i(f);if(typeof E=="object")for(let t=E.length,k=0;k{sm[i]={},Object.defineProperty(sm[i],"channels",{value:Tw[i].channels}),Object.defineProperty(sm[i],"labels",{value:Tw[i].labels});let o=SX(i);Object.keys(o).forEach(p=>{let E=o[p];sm[i][p]=xX(E),sm[i][p].raw=CX(E)})});G9.exports=sm});var _4=ce((rre,K9)=>{"use strict";var X9=(i,o)=>(...f)=>`[${i(...f)+o}m`,Q9=(i,o)=>(...f)=>{let p=i(...f);return`[${38+o};5;${p}m`},J9=(i,o)=>(...f)=>{let p=i(...f);return`[${38+o};2;${p[0]};${p[1]};${p[2]}m`},g4=i=>i,Z9=(i,o,f)=>[i,o,f],am=(i,o,f)=>{Object.defineProperty(i,o,{get:()=>{let p=f();return Object.defineProperty(i,o,{value:p,enumerable:!0,configurable:!0}),p},enumerable:!0,configurable:!0})},Cw,fm=(i,o,f,p)=>{Cw===void 0&&(Cw=Y9());let E=p?10:0,t={};for(let[k,L]of Object.entries(Cw)){let N=k==="ansi16"?"ansi":k;k===o?t[N]=i(f,E):typeof L=="object"&&(t[N]=i(L[o],E))}return t};function AX(){let i=new Map,o={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};o.color.gray=o.color.blackBright,o.bgColor.bgGray=o.bgColor.bgBlackBright,o.color.grey=o.color.blackBright,o.bgColor.bgGrey=o.bgColor.bgBlackBright;for(let[f,p]of Object.entries(o)){for(let[E,t]of Object.entries(p))o[E]={open:`[${t[0]}m`,close:`[${t[1]}m`},p[E]=o[E],i.set(t[0],t[1]);Object.defineProperty(o,f,{value:p,enumerable:!1})}return Object.defineProperty(o,"codes",{value:i,enumerable:!1}),o.color.close="",o.bgColor.close="",am(o.color,"ansi",()=>fm(X9,"ansi16",g4,!1)),am(o.color,"ansi256",()=>fm(Q9,"ansi256",g4,!1)),am(o.color,"ansi16m",()=>fm(J9,"rgb",Z9,!1)),am(o.bgColor,"ansi",()=>fm(X9,"ansi16",g4,!0)),am(o.bgColor,"ansi256",()=>fm(Q9,"ansi256",g4,!0)),am(o.bgColor,"ansi16m",()=>fm(J9,"rgb",Z9,!0)),o}Object.defineProperty(K9,"exports",{enumerable:!0,get:AX})});var tA=ce((ire,$9)=>{"use strict";var gg=m4(),RX=mw(),OX=_4(),xw=new Set(["","\x9B"]),kX=39,eA=i=>`${xw.values().next().value}[${i}m`,MX=i=>i.split(" ").map(o=>gg(o)),Aw=(i,o,f)=>{let p=[...o],E=!1,t=gg(RX(i[i.length-1]));for(let[k,L]of p.entries()){let N=gg(L);if(t+N<=f?i[i.length-1]+=L:(i.push(L),t=0),xw.has(L))E=!0;else if(E&&L==="m"){E=!1;continue}E||(t+=N,t===f&&k0&&i.length>1&&(i[i.length-2]+=i.pop())},NX=i=>{let o=i.split(" "),f=o.length;for(;f>0&&!(gg(o[f-1])>0);)f--;return f===o.length?i:o.slice(0,f).join(" ")+o.slice(f).join("")},LX=(i,o,f={})=>{if(f.trim!==!1&&i.trim()==="")return"";let p="",E="",t,k=MX(i),L=[""];for(let[N,C]of i.split(" ").entries()){f.trim!==!1&&(L[L.length-1]=L[L.length-1].trimLeft());let U=gg(L[L.length-1]);if(N!==0&&(U>=o&&(f.wordWrap===!1||f.trim===!1)&&(L.push(""),U=0),(U>0||f.trim===!1)&&(L[L.length-1]+=" ",U++)),f.hard&&k[N]>o){let q=o-U,W=1+Math.floor((k[N]-q-1)/o);Math.floor((k[N]-1)/o)o&&U>0&&k[N]>0){if(f.wordWrap===!1&&Uo&&f.wordWrap===!1){Aw(L,C,o);continue}L[L.length-1]+=C}f.trim!==!1&&(L=L.map(NX)),p=L.join(` +`);for(let[N,C]of[...p].entries()){if(E+=C,xw.has(C)){let q=parseFloat(/\d[^m]*/.exec(p.slice(N,N+4)));t=q===kX?null:q}let U=OX.codes.get(Number(t));t&&U&&(p[N+1]===` +`?E+=eA(U):C===` +`&&(E+=eA(t)))}return E};$9.exports=(i,o,f)=>String(i).normalize().replace(/\r\n/g,` +`).split(` +`).map(p=>LX(p,o,f)).join(` +`)});var iA=ce((ure,nA)=>{"use strict";var rA="[\uD800-\uDBFF][\uDC00-\uDFFF]",FX=i=>i&&i.exact?new RegExp(`^${rA}$`):new RegExp(rA,"g");nA.exports=FX});var Rw=ce((ore,uA)=>{"use strict";var bX=gw(),PX=iA(),oA=_4(),lA=["","\x9B"],E4=i=>`${lA[0]}[${i}m`,sA=(i,o,f)=>{let p=[];i=[...i];for(let E of i){let t=E;E.match(";")&&(E=E.split(";")[0][0]+"0");let k=oA.codes.get(parseInt(E,10));if(k){let L=i.indexOf(k.toString());L>=0?i.splice(L,1):p.push(E4(o?k:t))}else if(o){p.push(E4(0));break}else p.push(E4(t))}if(o&&(p=p.filter((E,t)=>p.indexOf(E)===t),f!==void 0)){let E=E4(oA.codes.get(parseInt(f,10)));p=p.reduce((t,k)=>k===E?[k,...t]:[...t,k],[])}return p.join("")};uA.exports=(i,o,f)=>{let p=[...i.normalize()],E=[];f=typeof f=="number"?f:p.length;let t=!1,k,L=0,N="";for(let[C,U]of p.entries()){let q=!1;if(lA.includes(U)){let W=/\d[^m]*/.exec(i.slice(C,C+18));k=W&&W.length>0?W[0]:void 0,Lo&&L<=f)N+=U;else if(L===o&&!t&&k!==void 0)N=sA(E);else if(L>=f){N+=sA(E,!0,k);break}}return N}});var fA=ce((lre,aA)=>{"use strict";var pd=Rw(),IX=m4();function D4(i,o,f){if(i.charAt(o)===" ")return o;for(let p=1;p<=3;p++)if(f){if(i.charAt(o+p)===" ")return o+p}else if(i.charAt(o-p)===" ")return o-p;return o}aA.exports=(i,o,f)=>{f=E0({position:"end",preferTruncationOnSpace:!1},f);let{position:p,space:E,preferTruncationOnSpace:t}=f,k="\u2026",L=1;if(typeof i!="string")throw new TypeError(`Expected \`input\` to be a string, got ${typeof i}`);if(typeof o!="number")throw new TypeError(`Expected \`columns\` to be a number, got ${typeof o}`);if(o<1)return"";if(o===1)return k;let N=IX(i);if(N<=o)return i;if(p==="start"){if(t){let C=D4(i,N-o+1,!0);return k+pd(i,C,N).trim()}return E===!0&&(k+=" ",L=2),k+pd(i,N-o+L,N)}if(p==="middle"){E===!0&&(k=" "+k+" ",L=3);let C=Math.floor(o/2);if(t){let U=D4(i,C),q=D4(i,N-(o-C)+1,!0);return pd(i,0,U)+k+pd(i,q,N).trim()}return pd(i,0,C)+k+pd(i,N-(o-C)+L,N)}if(p==="end"){if(t){let C=D4(i,o-1);return pd(i,0,C)+k}return E===!0&&(k=" "+k,L=2),pd(i,0,o-L)+k}throw new Error(`Expected \`options.position\` to be either \`start\`, \`middle\` or \`end\`, got ${p}`)}});var kw=ce(_g=>{"use strict";var cA=_g&&_g.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(_g,"__esModule",{value:!0});var BX=cA(tA()),UX=cA(fA()),Ow={};_g.default=(i,o,f)=>{let p=i+String(o)+String(f);if(Ow[p])return Ow[p];let E=i;if(f==="wrap"&&(E=BX.default(i,o,{trim:!1,hard:!0})),f.startsWith("truncate")){let t="end";f==="truncate-middle"&&(t="middle"),f==="truncate-start"&&(t="start"),E=UX.default(i,o,{position:t})}return Ow[p]=E,E}});var Nw=ce(Mw=>{"use strict";Object.defineProperty(Mw,"__esModule",{value:!0});var dA=i=>{let o="";if(i.childNodes.length>0)for(let f of i.childNodes){let p="";f.nodeName==="#text"?p=f.nodeValue:((f.nodeName==="ink-text"||f.nodeName==="ink-virtual-text")&&(p=dA(f)),p.length>0&&typeof f.internal_transform=="function"&&(p=f.internal_transform(p))),o+=p}return o};Mw.default=dA});var Lw=ce(l0=>{"use strict";var Eg=l0&&l0.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(l0,"__esModule",{value:!0});l0.setTextNodeValue=l0.createTextNode=l0.setStyle=l0.setAttribute=l0.removeChildNode=l0.insertBeforeNode=l0.appendChildNode=l0.createNode=l0.TEXT_NAME=void 0;var jX=Eg(eh()),pA=Eg(B9()),zX=Eg(U9()),qX=Eg(kw()),HX=Eg(Nw());l0.TEXT_NAME="#text";l0.createNode=i=>{var o;let f={nodeName:i,style:{},attributes:{},childNodes:[],parentNode:null,yogaNode:i==="ink-virtual-text"?void 0:jX.default.Node.create()};return i==="ink-text"&&((o=f.yogaNode)===null||o===void 0||o.setMeasureFunc(WX.bind(null,f))),f};l0.appendChildNode=(i,o)=>{var f;o.parentNode&&l0.removeChildNode(o.parentNode,o),o.parentNode=i,i.childNodes.push(o),o.yogaNode&&((f=i.yogaNode)===null||f===void 0||f.insertChild(o.yogaNode,i.yogaNode.getChildCount())),(i.nodeName==="ink-text"||i.nodeName==="ink-virtual-text")&&w4(i)};l0.insertBeforeNode=(i,o,f)=>{var p,E;o.parentNode&&l0.removeChildNode(o.parentNode,o),o.parentNode=i;let t=i.childNodes.indexOf(f);if(t>=0){i.childNodes.splice(t,0,o),o.yogaNode&&((p=i.yogaNode)===null||p===void 0||p.insertChild(o.yogaNode,t));return}i.childNodes.push(o),o.yogaNode&&((E=i.yogaNode)===null||E===void 0||E.insertChild(o.yogaNode,i.yogaNode.getChildCount())),(i.nodeName==="ink-text"||i.nodeName==="ink-virtual-text")&&w4(i)};l0.removeChildNode=(i,o)=>{var f,p;o.yogaNode&&((p=(f=o.parentNode)===null||f===void 0?void 0:f.yogaNode)===null||p===void 0||p.removeChild(o.yogaNode)),o.parentNode=null;let E=i.childNodes.indexOf(o);E>=0&&i.childNodes.splice(E,1),(i.nodeName==="ink-text"||i.nodeName==="ink-virtual-text")&&w4(i)};l0.setAttribute=(i,o,f)=>{i.attributes[o]=f};l0.setStyle=(i,o)=>{i.style=o,i.yogaNode&&zX.default(i.yogaNode,o)};l0.createTextNode=i=>{let o={nodeName:"#text",nodeValue:i,yogaNode:void 0,parentNode:null,style:{}};return l0.setTextNodeValue(o,i),o};var WX=function(i,o){var f,p;let E=i.nodeName==="#text"?i.nodeValue:HX.default(i),t=pA.default(E);if(t.width<=o||t.width>=1&&o>0&&o<1)return t;let k=(p=(f=i.style)===null||f===void 0?void 0:f.textWrap)!==null&&p!==void 0?p:"wrap",L=qX.default(E,o,k);return pA.default(L)},hA=i=>{var o;if(!(!i||!i.parentNode))return(o=i.yogaNode)!==null&&o!==void 0?o:hA(i.parentNode)},w4=i=>{let o=hA(i);o==null||o.markDirty()};l0.setTextNodeValue=(i,o)=>{typeof o!="string"&&(o=String(o)),i.nodeValue=o,w4(i)}});var th=ce((cre,vA)=>{"use strict";vA.exports={BINARY_TYPES:["nodebuffer","arraybuffer","fragments"],GUID:"258EAFA5-E914-47DA-95CA-C5AB0DC85B11",kStatusCode:Symbol("status-code"),kWebSocket:Symbol("websocket"),EMPTY_BUFFER:Buffer.alloc(0),NOOP:()=>{}}});var Dg=ce((dre,Fw)=>{"use strict";var{EMPTY_BUFFER:VX}=th();function mA(i,o){if(i.length===0)return VX;if(i.length===1)return i[0];let f=Buffer.allocUnsafe(o),p=0;for(let E=0;E{"use strict";var DA=Symbol("kDone"),bw=Symbol("kRun"),wA=class{constructor(o){this[DA]=()=>{this.pending--,this[bw]()},this.concurrency=o||Infinity,this.jobs=[],this.pending=0}add(o){this.jobs.push(o),this[bw]()}[bw](){if(this.pending!==this.concurrency&&this.jobs.length){let o=this.jobs.shift();this.pending++,o(this[DA])}}};EA.exports=wA});var Tg=ce((hre,TA)=>{"use strict";var wg=require("zlib"),CA=Dg(),GX=SA(),{kStatusCode:xA,NOOP:YX}=th(),KX=Buffer.from([0,0,255,255]),T4=Symbol("permessage-deflate"),G1=Symbol("total-length"),Sg=Symbol("callback"),hd=Symbol("buffers"),Pw=Symbol("error"),C4,AA=class{constructor(o,f,p){if(this._maxPayload=p|0,this._options=o||{},this._threshold=this._options.threshold!==void 0?this._options.threshold:1024,this._isServer=!!f,this._deflate=null,this._inflate=null,this.params=null,!C4){let E=this._options.concurrencyLimit!==void 0?this._options.concurrencyLimit:10;C4=new GX(E)}}static get extensionName(){return"permessage-deflate"}offer(){let o={};return this._options.serverNoContextTakeover&&(o.server_no_context_takeover=!0),this._options.clientNoContextTakeover&&(o.client_no_context_takeover=!0),this._options.serverMaxWindowBits&&(o.server_max_window_bits=this._options.serverMaxWindowBits),this._options.clientMaxWindowBits?o.client_max_window_bits=this._options.clientMaxWindowBits:this._options.clientMaxWindowBits==null&&(o.client_max_window_bits=!0),o}accept(o){return o=this.normalizeParams(o),this.params=this._isServer?this.acceptAsServer(o):this.acceptAsClient(o),this.params}cleanup(){if(this._inflate&&(this._inflate.close(),this._inflate=null),this._deflate){let o=this._deflate[Sg];this._deflate.close(),this._deflate=null,o&&o(new Error("The deflate stream was closed while data was being processed"))}}acceptAsServer(o){let f=this._options,p=o.find(E=>!(f.serverNoContextTakeover===!1&&E.server_no_context_takeover||E.server_max_window_bits&&(f.serverMaxWindowBits===!1||typeof f.serverMaxWindowBits=="number"&&f.serverMaxWindowBits>E.server_max_window_bits)||typeof f.clientMaxWindowBits=="number"&&!E.client_max_window_bits));if(!p)throw new Error("None of the extension offers can be accepted");return f.serverNoContextTakeover&&(p.server_no_context_takeover=!0),f.clientNoContextTakeover&&(p.client_no_context_takeover=!0),typeof f.serverMaxWindowBits=="number"&&(p.server_max_window_bits=f.serverMaxWindowBits),typeof f.clientMaxWindowBits=="number"?p.client_max_window_bits=f.clientMaxWindowBits:(p.client_max_window_bits===!0||f.clientMaxWindowBits===!1)&&delete p.client_max_window_bits,p}acceptAsClient(o){let f=o[0];if(this._options.clientNoContextTakeover===!1&&f.client_no_context_takeover)throw new Error('Unexpected parameter "client_no_context_takeover"');if(!f.client_max_window_bits)typeof this._options.clientMaxWindowBits=="number"&&(f.client_max_window_bits=this._options.clientMaxWindowBits);else if(this._options.clientMaxWindowBits===!1||typeof this._options.clientMaxWindowBits=="number"&&f.client_max_window_bits>this._options.clientMaxWindowBits)throw new Error('Unexpected or invalid parameter "client_max_window_bits"');return f}normalizeParams(o){return o.forEach(f=>{Object.keys(f).forEach(p=>{let E=f[p];if(E.length>1)throw new Error(`Parameter "${p}" must have only a single value`);if(E=E[0],p==="client_max_window_bits"){if(E!==!0){let t=+E;if(!Number.isInteger(t)||t<8||t>15)throw new TypeError(`Invalid value for parameter "${p}": ${E}`);E=t}else if(!this._isServer)throw new TypeError(`Invalid value for parameter "${p}": ${E}`)}else if(p==="server_max_window_bits"){let t=+E;if(!Number.isInteger(t)||t<8||t>15)throw new TypeError(`Invalid value for parameter "${p}": ${E}`);E=t}else if(p==="client_no_context_takeover"||p==="server_no_context_takeover"){if(E!==!0)throw new TypeError(`Invalid value for parameter "${p}": ${E}`)}else throw new Error(`Unknown parameter "${p}"`);f[p]=E})}),o}decompress(o,f,p){C4.add(E=>{this._decompress(o,f,(t,k)=>{E(),p(t,k)})})}compress(o,f,p){C4.add(E=>{this._compress(o,f,(t,k)=>{E(),p(t,k)})})}_decompress(o,f,p){let E=this._isServer?"client":"server";if(!this._inflate){let t=`${E}_max_window_bits`,k=typeof this.params[t]!="number"?wg.Z_DEFAULT_WINDOWBITS:this.params[t];this._inflate=wg.createInflateRaw(Gf(E0({},this._options.zlibInflateOptions),{windowBits:k})),this._inflate[T4]=this,this._inflate[G1]=0,this._inflate[hd]=[],this._inflate.on("error",QX),this._inflate.on("data",RA)}this._inflate[Sg]=p,this._inflate.write(o),f&&this._inflate.write(KX),this._inflate.flush(()=>{let t=this._inflate[Pw];if(t){this._inflate.close(),this._inflate=null,p(t);return}let k=CA.concat(this._inflate[hd],this._inflate[G1]);this._inflate._readableState.endEmitted?(this._inflate.close(),this._inflate=null):(this._inflate[G1]=0,this._inflate[hd]=[],f&&this.params[`${E}_no_context_takeover`]&&this._inflate.reset()),p(null,k)})}_compress(o,f,p){let E=this._isServer?"server":"client";if(!this._deflate){let t=`${E}_max_window_bits`,k=typeof this.params[t]!="number"?wg.Z_DEFAULT_WINDOWBITS:this.params[t];this._deflate=wg.createDeflateRaw(Gf(E0({},this._options.zlibDeflateOptions),{windowBits:k})),this._deflate[G1]=0,this._deflate[hd]=[],this._deflate.on("error",YX),this._deflate.on("data",XX)}this._deflate[Sg]=p,this._deflate.write(o),this._deflate.flush(wg.Z_SYNC_FLUSH,()=>{if(!this._deflate)return;let t=CA.concat(this._deflate[hd],this._deflate[G1]);f&&(t=t.slice(0,t.length-4)),this._deflate[Sg]=null,this._deflate[G1]=0,this._deflate[hd]=[],f&&this.params[`${E}_no_context_takeover`]&&this._deflate.reset(),p(null,t)})}};TA.exports=AA;function XX(i){this[hd].push(i),this[G1]+=i.length}function RA(i){if(this[G1]+=i.length,this[T4]._maxPayload<1||this[G1]<=this[T4]._maxPayload){this[hd].push(i);return}this[Pw]=new RangeError("Max payload size exceeded"),this[Pw][xA]=1009,this.removeListener("data",RA),this.reset()}function QX(i){this[T4]._inflate=null,i[xA]=1007,this[Sg](i)}});var Bw=ce((vre,Iw)=>{"use strict";function OA(i){return i>=1e3&&i<=1014&&i!==1004&&i!==1005&&i!==1006||i>=3e3&&i<=4999}function kA(i){let o=i.length,f=0;for(;f=o||(i[f+1]&192)!=128||(i[f+2]&192)!=128||i[f]===224&&(i[f+1]&224)==128||i[f]===237&&(i[f+1]&224)==160)return!1;f+=3}else if((i[f]&248)==240){if(f+3>=o||(i[f+1]&192)!=128||(i[f+2]&192)!=128||(i[f+3]&192)!=128||i[f]===240&&(i[f+1]&240)==128||i[f]===244&&i[f+1]>143||i[f]>244)return!1;f+=4}else return!1;return!0}try{let i=require("utf-8-validate");typeof i=="object"&&(i=i.Validation.isValidUTF8),Iw.exports={isValidStatusCode:OA,isValidUTF8(o){return o.length<150?kA(o):i(o)}}}catch(i){Iw.exports={isValidStatusCode:OA,isValidUTF8:kA}}});var zw=ce((mre,MA)=>{"use strict";var{Writable:JX}=require("stream"),NA=Tg(),{BINARY_TYPES:ZX,EMPTY_BUFFER:$X,kStatusCode:eQ,kWebSocket:tQ}=th(),{concat:Uw,toArrayBuffer:nQ,unmask:rQ}=Dg(),{isValidStatusCode:iQ,isValidUTF8:LA}=Bw(),Cg=0,FA=1,bA=2,PA=3,jw=4,uQ=5,IA=class extends JX{constructor(o,f,p,E){super();this._binaryType=o||ZX[0],this[tQ]=void 0,this._extensions=f||{},this._isServer=!!p,this._maxPayload=E|0,this._bufferedBytes=0,this._buffers=[],this._compressed=!1,this._payloadLength=0,this._mask=void 0,this._fragmented=0,this._masked=!1,this._fin=!1,this._opcode=0,this._totalPayloadLength=0,this._messageLength=0,this._fragments=[],this._state=Cg,this._loop=!1}_write(o,f,p){if(this._opcode===8&&this._state==Cg)return p();this._bufferedBytes+=o.length,this._buffers.push(o),this.startLoop(p)}consume(o){if(this._bufferedBytes-=o,o===this._buffers[0].length)return this._buffers.shift();if(o=p.length?f.set(this._buffers.shift(),E):(f.set(new Uint8Array(p.buffer,p.byteOffset,o),E),this._buffers[0]=p.slice(o)),o-=p.length}while(o>0);return f}startLoop(o){let f;this._loop=!0;do switch(this._state){case Cg:f=this.getInfo();break;case FA:f=this.getPayloadLength16();break;case bA:f=this.getPayloadLength64();break;case PA:this.getMask();break;case jw:f=this.getData(o);break;default:this._loop=!1;return}while(this._loop);o(f)}getInfo(){if(this._bufferedBytes<2){this._loop=!1;return}let o=this.consume(2);if((o[0]&48)!=0)return this._loop=!1,Ho(RangeError,"RSV2 and RSV3 must be clear",!0,1002);let f=(o[0]&64)==64;if(f&&!this._extensions[NA.extensionName])return this._loop=!1,Ho(RangeError,"RSV1 must be clear",!0,1002);if(this._fin=(o[0]&128)==128,this._opcode=o[0]&15,this._payloadLength=o[1]&127,this._opcode===0){if(f)return this._loop=!1,Ho(RangeError,"RSV1 must be clear",!0,1002);if(!this._fragmented)return this._loop=!1,Ho(RangeError,"invalid opcode 0",!0,1002);this._opcode=this._fragmented}else if(this._opcode===1||this._opcode===2){if(this._fragmented)return this._loop=!1,Ho(RangeError,`invalid opcode ${this._opcode}`,!0,1002);this._compressed=f}else if(this._opcode>7&&this._opcode<11){if(!this._fin)return this._loop=!1,Ho(RangeError,"FIN must be set",!0,1002);if(f)return this._loop=!1,Ho(RangeError,"RSV1 must be clear",!0,1002);if(this._payloadLength>125)return this._loop=!1,Ho(RangeError,`invalid payload length ${this._payloadLength}`,!0,1002)}else return this._loop=!1,Ho(RangeError,`invalid opcode ${this._opcode}`,!0,1002);if(!this._fin&&!this._fragmented&&(this._fragmented=this._opcode),this._masked=(o[1]&128)==128,this._isServer){if(!this._masked)return this._loop=!1,Ho(RangeError,"MASK must be set",!0,1002)}else if(this._masked)return this._loop=!1,Ho(RangeError,"MASK must be clear",!0,1002);if(this._payloadLength===126)this._state=FA;else if(this._payloadLength===127)this._state=bA;else return this.haveLength()}getPayloadLength16(){if(this._bufferedBytes<2){this._loop=!1;return}return this._payloadLength=this.consume(2).readUInt16BE(0),this.haveLength()}getPayloadLength64(){if(this._bufferedBytes<8){this._loop=!1;return}let o=this.consume(8),f=o.readUInt32BE(0);return f>Math.pow(2,53-32)-1?(this._loop=!1,Ho(RangeError,"Unsupported WebSocket frame: payload length > 2^53 - 1",!1,1009)):(this._payloadLength=f*Math.pow(2,32)+o.readUInt32BE(4),this.haveLength())}haveLength(){if(this._payloadLength&&this._opcode<8&&(this._totalPayloadLength+=this._payloadLength,this._totalPayloadLength>this._maxPayload&&this._maxPayload>0))return this._loop=!1,Ho(RangeError,"Max payload size exceeded",!1,1009);this._masked?this._state=PA:this._state=jw}getMask(){if(this._bufferedBytes<4){this._loop=!1;return}this._mask=this.consume(4),this._state=jw}getData(o){let f=$X;if(this._payloadLength){if(this._bufferedBytes7)return this.controlMessage(f);if(this._compressed){this._state=uQ,this.decompress(f,o);return}return f.length&&(this._messageLength=this._totalPayloadLength,this._fragments.push(f)),this.dataMessage()}decompress(o,f){this._extensions[NA.extensionName].decompress(o,this._fin,(E,t)=>{if(E)return f(E);if(t.length){if(this._messageLength+=t.length,this._messageLength>this._maxPayload&&this._maxPayload>0)return f(Ho(RangeError,"Max payload size exceeded",!1,1009));this._fragments.push(t)}let k=this.dataMessage();if(k)return f(k);this.startLoop(f)})}dataMessage(){if(this._fin){let o=this._messageLength,f=this._fragments;if(this._totalPayloadLength=0,this._messageLength=0,this._fragmented=0,this._fragments=[],this._opcode===2){let p;this._binaryType==="nodebuffer"?p=Uw(f,o):this._binaryType==="arraybuffer"?p=nQ(Uw(f,o)):p=f,this.emit("message",p)}else{let p=Uw(f,o);if(!LA(p))return this._loop=!1,Ho(Error,"invalid UTF-8 sequence",!0,1007);this.emit("message",p.toString())}}this._state=Cg}controlMessage(o){if(this._opcode===8)if(this._loop=!1,o.length===0)this.emit("conclude",1005,""),this.end();else{if(o.length===1)return Ho(RangeError,"invalid payload length 1",!0,1002);{let f=o.readUInt16BE(0);if(!iQ(f))return Ho(RangeError,`invalid status code ${f}`,!0,1002);let p=o.slice(2);if(!LA(p))return Ho(Error,"invalid UTF-8 sequence",!0,1007);this.emit("conclude",f,p.toString()),this.end()}}else this._opcode===9?this.emit("ping",o):this.emit("pong",o);this._state=Cg}};MA.exports=IA;function Ho(i,o,f,p){let E=new i(f?`Invalid WebSocket frame: ${o}`:o);return Error.captureStackTrace(E,Ho),E[eQ]=p,E}});var qw=ce((yre,BA)=>{"use strict";var{randomFillSync:oQ}=require("crypto"),UA=Tg(),{EMPTY_BUFFER:lQ}=th(),{isValidStatusCode:sQ}=Bw(),{mask:jA,toBuffer:Y1}=Dg(),nh=Buffer.alloc(4),K1=class{constructor(o,f){this._extensions=f||{},this._socket=o,this._firstFragment=!0,this._compress=!1,this._bufferedBytes=0,this._deflating=!1,this._queue=[]}static frame(o,f){let p=f.mask&&f.readOnly,E=f.mask?6:2,t=o.length;o.length>=65536?(E+=8,t=127):o.length>125&&(E+=2,t=126);let k=Buffer.allocUnsafe(p?o.length+E:E);return k[0]=f.fin?f.opcode|128:f.opcode,f.rsv1&&(k[0]|=64),k[1]=t,t===126?k.writeUInt16BE(o.length,2):t===127&&(k.writeUInt32BE(0,2),k.writeUInt32BE(o.length,6)),f.mask?(oQ(nh,0,4),k[1]|=128,k[E-4]=nh[0],k[E-3]=nh[1],k[E-2]=nh[2],k[E-1]=nh[3],p?(jA(o,nh,k,E,o.length),[k]):(jA(o,nh,o,0,o.length),[k,o])):[k,o]}close(o,f,p,E){let t;if(o===void 0)t=lQ;else{if(typeof o!="number"||!sQ(o))throw new TypeError("First argument must be a valid error code number");if(f===void 0||f==="")t=Buffer.allocUnsafe(2),t.writeUInt16BE(o,0);else{let k=Buffer.byteLength(f);if(k>123)throw new RangeError("The message must not be greater than 123 bytes");t=Buffer.allocUnsafe(2+k),t.writeUInt16BE(o,0),t.write(f,2)}}this._deflating?this.enqueue([this.doClose,t,p,E]):this.doClose(t,p,E)}doClose(o,f,p){this.sendFrame(K1.frame(o,{fin:!0,rsv1:!1,opcode:8,mask:f,readOnly:!1}),p)}ping(o,f,p){let E=Y1(o);if(E.length>125)throw new RangeError("The data size must not be greater than 125 bytes");this._deflating?this.enqueue([this.doPing,E,f,Y1.readOnly,p]):this.doPing(E,f,Y1.readOnly,p)}doPing(o,f,p,E){this.sendFrame(K1.frame(o,{fin:!0,rsv1:!1,opcode:9,mask:f,readOnly:p}),E)}pong(o,f,p){let E=Y1(o);if(E.length>125)throw new RangeError("The data size must not be greater than 125 bytes");this._deflating?this.enqueue([this.doPong,E,f,Y1.readOnly,p]):this.doPong(E,f,Y1.readOnly,p)}doPong(o,f,p,E){this.sendFrame(K1.frame(o,{fin:!0,rsv1:!1,opcode:10,mask:f,readOnly:p}),E)}send(o,f,p){let E=Y1(o),t=this._extensions[UA.extensionName],k=f.binary?2:1,L=f.compress;if(this._firstFragment?(this._firstFragment=!1,L&&t&&(L=E.length>=t._threshold),this._compress=L):(L=!1,k=0),f.fin&&(this._firstFragment=!0),t){let N={fin:f.fin,rsv1:L,opcode:k,mask:f.mask,readOnly:Y1.readOnly};this._deflating?this.enqueue([this.dispatch,E,this._compress,N,p]):this.dispatch(E,this._compress,N,p)}else this.sendFrame(K1.frame(E,{fin:f.fin,rsv1:!1,opcode:k,mask:f.mask,readOnly:Y1.readOnly}),p)}dispatch(o,f,p,E){if(!f){this.sendFrame(K1.frame(o,p),E);return}let t=this._extensions[UA.extensionName];this._bufferedBytes+=o.length,this._deflating=!0,t.compress(o,p.fin,(k,L)=>{if(this._socket.destroyed){let N=new Error("The socket was closed while data was being compressed");typeof E=="function"&&E(N);for(let C=0;C{"use strict";var xg=class{constructor(o,f){this.target=f,this.type=o}},qA=class extends xg{constructor(o,f){super("message",f);this.data=o}},HA=class extends xg{constructor(o,f,p){super("close",p);this.wasClean=p._closeFrameReceived&&p._closeFrameSent,this.reason=f,this.code=o}},WA=class extends xg{constructor(o){super("open",o)}},VA=class extends xg{constructor(o,f){super("error",f);this.message=o.message,this.error=o}},aQ={addEventListener(i,o,f){if(typeof o!="function")return;function p(N){o.call(this,new qA(N,this))}function E(N,C){o.call(this,new HA(N,C,this))}function t(N){o.call(this,new VA(N,this))}function k(){o.call(this,new WA(this))}let L=f&&f.once?"once":"on";i==="message"?(p._listener=o,this[L](i,p)):i==="close"?(E._listener=o,this[L](i,E)):i==="error"?(t._listener=o,this[L](i,t)):i==="open"?(k._listener=o,this[L](i,k)):this[L](i,o)},removeEventListener(i,o){let f=this.listeners(i);for(let p=0;p{"use strict";var Ag=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,1,1,1,0,0,1,1,0,1,1,0,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,0];function Pc(i,o,f){i[o]===void 0?i[o]=[f]:i[o].push(f)}function fQ(i){let o=Object.create(null);if(i===void 0||i==="")return o;let f=Object.create(null),p=!1,E=!1,t=!1,k,L,N=-1,C=-1,U=0;for(;U{let f=i[o];return Array.isArray(f)||(f=[f]),f.map(p=>[o].concat(Object.keys(p).map(E=>{let t=p[E];return Array.isArray(t)||(t=[t]),t.map(k=>k===!0?E:`${E}=${k}`).join("; ")})).join("; ")).join(", ")}).join(", ")}YA.exports={format:cQ,parse:fQ}});var Kw=ce((Ere,KA)=>{"use strict";var dQ=require("events"),pQ=require("https"),hQ=require("http"),XA=require("net"),vQ=require("tls"),{randomBytes:mQ,createHash:yQ}=require("crypto"),{URL:Ww}=require("url"),vd=Tg(),gQ=zw(),_Q=qw(),{BINARY_TYPES:QA,EMPTY_BUFFER:Vw,GUID:EQ,kStatusCode:DQ,kWebSocket:Qs,NOOP:JA}=th(),{addEventListener:wQ,removeEventListener:SQ}=GA(),{format:TQ,parse:CQ}=Hw(),{toBuffer:xQ}=Dg(),ZA=["CONNECTING","OPEN","CLOSING","CLOSED"],Gw=[8,13],AQ=30*1e3,Bi=class extends dQ{constructor(o,f,p){super();this._binaryType=QA[0],this._closeCode=1006,this._closeFrameReceived=!1,this._closeFrameSent=!1,this._closeMessage="",this._closeTimer=null,this._extensions={},this._protocol="",this._readyState=Bi.CONNECTING,this._receiver=null,this._sender=null,this._socket=null,o!==null?(this._bufferedAmount=0,this._isServer=!1,this._redirects=0,Array.isArray(f)?f=f.join(", "):typeof f=="object"&&f!==null&&(p=f,f=void 0),$A(this,o,f,p)):this._isServer=!0}get binaryType(){return this._binaryType}set binaryType(o){!QA.includes(o)||(this._binaryType=o,this._receiver&&(this._receiver._binaryType=o))}get bufferedAmount(){return this._socket?this._socket._writableState.length+this._sender._bufferedBytes:this._bufferedAmount}get extensions(){return Object.keys(this._extensions).join()}get protocol(){return this._protocol}get readyState(){return this._readyState}get url(){return this._url}setSocket(o,f,p){let E=new gQ(this.binaryType,this._extensions,this._isServer,p);this._sender=new _Q(o,this._extensions),this._receiver=E,this._socket=o,E[Qs]=this,o[Qs]=this,E.on("conclude",RQ),E.on("drain",OQ),E.on("error",kQ),E.on("message",MQ),E.on("ping",NQ),E.on("pong",LQ),o.setTimeout(0),o.setNoDelay(),f.length>0&&o.unshift(f),o.on("close",eR),o.on("data",x4),o.on("end",tR),o.on("error",nR),this._readyState=Bi.OPEN,this.emit("open")}emitClose(){if(!this._socket){this._readyState=Bi.CLOSED,this.emit("close",this._closeCode,this._closeMessage);return}this._extensions[vd.extensionName]&&this._extensions[vd.extensionName].cleanup(),this._receiver.removeAllListeners(),this._readyState=Bi.CLOSED,this.emit("close",this._closeCode,this._closeMessage)}close(o,f){if(this.readyState!==Bi.CLOSED){if(this.readyState===Bi.CONNECTING){let p="WebSocket was closed before the connection was established";return X1(this,this._req,p)}if(this.readyState===Bi.CLOSING){this._closeFrameSent&&this._closeFrameReceived&&this._socket.end();return}this._readyState=Bi.CLOSING,this._sender.close(o,f,!this._isServer,p=>{p||(this._closeFrameSent=!0,this._closeFrameReceived&&this._socket.end())}),this._closeTimer=setTimeout(this._socket.destroy.bind(this._socket),AQ)}}ping(o,f,p){if(this.readyState===Bi.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof o=="function"?(p=o,o=f=void 0):typeof f=="function"&&(p=f,f=void 0),typeof o=="number"&&(o=o.toString()),this.readyState!==Bi.OPEN){Yw(this,o,p);return}f===void 0&&(f=!this._isServer),this._sender.ping(o||Vw,f,p)}pong(o,f,p){if(this.readyState===Bi.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof o=="function"?(p=o,o=f=void 0):typeof f=="function"&&(p=f,f=void 0),typeof o=="number"&&(o=o.toString()),this.readyState!==Bi.OPEN){Yw(this,o,p);return}f===void 0&&(f=!this._isServer),this._sender.pong(o||Vw,f,p)}send(o,f,p){if(this.readyState===Bi.CONNECTING)throw new Error("WebSocket is not open: readyState 0 (CONNECTING)");if(typeof f=="function"&&(p=f,f={}),typeof o=="number"&&(o=o.toString()),this.readyState!==Bi.OPEN){Yw(this,o,p);return}let E=E0({binary:typeof o!="string",mask:!this._isServer,compress:!0,fin:!0},f);this._extensions[vd.extensionName]||(E.compress=!1),this._sender.send(o||Vw,E,p)}terminate(){if(this.readyState!==Bi.CLOSED){if(this.readyState===Bi.CONNECTING){let o="WebSocket was closed before the connection was established";return X1(this,this._req,o)}this._socket&&(this._readyState=Bi.CLOSING,this._socket.destroy())}}};ZA.forEach((i,o)=>{let f={enumerable:!0,value:o};Object.defineProperty(Bi.prototype,i,f),Object.defineProperty(Bi,i,f)});["binaryType","bufferedAmount","extensions","protocol","readyState","url"].forEach(i=>{Object.defineProperty(Bi.prototype,i,{enumerable:!0})});["open","error","close","message"].forEach(i=>{Object.defineProperty(Bi.prototype,`on${i}`,{configurable:!0,enumerable:!0,get(){let o=this.listeners(i);for(let f=0;f{X1(i,W,"Opening handshake has timed out")}),W.on("error",ne=>{W===null||W.aborted||(W=i._req=null,i._readyState=Bi.CLOSING,i.emit("error",ne),i.emitClose())}),W.on("response",ne=>{let m=ne.headers.location,we=ne.statusCode;if(m&&E.followRedirects&&we>=300&&we<400){if(++i._redirects>E.maxRedirects){X1(i,W,"Maximum redirects exceeded");return}W.abort();let Se=new Ww(m,o);$A(i,Se,f,p)}else i.emit("unexpected-response",W,ne)||X1(i,W,`Unexpected server response: ${ne.statusCode}`)}),W.on("upgrade",(ne,m,we)=>{if(i.emit("upgrade",ne),i.readyState!==Bi.CONNECTING)return;W=i._req=null;let Se=yQ("sha1").update(C+EQ).digest("base64");if(ne.headers["sec-websocket-accept"]!==Se){X1(i,m,"Invalid Sec-WebSocket-Accept header");return}let he=ne.headers["sec-websocket-protocol"],ge=(f||"").split(/, */),ze;if(!f&&he?ze="Server sent a subprotocol but none was requested":f&&!he?ze="Server sent no subprotocol":he&&!ge.includes(he)&&(ze="Server sent an invalid subprotocol"),ze){X1(i,m,ze);return}if(he&&(i._protocol=he),q)try{let pe=CQ(ne.headers["sec-websocket-extensions"]);pe[vd.extensionName]&&(q.accept(pe[vd.extensionName]),i._extensions[vd.extensionName]=q)}catch(pe){X1(i,m,"Invalid Sec-WebSocket-Extensions header");return}i.setSocket(m,we,E.maxPayload)})}function FQ(i){return i.path=i.socketPath,XA.connect(i)}function bQ(i){return i.path=void 0,!i.servername&&i.servername!==""&&(i.servername=XA.isIP(i.host)?"":i.host),vQ.connect(i)}function X1(i,o,f){i._readyState=Bi.CLOSING;let p=new Error(f);Error.captureStackTrace(p,X1),o.setHeader?(o.abort(),o.socket&&!o.socket.destroyed&&o.socket.destroy(),o.once("abort",i.emitClose.bind(i)),i.emit("error",p)):(o.destroy(p),o.once("error",i.emit.bind(i,"error")),o.once("close",i.emitClose.bind(i)))}function Yw(i,o,f){if(o){let p=xQ(o).length;i._socket?i._sender._bufferedBytes+=p:i._bufferedAmount+=p}if(f){let p=new Error(`WebSocket is not open: readyState ${i.readyState} (${ZA[i.readyState]})`);f(p)}}function RQ(i,o){let f=this[Qs];f._socket.removeListener("data",x4),f._socket.resume(),f._closeFrameReceived=!0,f._closeMessage=o,f._closeCode=i,i===1005?f.close():f.close(i,o)}function OQ(){this[Qs]._socket.resume()}function kQ(i){let o=this[Qs];o._socket.removeListener("data",x4),o._readyState=Bi.CLOSING,o._closeCode=i[DQ],o.emit("error",i),o._socket.destroy()}function rR(){this[Qs].emitClose()}function MQ(i){this[Qs].emit("message",i)}function NQ(i){let o=this[Qs];o.pong(i,!o._isServer,JA),o.emit("ping",i)}function LQ(i){this[Qs].emit("pong",i)}function eR(){let i=this[Qs];this.removeListener("close",eR),this.removeListener("end",tR),i._readyState=Bi.CLOSING,i._socket.read(),i._receiver.end(),this.removeListener("data",x4),this[Qs]=void 0,clearTimeout(i._closeTimer),i._receiver._writableState.finished||i._receiver._writableState.errorEmitted?i.emitClose():(i._receiver.on("error",rR),i._receiver.on("finish",rR))}function x4(i){this[Qs]._receiver.write(i)||this.pause()}function tR(){let i=this[Qs];i._readyState=Bi.CLOSING,i._receiver.end(),this.end()}function nR(){let i=this[Qs];this.removeListener("error",nR),this.on("error",JA),i&&(i._readyState=Bi.CLOSING,this.destroy())}});var lR=ce((Dre,iR)=>{"use strict";var{Duplex:PQ}=require("stream");function uR(i){i.emit("close")}function IQ(){!this.destroyed&&this._writableState.finished&&this.destroy()}function oR(i){this.removeListener("error",oR),this.destroy(),this.listenerCount("error")===0&&this.emit("error",i)}function BQ(i,o){let f=!0;function p(){f&&i._socket.resume()}i.readyState===i.CONNECTING?i.once("open",function(){i._receiver.removeAllListeners("drain"),i._receiver.on("drain",p)}):(i._receiver.removeAllListeners("drain"),i._receiver.on("drain",p));let E=new PQ(Gf(E0({},o),{autoDestroy:!1,emitClose:!1,objectMode:!1,writableObjectMode:!1}));return i.on("message",function(k){E.push(k)||(f=!1,i._socket.pause())}),i.once("error",function(k){E.destroyed||E.destroy(k)}),i.once("close",function(){E.destroyed||E.push(null)}),E._destroy=function(t,k){if(i.readyState===i.CLOSED){k(t),process.nextTick(uR,E);return}let L=!1;i.once("error",function(C){L=!0,k(C)}),i.once("close",function(){L||k(t),process.nextTick(uR,E)}),i.terminate()},E._final=function(t){if(i.readyState===i.CONNECTING){i.once("open",function(){E._final(t)});return}i._socket!==null&&(i._socket._writableState.finished?(t(),E._readableState.endEmitted&&E.destroy()):(i._socket.once("finish",function(){t()}),i.close()))},E._read=function(){i.readyState===i.OPEN&&!f&&(f=!0,i._receiver._writableState.needDrain||i._socket.resume())},E._write=function(t,k,L){if(i.readyState===i.CONNECTING){i.once("open",function(){E._write(t,k,L)});return}i.send(t,L)},E.on("end",IQ),E.on("error",oR),E}iR.exports=BQ});var fR=ce((wre,sR)=>{"use strict";var UQ=require("events"),{createHash:jQ}=require("crypto"),{createServer:zQ,STATUS_CODES:Xw}=require("http"),rh=Tg(),qQ=Kw(),{format:HQ,parse:WQ}=Hw(),{GUID:VQ,kWebSocket:GQ}=th(),YQ=/^[+/0-9A-Za-z]{22}==$/,aR=class extends UQ{constructor(o,f){super();if(o=E0({maxPayload:100*1024*1024,perMessageDeflate:!1,handleProtocols:null,clientTracking:!0,verifyClient:null,noServer:!1,backlog:null,server:null,host:null,path:null,port:null},o),o.port==null&&!o.server&&!o.noServer)throw new TypeError('One of the "port", "server", or "noServer" options must be specified');if(o.port!=null?(this._server=zQ((p,E)=>{let t=Xw[426];E.writeHead(426,{"Content-Length":t.length,"Content-Type":"text/plain"}),E.end(t)}),this._server.listen(o.port,o.host,o.backlog,f)):o.server&&(this._server=o.server),this._server){let p=this.emit.bind(this,"connection");this._removeListeners=KQ(this._server,{listening:this.emit.bind(this,"listening"),error:this.emit.bind(this,"error"),upgrade:(E,t,k)=>{this.handleUpgrade(E,t,k,p)}})}o.perMessageDeflate===!0&&(o.perMessageDeflate={}),o.clientTracking&&(this.clients=new Set),this.options=o}address(){if(this.options.noServer)throw new Error('The server is operating in "noServer" mode');return this._server?this._server.address():null}close(o){if(o&&this.once("close",o),this.clients)for(let p of this.clients)p.terminate();let f=this._server;if(f&&(this._removeListeners(),this._removeListeners=this._server=null,this.options.port!=null)){f.close(()=>this.emit("close"));return}process.nextTick(XQ,this)}shouldHandle(o){if(this.options.path){let f=o.url.indexOf("?");if((f!==-1?o.url.slice(0,f):o.url)!==this.options.path)return!1}return!0}handleUpgrade(o,f,p,E){f.on("error",Qw);let t=o.headers["sec-websocket-key"]!==void 0?o.headers["sec-websocket-key"].trim():!1,k=+o.headers["sec-websocket-version"],L={};if(o.method!=="GET"||o.headers.upgrade.toLowerCase()!=="websocket"||!t||!YQ.test(t)||k!==8&&k!==13||!this.shouldHandle(o))return A4(f,400);if(this.options.perMessageDeflate){let N=new rh(this.options.perMessageDeflate,!0,this.options.maxPayload);try{let C=WQ(o.headers["sec-websocket-extensions"]);C[rh.extensionName]&&(N.accept(C[rh.extensionName]),L[rh.extensionName]=N)}catch(C){return A4(f,400)}}if(this.options.verifyClient){let N={origin:o.headers[`${k===8?"sec-websocket-origin":"origin"}`],secure:!!(o.socket.authorized||o.socket.encrypted),req:o};if(this.options.verifyClient.length===2){this.options.verifyClient(N,(C,U,q,W)=>{if(!C)return A4(f,U||401,q,W);this.completeUpgrade(t,L,o,f,p,E)});return}if(!this.options.verifyClient(N))return A4(f,401)}this.completeUpgrade(t,L,o,f,p,E)}completeUpgrade(o,f,p,E,t,k){if(!E.readable||!E.writable)return E.destroy();if(E[GQ])throw new Error("server.handleUpgrade() was called more than once with the same socket, possibly due to a misconfiguration");let L=jQ("sha1").update(o+VQ).digest("base64"),N=["HTTP/1.1 101 Switching Protocols","Upgrade: websocket","Connection: Upgrade",`Sec-WebSocket-Accept: ${L}`],C=new qQ(null),U=p.headers["sec-websocket-protocol"];if(U&&(U=U.split(",").map(QQ),this.options.handleProtocols?U=this.options.handleProtocols(U,p):U=U[0],U&&(N.push(`Sec-WebSocket-Protocol: ${U}`),C._protocol=U)),f[rh.extensionName]){let q=f[rh.extensionName].params,W=HQ({[rh.extensionName]:[q]});N.push(`Sec-WebSocket-Extensions: ${W}`),C._extensions=f}this.emit("headers",N,p),E.write(N.concat(`\r +`).join(`\r +`)),E.removeListener("error",Qw),C.setSocket(E,t,this.options.maxPayload),this.clients&&(this.clients.add(C),C.on("close",()=>this.clients.delete(C))),k(C,p)}};sR.exports=aR;function KQ(i,o){for(let f of Object.keys(o))i.on(f,o[f]);return function(){for(let p of Object.keys(o))i.removeListener(p,o[p])}}function XQ(i){i.emit("close")}function Qw(){this.destroy()}function A4(i,o,f,p){i.writable&&(f=f||Xw[o],p=E0({Connection:"close","Content-Type":"text/html","Content-Length":Buffer.byteLength(f)},p),i.write(`HTTP/1.1 ${o} ${Xw[o]}\r +`+Object.keys(p).map(E=>`${E}: ${p[E]}`).join(`\r +`)+`\r +\r +`+f)),i.removeListener("error",Qw),i.destroy()}function QQ(i){return i.trim()}});var dR=ce((Sre,cR)=>{"use strict";var Rg=Kw();Rg.createWebSocketStream=lR();Rg.Server=fR();Rg.Receiver=zw();Rg.Sender=qw();cR.exports=Rg});var pR=ce(R4=>{"use strict";var JQ=R4&&R4.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(R4,"__esModule",{value:!0});var ZQ=JQ(dR()),Og=global;Og.WebSocket||(Og.WebSocket=ZQ.default);Og.window||(Og.window=global);Og.window.__REACT_DEVTOOLS_COMPONENT_FILTERS__=[{type:1,value:7,isEnabled:!0},{type:2,value:"InternalApp",isEnabled:!0,isValid:!0},{type:2,value:"InternalAppContext",isEnabled:!0,isValid:!0},{type:2,value:"InternalStdoutContext",isEnabled:!0,isValid:!0},{type:2,value:"InternalStderrContext",isEnabled:!0,isValid:!0},{type:2,value:"InternalStdinContext",isEnabled:!0,isValid:!0},{type:2,value:"InternalFocusContext",isEnabled:!0,isValid:!0}]});var hR=ce((O4,Jw)=>{(function(i,o){typeof O4=="object"&&typeof Jw=="object"?Jw.exports=o():typeof define=="function"&&define.amd?define([],o):typeof O4=="object"?O4.ReactDevToolsBackend=o():i.ReactDevToolsBackend=o()})(window,function(){return function(i){var o={};function f(p){if(o[p])return o[p].exports;var E=o[p]={i:p,l:!1,exports:{}};return i[p].call(E.exports,E,E.exports,f),E.l=!0,E.exports}return f.m=i,f.c=o,f.d=function(p,E,t){f.o(p,E)||Object.defineProperty(p,E,{enumerable:!0,get:t})},f.r=function(p){typeof Symbol!="undefined"&&Symbol.toStringTag&&Object.defineProperty(p,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(p,"__esModule",{value:!0})},f.t=function(p,E){if(1&E&&(p=f(p)),8&E||4&E&&typeof p=="object"&&p&&p.__esModule)return p;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:p}),2&E&&typeof p!="string")for(var k in p)f.d(t,k,function(L){return p[L]}.bind(null,k));return t},f.n=function(p){var E=p&&p.__esModule?function(){return p.default}:function(){return p};return f.d(E,"a",E),E},f.o=function(p,E){return Object.prototype.hasOwnProperty.call(p,E)},f.p="",f(f.s=20)}([function(i,o,f){"use strict";i.exports=f(12)},function(i,o,f){"use strict";var p=Object.getOwnPropertySymbols,E=Object.prototype.hasOwnProperty,t=Object.prototype.propertyIsEnumerable;function k(L){if(L==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(L)}i.exports=function(){try{if(!Object.assign)return!1;var L=new String("abc");if(L[5]="de",Object.getOwnPropertyNames(L)[0]==="5")return!1;for(var N={},C=0;C<10;C++)N["_"+String.fromCharCode(C)]=C;if(Object.getOwnPropertyNames(N).map(function(q){return N[q]}).join("")!=="0123456789")return!1;var U={};return"abcdefghijklmnopqrst".split("").forEach(function(q){U[q]=q}),Object.keys(Object.assign({},U)).join("")==="abcdefghijklmnopqrst"}catch(q){return!1}}()?Object.assign:function(L,N){for(var C,U,q=k(L),W=1;W=le||en<0||$t&&At-Ke>=wt}function ue(){var At=Se();if(Ce(At))return je(At);$e=setTimeout(ue,function(en){var ln=le-(en-ft);return $t?we(ln,wt-(en-Ke)):ln}(At))}function je(At){return $e=void 0,at&&Ge?Q(At):(Ge=rt=void 0,xt)}function ct(){var At=Se(),en=Ce(At);if(Ge=arguments,rt=this,ft=At,en){if($e===void 0)return ae(ft);if($t)return $e=setTimeout(ue,le),Q(ft)}return $e===void 0&&($e=setTimeout(ue,le)),xt}return le=pe(le)||0,ge(Ue)&&(jt=!!Ue.leading,wt=($t="maxWait"in Ue)?m(pe(Ue.maxWait)||0,le):wt,at="trailing"in Ue?!!Ue.trailing:at),ct.cancel=function(){$e!==void 0&&clearTimeout($e),Ke=0,Ge=ft=rt=$e=void 0},ct.flush=function(){return $e===void 0?xt:je(Se())},ct}function ge(Oe){var le=E(Oe);return!!Oe&&(le=="object"||le=="function")}function ze(Oe){return E(Oe)=="symbol"||function(le){return!!le&&E(le)=="object"}(Oe)&&ne.call(Oe)=="[object Symbol]"}function pe(Oe){if(typeof Oe=="number")return Oe;if(ze(Oe))return NaN;if(ge(Oe)){var le=typeof Oe.valueOf=="function"?Oe.valueOf():Oe;Oe=ge(le)?le+"":le}if(typeof Oe!="string")return Oe===0?Oe:+Oe;Oe=Oe.replace(t,"");var Ue=L.test(Oe);return Ue||N.test(Oe)?C(Oe.slice(2),Ue?2:8):k.test(Oe)?NaN:+Oe}i.exports=function(Oe,le,Ue){var Ge=!0,rt=!0;if(typeof Oe!="function")throw new TypeError("Expected a function");return ge(Ue)&&(Ge="leading"in Ue?!!Ue.leading:Ge,rt="trailing"in Ue?!!Ue.trailing:rt),he(Oe,le,{leading:Ge,maxWait:le,trailing:rt})}}).call(this,f(4))},function(i,o,f){(function(p){function E(Q){return(E=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(ae){return typeof ae}:function(ae){return ae&&typeof Symbol=="function"&&ae.constructor===Symbol&&ae!==Symbol.prototype?"symbol":typeof ae})(Q)}var t;o=i.exports=m,t=(p===void 0?"undefined":E(p))==="object"&&p.env&&p.env.NODE_DEBUG&&/\bsemver\b/i.test(p.env.NODE_DEBUG)?function(){var Q=Array.prototype.slice.call(arguments,0);Q.unshift("SEMVER"),console.log.apply(console,Q)}:function(){},o.SEMVER_SPEC_VERSION="2.0.0";var k=Number.MAX_SAFE_INTEGER||9007199254740991,L=o.re=[],N=o.src=[],C=o.tokens={},U=0;function q(Q){C[Q]=U++}q("NUMERICIDENTIFIER"),N[C.NUMERICIDENTIFIER]="0|[1-9]\\d*",q("NUMERICIDENTIFIERLOOSE"),N[C.NUMERICIDENTIFIERLOOSE]="[0-9]+",q("NONNUMERICIDENTIFIER"),N[C.NONNUMERICIDENTIFIER]="\\d*[a-zA-Z-][a-zA-Z0-9-]*",q("MAINVERSION"),N[C.MAINVERSION]="("+N[C.NUMERICIDENTIFIER]+")\\.("+N[C.NUMERICIDENTIFIER]+")\\.("+N[C.NUMERICIDENTIFIER]+")",q("MAINVERSIONLOOSE"),N[C.MAINVERSIONLOOSE]="("+N[C.NUMERICIDENTIFIERLOOSE]+")\\.("+N[C.NUMERICIDENTIFIERLOOSE]+")\\.("+N[C.NUMERICIDENTIFIERLOOSE]+")",q("PRERELEASEIDENTIFIER"),N[C.PRERELEASEIDENTIFIER]="(?:"+N[C.NUMERICIDENTIFIER]+"|"+N[C.NONNUMERICIDENTIFIER]+")",q("PRERELEASEIDENTIFIERLOOSE"),N[C.PRERELEASEIDENTIFIERLOOSE]="(?:"+N[C.NUMERICIDENTIFIERLOOSE]+"|"+N[C.NONNUMERICIDENTIFIER]+")",q("PRERELEASE"),N[C.PRERELEASE]="(?:-("+N[C.PRERELEASEIDENTIFIER]+"(?:\\."+N[C.PRERELEASEIDENTIFIER]+")*))",q("PRERELEASELOOSE"),N[C.PRERELEASELOOSE]="(?:-?("+N[C.PRERELEASEIDENTIFIERLOOSE]+"(?:\\."+N[C.PRERELEASEIDENTIFIERLOOSE]+")*))",q("BUILDIDENTIFIER"),N[C.BUILDIDENTIFIER]="[0-9A-Za-z-]+",q("BUILD"),N[C.BUILD]="(?:\\+("+N[C.BUILDIDENTIFIER]+"(?:\\."+N[C.BUILDIDENTIFIER]+")*))",q("FULL"),q("FULLPLAIN"),N[C.FULLPLAIN]="v?"+N[C.MAINVERSION]+N[C.PRERELEASE]+"?"+N[C.BUILD]+"?",N[C.FULL]="^"+N[C.FULLPLAIN]+"$",q("LOOSEPLAIN"),N[C.LOOSEPLAIN]="[v=\\s]*"+N[C.MAINVERSIONLOOSE]+N[C.PRERELEASELOOSE]+"?"+N[C.BUILD]+"?",q("LOOSE"),N[C.LOOSE]="^"+N[C.LOOSEPLAIN]+"$",q("GTLT"),N[C.GTLT]="((?:<|>)?=?)",q("XRANGEIDENTIFIERLOOSE"),N[C.XRANGEIDENTIFIERLOOSE]=N[C.NUMERICIDENTIFIERLOOSE]+"|x|X|\\*",q("XRANGEIDENTIFIER"),N[C.XRANGEIDENTIFIER]=N[C.NUMERICIDENTIFIER]+"|x|X|\\*",q("XRANGEPLAIN"),N[C.XRANGEPLAIN]="[v=\\s]*("+N[C.XRANGEIDENTIFIER]+")(?:\\.("+N[C.XRANGEIDENTIFIER]+")(?:\\.("+N[C.XRANGEIDENTIFIER]+")(?:"+N[C.PRERELEASE]+")?"+N[C.BUILD]+"?)?)?",q("XRANGEPLAINLOOSE"),N[C.XRANGEPLAINLOOSE]="[v=\\s]*("+N[C.XRANGEIDENTIFIERLOOSE]+")(?:\\.("+N[C.XRANGEIDENTIFIERLOOSE]+")(?:\\.("+N[C.XRANGEIDENTIFIERLOOSE]+")(?:"+N[C.PRERELEASELOOSE]+")?"+N[C.BUILD]+"?)?)?",q("XRANGE"),N[C.XRANGE]="^"+N[C.GTLT]+"\\s*"+N[C.XRANGEPLAIN]+"$",q("XRANGELOOSE"),N[C.XRANGELOOSE]="^"+N[C.GTLT]+"\\s*"+N[C.XRANGEPLAINLOOSE]+"$",q("COERCE"),N[C.COERCE]="(^|[^\\d])(\\d{1,16})(?:\\.(\\d{1,16}))?(?:\\.(\\d{1,16}))?(?:$|[^\\d])",q("COERCERTL"),L[C.COERCERTL]=new RegExp(N[C.COERCE],"g"),q("LONETILDE"),N[C.LONETILDE]="(?:~>?)",q("TILDETRIM"),N[C.TILDETRIM]="(\\s*)"+N[C.LONETILDE]+"\\s+",L[C.TILDETRIM]=new RegExp(N[C.TILDETRIM],"g"),q("TILDE"),N[C.TILDE]="^"+N[C.LONETILDE]+N[C.XRANGEPLAIN]+"$",q("TILDELOOSE"),N[C.TILDELOOSE]="^"+N[C.LONETILDE]+N[C.XRANGEPLAINLOOSE]+"$",q("LONECARET"),N[C.LONECARET]="(?:\\^)",q("CARETTRIM"),N[C.CARETTRIM]="(\\s*)"+N[C.LONECARET]+"\\s+",L[C.CARETTRIM]=new RegExp(N[C.CARETTRIM],"g"),q("CARET"),N[C.CARET]="^"+N[C.LONECARET]+N[C.XRANGEPLAIN]+"$",q("CARETLOOSE"),N[C.CARETLOOSE]="^"+N[C.LONECARET]+N[C.XRANGEPLAINLOOSE]+"$",q("COMPARATORLOOSE"),N[C.COMPARATORLOOSE]="^"+N[C.GTLT]+"\\s*("+N[C.LOOSEPLAIN]+")$|^$",q("COMPARATOR"),N[C.COMPARATOR]="^"+N[C.GTLT]+"\\s*("+N[C.FULLPLAIN]+")$|^$",q("COMPARATORTRIM"),N[C.COMPARATORTRIM]="(\\s*)"+N[C.GTLT]+"\\s*("+N[C.LOOSEPLAIN]+"|"+N[C.XRANGEPLAIN]+")",L[C.COMPARATORTRIM]=new RegExp(N[C.COMPARATORTRIM],"g"),q("HYPHENRANGE"),N[C.HYPHENRANGE]="^\\s*("+N[C.XRANGEPLAIN]+")\\s+-\\s+("+N[C.XRANGEPLAIN]+")\\s*$",q("HYPHENRANGELOOSE"),N[C.HYPHENRANGELOOSE]="^\\s*("+N[C.XRANGEPLAINLOOSE]+")\\s+-\\s+("+N[C.XRANGEPLAINLOOSE]+")\\s*$",q("STAR"),N[C.STAR]="(<|>)?=?\\s*\\*";for(var W=0;W256||!(ae.loose?L[C.LOOSE]:L[C.FULL]).test(Q))return null;try{return new m(Q,ae)}catch(Ce){return null}}function m(Q,ae){if(ae&&E(ae)==="object"||(ae={loose:!!ae,includePrerelease:!1}),Q instanceof m){if(Q.loose===ae.loose)return Q;Q=Q.version}else if(typeof Q!="string")throw new TypeError("Invalid Version: "+Q);if(Q.length>256)throw new TypeError("version is longer than 256 characters");if(!(this instanceof m))return new m(Q,ae);t("SemVer",Q,ae),this.options=ae,this.loose=!!ae.loose;var Ce=Q.trim().match(ae.loose?L[C.LOOSE]:L[C.FULL]);if(!Ce)throw new TypeError("Invalid Version: "+Q);if(this.raw=Q,this.major=+Ce[1],this.minor=+Ce[2],this.patch=+Ce[3],this.major>k||this.major<0)throw new TypeError("Invalid major version");if(this.minor>k||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>k||this.patch<0)throw new TypeError("Invalid patch version");Ce[4]?this.prerelease=Ce[4].split(".").map(function(ue){if(/^[0-9]+$/.test(ue)){var je=+ue;if(je>=0&&je=0;)typeof this.prerelease[Ce]=="number"&&(this.prerelease[Ce]++,Ce=-2);Ce===-1&&this.prerelease.push(0)}ae&&(this.prerelease[0]===ae?isNaN(this.prerelease[1])&&(this.prerelease=[ae,0]):this.prerelease=[ae,0]);break;default:throw new Error("invalid increment argument: "+Q)}return this.format(),this.raw=this.version,this},o.inc=function(Q,ae,Ce,ue){typeof Ce=="string"&&(ue=Ce,Ce=void 0);try{return new m(Q,Ce).inc(ae,ue).version}catch(je){return null}},o.diff=function(Q,ae){if(pe(Q,ae))return null;var Ce=ne(Q),ue=ne(ae),je="";if(Ce.prerelease.length||ue.prerelease.length){je="pre";var ct="prerelease"}for(var At in Ce)if((At==="major"||At==="minor"||At==="patch")&&Ce[At]!==ue[At])return je+At;return ct},o.compareIdentifiers=Se;var we=/^[0-9]+$/;function Se(Q,ae){var Ce=we.test(Q),ue=we.test(ae);return Ce&&ue&&(Q=+Q,ae=+ae),Q===ae?0:Ce&&!ue?-1:ue&&!Ce?1:Q0}function ze(Q,ae,Ce){return he(Q,ae,Ce)<0}function pe(Q,ae,Ce){return he(Q,ae,Ce)===0}function Oe(Q,ae,Ce){return he(Q,ae,Ce)!==0}function le(Q,ae,Ce){return he(Q,ae,Ce)>=0}function Ue(Q,ae,Ce){return he(Q,ae,Ce)<=0}function Ge(Q,ae,Ce,ue){switch(ae){case"===":return E(Q)==="object"&&(Q=Q.version),E(Ce)==="object"&&(Ce=Ce.version),Q===Ce;case"!==":return E(Q)==="object"&&(Q=Q.version),E(Ce)==="object"&&(Ce=Ce.version),Q!==Ce;case"":case"=":case"==":return pe(Q,Ce,ue);case"!=":return Oe(Q,Ce,ue);case">":return ge(Q,Ce,ue);case">=":return le(Q,Ce,ue);case"<":return ze(Q,Ce,ue);case"<=":return Ue(Q,Ce,ue);default:throw new TypeError("Invalid operator: "+ae)}}function rt(Q,ae){if(ae&&E(ae)==="object"||(ae={loose:!!ae,includePrerelease:!1}),Q instanceof rt){if(Q.loose===!!ae.loose)return Q;Q=Q.value}if(!(this instanceof rt))return new rt(Q,ae);t("comparator",Q,ae),this.options=ae,this.loose=!!ae.loose,this.parse(Q),this.semver===wt?this.value="":this.value=this.operator+this.semver.version,t("comp",this)}o.rcompareIdentifiers=function(Q,ae){return Se(ae,Q)},o.major=function(Q,ae){return new m(Q,ae).major},o.minor=function(Q,ae){return new m(Q,ae).minor},o.patch=function(Q,ae){return new m(Q,ae).patch},o.compare=he,o.compareLoose=function(Q,ae){return he(Q,ae,!0)},o.compareBuild=function(Q,ae,Ce){var ue=new m(Q,Ce),je=new m(ae,Ce);return ue.compare(je)||ue.compareBuild(je)},o.rcompare=function(Q,ae,Ce){return he(ae,Q,Ce)},o.sort=function(Q,ae){return Q.sort(function(Ce,ue){return o.compareBuild(Ce,ue,ae)})},o.rsort=function(Q,ae){return Q.sort(function(Ce,ue){return o.compareBuild(ue,Ce,ae)})},o.gt=ge,o.lt=ze,o.eq=pe,o.neq=Oe,o.gte=le,o.lte=Ue,o.cmp=Ge,o.Comparator=rt;var wt={};function xt(Q,ae){if(ae&&E(ae)==="object"||(ae={loose:!!ae,includePrerelease:!1}),Q instanceof xt)return Q.loose===!!ae.loose&&Q.includePrerelease===!!ae.includePrerelease?Q:new xt(Q.raw,ae);if(Q instanceof rt)return new xt(Q.value,ae);if(!(this instanceof xt))return new xt(Q,ae);if(this.options=ae,this.loose=!!ae.loose,this.includePrerelease=!!ae.includePrerelease,this.raw=Q,this.set=Q.split(/\s*\|\|\s*/).map(function(Ce){return this.parseRange(Ce.trim())},this).filter(function(Ce){return Ce.length}),!this.set.length)throw new TypeError("Invalid SemVer Range: "+Q);this.format()}function $e(Q,ae){for(var Ce=!0,ue=Q.slice(),je=ue.pop();Ce&&ue.length;)Ce=ue.every(function(ct){return je.intersects(ct,ae)}),je=ue.pop();return Ce}function ft(Q){return!Q||Q.toLowerCase()==="x"||Q==="*"}function Ke(Q,ae,Ce,ue,je,ct,At,en,ln,An,nr,un,Wt){return((ae=ft(Ce)?"":ft(ue)?">="+Ce+".0.0":ft(je)?">="+Ce+"."+ue+".0":">="+ae)+" "+(en=ft(ln)?"":ft(An)?"<"+(+ln+1)+".0.0":ft(nr)?"<"+ln+"."+(+An+1)+".0":un?"<="+ln+"."+An+"."+nr+"-"+un:"<="+en)).trim()}function jt(Q,ae,Ce){for(var ue=0;ue0){var je=Q[ue].semver;if(je.major===ae.major&&je.minor===ae.minor&&je.patch===ae.patch)return!0}return!1}return!0}function $t(Q,ae,Ce){try{ae=new xt(ae,Ce)}catch(ue){return!1}return ae.test(Q)}function at(Q,ae,Ce,ue){var je,ct,At,en,ln;switch(Q=new m(Q,ue),ae=new xt(ae,ue),Ce){case">":je=ge,ct=Ue,At=ze,en=">",ln=">=";break;case"<":je=ze,ct=le,At=ge,en="<",ln="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if($t(Q,ae,ue))return!1;for(var An=0;An=0.0.0")),un=un||vr,Wt=Wt||vr,je(vr.semver,un.semver,ue)?un=vr:At(vr.semver,Wt.semver,ue)&&(Wt=vr)}),un.operator===en||un.operator===ln||(!Wt.operator||Wt.operator===en)&&ct(Q,Wt.semver)||Wt.operator===ln&&At(Q,Wt.semver))return!1}return!0}rt.prototype.parse=function(Q){var ae=this.options.loose?L[C.COMPARATORLOOSE]:L[C.COMPARATOR],Ce=Q.match(ae);if(!Ce)throw new TypeError("Invalid comparator: "+Q);this.operator=Ce[1]!==void 0?Ce[1]:"",this.operator==="="&&(this.operator=""),Ce[2]?this.semver=new m(Ce[2],this.options.loose):this.semver=wt},rt.prototype.toString=function(){return this.value},rt.prototype.test=function(Q){if(t("Comparator.test",Q,this.options.loose),this.semver===wt||Q===wt)return!0;if(typeof Q=="string")try{Q=new m(Q,this.options)}catch(ae){return!1}return Ge(Q,this.operator,this.semver,this.options)},rt.prototype.intersects=function(Q,ae){if(!(Q instanceof rt))throw new TypeError("a Comparator is required");var Ce;if(ae&&E(ae)==="object"||(ae={loose:!!ae,includePrerelease:!1}),this.operator==="")return this.value===""||(Ce=new xt(Q.value,ae),$t(this.value,Ce,ae));if(Q.operator==="")return Q.value===""||(Ce=new xt(this.value,ae),$t(Q.semver,Ce,ae));var ue=!(this.operator!==">="&&this.operator!==">"||Q.operator!==">="&&Q.operator!==">"),je=!(this.operator!=="<="&&this.operator!=="<"||Q.operator!=="<="&&Q.operator!=="<"),ct=this.semver.version===Q.semver.version,At=!(this.operator!==">="&&this.operator!=="<="||Q.operator!==">="&&Q.operator!=="<="),en=Ge(this.semver,"<",Q.semver,ae)&&(this.operator===">="||this.operator===">")&&(Q.operator==="<="||Q.operator==="<"),ln=Ge(this.semver,">",Q.semver,ae)&&(this.operator==="<="||this.operator==="<")&&(Q.operator===">="||Q.operator===">");return ue||je||ct&&At||en||ln},o.Range=xt,xt.prototype.format=function(){return this.range=this.set.map(function(Q){return Q.join(" ").trim()}).join("||").trim(),this.range},xt.prototype.toString=function(){return this.range},xt.prototype.parseRange=function(Q){var ae=this.options.loose;Q=Q.trim();var Ce=ae?L[C.HYPHENRANGELOOSE]:L[C.HYPHENRANGE];Q=Q.replace(Ce,Ke),t("hyphen replace",Q),Q=Q.replace(L[C.COMPARATORTRIM],"$1$2$3"),t("comparator trim",Q,L[C.COMPARATORTRIM]),Q=(Q=(Q=Q.replace(L[C.TILDETRIM],"$1~")).replace(L[C.CARETTRIM],"$1^")).split(/\s+/).join(" ");var ue=ae?L[C.COMPARATORLOOSE]:L[C.COMPARATOR],je=Q.split(" ").map(function(ct){return function(At,en){return t("comp",At,en),At=function(ln,An){return ln.trim().split(/\s+/).map(function(nr){return function(un,Wt){t("caret",un,Wt);var vr=Wt.loose?L[C.CARETLOOSE]:L[C.CARET];return un.replace(vr,function(w,Ut,Vn,fr,Fr){var ur;return t("caret",un,w,Ut,Vn,fr,Fr),ft(Ut)?ur="":ft(Vn)?ur=">="+Ut+".0.0 <"+(+Ut+1)+".0.0":ft(fr)?ur=Ut==="0"?">="+Ut+"."+Vn+".0 <"+Ut+"."+(+Vn+1)+".0":">="+Ut+"."+Vn+".0 <"+(+Ut+1)+".0.0":Fr?(t("replaceCaret pr",Fr),ur=Ut==="0"?Vn==="0"?">="+Ut+"."+Vn+"."+fr+"-"+Fr+" <"+Ut+"."+Vn+"."+(+fr+1):">="+Ut+"."+Vn+"."+fr+"-"+Fr+" <"+Ut+"."+(+Vn+1)+".0":">="+Ut+"."+Vn+"."+fr+"-"+Fr+" <"+(+Ut+1)+".0.0"):(t("no pr"),ur=Ut==="0"?Vn==="0"?">="+Ut+"."+Vn+"."+fr+" <"+Ut+"."+Vn+"."+(+fr+1):">="+Ut+"."+Vn+"."+fr+" <"+Ut+"."+(+Vn+1)+".0":">="+Ut+"."+Vn+"."+fr+" <"+(+Ut+1)+".0.0"),t("caret return",ur),ur})}(nr,An)}).join(" ")}(At,en),t("caret",At),At=function(ln,An){return ln.trim().split(/\s+/).map(function(nr){return function(un,Wt){var vr=Wt.loose?L[C.TILDELOOSE]:L[C.TILDE];return un.replace(vr,function(w,Ut,Vn,fr,Fr){var ur;return t("tilde",un,w,Ut,Vn,fr,Fr),ft(Ut)?ur="":ft(Vn)?ur=">="+Ut+".0.0 <"+(+Ut+1)+".0.0":ft(fr)?ur=">="+Ut+"."+Vn+".0 <"+Ut+"."+(+Vn+1)+".0":Fr?(t("replaceTilde pr",Fr),ur=">="+Ut+"."+Vn+"."+fr+"-"+Fr+" <"+Ut+"."+(+Vn+1)+".0"):ur=">="+Ut+"."+Vn+"."+fr+" <"+Ut+"."+(+Vn+1)+".0",t("tilde return",ur),ur})}(nr,An)}).join(" ")}(At,en),t("tildes",At),At=function(ln,An){return t("replaceXRanges",ln,An),ln.split(/\s+/).map(function(nr){return function(un,Wt){un=un.trim();var vr=Wt.loose?L[C.XRANGELOOSE]:L[C.XRANGE];return un.replace(vr,function(w,Ut,Vn,fr,Fr,ur){t("xRange",un,w,Ut,Vn,fr,Fr,ur);var br=ft(Vn),Kt=br||ft(fr),vu=Kt||ft(Fr),a0=vu;return Ut==="="&&a0&&(Ut=""),ur=Wt.includePrerelease?"-0":"",br?w=Ut===">"||Ut==="<"?"<0.0.0-0":"*":Ut&&a0?(Kt&&(fr=0),Fr=0,Ut===">"?(Ut=">=",Kt?(Vn=+Vn+1,fr=0,Fr=0):(fr=+fr+1,Fr=0)):Ut==="<="&&(Ut="<",Kt?Vn=+Vn+1:fr=+fr+1),w=Ut+Vn+"."+fr+"."+Fr+ur):Kt?w=">="+Vn+".0.0"+ur+" <"+(+Vn+1)+".0.0"+ur:vu&&(w=">="+Vn+"."+fr+".0"+ur+" <"+Vn+"."+(+fr+1)+".0"+ur),t("xRange return",w),w})}(nr,An)}).join(" ")}(At,en),t("xrange",At),At=function(ln,An){return t("replaceStars",ln,An),ln.trim().replace(L[C.STAR],"")}(At,en),t("stars",At),At}(ct,this.options)},this).join(" ").split(/\s+/);return this.options.loose&&(je=je.filter(function(ct){return!!ct.match(ue)})),je=je.map(function(ct){return new rt(ct,this.options)},this)},xt.prototype.intersects=function(Q,ae){if(!(Q instanceof xt))throw new TypeError("a Range is required");return this.set.some(function(Ce){return $e(Ce,ae)&&Q.set.some(function(ue){return $e(ue,ae)&&Ce.every(function(je){return ue.every(function(ct){return je.intersects(ct,ae)})})})})},o.toComparators=function(Q,ae){return new xt(Q,ae).set.map(function(Ce){return Ce.map(function(ue){return ue.value}).join(" ").trim().split(" ")})},xt.prototype.test=function(Q){if(!Q)return!1;if(typeof Q=="string")try{Q=new m(Q,this.options)}catch(Ce){return!1}for(var ae=0;ae":ct.prerelease.length===0?ct.patch++:ct.prerelease.push(0),ct.raw=ct.format();case"":case">=":Ce&&!ge(Ce,ct)||(Ce=ct);break;case"<":case"<=":break;default:throw new Error("Unexpected operation: "+je.operator)}});return Ce&&Q.test(Ce)?Ce:null},o.validRange=function(Q,ae){try{return new xt(Q,ae).range||"*"}catch(Ce){return null}},o.ltr=function(Q,ae,Ce){return at(Q,ae,"<",Ce)},o.gtr=function(Q,ae,Ce){return at(Q,ae,">",Ce)},o.outside=at,o.prerelease=function(Q,ae){var Ce=ne(Q,ae);return Ce&&Ce.prerelease.length?Ce.prerelease:null},o.intersects=function(Q,ae,Ce){return Q=new xt(Q,Ce),ae=new xt(ae,Ce),Q.intersects(ae)},o.coerce=function(Q,ae){if(Q instanceof m)return Q;if(typeof Q=="number"&&(Q=String(Q)),typeof Q!="string")return null;var Ce=null;if((ae=ae||{}).rtl){for(var ue;(ue=L[C.COERCERTL].exec(Q))&&(!Ce||Ce.index+Ce[0].length!==Q.length);)Ce&&ue.index+ue[0].length===Ce.index+Ce[0].length||(Ce=ue),L[C.COERCERTL].lastIndex=ue.index+ue[1].length+ue[2].length;L[C.COERCERTL].lastIndex=-1}else Ce=Q.match(L[C.COERCE]);return Ce===null?null:ne(Ce[2]+"."+(Ce[3]||"0")+"."+(Ce[4]||"0"),ae)}}).call(this,f(5))},function(i,o){function f(E){return(f=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(t){return typeof t}:function(t){return t&&typeof Symbol=="function"&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(E)}var p;p=function(){return this}();try{p=p||new Function("return this")()}catch(E){(typeof window=="undefined"?"undefined":f(window))==="object"&&(p=window)}i.exports=p},function(i,o){var f,p,E=i.exports={};function t(){throw new Error("setTimeout has not been defined")}function k(){throw new Error("clearTimeout has not been defined")}function L(Se){if(f===setTimeout)return setTimeout(Se,0);if((f===t||!f)&&setTimeout)return f=setTimeout,setTimeout(Se,0);try{return f(Se,0)}catch(he){try{return f.call(null,Se,0)}catch(ge){return f.call(this,Se,0)}}}(function(){try{f=typeof setTimeout=="function"?setTimeout:t}catch(Se){f=t}try{p=typeof clearTimeout=="function"?clearTimeout:k}catch(Se){p=k}})();var N,C=[],U=!1,q=-1;function W(){U&&N&&(U=!1,N.length?C=N.concat(C):q=-1,C.length&&ne())}function ne(){if(!U){var Se=L(W);U=!0;for(var he=C.length;he;){for(N=C,C=[];++q1)for(var ge=1;gethis[k])return Oe(this,this[m].get($e)),!1;var at=this[m].get($e).value;return this[q]&&(this[W]||this[q]($e,at.value)),at.now=jt,at.maxAge=Ke,at.value=ft,this[L]+=$t-at.length,at.length=$t,this.get($e),pe(this),!0}var Q=new le($e,ft,$t,jt,Ke);return Q.length>this[k]?(this[q]&&this[q]($e,ft),!1):(this[L]+=Q.length,this[ne].unshift(Q),this[m].set($e,this[ne].head),pe(this),!0)}},{key:"has",value:function($e){if(!this[m].has($e))return!1;var ft=this[m].get($e).value;return!ze(this,ft)}},{key:"get",value:function($e){return ge(this,$e,!0)}},{key:"peek",value:function($e){return ge(this,$e,!1)}},{key:"pop",value:function(){var $e=this[ne].tail;return $e?(Oe(this,$e),$e.value):null}},{key:"del",value:function($e){Oe(this,this[m].get($e))}},{key:"load",value:function($e){this.reset();for(var ft=Date.now(),Ke=$e.length-1;Ke>=0;Ke--){var jt=$e[Ke],$t=jt.e||0;if($t===0)this.set(jt.k,jt.v);else{var at=$t-ft;at>0&&this.set(jt.k,jt.v,at)}}}},{key:"prune",value:function(){var $e=this;this[m].forEach(function(ft,Ke){return ge($e,Ke,!1)})}},{key:"max",set:function($e){if(typeof $e!="number"||$e<0)throw new TypeError("max must be a non-negative number");this[k]=$e||1/0,pe(this)},get:function(){return this[k]}},{key:"allowStale",set:function($e){this[C]=!!$e},get:function(){return this[C]}},{key:"maxAge",set:function($e){if(typeof $e!="number")throw new TypeError("maxAge must be a non-negative number");this[U]=$e,pe(this)},get:function(){return this[U]}},{key:"lengthCalculator",set:function($e){var ft=this;typeof $e!="function"&&($e=Se),$e!==this[N]&&(this[N]=$e,this[L]=0,this[ne].forEach(function(Ke){Ke.length=ft[N](Ke.value,Ke.key),ft[L]+=Ke.length})),pe(this)},get:function(){return this[N]}},{key:"length",get:function(){return this[L]}},{key:"itemCount",get:function(){return this[ne].length}}])&&E(rt.prototype,wt),xt&&E(rt,xt),Ge}(),ge=function(Ge,rt,wt){var xt=Ge[m].get(rt);if(xt){var $e=xt.value;if(ze(Ge,$e)){if(Oe(Ge,xt),!Ge[C])return}else wt&&(Ge[we]&&(xt.value.now=Date.now()),Ge[ne].unshiftNode(xt));return $e.value}},ze=function(Ge,rt){if(!rt||!rt.maxAge&&!Ge[U])return!1;var wt=Date.now()-rt.now;return rt.maxAge?wt>rt.maxAge:Ge[U]&&wt>Ge[U]},pe=function(Ge){if(Ge[L]>Ge[k])for(var rt=Ge[ne].tail;Ge[L]>Ge[k]&&rt!==null;){var wt=rt.prev;Oe(Ge,rt),rt=wt}},Oe=function(Ge,rt){if(rt){var wt=rt.value;Ge[q]&&Ge[q](wt.key,wt.value),Ge[L]-=wt.length,Ge[m].delete(wt.key),Ge[ne].removeNode(rt)}},le=function Ge(rt,wt,xt,$e,ft){p(this,Ge),this.key=rt,this.value=wt,this.length=xt,this.now=$e,this.maxAge=ft||0},Ue=function(Ge,rt,wt,xt){var $e=wt.value;ze(Ge,$e)&&(Oe(Ge,wt),Ge[C]||($e=void 0)),$e&&rt.call(xt,$e.value,$e.key,Ge)};i.exports=he},function(i,o,f){(function(p){function E(t){return(E=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(k){return typeof k}:function(k){return k&&typeof Symbol=="function"&&k.constructor===Symbol&&k!==Symbol.prototype?"symbol":typeof k})(t)}i.exports=function(){if(typeof document=="undefined"||!document.addEventListener)return null;var t,k,L,N={};return N.copy=function(){var C=!1,U=null,q=!1;function W(){C=!1,U=null,q&&window.getSelection().removeAllRanges(),q=!1}return document.addEventListener("copy",function(ne){if(C){for(var m in U)ne.clipboardData.setData(m,U[m]);ne.preventDefault()}}),function(ne){return new Promise(function(m,we){C=!0,typeof ne=="string"?U={"text/plain":ne}:ne instanceof Node?U={"text/html":new XMLSerializer().serializeToString(ne)}:ne instanceof Object?U=ne:we("Invalid data type. Must be string, DOM node, or an object mapping MIME types to strings."),function Se(he){try{if(document.execCommand("copy"))W(),m();else{if(he)throw W(),new Error("Unable to copy. Perhaps it's not available in your browser?");(function(){var ge=document.getSelection();if(!document.queryCommandEnabled("copy")&&ge.isCollapsed){var ze=document.createRange();ze.selectNodeContents(document.body),ge.removeAllRanges(),ge.addRange(ze),q=!0}})(),Se(!0)}}catch(ge){W(),we(ge)}}(!1)})}}(),N.paste=(L=!1,document.addEventListener("paste",function(C){if(L){L=!1,C.preventDefault();var U=t;t=null,U(C.clipboardData.getData(k))}}),function(C){return new Promise(function(U,q){L=!0,t=U,k=C||"text/plain";try{document.execCommand("paste")||(L=!1,q(new Error("Unable to paste. Pasting only works in Internet Explorer at the moment.")))}catch(W){L=!1,q(new Error(W))}})}),typeof ClipboardEvent=="undefined"&&window.clipboardData!==void 0&&window.clipboardData.setData!==void 0&&(function(C){function U(pe,Oe){return function(){pe.apply(Oe,arguments)}}function q(pe){if(E(this)!="object")throw new TypeError("Promises must be constructed via new");if(typeof pe!="function")throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],he(pe,U(ne,this),U(m,this))}function W(pe){var Oe=this;return this._state===null?void this._deferreds.push(pe):void ge(function(){var le=Oe._state?pe.onFulfilled:pe.onRejected;if(le!==null){var Ue;try{Ue=le(Oe._value)}catch(Ge){return void pe.reject(Ge)}pe.resolve(Ue)}else(Oe._state?pe.resolve:pe.reject)(Oe._value)})}function ne(pe){try{if(pe===this)throw new TypeError("A promise cannot be resolved with itself.");if(pe&&(E(pe)=="object"||typeof pe=="function")){var Oe=pe.then;if(typeof Oe=="function")return void he(U(Oe,pe),U(ne,this),U(m,this))}this._state=!0,this._value=pe,we.call(this)}catch(le){m.call(this,le)}}function m(pe){this._state=!1,this._value=pe,we.call(this)}function we(){for(var pe=0,Oe=this._deferreds.length;Oe>pe;pe++)W.call(this,this._deferreds[pe]);this._deferreds=null}function Se(pe,Oe,le,Ue){this.onFulfilled=typeof pe=="function"?pe:null,this.onRejected=typeof Oe=="function"?Oe:null,this.resolve=le,this.reject=Ue}function he(pe,Oe,le){var Ue=!1;try{pe(function(Ge){Ue||(Ue=!0,Oe(Ge))},function(Ge){Ue||(Ue=!0,le(Ge))})}catch(Ge){if(Ue)return;Ue=!0,le(Ge)}}var ge=q.immediateFn||typeof p=="function"&&p||function(pe){setTimeout(pe,1)},ze=Array.isArray||function(pe){return Object.prototype.toString.call(pe)==="[object Array]"};q.prototype.catch=function(pe){return this.then(null,pe)},q.prototype.then=function(pe,Oe){var le=this;return new q(function(Ue,Ge){W.call(le,new Se(pe,Oe,Ue,Ge))})},q.all=function(){var pe=Array.prototype.slice.call(arguments.length===1&&ze(arguments[0])?arguments[0]:arguments);return new q(function(Oe,le){function Ue(wt,xt){try{if(xt&&(E(xt)=="object"||typeof xt=="function")){var $e=xt.then;if(typeof $e=="function")return void $e.call(xt,function(ft){Ue(wt,ft)},le)}pe[wt]=xt,--Ge==0&&Oe(pe)}catch(ft){le(ft)}}if(pe.length===0)return Oe([]);for(var Ge=pe.length,rt=0;rtUe;Ue++)pe[Ue].then(Oe,le)})},i.exports?i.exports=q:C.Promise||(C.Promise=q)}(this),N.copy=function(C){return new Promise(function(U,q){if(typeof C!="string"&&!("text/plain"in C))throw new Error("You must provide a text/plain type.");var W=typeof C=="string"?C:C["text/plain"];window.clipboardData.setData("Text",W)?U():q(new Error("Copying was rejected."))})},N.paste=function(){return new Promise(function(C,U){var q=window.clipboardData.getData("Text");q?C(q):U(new Error("Pasting was rejected."))})}),N}()}).call(this,f(13).setImmediate)},function(i,o,f){"use strict";i.exports=f(15)},function(i,o,f){"use strict";f.r(o),o.default=`:root { + /** + * IMPORTANT: When new theme variables are added below\u2013 also add them to SettingsContext updateThemeVariables() + */ + + /* Light theme */ + --light-color-attribute-name: #ef6632; + --light-color-attribute-name-not-editable: #23272f; + --light-color-attribute-name-inverted: rgba(255, 255, 255, 0.7); + --light-color-attribute-value: #1a1aa6; + --light-color-attribute-value-inverted: #ffffff; + --light-color-attribute-editable-value: #1a1aa6; + --light-color-background: #ffffff; + --light-color-background-hover: rgba(0, 136, 250, 0.1); + --light-color-background-inactive: #e5e5e5; + --light-color-background-invalid: #fff0f0; + --light-color-background-selected: #0088fa; + --light-color-button-background: #ffffff; + --light-color-button-background-focus: #ededed; + --light-color-button: #5f6673; + --light-color-button-disabled: #cfd1d5; + --light-color-button-active: #0088fa; + --light-color-button-focus: #23272f; + --light-color-button-hover: #23272f; + --light-color-border: #eeeeee; + --light-color-commit-did-not-render-fill: #cfd1d5; + --light-color-commit-did-not-render-fill-text: #000000; + --light-color-commit-did-not-render-pattern: #cfd1d5; + --light-color-commit-did-not-render-pattern-text: #333333; + --light-color-commit-gradient-0: #37afa9; + --light-color-commit-gradient-1: #63b19e; + --light-color-commit-gradient-2: #80b393; + --light-color-commit-gradient-3: #97b488; + --light-color-commit-gradient-4: #abb67d; + --light-color-commit-gradient-5: #beb771; + --light-color-commit-gradient-6: #cfb965; + --light-color-commit-gradient-7: #dfba57; + --light-color-commit-gradient-8: #efbb49; + --light-color-commit-gradient-9: #febc38; + --light-color-commit-gradient-text: #000000; + --light-color-component-name: #6a51b2; + --light-color-component-name-inverted: #ffffff; + --light-color-component-badge-background: rgba(0, 0, 0, 0.1); + --light-color-component-badge-background-inverted: rgba(255, 255, 255, 0.25); + --light-color-component-badge-count: #777d88; + --light-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7); + --light-color-context-background: rgba(0,0,0,.9); + --light-color-context-background-hover: rgba(255, 255, 255, 0.1); + --light-color-context-background-selected: #178fb9; + --light-color-context-border: #3d424a; + --light-color-context-text: #ffffff; + --light-color-context-text-selected: #ffffff; + --light-color-dim: #777d88; + --light-color-dimmer: #cfd1d5; + --light-color-dimmest: #eff0f1; + --light-color-error-background: hsl(0, 100%, 97%); + --light-color-error-border: hsl(0, 100%, 92%); + --light-color-error-text: #ff0000; + --light-color-expand-collapse-toggle: #777d88; + --light-color-link: #0000ff; + --light-color-modal-background: rgba(255, 255, 255, 0.75); + --light-color-record-active: #fc3a4b; + --light-color-record-hover: #3578e5; + --light-color-record-inactive: #0088fa; + --light-color-scroll-thumb: #c2c2c2; + --light-color-scroll-track: #fafafa; + --light-color-search-match: yellow; + --light-color-search-match-current: #f7923b; + --light-color-selected-tree-highlight-active: rgba(0, 136, 250, 0.1); + --light-color-selected-tree-highlight-inactive: rgba(0, 0, 0, 0.05); + --light-color-shadow: rgba(0, 0, 0, 0.25); + --light-color-tab-selected-border: #0088fa; + --light-color-text: #000000; + --light-color-text-invalid: #ff0000; + --light-color-text-selected: #ffffff; + --light-color-toggle-background-invalid: #fc3a4b; + --light-color-toggle-background-on: #0088fa; + --light-color-toggle-background-off: #cfd1d5; + --light-color-toggle-text: #ffffff; + --light-color-tooltip-background: rgba(0, 0, 0, 0.9); + --light-color-tooltip-text: #ffffff; + + /* Dark theme */ + --dark-color-attribute-name: #9d87d2; + --dark-color-attribute-name-not-editable: #ededed; + --dark-color-attribute-name-inverted: #282828; + --dark-color-attribute-value: #cedae0; + --dark-color-attribute-value-inverted: #ffffff; + --dark-color-attribute-editable-value: yellow; + --dark-color-background: #282c34; + --dark-color-background-hover: rgba(255, 255, 255, 0.1); + --dark-color-background-inactive: #3d424a; + --dark-color-background-invalid: #5c0000; + --dark-color-background-selected: #178fb9; + --dark-color-button-background: #282c34; + --dark-color-button-background-focus: #3d424a; + --dark-color-button: #afb3b9; + --dark-color-button-active: #61dafb; + --dark-color-button-disabled: #4f5766; + --dark-color-button-focus: #a2e9fc; + --dark-color-button-hover: #ededed; + --dark-color-border: #3d424a; + --dark-color-commit-did-not-render-fill: #777d88; + --dark-color-commit-did-not-render-fill-text: #000000; + --dark-color-commit-did-not-render-pattern: #666c77; + --dark-color-commit-did-not-render-pattern-text: #ffffff; + --dark-color-commit-gradient-0: #37afa9; + --dark-color-commit-gradient-1: #63b19e; + --dark-color-commit-gradient-2: #80b393; + --dark-color-commit-gradient-3: #97b488; + --dark-color-commit-gradient-4: #abb67d; + --dark-color-commit-gradient-5: #beb771; + --dark-color-commit-gradient-6: #cfb965; + --dark-color-commit-gradient-7: #dfba57; + --dark-color-commit-gradient-8: #efbb49; + --dark-color-commit-gradient-9: #febc38; + --dark-color-commit-gradient-text: #000000; + --dark-color-component-name: #61dafb; + --dark-color-component-name-inverted: #282828; + --dark-color-component-badge-background: rgba(255, 255, 255, 0.25); + --dark-color-component-badge-background-inverted: rgba(0, 0, 0, 0.25); + --dark-color-component-badge-count: #8f949d; + --dark-color-component-badge-count-inverted: rgba(255, 255, 255, 0.7); + --dark-color-context-background: rgba(255,255,255,.9); + --dark-color-context-background-hover: rgba(0, 136, 250, 0.1); + --dark-color-context-background-selected: #0088fa; + --dark-color-context-border: #eeeeee; + --dark-color-context-text: #000000; + --dark-color-context-text-selected: #ffffff; + --dark-color-dim: #8f949d; + --dark-color-dimmer: #777d88; + --dark-color-dimmest: #4f5766; + --dark-color-error-background: #200; + --dark-color-error-border: #900; + --dark-color-error-text: #f55; + --dark-color-expand-collapse-toggle: #8f949d; + --dark-color-link: #61dafb; + --dark-color-modal-background: rgba(0, 0, 0, 0.75); + --dark-color-record-active: #fc3a4b; + --dark-color-record-hover: #a2e9fc; + --dark-color-record-inactive: #61dafb; + --dark-color-scroll-thumb: #afb3b9; + --dark-color-scroll-track: #313640; + --dark-color-search-match: yellow; + --dark-color-search-match-current: #f7923b; + --dark-color-selected-tree-highlight-active: rgba(23, 143, 185, 0.15); + --dark-color-selected-tree-highlight-inactive: rgba(255, 255, 255, 0.05); + --dark-color-shadow: rgba(0, 0, 0, 0.5); + --dark-color-tab-selected-border: #178fb9; + --dark-color-text: #ffffff; + --dark-color-text-invalid: #ff8080; + --dark-color-text-selected: #ffffff; + --dark-color-toggle-background-invalid: #fc3a4b; + --dark-color-toggle-background-on: #178fb9; + --dark-color-toggle-background-off: #777d88; + --dark-color-toggle-text: #ffffff; + --dark-color-tooltip-background: rgba(255, 255, 255, 0.9); + --dark-color-tooltip-text: #000000; + + /* Font smoothing */ + --light-font-smoothing: auto; + --dark-font-smoothing: antialiased; + --font-smoothing: auto; + + /* Compact density */ + --compact-font-size-monospace-small: 9px; + --compact-font-size-monospace-normal: 11px; + --compact-font-size-monospace-large: 15px; + --compact-font-size-sans-small: 10px; + --compact-font-size-sans-normal: 12px; + --compact-font-size-sans-large: 14px; + --compact-line-height-data: 18px; + --compact-root-font-size: 16px; + + /* Comfortable density */ + --comfortable-font-size-monospace-small: 10px; + --comfortable-font-size-monospace-normal: 13px; + --comfortable-font-size-monospace-large: 17px; + --comfortable-font-size-sans-small: 12px; + --comfortable-font-size-sans-normal: 14px; + --comfortable-font-size-sans-large: 16px; + --comfortable-line-height-data: 22px; + --comfortable-root-font-size: 20px; + + /* GitHub.com system fonts */ + --font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, + Courier, monospace; + --font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, + Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol; + + /* Constant values shared between JS and CSS */ + --interaction-commit-size: 10px; + --interaction-label-width: 200px; +} +`},function(i,o,f){"use strict";function p(N){var C=this;if(C instanceof p||(C=new p),C.tail=null,C.head=null,C.length=0,N&&typeof N.forEach=="function")N.forEach(function(W){C.push(W)});else if(arguments.length>0)for(var U=0,q=arguments.length;U1)U=C;else{if(!this.head)throw new TypeError("Reduce of empty list with no initial value");q=this.head.next,U=this.head.value}for(var W=0;q!==null;W++)U=N(U,q.value,W),q=q.next;return U},p.prototype.reduceReverse=function(N,C){var U,q=this.tail;if(arguments.length>1)U=C;else{if(!this.tail)throw new TypeError("Reduce of empty list with no initial value");q=this.tail.prev,U=this.tail.value}for(var W=this.length-1;q!==null;W--)U=N(U,q.value,W),q=q.prev;return U},p.prototype.toArray=function(){for(var N=new Array(this.length),C=0,U=this.head;U!==null;C++)N[C]=U.value,U=U.next;return N},p.prototype.toArrayReverse=function(){for(var N=new Array(this.length),C=0,U=this.tail;U!==null;C++)N[C]=U.value,U=U.prev;return N},p.prototype.slice=function(N,C){(C=C||this.length)<0&&(C+=this.length),(N=N||0)<0&&(N+=this.length);var U=new p;if(Cthis.length&&(C=this.length);for(var q=0,W=this.head;W!==null&&qthis.length&&(C=this.length);for(var q=this.length,W=this.tail;W!==null&&q>C;q--)W=W.prev;for(;W!==null&&q>N;q--,W=W.prev)U.push(W.value);return U},p.prototype.splice=function(N,C){N>this.length&&(N=this.length-1),N<0&&(N=this.length+N);for(var U=0,q=this.head;q!==null&&U=0&&(L._idleTimeoutId=setTimeout(function(){L._onTimeout&&L._onTimeout()},N))},f(14),o.setImmediate=typeof self!="undefined"&&self.setImmediate||p!==void 0&&p.setImmediate||this&&this.setImmediate,o.clearImmediate=typeof self!="undefined"&&self.clearImmediate||p!==void 0&&p.clearImmediate||this&&this.clearImmediate}).call(this,f(4))},function(i,o,f){(function(p,E){(function(t,k){"use strict";if(!t.setImmediate){var L,N,C,U,q,W=1,ne={},m=!1,we=t.document,Se=Object.getPrototypeOf&&Object.getPrototypeOf(t);Se=Se&&Se.setTimeout?Se:t,{}.toString.call(t.process)==="[object process]"?L=function(ze){E.nextTick(function(){ge(ze)})}:function(){if(t.postMessage&&!t.importScripts){var ze=!0,pe=t.onmessage;return t.onmessage=function(){ze=!1},t.postMessage("","*"),t.onmessage=pe,ze}}()?(U="setImmediate$"+Math.random()+"$",q=function(ze){ze.source===t&&typeof ze.data=="string"&&ze.data.indexOf(U)===0&&ge(+ze.data.slice(U.length))},t.addEventListener?t.addEventListener("message",q,!1):t.attachEvent("onmessage",q),L=function(ze){t.postMessage(U+ze,"*")}):t.MessageChannel?((C=new MessageChannel).port1.onmessage=function(ze){ge(ze.data)},L=function(ze){C.port2.postMessage(ze)}):we&&"onreadystatechange"in we.createElement("script")?(N=we.documentElement,L=function(ze){var pe=we.createElement("script");pe.onreadystatechange=function(){ge(ze),pe.onreadystatechange=null,N.removeChild(pe),pe=null},N.appendChild(pe)}):L=function(ze){setTimeout(ge,0,ze)},Se.setImmediate=function(ze){typeof ze!="function"&&(ze=new Function(""+ze));for(var pe=new Array(arguments.length-1),Oe=0;Oeae;ae++)if((Q=he(at,jt,ae))!==-1){Se=ae,jt=Q;break e}jt=-1}}e:{if(at=$t,(Q=W().get(Ke.primitive))!==void 0){for(ae=0;aejt-at?null:$t.slice(at,jt-1))!==null){if(jt=0,rt!==null){for(;jt<$t.length&&jtjt;rt--)wt=$e.pop()}for(rt=$t.length-jt-1;1<=rt;rt--)jt=[],wt.push({id:null,isStateEditable:!1,name:ze($t[rt-1].functionName),value:void 0,subHooks:jt}),$e.push(wt),wt=jt;rt=$t}jt=($t=Ke.primitive)==="Context"||$t==="DebugValue"?null:xt++,wt.push({id:jt,isStateEditable:$t==="Reducer"||$t==="State",name:$t,value:Ke.value,subHooks:[]})}return function Ce(ue,je){for(var ct=[],At=0;At-1&&(ne=ne.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(\),.*$)/g,""));var m=ne.replace(/^\s+/,"").replace(/\(eval code/g,"("),we=m.match(/ (\((.+):(\d+):(\d+)\)$)/),Se=(m=we?m.replace(we[0],""):m).split(/\s+/).slice(1),he=this.extractLocation(we?we[1]:Se.pop()),ge=Se.join(" ")||void 0,ze=["eval",""].indexOf(he[0])>-1?void 0:he[0];return new N({functionName:ge,fileName:ze,lineNumber:he[1],columnNumber:he[2],source:ne})},this)},parseFFOrSafari:function(W){return W.stack.split(` +`).filter(function(ne){return!ne.match(q)},this).map(function(ne){if(ne.indexOf(" > eval")>-1&&(ne=ne.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),ne.indexOf("@")===-1&&ne.indexOf(":")===-1)return new N({functionName:ne});var m=/((.*".+"[^@]*)?[^@]*)(?:@)/,we=ne.match(m),Se=we&&we[1]?we[1]:void 0,he=this.extractLocation(ne.replace(m,""));return new N({functionName:Se,fileName:he[0],lineNumber:he[1],columnNumber:he[2],source:ne})},this)},parseOpera:function(W){return!W.stacktrace||W.message.indexOf(` +`)>-1&&W.message.split(` +`).length>W.stacktrace.split(` +`).length?this.parseOpera9(W):W.stack?this.parseOpera11(W):this.parseOpera10(W)},parseOpera9:function(W){for(var ne=/Line (\d+).*script (?:in )?(\S+)/i,m=W.message.split(` +`),we=[],Se=2,he=m.length;Se/,"$2").replace(/\([^)]*\)/g,"")||void 0;he.match(/\(([^)]*)\)/)&&(m=he.replace(/^[^(]+\(([^)]*)\)$/,"$1"));var ze=m===void 0||m==="[arguments not available]"?void 0:m.split(",");return new N({functionName:ge,args:ze,fileName:Se[0],lineNumber:Se[1],columnNumber:Se[2],source:ne})},this)}}})=="function"?p.apply(o,E):p)===void 0||(i.exports=t)})()},function(i,o,f){var p,E,t;(function(k,L){"use strict";E=[],(t=typeof(p=function(){function N(ge){return ge.charAt(0).toUpperCase()+ge.substring(1)}function C(ge){return function(){return this[ge]}}var U=["isConstructor","isEval","isNative","isToplevel"],q=["columnNumber","lineNumber"],W=["fileName","functionName","source"],ne=U.concat(q,W,["args"]);function m(ge){if(ge)for(var ze=0;ze1?xe-1:0),ke=1;ke=0&&xe.splice(Z,1)}}}])&&p(z.prototype,G),$&&p(z,$),B}(),t=f(2),k=f.n(t);try{var L=f(9).default,N=function(B){var z=new RegExp("".concat(B,": ([0-9]+)")),G=L.match(z);return parseInt(G[1],10)};N("comfortable-line-height-data"),N("compact-line-height-data")}catch(B){}function C(B){try{return sessionStorage.getItem(B)}catch(z){return null}}function U(B){try{sessionStorage.removeItem(B)}catch(z){}}function q(B,z){try{return sessionStorage.setItem(B,z)}catch(G){}}var W=function(B,z){return B===z},ne=f(1),m=f.n(ne);function we(B){return B.ownerDocument?B.ownerDocument.defaultView:null}function Se(B){var z=we(B);return z?z.frameElement:null}function he(B){var z=pe(B);return ge([B.getBoundingClientRect(),{top:z.borderTop,left:z.borderLeft,bottom:z.borderBottom,right:z.borderRight,width:0,height:0}])}function ge(B){return B.reduce(function(z,G){return z==null?G:{top:z.top+G.top,left:z.left+G.left,width:z.width,height:z.height,bottom:z.bottom+G.bottom,right:z.right+G.right}})}function ze(B,z){var G=Se(B);if(G&&G!==z){for(var $=[B.getBoundingClientRect()],De=G,me=!1;De;){var xe=he(De);if($.push(xe),De=Se(De),me)break;De&&we(De)===z&&(me=!0)}return ge($)}return B.getBoundingClientRect()}function pe(B){var z=window.getComputedStyle(B);return{borderLeft:parseInt(z.borderLeftWidth,10),borderRight:parseInt(z.borderRightWidth,10),borderTop:parseInt(z.borderTopWidth,10),borderBottom:parseInt(z.borderBottomWidth,10),marginLeft:parseInt(z.marginLeft,10),marginRight:parseInt(z.marginRight,10),marginTop:parseInt(z.marginTop,10),marginBottom:parseInt(z.marginBottom,10),paddingLeft:parseInt(z.paddingLeft,10),paddingRight:parseInt(z.paddingRight,10),paddingTop:parseInt(z.paddingTop,10),paddingBottom:parseInt(z.paddingBottom,10)}}function Oe(B,z){var G;if(typeof Symbol=="undefined"||B[Symbol.iterator]==null){if(Array.isArray(B)||(G=function(ke,Xe){if(!!ke){if(typeof ke=="string")return le(ke,Xe);var ht=Object.prototype.toString.call(ke).slice(8,-1);if(ht==="Object"&&ke.constructor&&(ht=ke.constructor.name),ht==="Map"||ht==="Set")return Array.from(ke);if(ht==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(ht))return le(ke,Xe)}}(B))||z&&B&&typeof B.length=="number"){G&&(B=G);var $=0,De=function(){};return{s:De,n:function(){return $>=B.length?{done:!0}:{done:!1,value:B[$++]}},e:function(ke){throw ke},f:De}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var me,xe=!0,Z=!1;return{s:function(){G=B[Symbol.iterator]()},n:function(){var ke=G.next();return xe=ke.done,ke},e:function(ke){Z=!0,me=ke},f:function(){try{xe||G.return==null||G.return()}finally{if(Z)throw me}}}}function le(B,z){(z==null||z>B.length)&&(z=B.length);for(var G=0,$=new Array(z);Gxe.left+xe.width&&(ie=xe.left+xe.width-ht-5),{style:{top:ke+="px",left:ie+="px"}}}(z,G,{width:$.width,height:$.height});m()(this.tip.style,De.style)}}]),B}(),$e=function(){function B(){Ue(this,B);var z=window.__REACT_DEVTOOLS_TARGET_WINDOW__||window;this.window=z;var G=window.__REACT_DEVTOOLS_TARGET_WINDOW__||window;this.tipBoundsWindow=G;var $=z.document;this.container=$.createElement("div"),this.container.style.zIndex="10000000",this.tip=new xt($,this.container),this.rects=[],$.body.appendChild(this.container)}return rt(B,[{key:"remove",value:function(){this.tip.remove(),this.rects.forEach(function(z){z.remove()}),this.rects.length=0,this.container.parentNode&&this.container.parentNode.removeChild(this.container)}},{key:"inspect",value:function(z,G){for(var $=this,De=z.filter(function(Tt){return Tt.nodeType===Node.ELEMENT_NODE});this.rects.length>De.length;)this.rects.pop().remove();if(De.length!==0){for(;this.rects.length1&&arguments[1]!==void 0?arguments[1]:W,tt=void 0,Tt=[],kt=void 0,bt=!1,on=function(Lt,gn){return qe(Lt,Tt[gn])},tn=function(){for(var Lt=arguments.length,gn=Array(Lt),lr=0;lr5&&arguments[5]!==void 0?arguments[5]:0,Z=Co(B);switch(Z){case"html_element":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:B.tagName,type:Z};case"function":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:typeof B.name!="function"&&B.name?B.name:"function",type:Z};case"string":return B.length<=500?B:B.slice(0,500)+"...";case"bigint":case"symbol":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:B.toString(),type:Z};case"react_element":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:L0(B)||"Unknown",type:Z};case"array_buffer":case"data_view":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:Z==="data_view"?"DataView":"ArrayBuffer",size:B.byteLength,type:Z};case"array":return me=De($),xe>=2&&!me?a0(Z,!0,B,z,$):B.map(function(ht,ie){return So(ht,z,G,$.concat([ie]),De,me?1:xe+1)});case"html_all_collection":case"typed_array":case"iterator":if(me=De($),xe>=2&&!me)return a0(Z,!0,B,z,$);var ke={unserializable:!0,type:Z,readonly:!0,size:Z==="typed_array"?B.length:void 0,preview_short:Si(B,!1),preview_long:Si(B,!0),name:B.constructor&&B.constructor.name!=="Object"?B.constructor.name:""};return Kt(B[Symbol.iterator])&&Array.from(B).forEach(function(ht,ie){return ke[ie]=So(ht,z,G,$.concat([ie]),De,me?1:xe+1)}),G.push($),ke;case"opaque_iterator":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:B[Symbol.toStringTag],type:Z};case"date":case"regexp":return z.push($),{inspectable:!1,preview_short:Si(B,!1),preview_long:Si(B,!0),name:B.toString(),type:Z};case"object":if(me=De($),xe>=2&&!me)return a0(Z,!0,B,z,$);var Xe={};return eu(B).forEach(function(ht){var ie=ht.toString();Xe[ie]=So(B[ht],z,G,$.concat([ie]),De,me?1:xe+1)}),Xe;case"infinity":case"nan":case"undefined":return z.push($),{type:Z};default:return B}}function Go(B){return(Go=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(z){return typeof z}:function(z){return z&&typeof Symbol=="function"&&z.constructor===Symbol&&z!==Symbol.prototype?"symbol":typeof z})(B)}function Os(B){return function(z){if(Array.isArray(z))return Yo(z)}(B)||function(z){if(typeof Symbol!="undefined"&&Symbol.iterator in Object(z))return Array.from(z)}(B)||function(z,G){if(!!z){if(typeof z=="string")return Yo(z,G);var $=Object.prototype.toString.call(z).slice(8,-1);if($==="Object"&&z.constructor&&($=z.constructor.name),$==="Map"||$==="Set")return Array.from(z);if($==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test($))return Yo(z,G)}}(B)||function(){throw new TypeError(`Invalid attempt to spread non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}()}function Yo(B,z){(z==null||z>B.length)&&(z=B.length);for(var G=0,$=new Array(z);Gz.toString()?1:z.toString()>B.toString()?-1:0}function eu(B){for(var z=[],G=B,$=function(){var De=[].concat(Os(Object.keys(G)),Os(Object.getOwnPropertySymbols(G))),me=Object.getOwnPropertyDescriptors(G);De.forEach(function(xe){me[xe].enumerable&&z.push(xe)}),G=Object.getPrototypeOf(G)};G!=null;)$();return z}function ai(B){var z=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"Anonymous",G=Ko.get(B);if(G!=null)return G;var $=z;return typeof B.displayName=="string"?$=B.displayName:typeof B.name=="string"&&B.name!==""&&($=B.name),Ko.set(B,$),$}var mr=0;function Xo(){return++mr}function W0(B){var z=qt.get(B);if(z!==void 0)return z;for(var G=new Array(B.length),$=0;$1&&arguments[1]!==void 0?arguments[1]:50;return B.length>z?B.substr(0,z)+"\u2026":B}function Si(B,z){if(B!=null&&hasOwnProperty.call(B,vu.type))return z?B[vu.preview_long]:B[vu.preview_short];switch(Co(B)){case"html_element":return"<".concat(tu(B.tagName.toLowerCase())," />");case"function":return tu("\u0192 ".concat(typeof B.name=="function"?"":B.name,"() {}"));case"string":return'"'.concat(B,'"');case"bigint":return tu(B.toString()+"n");case"regexp":case"symbol":return tu(B.toString());case"react_element":return"<".concat(tu(L0(B)||"Unknown")," />");case"array_buffer":return"ArrayBuffer(".concat(B.byteLength,")");case"data_view":return"DataView(".concat(B.buffer.byteLength,")");case"array":if(z){for(var G="",$=0;$0&&(G+=", "),!((G+=Si(B[$],!1)).length>50));$++);return"[".concat(tu(G),"]")}var De=hasOwnProperty.call(B,vu.size)?B[vu.size]:B.length;return"Array(".concat(De,")");case"typed_array":var me="".concat(B.constructor.name,"(").concat(B.length,")");if(z){for(var xe="",Z=0;Z0&&(xe+=", "),!((xe+=B[Z]).length>50));Z++);return"".concat(me," [").concat(tu(xe),"]")}return me;case"iterator":var ke=B.constructor.name;if(z){for(var Xe=Array.from(B),ht="",ie=0;ie0&&(ht+=", "),Array.isArray(qe)){var tt=Si(qe[0],!0),Tt=Si(qe[1],!1);ht+="".concat(tt," => ").concat(Tt)}else ht+=Si(qe,!1);if(ht.length>50)break}return"".concat(ke,"(").concat(B.size,") {").concat(tu(ht),"}")}return"".concat(ke,"(").concat(B.size,")");case"opaque_iterator":return B[Symbol.toStringTag];case"date":return B.toString();case"object":if(z){for(var kt=eu(B).sort(_i),bt="",on=0;on0&&(bt+=", "),(bt+="".concat(tn.toString(),": ").concat(Si(B[tn],!1))).length>50)break}return"{".concat(tu(bt),"}")}return"{\u2026}";case"boolean":case"number":case"infinity":case"nan":case"null":case"undefined":return B;default:try{return tu(""+B)}catch(Lt){return"unserializable"}}}var ks=f(7);function Hl(B){return(Hl=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(z){return typeof z}:function(z){return z&&typeof Symbol=="function"&&z.constructor===Symbol&&z!==Symbol.prototype?"symbol":typeof z})(B)}function F0(B,z){var G=Object.keys(B);if(Object.getOwnPropertySymbols){var $=Object.getOwnPropertySymbols(B);z&&($=$.filter(function(De){return Object.getOwnPropertyDescriptor(B,De).enumerable})),G.push.apply(G,$)}return G}function f0(B){for(var z=1;z2&&arguments[2]!==void 0?arguments[2]:[];if(B!==null){var $=[],De=[],me=So(B,$,De,G,z);return{data:me,cleaned:$,unserializable:De}}return null}function G0(B){var z,G,$=(z=B,G=new Set,JSON.stringify(z,function(xe,Z){if(Hl(Z)==="object"&&Z!==null){if(G.has(Z))return;G.add(Z)}return typeof Z=="bigint"?Z.toString()+"n":Z})),De=$===void 0?"undefined":$,me=window.__REACT_DEVTOOLS_GLOBAL_HOOK__.clipboardCopyText;typeof me=="function"?me(De).catch(function(xe){}):Object(ks.copy)(De)}function fi(B,z){var G=arguments.length>2&&arguments[2]!==void 0?arguments[2]:0,$=z[G],De=Array.isArray(B)?B.slice():f0({},B);return G+1===z.length?Array.isArray(De)?De.splice($,1):delete De[$]:De[$]=fi(B[$],z,G+1),De}function Zt(B,z,G){var $=arguments.length>3&&arguments[3]!==void 0?arguments[3]:0,De=z[$],me=Array.isArray(B)?B.slice():f0({},B);if($+1===z.length){var xe=G[$];me[xe]=me[De],Array.isArray(me)?me.splice(De,1):delete me[De]}else me[De]=Zt(B[De],z,G,$+1);return me}function Ln(B,z,G){var $=arguments.length>3&&arguments[3]!==void 0?arguments[3]:0;if($>=z.length)return G;var De=z[$],me=Array.isArray(B)?B.slice():f0({},B);return me[De]=Ln(B[De],z,G,$+1),me}var Di=f(8);function ci(B,z){var G=Object.keys(B);if(Object.getOwnPropertySymbols){var $=Object.getOwnPropertySymbols(B);z&&($=$.filter(function(De){return Object.getOwnPropertyDescriptor(B,De).enumerable})),G.push.apply(G,$)}return G}function Ht(B){for(var z=1;z=B.length?{done:!0}:{done:!1,value:B[$++]}},e:function(ke){throw ke},f:De}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var me,xe=!0,Z=!1;return{s:function(){G=B[Symbol.iterator]()},n:function(){var ke=G.next();return xe=ke.done,ke},e:function(ke){Z=!0,me=ke},f:function(){try{xe||G.return==null||G.return()}finally{if(Z)throw me}}}}function Wl(B,z){if(B){if(typeof B=="string")return xo(B,z);var G=Object.prototype.toString.call(B).slice(8,-1);return G==="Object"&&B.constructor&&(G=B.constructor.name),G==="Map"||G==="Set"?Array.from(B):G==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(G)?xo(B,z):void 0}}function xo(B,z){(z==null||z>B.length)&&(z=B.length);for(var G=0,$=new Array(z);G0){var vt=me(se);if(vt!=null){var Xt,xn=Ui(Mo);try{for(xn.s();!(Xt=xn.n()).done;)if(Xt.value.test(vt))return!0}catch(er){xn.e(er)}finally{xn.f()}}}if(re!=null&&ds.size>0){var _n,yn=re.fileName,En=Ui(ds);try{for(En.s();!(_n=En.n()).done;)if(_n.value.test(yn))return!0}catch(er){En.e(er)}finally{En.f()}}return!1}function yu(se){var re=se.type;switch(se.tag){case Tt:case _r:return 1;case tt:case Cn:return 5;case tn:return 6;case Lt:return 11;case lr:return 7;case gn:case Qn:case on:return 9;case Ar:case Rr:return 8;case nt:return 12;case _t:return 13;default:switch(xe(re)){case 60111:case"Symbol(react.concurrent_mode)":case"Symbol(react.async_mode)":return 9;case 60109:case"Symbol(react.provider)":return 2;case 60110:case"Symbol(react.context)":return 2;case 60108:case"Symbol(react.strict_mode)":return 9;case 60114:case"Symbol(react.profiler)":return 10;default:return 9}}}function pi(se){if(Fo.has(se))return se;var re=se.alternate;return re!=null&&Fo.has(re)?re:(Fo.add(se),se)}window.__REACT_DEVTOOLS_COMPONENT_FILTERS__!=null?ps(window.__REACT_DEVTOOLS_COMPONENT_FILTERS__):ps([{type:1,value:7,isEnabled:!0}]);var T0=new Map,Q0=new Map,Fo=new Set,ta=new Map,Kl=new Map,Ki=-1;function Yr(se){if(!T0.has(se)){var re=Xo();T0.set(se,re),Q0.set(re,se)}return T0.get(se)}function fo(se){switch(yu(se)){case 1:if(I0!==null){var re=Yr(pi(se)),Le=gi(se);Le!==null&&I0.set(re,Le)}}}var Oi={};function gi(se){switch(yu(se)){case 1:var re=se.stateNode,Le=Oi,Ae=Oi;return re!=null&&(re.constructor&&re.constructor.contextType!=null?Ae=re.context:(Le=re.context)&&Object.keys(Le).length===0&&(Le=Oi)),[Le,Ae];default:return null}}function ff(se){switch(yu(se)){case 1:if(I0!==null){var re=Yr(pi(se)),Le=I0.has(re)?I0.get(re):null,Ae=gi(se);if(Le==null||Ae==null)return null;var ot=Y0(Le,2),vt=ot[0],Xt=ot[1],xn=Y0(Ae,2),_n=xn[0],yn=xn[1];if(_n!==Oi)return J0(vt,_n);if(yn!==Oi)return Xt!==yn}}return null}function cf(se,re){if(se==null||re==null)return!1;if(re.hasOwnProperty("baseState")&&re.hasOwnProperty("memoizedState")&&re.hasOwnProperty("next")&&re.hasOwnProperty("queue"))for(;re!==null;){if(re.memoizedState!==se.memoizedState)return!0;re=re.next,se=se.next}return!1}function J0(se,re){if(se==null||re==null||re.hasOwnProperty("baseState")&&re.hasOwnProperty("memoizedState")&&re.hasOwnProperty("next")&&re.hasOwnProperty("queue"))return null;var Le,Ae=[],ot=Ui(new Set([].concat(Yi(Object.keys(se)),Yi(Object.keys(re)))));try{for(ot.s();!(Le=ot.n()).done;){var vt=Le.value;se[vt]!==re[vt]&&Ae.push(vt)}}catch(Xt){ot.e(Xt)}finally{ot.f()}return Ae}function Z0(se,re){switch(re.tag){case Tt:case tt:case kt:case Ar:case Rr:return(oo(re)&ie)===ie;default:return se.memoizedProps!==re.memoizedProps||se.memoizedState!==re.memoizedState||se.ref!==re.ref}}var Te=[],et=[],Ve=[],Gt=[],Yt=new Map,sr=0,Br=null;function wn(se){Te.push(se)}function fu(se){if(Te.length!==0||et.length!==0||Ve.length!==0||Br!==null||Ru){var re=et.length+Ve.length+(Br===null?0:1),Le=new Array(3+sr+(re>0?2+re:0)+Te.length),Ae=0;if(Le[Ae++]=z,Le[Ae++]=Ki,Le[Ae++]=sr,Yt.forEach(function(xn,_n){Le[Ae++]=_n.length;for(var yn=W0(_n),En=0;En0){Le[Ae++]=2,Le[Ae++]=re;for(var ot=et.length-1;ot>=0;ot--)Le[Ae++]=et[ot];for(var vt=0;vt0?se.forEach(function(re){B.emit("operations",re)}):(wr!==null&&(ru=!0),B.getFiberRoots(z).forEach(function(re){Xu(Ki=Yr(pi(re.current)),re.current),Ru&&re.memoizedInteractions!=null&&($o={changeDescriptions:Xl?new Map:null,durations:[],commitTime:Vl()-Yu,interactions:Array.from(re.memoizedInteractions).map(function(Le){return Ht(Ht({},Le),{},{timestamp:Le.timestamp-Yu})}),maxActualDuration:0,priorityLevel:null}),Vr(re.current,null,!1,!1),fu(),Ki=-1}))},getBestMatchForTrackedPath:function(){if(wr===null||$0===null)return null;for(var se=$0;se!==null&&Vu(se);)se=se.return;return se===null?null:{id:Yr(pi(se)),isFullMatch:Xi===wr.length-1}},getDisplayNameForFiberID:function(se){var re=Q0.get(se);return re!=null?me(re):null},getFiberIDForNative:function(se){var re=arguments.length>1&&arguments[1]!==void 0&&arguments[1],Le=G.findFiberByHostInstance(se);if(Le!=null){if(re)for(;Le!==null&&Vu(Le);)Le=Le.return;return Yr(pi(Le))}return null},getInstanceAndStyle:function(se){var re=null,Le=null,Ae=Uu(se);return Ae!==null&&(re=Ae.stateNode,Ae.memoizedProps!==null&&(Le=Ae.memoizedProps.style)),{instance:re,style:Le}},getOwnersList:function(se){var re=Uu(se);if(re==null)return null;var Le=re._debugOwner,Ae=[{displayName:me(re)||"Anonymous",id:se,type:yu(re)}];if(Le)for(var ot=Le;ot!==null;)Ae.unshift({displayName:me(ot)||"Anonymous",id:Yr(pi(ot)),type:yu(ot)}),ot=ot._debugOwner||null;return Ae},getPathForElement:function(se){var re=Q0.get(se);if(re==null)return null;for(var Le=[];re!==null;)Le.push(y0(re)),re=re.return;return Le.reverse(),Le},getProfilingData:function(){var se=[];if(hs===null)throw Error("getProfilingData() called before any profiling data was recorded");return hs.forEach(function(re,Le){var Ae=[],ot=[],vt=new Map,Xt=new Map,xn=El!==null&&El.get(Le)||"Unknown";R0!=null&&R0.forEach(function(_n,yn){co!=null&&co.get(yn)===Le&&ot.push([yn,_n])}),re.forEach(function(_n,yn){var En=_n.changeDescriptions,er=_n.durations,It=_n.interactions,xi=_n.maxActualDuration,Sr=_n.priorityLevel,cr=_n.commitTime,Y=[];It.forEach(function(hi){vt.has(hi.id)||vt.set(hi.id,hi),Y.push(hi.id);var Qi=Xt.get(hi.id);Qi!=null?Qi.push(yn):Xt.set(hi.id,[yn])});for(var Qr=[],Jr=[],Ur=0;Ur1?Wn.set(En,er-1):Wn.delete(En),Xr.delete(_n)}(Ki),Kr(Le,!1))}else Xu(Ki,Le),Vr(Le,null,!1,!1);if(Ru&&ot){var xn=hs.get(Ki);xn!=null?xn.push($o):hs.set(Ki,[$o])}fu(),No&&B.emit("traceUpdates",Lo),Ki=-1},handleCommitFiberUnmount:function(se){Kr(se,!1)},inspectElement:function(se,re){if(Li(se)){if(re!=null){A0(re);var Le=null;return re[0]==="hooks"&&(Le="hooks"),{id:se,type:"hydrated-path",path:re,value:Ei(Lu(zi,re),Fi(null,Le),re)}}return{id:se,type:"no-change"}}if(Is=!1,zi!==null&&zi.id===se||(x0={}),(zi=na(se))===null)return{id:se,type:"not-found"};re!=null&&A0(re),function(ot){var vt=ot.hooks,Xt=ot.id,xn=ot.props,_n=Q0.get(Xt);if(_n!=null){var yn=_n.elementType,En=_n.stateNode,er=_n.tag,It=_n.type;switch(er){case Tt:case _r:case Cn:$.$r=En;break;case tt:$.$r={hooks:vt,props:xn,type:It};break;case tn:$.$r={props:xn,type:It.render};break;case Ar:case Rr:$.$r={props:xn,type:yn!=null&&yn.type!=null?yn.type:It};break;default:$.$r=null}}else console.warn('Could not find Fiber with id "'.concat(Xt,'"'))}(zi);var Ae=Ht({},zi);return Ae.context=Ei(Ae.context,Fi("context",null)),Ae.hooks=Ei(Ae.hooks,Fi("hooks","hooks")),Ae.props=Ei(Ae.props,Fi("props",null)),Ae.state=Ei(Ae.state,Fi("state",null)),{id:se,type:"full-data",value:Ae}},logElementToConsole:function(se){var re=Li(se)?zi:na(se);if(re!==null){var Le=typeof console.groupCollapsed=="function";Le&&console.groupCollapsed("[Click to expand] %c<".concat(re.displayName||"Component"," />"),"color: var(--dom-tag-name-color); font-weight: normal;"),re.props!==null&&console.log("Props:",re.props),re.state!==null&&console.log("State:",re.state),re.hooks!==null&&console.log("Hooks:",re.hooks);var Ae=_l(se);Ae!==null&&console.log("Nodes:",Ae),re.source!==null&&console.log("Location:",re.source),(window.chrome||/firefox/i.test(navigator.userAgent))&&console.log("Right-click any value to save it as a global variable for further inspection."),Le&&console.groupEnd()}else console.warn('Could not find Fiber with id "'.concat(se,'"'))},prepareViewAttributeSource:function(se,re){Li(se)&&(window.$attribute=Lu(zi,re))},prepareViewElementSource:function(se){var re=Q0.get(se);if(re!=null){var Le=re.elementType,Ae=re.tag,ot=re.type;switch(Ae){case Tt:case _r:case Cn:case tt:$.$type=ot;break;case tn:$.$type=ot.render;break;case Ar:case Rr:$.$type=Le!=null&&Le.type!=null?Le.type:ot;break;default:$.$type=null}}else console.warn('Could not find Fiber with id "'.concat(se,'"'))},overrideSuspense:function(se,re){if(typeof ko!="function"||typeof Zo!="function")throw new Error("Expected overrideSuspense() to not get called for earlier React versions.");re?(Ku.add(se),Ku.size===1&&ko(vs)):(Ku.delete(se),Ku.size===0&&ko(df));var Le=Q0.get(se);Le!=null&&Zo(Le)},overrideValueAtPath:function(se,re,Le,Ae,ot){var vt=Uu(re);if(vt!==null){var Xt=vt.stateNode;switch(se){case"context":switch(Ae=Ae.slice(1),vt.tag){case Tt:Ae.length===0?Xt.context=ot:To(Xt.context,Ae,ot),Xt.forceUpdate()}break;case"hooks":typeof nu=="function"&&nu(vt,Le,Ae,ot);break;case"props":switch(vt.tag){case Tt:vt.pendingProps=Ln(Xt.props,Ae,ot),Xt.forceUpdate();break;default:typeof X0=="function"&&X0(vt,Ae,ot)}break;case"state":switch(vt.tag){case Tt:To(Xt.state,Ae,ot),Xt.forceUpdate()}}}},renamePath:function(se,re,Le,Ae,ot){var vt=Uu(re);if(vt!==null){var Xt=vt.stateNode;switch(se){case"context":switch(Ae=Ae.slice(1),ot=ot.slice(1),vt.tag){case Tt:Ae.length===0||Hr(Xt.context,Ae,ot),Xt.forceUpdate()}break;case"hooks":typeof S0=="function"&&S0(vt,Le,Ae,ot);break;case"props":Xt===null?typeof di=="function"&&di(vt,Ae,ot):(vt.pendingProps=Zt(Xt.props,Ae,ot),Xt.forceUpdate());break;case"state":Hr(Xt.state,Ae,ot),Xt.forceUpdate()}}},renderer:G,setTraceUpdatesEnabled:function(se){No=se},setTrackedPath:Ci,startProfiling:ra,stopProfiling:function(){Ru=!1,Xl=!1},storeAsGlobal:function(se,re,Le){if(Li(se)){var Ae=Lu(zi,re),ot="$reactTemp".concat(Le);window[ot]=Ae,console.log(ot),console.log(Ae)}},updateComponentFilters:function(se){if(Ru)throw Error("Cannot modify filter preferences while profiling");B.getFiberRoots(z).forEach(function(re){Ki=Yr(pi(re.current)),Bu(re.current),Kr(re.current,!1),Ki=-1}),ps(se),Wn.clear(),B.getFiberRoots(z).forEach(function(re){Xu(Ki=Yr(pi(re.current)),re.current),Vr(re.current,null,!1,!1),fu(re),Ki=-1})}}}var Xn;function Qo(B){return(Qo=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(z){return typeof z}:function(z){return z&&typeof Symbol=="function"&&z.constructor===Symbol&&z!==Symbol.prototype?"symbol":typeof z})(B)}function lo(B,z,G){if(Xn===void 0)try{throw Error()}catch(De){var $=De.stack.trim().match(/\n( *(at )?)/);Xn=$&&$[1]||""}return` +`+Xn+B}var b0=!1;function yl(B,z,G){if(!B||b0)return"";var $,De=Error.prepareStackTrace;Error.prepareStackTrace=void 0,b0=!0;var me=G.current;G.current=null;try{if(z){var xe=function(){throw Error()};if(Object.defineProperty(xe.prototype,"props",{set:function(){throw Error()}}),(typeof Reflect=="undefined"?"undefined":Qo(Reflect))==="object"&&Reflect.construct){try{Reflect.construct(xe,[])}catch(qe){$=qe}Reflect.construct(B,[],xe)}else{try{xe.call()}catch(qe){$=qe}B.call(xe.prototype)}}else{try{throw Error()}catch(qe){$=qe}B()}}catch(qe){if(qe&&$&&typeof qe.stack=="string"){for(var Z=qe.stack.split(` +`),ke=$.stack.split(` +`),Xe=Z.length-1,ht=ke.length-1;Xe>=1&&ht>=0&&Z[Xe]!==ke[ht];)ht--;for(;Xe>=1&&ht>=0;Xe--,ht--)if(Z[Xe]!==ke[ht]){if(Xe!==1||ht!==1)do if(Xe--,--ht<0||Z[Xe]!==ke[ht])return` +`+Z[Xe].replace(" at new "," at ");while(Xe>=1&&ht>=0);break}}}finally{b0=!1,Error.prepareStackTrace=De,G.current=me}var ie=B?B.displayName||B.name:"";return ie?lo(ie):""}function Ro(B,z,G,$){return yl(B,!1,$)}function Et(B,z,G){var $=B.HostComponent,De=B.LazyComponent,me=B.SuspenseComponent,xe=B.SuspenseListComponent,Z=B.FunctionComponent,ke=B.IndeterminateComponent,Xe=B.SimpleMemoComponent,ht=B.ForwardRef,ie=B.Block,qe=B.ClassComponent;switch(z.tag){case $:return lo(z.type);case De:return lo("Lazy");case me:return lo("Suspense");case xe:return lo("SuspenseList");case Z:case ke:case Xe:return Ro(z.type,0,0,G);case ht:return Ro(z.type.render,0,0,G);case ie:return Ro(z.type._render,0,0,G);case qe:return function(tt,Tt,kt,bt){return yl(tt,!0,bt)}(z.type,0,0,G);default:return""}}function Pt(B,z,G){try{var $="",De=z;do $+=Et(B,De,G),De=De.return;while(De);return $}catch(me){return` +Error generating stack: `+me.message+` +`+me.stack}}function Bn(B,z){var G;if(typeof Symbol=="undefined"||B[Symbol.iterator]==null){if(Array.isArray(B)||(G=function(ke,Xe){if(!!ke){if(typeof ke=="string")return Ir(ke,Xe);var ht=Object.prototype.toString.call(ke).slice(8,-1);if(ht==="Object"&&ke.constructor&&(ht=ke.constructor.name),ht==="Map"||ht==="Set")return Array.from(ke);if(ht==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(ht))return Ir(ke,Xe)}}(B))||z&&B&&typeof B.length=="number"){G&&(B=G);var $=0,De=function(){};return{s:De,n:function(){return $>=B.length?{done:!0}:{done:!1,value:B[$++]}},e:function(ke){throw ke},f:De}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}var me,xe=!0,Z=!1;return{s:function(){G=B[Symbol.iterator]()},n:function(){var ke=G.next();return xe=ke.done,ke},e:function(ke){Z=!0,me=ke},f:function(){try{xe||G.return==null||G.return()}finally{if(Z)throw me}}}}function Ir(B,z){(z==null||z>B.length)&&(z=B.length);for(var G=0,$=new Array(z);G0?Xe[Xe.length-1]:null,qe=ie!==null&&(Wr.test(ie)||wu.test(ie));if(!qe){var tt,Tt=Bn(c0.values());try{for(Tt.s();!(tt=Tt.n()).done;){var kt=tt.value,bt=kt.currentDispatcherRef,on=kt.getCurrentFiber,tn=kt.workTagMap,Lt=on();if(Lt!=null){var gn=Pt(tn,Lt,bt);gn!==""&&Xe.push(gn);break}}}catch(lr){Tt.e(lr)}finally{Tt.f()}}}catch(lr){}me.apply(void 0,Xe)};xe.__REACT_DEVTOOLS_ORIGINAL_METHOD__=me,Ti[De]=xe}catch(Z){}})}}function Fu(B){return(Fu=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(z){return typeof z}:function(z){return z&&typeof Symbol=="function"&&z.constructor===Symbol&&z!==Symbol.prototype?"symbol":typeof z})(B)}function fs(B,z){for(var G=0;GB.length)&&(z=B.length);for(var G=0,$=new Array(z);G1?Z-1:0),Xe=1;Xe0?ie[ie.length-1]:0),ie.push(nn),Z.set(Ze,Xe(Ft._topLevelWrapper));try{var sn=nt.apply(this,_t);return ie.pop(),sn}catch(yr){throw ie=[],yr}finally{if(ie.length===0){var Yn=Z.get(Ze);if(Yn===void 0)throw new Error("Expected to find root ID.");lr(Yn)}}},performUpdateIfNecessary:function(nt,_t){var Ze=_t[0];if(w0(Ze)===9)return nt.apply(this,_t);var Ft=Xe(Ze);ie.push(Ft);var nn=Gn(Ze);try{var sn=nt.apply(this,_t),Yn=Gn(Ze);return ht(nn,Yn)||Tt(Ze,Ft,Yn),ie.pop(),sn}catch(nu){throw ie=[],nu}finally{if(ie.length===0){var yr=Z.get(Ze);if(yr===void 0)throw new Error("Expected to find root ID.");lr(yr)}}},receiveComponent:function(nt,_t){var Ze=_t[0];if(w0(Ze)===9)return nt.apply(this,_t);var Ft=Xe(Ze);ie.push(Ft);var nn=Gn(Ze);try{var sn=nt.apply(this,_t),Yn=Gn(Ze);return ht(nn,Yn)||Tt(Ze,Ft,Yn),ie.pop(),sn}catch(nu){throw ie=[],nu}finally{if(ie.length===0){var yr=Z.get(Ze);if(yr===void 0)throw new Error("Expected to find root ID.");lr(yr)}}},unmountComponent:function(nt,_t){var Ze=_t[0];if(w0(Ze)===9)return nt.apply(this,_t);var Ft=Xe(Ze);ie.push(Ft);try{var nn=nt.apply(this,_t);return ie.pop(),function(Yn,yr){tn.push(yr),me.delete(yr)}(0,Ft),nn}catch(Yn){throw ie=[],Yn}finally{if(ie.length===0){var sn=Z.get(Ze);if(sn===void 0)throw new Error("Expected to find root ID.");lr(sn)}}}}));var bt=[],on=new Map,tn=[],Lt=0,gn=null;function lr(nt){if(bt.length!==0||tn.length!==0||gn!==null){var _t=tn.length+(gn===null?0:1),Ze=new Array(3+Lt+(_t>0?2+_t:0)+bt.length),Ft=0;if(Ze[Ft++]=z,Ze[Ft++]=nt,Ze[Ft++]=Lt,on.forEach(function(Yn,yr){Ze[Ft++]=yr.length;for(var nu=W0(yr),Cu=0;Cu0){Ze[Ft++]=2,Ze[Ft++]=_t;for(var nn=0;nn"),"color: var(--dom-tag-name-color); font-weight: normal;"),_t.props!==null&&console.log("Props:",_t.props),_t.state!==null&&console.log("State:",_t.state),_t.context!==null&&console.log("Context:",_t.context);var Ft=De(nt);Ft!==null&&console.log("Node:",Ft),(window.chrome||/firefox/i.test(navigator.userAgent))&&console.log("Right-click any value to save it as a global variable for further inspection."),Ze&&console.groupEnd()}else console.warn('Could not find element with id "'.concat(nt,'"'))},overrideSuspense:function(){throw new Error("overrideSuspense not supported by this renderer")},overrideValueAtPath:function(nt,_t,Ze,Ft,nn){var sn=me.get(_t);if(sn!=null){var Yn=sn._instance;if(Yn!=null)switch(nt){case"context":To(Yn.context,Ft,nn),p0(Yn);break;case"hooks":throw new Error("Hooks not supported by this renderer");case"props":var yr=sn._currentElement;sn._currentElement=K0(K0({},yr),{},{props:Ln(yr.props,Ft,nn)}),p0(Yn);break;case"state":To(Yn.state,Ft,nn),p0(Yn)}}},renamePath:function(nt,_t,Ze,Ft,nn){var sn=me.get(_t);if(sn!=null){var Yn=sn._instance;if(Yn!=null)switch(nt){case"context":Hr(Yn.context,Ft,nn),p0(Yn);break;case"hooks":throw new Error("Hooks not supported by this renderer");case"props":var yr=sn._currentElement;sn._currentElement=K0(K0({},yr),{},{props:Zt(yr.props,Ft,nn)}),p0(Yn);break;case"state":Hr(Yn.state,Ft,nn),p0(Yn)}}},prepareViewAttributeSource:function(nt,_t){var Ze=Rr(nt);Ze!==null&&(window.$attribute=Lu(Ze,_t))},prepareViewElementSource:function(nt){var _t=me.get(nt);if(_t!=null){var Ze=_t._currentElement;Ze!=null?$.$type=Ze.type:console.warn('Could not find element with id "'.concat(nt,'"'))}else console.warn('Could not find instance with id "'.concat(nt,'"'))},renderer:G,setTraceUpdatesEnabled:function(nt){},setTrackedPath:function(nt){},startProfiling:function(){},stopProfiling:function(){},storeAsGlobal:function(nt,_t,Ze){var Ft=Rr(nt);if(Ft!==null){var nn=Lu(Ft,_t),sn="$reactTemp".concat(Ze);window[sn]=nn,console.log(sn),console.log(nn)}},updateComponentFilters:function(nt){}}}function ri(B,z){var G=!1,$={bottom:0,left:0,right:0,top:0},De=z[B];if(De!=null){for(var me=0,xe=Object.keys($);me0?"development":"production";var bt=Function.prototype.toString;if(kt.Mount&&kt.Mount._renderNewRootComponent){var on=bt.call(kt.Mount._renderNewRootComponent);return on.indexOf("function")!==0?"production":on.indexOf("storedMeasure")!==-1?"development":on.indexOf("should be a pure function")!==-1?on.indexOf("NODE_ENV")!==-1||on.indexOf("development")!==-1||on.indexOf("true")!==-1?"development":on.indexOf("nextElement")!==-1||on.indexOf("nextComponent")!==-1?"unminified":"development":on.indexOf("nextElement")!==-1||on.indexOf("nextComponent")!==-1?"unminified":"outdated"}}catch(tn){}return"production"}(ke);try{var ie=window.__REACT_DEVTOOLS_APPEND_COMPONENT_STACK__!==!1,qe=window.__REACT_DEVTOOLS_BREAK_ON_CONSOLE_ERRORS__===!0;(ie||qe)&&(so(ke),Gl({appendComponentStack:ie,breakOnConsoleErrors:qe}))}catch(kt){}var tt=B.__REACT_DEVTOOLS_ATTACH__;if(typeof tt=="function"){var Tt=tt(Z,Xe,ke,B);Z.rendererInterfaces.set(Xe,Tt)}return Z.emit("renderer",{id:Xe,renderer:ke,reactBuildType:ht}),Xe},on:function(ke,Xe){me[ke]||(me[ke]=[]),me[ke].push(Xe)},off:function(ke,Xe){if(me[ke]){var ht=me[ke].indexOf(Xe);ht!==-1&&me[ke].splice(ht,1),me[ke].length||delete me[ke]}},sub:function(ke,Xe){return Z.on(ke,Xe),function(){return Z.off(ke,Xe)}},supportsFiber:!0,checkDCE:function(ke){try{Function.prototype.toString.call(ke).indexOf("^_^")>-1&&(G=!0,setTimeout(function(){throw new Error("React is running in production mode, but dead code elimination has not been applied. Read how to correctly configure React for production: https://reactjs.org/link/perf-use-production-build")}))}catch(Xe){}},onCommitFiberUnmount:function(ke,Xe){var ht=De.get(ke);ht!=null&&ht.handleCommitFiberUnmount(Xe)},onCommitFiberRoot:function(ke,Xe,ht){var ie=Z.getFiberRoots(ke),qe=Xe.current,tt=ie.has(Xe),Tt=qe.memoizedState==null||qe.memoizedState.element==null;tt||Tt?tt&&Tt&&ie.delete(Xe):ie.add(Xe);var kt=De.get(ke);kt!=null&&kt.handleCommitFiberRoot(Xe,ht)}};Object.defineProperty(B,"__REACT_DEVTOOLS_GLOBAL_HOOK__",{configurable:!1,enumerable:!1,get:function(){return Z}})})(window);var h0=window.__REACT_DEVTOOLS_GLOBAL_HOOK__,Fs=[{type:1,value:7,isEnabled:!0}];function Ni(B){if(h0!=null){var z=B||{},G=z.host,$=G===void 0?"localhost":G,De=z.nativeStyleEditorValidAttributes,me=z.useHttps,xe=me!==void 0&&me,Z=z.port,ke=Z===void 0?8097:Z,Xe=z.websocket,ht=z.resolveRNStyle,ie=ht===void 0?null:ht,qe=z.isAppActive,tt=xe?"wss":"ws",Tt=null;if((qe===void 0?function(){return!0}:qe)()){var kt=null,bt=[],on=tt+"://"+$+":"+ke,tn=Xe||new window.WebSocket(on);tn.onclose=function(){kt!==null&&kt.emit("shutdown"),Lt()},tn.onerror=function(){Lt()},tn.onmessage=function(gn){var lr;try{if(typeof gn.data!="string")throw Error();lr=JSON.parse(gn.data)}catch(Qn){return void console.error("[React DevTools] Failed to parse JSON: "+gn.data)}bt.forEach(function(Qn){try{Qn(lr)}catch(_r){throw console.log("[React DevTools] Error calling listener",lr),console.log("error:",_r),_r}})},tn.onopen=function(){(kt=new ao({listen:function(Cn){return bt.push(Cn),function(){var Ar=bt.indexOf(Cn);Ar>=0&&bt.splice(Ar,1)}},send:function(Cn,Ar,v0){tn.readyState===tn.OPEN?tn.send(JSON.stringify({event:Cn,payload:Ar})):(kt!==null&&kt.shutdown(),Lt())}})).addListener("inspectElement",function(Cn){var Ar=Cn.id,v0=Cn.rendererID,Rr=gn.rendererInterfaces[v0];if(Rr!=null){var nt=Rr.findNativeNodesForFiberID(Ar);nt!=null&&nt[0]!=null&&gn.emit("showNativeHighlight",nt[0])}}),kt.addListener("updateComponentFilters",function(Cn){Fs=Cn}),window.__REACT_DEVTOOLS_COMPONENT_FILTERS__==null&&kt.send("overrideComponentFilters",Fs);var gn=new Hn(kt);if(gn.addListener("shutdown",function(){h0.emit("shutdown")}),function(Cn,Ar,v0){if(Cn==null)return function(){};var Rr=[Cn.sub("renderer-attached",function(Ze){var Ft=Ze.id,nn=(Ze.renderer,Ze.rendererInterface);Ar.setRendererInterface(Ft,nn),nn.flushInitialOperations()}),Cn.sub("unsupported-renderer-version",function(Ze){Ar.onUnsupportedRenderer(Ze)}),Cn.sub("operations",Ar.onHookOperations),Cn.sub("traceUpdates",Ar.onTraceUpdates)],nt=function(Ze,Ft){var nn=Cn.rendererInterfaces.get(Ze);nn==null&&(typeof Ft.findFiberByHostInstance=="function"?nn=Ms(Cn,Ze,Ft,v0):Ft.ComponentTree&&(nn=ic(Cn,Ze,Ft,v0)),nn!=null&&Cn.rendererInterfaces.set(Ze,nn)),nn!=null?Cn.emit("renderer-attached",{id:Ze,renderer:Ft,rendererInterface:nn}):Cn.emit("unsupported-renderer-version",Ze)};Cn.renderers.forEach(function(Ze,Ft){nt(Ft,Ze)}),Rr.push(Cn.sub("renderer",function(Ze){var Ft=Ze.id,nn=Ze.renderer;nt(Ft,nn)})),Cn.emit("react-devtools",Ar),Cn.reactDevtoolsAgent=Ar;var _t=function(){Rr.forEach(function(Ze){return Ze()}),Cn.rendererInterfaces.forEach(function(Ze){Ze.cleanup()}),Cn.reactDevtoolsAgent=null};Ar.addListener("shutdown",_t),Rr.push(function(){Ar.removeListener("shutdown",_t)})}(h0,gn,window),ie!=null||h0.resolveRNStyle!=null)ea(kt,gn,ie||h0.resolveRNStyle,De||h0.nativeStyleEditorValidAttributes||null);else{var lr,Qn,_r=function(){kt!==null&&ea(kt,gn,lr,Qn)};h0.hasOwnProperty("resolveRNStyle")||Object.defineProperty(h0,"resolveRNStyle",{enumerable:!1,get:function(){return lr},set:function(Cn){lr=Cn,_r()}}),h0.hasOwnProperty("nativeStyleEditorValidAttributes")||Object.defineProperty(h0,"nativeStyleEditorValidAttributes",{enumerable:!1,get:function(){return Qn},set:function(Cn){Qn=Cn,_r()}})}}}else Lt()}function Lt(){Tt===null&&(Tt=setTimeout(function(){return Ni(B)},2e3))}}}])})});var mR=ce(vR=>{"use strict";Object.defineProperty(vR,"__esModule",{value:!0});pR();var $Q=hR();$Q.connectToDevTools()});var DR=ce(kg=>{"use strict";var yR=kg&&kg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(kg,"__esModule",{value:!0});var gR=h4(),eJ=yR(D9()),_R=yR(eh()),ss=Lw();process.env.DEV==="true"&&mR();var ER=i=>{i==null||i.unsetMeasureFunc(),i==null||i.freeRecursive()};kg.default=eJ.default({schedulePassiveEffects:gR.unstable_scheduleCallback,cancelPassiveEffects:gR.unstable_cancelCallback,now:Date.now,getRootHostContext:()=>({isInsideText:!1}),prepareForCommit:()=>{},resetAfterCommit:i=>{if(i.isStaticDirty){i.isStaticDirty=!1,typeof i.onImmediateRender=="function"&&i.onImmediateRender();return}typeof i.onRender=="function"&&i.onRender()},getChildHostContext:(i,o)=>{let f=i.isInsideText,p=o==="ink-text"||o==="ink-virtual-text";return f===p?i:{isInsideText:p}},shouldSetTextContent:()=>!1,createInstance:(i,o,f,p)=>{if(p.isInsideText&&i==="ink-box")throw new Error(" can\u2019t be nested inside component");let E=i==="ink-text"&&p.isInsideText?"ink-virtual-text":i,t=ss.createNode(E);for(let[k,L]of Object.entries(o))k!=="children"&&(k==="style"?ss.setStyle(t,L):k==="internal_transform"?t.internal_transform=L:k==="internal_static"?t.internal_static=!0:ss.setAttribute(t,k,L));return t},createTextInstance:(i,o,f)=>{if(!f.isInsideText)throw new Error(`Text string "${i}" must be rendered inside component`);return ss.createTextNode(i)},resetTextContent:()=>{},hideTextInstance:i=>{ss.setTextNodeValue(i,"")},unhideTextInstance:(i,o)=>{ss.setTextNodeValue(i,o)},getPublicInstance:i=>i,hideInstance:i=>{var o;(o=i.yogaNode)===null||o===void 0||o.setDisplay(_R.default.DISPLAY_NONE)},unhideInstance:i=>{var o;(o=i.yogaNode)===null||o===void 0||o.setDisplay(_R.default.DISPLAY_FLEX)},appendInitialChild:ss.appendChildNode,appendChild:ss.appendChildNode,insertBefore:ss.insertBeforeNode,finalizeInitialChildren:(i,o,f,p)=>(i.internal_static&&(p.isStaticDirty=!0,p.staticNode=i),!1),supportsMutation:!0,appendChildToContainer:ss.appendChildNode,insertInContainerBefore:ss.insertBeforeNode,removeChildFromContainer:(i,o)=>{ss.removeChildNode(i,o),ER(o.yogaNode)},prepareUpdate:(i,o,f,p,E)=>{i.internal_static&&(E.isStaticDirty=!0);let t={},k=Object.keys(p);for(let L of k)if(p[L]!==f[L]){if(L==="style"&&typeof p.style=="object"&&typeof f.style=="object"){let C=p.style,U=f.style,q=Object.keys(C);for(let W of q){if(W==="borderStyle"||W==="borderColor"){if(typeof t.style!="object"){let ne={};t.style=ne}t.style.borderStyle=C.borderStyle,t.style.borderColor=C.borderColor}if(C[W]!==U[W]){if(typeof t.style!="object"){let ne={};t.style=ne}t.style[W]=C[W]}}continue}t[L]=p[L]}return t},commitUpdate:(i,o)=>{for(let[f,p]of Object.entries(o))f!=="children"&&(f==="style"?ss.setStyle(i,p):f==="internal_transform"?i.internal_transform=p:f==="internal_static"?i.internal_static=!0:ss.setAttribute(i,f,p))},commitTextUpdate:(i,o,f)=>{ss.setTextNodeValue(i,f)},removeChild:(i,o)=>{ss.removeChildNode(i,o),ER(o.yogaNode)}})});var SR=ce((Are,wR)=>{"use strict";wR.exports=(i,o=1,f)=>{if(f=E0({indent:" ",includeEmptyLines:!1},f),typeof i!="string")throw new TypeError(`Expected \`input\` to be a \`string\`, got \`${typeof i}\``);if(typeof o!="number")throw new TypeError(`Expected \`count\` to be a \`number\`, got \`${typeof o}\``);if(typeof f.indent!="string")throw new TypeError(`Expected \`options.indent\` to be a \`string\`, got \`${typeof f.indent}\``);if(o===0)return i;let p=f.includeEmptyLines?/^/gm:/^(?!\s*$)/gm;return i.replace(p,f.indent.repeat(o))}});var TR=ce(Mg=>{"use strict";var tJ=Mg&&Mg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Mg,"__esModule",{value:!0});var k4=tJ(eh());Mg.default=i=>i.getComputedWidth()-i.getComputedPadding(k4.default.EDGE_LEFT)-i.getComputedPadding(k4.default.EDGE_RIGHT)-i.getComputedBorder(k4.default.EDGE_LEFT)-i.getComputedBorder(k4.default.EDGE_RIGHT)});var xR=ce((Ore,CR)=>{CR.exports={single:{topLeft:"\u250C",topRight:"\u2510",bottomRight:"\u2518",bottomLeft:"\u2514",vertical:"\u2502",horizontal:"\u2500"},double:{topLeft:"\u2554",topRight:"\u2557",bottomRight:"\u255D",bottomLeft:"\u255A",vertical:"\u2551",horizontal:"\u2550"},round:{topLeft:"\u256D",topRight:"\u256E",bottomRight:"\u256F",bottomLeft:"\u2570",vertical:"\u2502",horizontal:"\u2500"},bold:{topLeft:"\u250F",topRight:"\u2513",bottomRight:"\u251B",bottomLeft:"\u2517",vertical:"\u2503",horizontal:"\u2501"},singleDouble:{topLeft:"\u2553",topRight:"\u2556",bottomRight:"\u255C",bottomLeft:"\u2559",vertical:"\u2551",horizontal:"\u2500"},doubleSingle:{topLeft:"\u2552",topRight:"\u2555",bottomRight:"\u255B",bottomLeft:"\u2558",vertical:"\u2502",horizontal:"\u2550"},classic:{topLeft:"+",topRight:"+",bottomRight:"+",bottomLeft:"+",vertical:"|",horizontal:"-"}}});var RR=ce((kre,Zw)=>{"use strict";var AR=xR();Zw.exports=AR;Zw.exports.default=AR});var kR=ce((Mre,OR)=>{"use strict";OR.exports=(i,o=process.argv)=>{let f=i.startsWith("-")?"":i.length===1?"-":"--",p=o.indexOf(f+i),E=o.indexOf("--");return p!==-1&&(E===-1||p{"use strict";var nJ=require("os"),NR=require("tty"),of=kR(),{env:Wo}=process,md;of("no-color")||of("no-colors")||of("color=false")||of("color=never")?md=0:(of("color")||of("colors")||of("color=true")||of("color=always"))&&(md=1);"FORCE_COLOR"in Wo&&(Wo.FORCE_COLOR==="true"?md=1:Wo.FORCE_COLOR==="false"?md=0:md=Wo.FORCE_COLOR.length===0?1:Math.min(parseInt(Wo.FORCE_COLOR,10),3));function $w(i){return i===0?!1:{level:i,hasBasic:!0,has256:i>=2,has16m:i>=3}}function e3(i,o){if(md===0)return 0;if(of("color=16m")||of("color=full")||of("color=truecolor"))return 3;if(of("color=256"))return 2;if(i&&!o&&md===void 0)return 0;let f=md||0;if(Wo.TERM==="dumb")return f;if(process.platform==="win32"){let p=nJ.release().split(".");return Number(p[0])>=10&&Number(p[2])>=10586?Number(p[2])>=14931?3:2:1}if("CI"in Wo)return["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some(p=>p in Wo)||Wo.CI_NAME==="codeship"?1:f;if("TEAMCITY_VERSION"in Wo)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(Wo.TEAMCITY_VERSION)?1:0;if("GITHUB_ACTIONS"in Wo)return 1;if(Wo.COLORTERM==="truecolor")return 3;if("TERM_PROGRAM"in Wo){let p=parseInt((Wo.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(Wo.TERM_PROGRAM){case"iTerm.app":return p>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(Wo.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(Wo.TERM)||"COLORTERM"in Wo?1:f}function rJ(i){let o=e3(i,i&&i.isTTY);return $w(o)}MR.exports={supportsColor:rJ,stdout:$w(e3(!0,NR.isatty(1))),stderr:$w(e3(!0,NR.isatty(2)))}});var bR=ce((Lre,FR)=>{"use strict";var iJ=(i,o,f)=>{let p=i.indexOf(o);if(p===-1)return i;let E=o.length,t=0,k="";do k+=i.substr(t,p-t)+o+f,t=p+E,p=i.indexOf(o,t);while(p!==-1);return k+=i.substr(t),k},uJ=(i,o,f,p)=>{let E=0,t="";do{let k=i[p-1]==="\r";t+=i.substr(E,(k?p-1:p)-E)+o+(k?`\r +`:` +`)+f,E=p+1,p=i.indexOf(` +`,E)}while(p!==-1);return t+=i.substr(E),t};FR.exports={stringReplaceAll:iJ,stringEncaseCRLFWithFirstIndex:uJ}});var jR=ce((Fre,PR)=>{"use strict";var oJ=/(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi,IR=/(?:^|\.)(\w+)(?:\(([^)]*)\))?/g,lJ=/^(['"])((?:\\.|(?!\1)[^\\])*)\1$/,sJ=/\\(u(?:[a-f\d]{4}|{[a-f\d]{1,6}})|x[a-f\d]{2}|.)|([^\\])/gi,aJ=new Map([["n",` +`],["r","\r"],["t"," "],["b","\b"],["f","\f"],["v","\v"],["0","\0"],["\\","\\"],["e",""],["a","\x07"]]);function BR(i){let o=i[0]==="u",f=i[1]==="{";return o&&!f&&i.length===5||i[0]==="x"&&i.length===3?String.fromCharCode(parseInt(i.slice(1),16)):o&&f?String.fromCodePoint(parseInt(i.slice(2,-1),16)):aJ.get(i)||i}function fJ(i,o){let f=[],p=o.trim().split(/\s*,\s*/g),E;for(let t of p){let k=Number(t);if(!Number.isNaN(k))f.push(k);else if(E=t.match(lJ))f.push(E[2].replace(sJ,(L,N,C)=>N?BR(N):C));else throw new Error(`Invalid Chalk template style argument: ${t} (in style '${i}')`)}return f}function cJ(i){IR.lastIndex=0;let o=[],f;for(;(f=IR.exec(i))!==null;){let p=f[1];if(f[2]){let E=fJ(p,f[2]);o.push([p].concat(E))}else o.push([p])}return o}function UR(i,o){let f={};for(let E of o)for(let t of E.styles)f[t[0]]=E.inverse?null:t.slice(1);let p=i;for(let[E,t]of Object.entries(f))if(!!Array.isArray(t)){if(!(E in p))throw new Error(`Unknown Chalk style: ${E}`);p=t.length>0?p[E](...t):p[E]}return p}PR.exports=(i,o)=>{let f=[],p=[],E=[];if(o.replace(oJ,(t,k,L,N,C,U)=>{if(k)E.push(BR(k));else if(N){let q=E.join("");E=[],p.push(f.length===0?q:UR(i,f)(q)),f.push({inverse:L,styles:cJ(N)})}else if(C){if(f.length===0)throw new Error("Found extraneous } in Chalk template literal");p.push(UR(i,f)(E.join(""))),E=[],f.pop()}else E.push(U)}),p.push(E.join("")),f.length>0){let t=`Chalk template literal is missing ${f.length} closing bracket${f.length===1?"":"s"} (\`}\`)`;throw new Error(t)}return p.join("")}});var u3=ce((bre,zR)=>{"use strict";var Ng=_4(),{stdout:t3,stderr:n3}=LR(),{stringReplaceAll:dJ,stringEncaseCRLFWithFirstIndex:pJ}=bR(),{isArray:M4}=Array,qR=["ansi","ansi","ansi256","ansi16m"],cm=Object.create(null),hJ=(i,o={})=>{if(o.level&&!(Number.isInteger(o.level)&&o.level>=0&&o.level<=3))throw new Error("The `level` option should be an integer from 0 to 3");let f=t3?t3.level:0;i.level=o.level===void 0?f:o.level},HR=class{constructor(o){return WR(o)}},WR=i=>{let o={};return hJ(o,i),o.template=(...f)=>VR(o.template,...f),Object.setPrototypeOf(o,N4.prototype),Object.setPrototypeOf(o.template,o),o.template.constructor=()=>{throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.")},o.template.Instance=HR,o.template};function N4(i){return WR(i)}for(let[i,o]of Object.entries(Ng))cm[i]={get(){let f=L4(this,r3(o.open,o.close,this._styler),this._isEmpty);return Object.defineProperty(this,i,{value:f}),f}};cm.visible={get(){let i=L4(this,this._styler,!0);return Object.defineProperty(this,"visible",{value:i}),i}};var GR=["rgb","hex","keyword","hsl","hsv","hwb","ansi","ansi256"];for(let i of GR)cm[i]={get(){let{level:o}=this;return function(...f){let p=r3(Ng.color[qR[o]][i](...f),Ng.color.close,this._styler);return L4(this,p,this._isEmpty)}}};for(let i of GR){let o="bg"+i[0].toUpperCase()+i.slice(1);cm[o]={get(){let{level:f}=this;return function(...p){let E=r3(Ng.bgColor[qR[f]][i](...p),Ng.bgColor.close,this._styler);return L4(this,E,this._isEmpty)}}}}var vJ=Object.defineProperties(()=>{},Gf(E0({},cm),{level:{enumerable:!0,get(){return this._generator.level},set(i){this._generator.level=i}}})),r3=(i,o,f)=>{let p,E;return f===void 0?(p=i,E=o):(p=f.openAll+i,E=o+f.closeAll),{open:i,close:o,openAll:p,closeAll:E,parent:f}},L4=(i,o,f)=>{let p=(...E)=>M4(E[0])&&M4(E[0].raw)?YR(p,VR(p,...E)):YR(p,E.length===1?""+E[0]:E.join(" "));return Object.setPrototypeOf(p,vJ),p._generator=i,p._styler=o,p._isEmpty=f,p},YR=(i,o)=>{if(i.level<=0||!o)return i._isEmpty?"":o;let f=i._styler;if(f===void 0)return o;let{openAll:p,closeAll:E}=f;if(o.indexOf("")!==-1)for(;f!==void 0;)o=dJ(o,f.close,f.open),f=f.parent;let t=o.indexOf(` +`);return t!==-1&&(o=pJ(o,E,p,t)),p+o+E},i3,VR=(i,...o)=>{let[f]=o;if(!M4(f)||!M4(f.raw))return o.join(" ");let p=o.slice(1),E=[f.raw[0]];for(let t=1;t{"use strict";var mJ=Lg&&Lg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Lg,"__esModule",{value:!0});var Fg=mJ(u3()),yJ=/^(rgb|hsl|hsv|hwb)\(\s?(\d+),\s?(\d+),\s?(\d+)\s?\)$/,gJ=/^(ansi|ansi256)\(\s?(\d+)\s?\)$/,b4=(i,o)=>o==="foreground"?i:"bg"+i[0].toUpperCase()+i.slice(1);Lg.default=(i,o,f)=>{if(!o)return i;if(o in Fg.default){let E=b4(o,f);return Fg.default[E](i)}if(o.startsWith("#")){let E=b4("hex",f);return Fg.default[E](o)(i)}if(o.startsWith("ansi")){let E=gJ.exec(o);if(!E)return i;let t=b4(E[1],f),k=Number(E[2]);return Fg.default[t](k)(i)}if(o.startsWith("rgb")||o.startsWith("hsl")||o.startsWith("hsv")||o.startsWith("hwb")){let E=yJ.exec(o);if(!E)return i;let t=b4(E[1],f),k=Number(E[2]),L=Number(E[3]),N=Number(E[4]);return Fg.default[t](k,L,N)(i)}return i}});var XR=ce(bg=>{"use strict";var KR=bg&&bg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(bg,"__esModule",{value:!0});var _J=KR(RR()),l3=KR(o3());bg.default=(i,o,f,p)=>{if(typeof f.style.borderStyle=="string"){let E=f.yogaNode.getComputedWidth(),t=f.yogaNode.getComputedHeight(),k=f.style.borderColor,L=_J.default[f.style.borderStyle],N=l3.default(L.topLeft+L.horizontal.repeat(E-2)+L.topRight,k,"foreground"),C=(l3.default(L.vertical,k,"foreground")+` +`).repeat(t-2),U=l3.default(L.bottomLeft+L.horizontal.repeat(E-2)+L.bottomRight,k,"foreground");p.write(i,o,N,{transformers:[]}),p.write(i,o+1,C,{transformers:[]}),p.write(i+E-1,o+1,C,{transformers:[]}),p.write(i,o+t-1,U,{transformers:[]})}}});var JR=ce(Pg=>{"use strict";var ih=Pg&&Pg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Pg,"__esModule",{value:!0});var EJ=ih(eh()),DJ=ih(Dw()),wJ=ih(SR()),SJ=ih(kw()),TJ=ih(TR()),CJ=ih(Nw()),xJ=ih(XR()),AJ=(i,o)=>{var f;let p=(f=i.childNodes[0])===null||f===void 0?void 0:f.yogaNode;if(p){let E=p.getComputedLeft(),t=p.getComputedTop();o=` +`.repeat(t)+wJ.default(o,E)}return o},QR=(i,o,f)=>{var p;let{offsetX:E=0,offsetY:t=0,transformers:k=[],skipStaticElements:L}=f;if(L&&i.internal_static)return;let{yogaNode:N}=i;if(N){if(N.getDisplay()===EJ.default.DISPLAY_NONE)return;let C=E+N.getComputedLeft(),U=t+N.getComputedTop(),q=k;if(typeof i.internal_transform=="function"&&(q=[i.internal_transform,...k]),i.nodeName==="ink-text"){let W=CJ.default(i);if(W.length>0){let ne=DJ.default(W),m=TJ.default(N);if(ne>m){let we=(p=i.style.textWrap)!==null&&p!==void 0?p:"wrap";W=SJ.default(W,m,we)}W=AJ(i,W),o.write(C,U,W,{transformers:q})}return}if(i.nodeName==="ink-box"&&xJ.default(C,U,i,o),i.nodeName==="ink-root"||i.nodeName==="ink-box")for(let W of i.childNodes)QR(W,o,{offsetX:C,offsetY:U,transformers:q,skipStaticElements:L})}};Pg.default=QR});var $R=ce((Ure,ZR)=>{"use strict";ZR.exports=i=>{i=Object.assign({onlyFirst:!1},i);let o=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(o,i.onlyFirst?void 0:"g")}});var t7=ce((jre,s3)=>{"use strict";var RJ=$R(),e7=i=>typeof i=="string"?i.replace(RJ(),""):i;s3.exports=e7;s3.exports.default=e7});var i7=ce((zre,n7)=>{"use strict";var r7="[\uD800-\uDBFF][\uDC00-\uDFFF]";n7.exports=i=>i&&i.exact?new RegExp(`^${r7}$`):new RegExp(r7,"g")});var o7=ce((qre,a3)=>{"use strict";var OJ=t7(),kJ=i7(),u7=i=>OJ(i).replace(kJ()," ").length;a3.exports=u7;a3.exports.default=u7});var f7=ce(Ig=>{"use strict";var l7=Ig&&Ig.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Ig,"__esModule",{value:!0});var s7=l7(Rw()),MJ=l7(o7()),a7=class{constructor(o){this.writes=[];let{width:f,height:p}=o;this.width=f,this.height=p}write(o,f,p,E){let{transformers:t}=E;!p||this.writes.push({x:o,y:f,text:p,transformers:t})}get(){let o=[];for(let p=0;pp.trimRight()).join(` +`),height:o.length}}};Ig.default=a7});var p7=ce(Bg=>{"use strict";var f3=Bg&&Bg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Bg,"__esModule",{value:!0});var NJ=f3(eh()),c7=f3(JR()),d7=f3(f7());Bg.default=(i,o)=>{var f;if(i.yogaNode.setWidth(o),i.yogaNode){i.yogaNode.calculateLayout(void 0,void 0,NJ.default.DIRECTION_LTR);let p=new d7.default({width:i.yogaNode.getComputedWidth(),height:i.yogaNode.getComputedHeight()});c7.default(i,p,{skipStaticElements:!0});let E;((f=i.staticNode)===null||f===void 0?void 0:f.yogaNode)&&(E=new d7.default({width:i.staticNode.yogaNode.getComputedWidth(),height:i.staticNode.yogaNode.getComputedHeight()}),c7.default(i.staticNode,E,{skipStaticElements:!1}));let{output:t,height:k}=p.get();return{output:t,outputHeight:k,staticOutput:E?`${E.get().output} +`:""}}return{output:"",outputHeight:0,staticOutput:""}}});var y7=ce((Vre,h7)=>{"use strict";var v7=require("stream"),m7=["assert","count","countReset","debug","dir","dirxml","error","group","groupCollapsed","groupEnd","info","log","table","time","timeEnd","timeLog","trace","warn"],c3={},LJ=i=>{let o=new v7.PassThrough,f=new v7.PassThrough;o.write=E=>i("stdout",E),f.write=E=>i("stderr",E);let p=new console.Console(o,f);for(let E of m7)c3[E]=console[E],console[E]=p[E];return()=>{for(let E of m7)console[E]=c3[E];c3={}}};h7.exports=LJ});var p3=ce(d3=>{"use strict";Object.defineProperty(d3,"__esModule",{value:!0});d3.default=new WeakMap});var v3=ce(h3=>{"use strict";Object.defineProperty(h3,"__esModule",{value:!0});var FJ=su(),g7=FJ.createContext({exit:()=>{}});g7.displayName="InternalAppContext";h3.default=g7});var y3=ce(m3=>{"use strict";Object.defineProperty(m3,"__esModule",{value:!0});var bJ=su(),_7=bJ.createContext({stdin:void 0,setRawMode:()=>{},isRawModeSupported:!1,internal_exitOnCtrlC:!0});_7.displayName="InternalStdinContext";m3.default=_7});var _3=ce(g3=>{"use strict";Object.defineProperty(g3,"__esModule",{value:!0});var PJ=su(),E7=PJ.createContext({stdout:void 0,write:()=>{}});E7.displayName="InternalStdoutContext";g3.default=E7});var D3=ce(E3=>{"use strict";Object.defineProperty(E3,"__esModule",{value:!0});var IJ=su(),D7=IJ.createContext({stderr:void 0,write:()=>{}});D7.displayName="InternalStderrContext";E3.default=D7});var P4=ce(w3=>{"use strict";Object.defineProperty(w3,"__esModule",{value:!0});var BJ=su(),w7=BJ.createContext({activeId:void 0,add:()=>{},remove:()=>{},activate:()=>{},deactivate:()=>{},enableFocus:()=>{},disableFocus:()=>{},focusNext:()=>{},focusPrevious:()=>{}});w7.displayName="InternalFocusContext";w3.default=w7});var T7=ce((Zre,S7)=>{"use strict";var UJ=/[|\\{}()[\]^$+*?.-]/g;S7.exports=i=>{if(typeof i!="string")throw new TypeError("Expected a string");return i.replace(UJ,"\\$&")}});var R7=ce(($re,C7)=>{"use strict";var jJ=T7(),x7=[].concat(require("module").builtinModules,"bootstrap_node","node").map(i=>new RegExp(`(?:\\(${i}\\.js:\\d+:\\d+\\)$|^\\s*at ${i}\\.js:\\d+:\\d+$)`));x7.push(/\(internal\/[^:]+:\d+:\d+\)$/,/\s*at internal\/[^:]+:\d+:\d+$/,/\/\.node-spawn-wrap-\w+-\w+\/node:\d+:\d+\)?$/);var I4=class{constructor(o){o=E0({ignoredPackages:[]},o),"internals"in o||(o.internals=I4.nodeInternals()),"cwd"in o||(o.cwd=process.cwd()),this._cwd=o.cwd.replace(/\\/g,"/"),this._internals=[].concat(o.internals,zJ(o.ignoredPackages)),this._wrapCallSite=o.wrapCallSite||!1}static nodeInternals(){return[...x7]}clean(o,f=0){f=" ".repeat(f),Array.isArray(o)||(o=o.split(` +`)),!/^\s*at /.test(o[0])&&/^\s*at /.test(o[1])&&(o=o.slice(1));let p=!1,E=null,t=[];return o.forEach(k=>{if(k=k.replace(/\\/g,"/"),this._internals.some(N=>N.test(k)))return;let L=/^\s*at /.test(k);p?k=k.trimEnd().replace(/^(\s+)at /,"$1"):(k=k.trim(),L&&(k=k.slice(3))),k=k.replace(`${this._cwd}/`,""),k&&(L?(E&&(t.push(E),E=null),t.push(k)):(p=!0,E=k))}),t.map(k=>`${f}${k} +`).join("")}captureString(o,f=this.captureString){typeof o=="function"&&(f=o,o=Infinity);let{stackTraceLimit:p}=Error;o&&(Error.stackTraceLimit=o);let E={};Error.captureStackTrace(E,f);let{stack:t}=E;return Error.stackTraceLimit=p,this.clean(t)}capture(o,f=this.capture){typeof o=="function"&&(f=o,o=Infinity);let{prepareStackTrace:p,stackTraceLimit:E}=Error;Error.prepareStackTrace=(L,N)=>this._wrapCallSite?N.map(this._wrapCallSite):N,o&&(Error.stackTraceLimit=o);let t={};Error.captureStackTrace(t,f);let{stack:k}=t;return Object.assign(Error,{prepareStackTrace:p,stackTraceLimit:E}),k}at(o=this.at){let[f]=this.capture(1,o);if(!f)return{};let p={line:f.getLineNumber(),column:f.getColumnNumber()};A7(p,f.getFileName(),this._cwd),f.isConstructor()&&(p.constructor=!0),f.isEval()&&(p.evalOrigin=f.getEvalOrigin()),f.isNative()&&(p.native=!0);let E;try{E=f.getTypeName()}catch(L){}E&&E!=="Object"&&E!=="[object Object]"&&(p.type=E);let t=f.getFunctionName();t&&(p.function=t);let k=f.getMethodName();return k&&t!==k&&(p.method=k),p}parseLine(o){let f=o&&o.match(qJ);if(!f)return null;let p=f[1]==="new",E=f[2],t=f[3],k=f[4],L=Number(f[5]),N=Number(f[6]),C=f[7],U=f[8],q=f[9],W=f[10]==="native",ne=f[11]===")",m,we={};if(U&&(we.line=Number(U)),q&&(we.column=Number(q)),ne&&C){let Se=0;for(let he=C.length-1;he>0;he--)if(C.charAt(he)===")")Se++;else if(C.charAt(he)==="("&&C.charAt(he-1)===" "&&(Se--,Se===-1&&C.charAt(he-1)===" ")){let ge=C.slice(0,he-1);C=C.slice(he+1),E+=` (${ge}`;break}}if(E){let Se=E.match(HJ);Se&&(E=Se[1],m=Se[2])}return A7(we,C,this._cwd),p&&(we.constructor=!0),t&&(we.evalOrigin=t,we.evalLine=L,we.evalColumn=N,we.evalFile=k&&k.replace(/\\/g,"/")),W&&(we.native=!0),E&&(we.function=E),m&&E!==m&&(we.method=m),we}};function A7(i,o,f){o&&(o=o.replace(/\\/g,"/"),o.startsWith(`${f}/`)&&(o=o.slice(f.length+1)),i.file=o)}function zJ(i){if(i.length===0)return[];let o=i.map(f=>jJ(f));return new RegExp(`[/\\\\]node_modules[/\\\\](?:${o.join("|")})[/\\\\][^:]+:\\d+:\\d+`)}var qJ=new RegExp("^(?:\\s*at )?(?:(new) )?(?:(.*?) \\()?(?:eval at ([^ ]+) \\((.+?):(\\d+):(\\d+)\\), )?(?:(.+?):(\\d+):(\\d+)|(native))(\\)?)$"),HJ=/^(.*?) \[as (.*?)\]$/;C7.exports=I4});var k7=ce((eie,O7)=>{"use strict";O7.exports=(i,o)=>i.replace(/^\t+/gm,f=>" ".repeat(f.length*(o||2)))});var N7=ce((tie,M7)=>{"use strict";var WJ=k7(),VJ=(i,o)=>{let f=[],p=i-o,E=i+o;for(let t=p;t<=E;t++)f.push(t);return f};M7.exports=(i,o,f)=>{if(typeof i!="string")throw new TypeError("Source code is missing.");if(!o||o<1)throw new TypeError("Line number must start from `1`.");if(i=WJ(i).split(/\r?\n/),!(o>i.length))return f=E0({around:3},f),VJ(o,f.around).filter(p=>i[p-1]!==void 0).map(p=>({line:p,value:i[p-1]}))}});var B4=ce(Zf=>{"use strict";var GJ=Zf&&Zf.__createBinding||(Object.create?function(i,o,f,p){p===void 0&&(p=f),Object.defineProperty(i,p,{enumerable:!0,get:function(){return o[f]}})}:function(i,o,f,p){p===void 0&&(p=f),i[p]=o[f]}),YJ=Zf&&Zf.__setModuleDefault||(Object.create?function(i,o){Object.defineProperty(i,"default",{enumerable:!0,value:o})}:function(i,o){i.default=o}),KJ=Zf&&Zf.__importStar||function(i){if(i&&i.__esModule)return i;var o={};if(i!=null)for(var f in i)f!=="default"&&Object.hasOwnProperty.call(i,f)&&GJ(o,i,f);return YJ(o,i),o},XJ=Zf&&Zf.__rest||function(i,o){var f={};for(var p in i)Object.prototype.hasOwnProperty.call(i,p)&&o.indexOf(p)<0&&(f[p]=i[p]);if(i!=null&&typeof Object.getOwnPropertySymbols=="function")for(var E=0,p=Object.getOwnPropertySymbols(i);E{var{children:f}=i,p=XJ(i,["children"]);let E=Object.assign(Object.assign({},p),{marginLeft:p.marginLeft||p.marginX||p.margin||0,marginRight:p.marginRight||p.marginX||p.margin||0,marginTop:p.marginTop||p.marginY||p.margin||0,marginBottom:p.marginBottom||p.marginY||p.margin||0,paddingLeft:p.paddingLeft||p.paddingX||p.padding||0,paddingRight:p.paddingRight||p.paddingX||p.padding||0,paddingTop:p.paddingTop||p.paddingY||p.padding||0,paddingBottom:p.paddingBottom||p.paddingY||p.padding||0});return L7.default.createElement("ink-box",{ref:o,style:E},f)});S3.displayName="Box";S3.defaultProps={flexDirection:"row",flexGrow:0,flexShrink:1};Zf.default=S3});var x3=ce(Ug=>{"use strict";var T3=Ug&&Ug.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Ug,"__esModule",{value:!0});var QJ=T3(su()),dm=T3(u3()),F7=T3(o3()),C3=({color:i,backgroundColor:o,dimColor:f,bold:p,italic:E,underline:t,strikethrough:k,inverse:L,wrap:N,children:C})=>{if(C==null)return null;let U=q=>(f&&(q=dm.default.dim(q)),i&&(q=F7.default(q,i,"foreground")),o&&(q=F7.default(q,o,"background")),p&&(q=dm.default.bold(q)),E&&(q=dm.default.italic(q)),t&&(q=dm.default.underline(q)),k&&(q=dm.default.strikethrough(q)),L&&(q=dm.default.inverse(q)),q);return QJ.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row",textWrap:N},internal_transform:U},C)};C3.displayName="Text";C3.defaultProps={dimColor:!1,bold:!1,italic:!1,underline:!1,strikethrough:!1,wrap:"wrap"};Ug.default=C3});var B7=ce($f=>{"use strict";var JJ=$f&&$f.__createBinding||(Object.create?function(i,o,f,p){p===void 0&&(p=f),Object.defineProperty(i,p,{enumerable:!0,get:function(){return o[f]}})}:function(i,o,f,p){p===void 0&&(p=f),i[p]=o[f]}),ZJ=$f&&$f.__setModuleDefault||(Object.create?function(i,o){Object.defineProperty(i,"default",{enumerable:!0,value:o})}:function(i,o){i.default=o}),$J=$f&&$f.__importStar||function(i){if(i&&i.__esModule)return i;var o={};if(i!=null)for(var f in i)f!=="default"&&Object.hasOwnProperty.call(i,f)&&JJ(o,i,f);return ZJ(o,i),o},jg=$f&&$f.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty($f,"__esModule",{value:!0});var b7=$J(require("fs")),Vo=jg(su()),P7=jg(R7()),eZ=jg(N7()),Q1=jg(B4()),Ic=jg(x3()),I7=new P7.default({cwd:process.cwd(),internals:P7.default.nodeInternals()}),tZ=({error:i})=>{let o=i.stack?i.stack.split(` +`).slice(1):void 0,f=o?I7.parseLine(o[0]):void 0,p,E=0;if((f==null?void 0:f.file)&&(f==null?void 0:f.line)&&b7.existsSync(f.file)){let t=b7.readFileSync(f.file,"utf8");if(p=eZ.default(t,f.line),p)for(let{line:k}of p)E=Math.max(E,String(k).length)}return Vo.default.createElement(Q1.default,{flexDirection:"column",padding:1},Vo.default.createElement(Q1.default,null,Vo.default.createElement(Ic.default,{backgroundColor:"red",color:"white"}," ","ERROR"," "),Vo.default.createElement(Ic.default,null," ",i.message)),f&&Vo.default.createElement(Q1.default,{marginTop:1},Vo.default.createElement(Ic.default,{dimColor:!0},f.file,":",f.line,":",f.column)),f&&p&&Vo.default.createElement(Q1.default,{marginTop:1,flexDirection:"column"},p.map(({line:t,value:k})=>Vo.default.createElement(Q1.default,{key:t},Vo.default.createElement(Q1.default,{width:E+1},Vo.default.createElement(Ic.default,{dimColor:t!==f.line,backgroundColor:t===f.line?"red":void 0,color:t===f.line?"white":void 0},String(t).padStart(E," "),":")),Vo.default.createElement(Ic.default,{key:t,backgroundColor:t===f.line?"red":void 0,color:t===f.line?"white":void 0}," "+k)))),i.stack&&Vo.default.createElement(Q1.default,{marginTop:1,flexDirection:"column"},i.stack.split(` +`).slice(1).map(t=>{let k=I7.parseLine(t);return k?Vo.default.createElement(Q1.default,{key:t},Vo.default.createElement(Ic.default,{dimColor:!0},"- "),Vo.default.createElement(Ic.default,{dimColor:!0,bold:!0},k.function),Vo.default.createElement(Ic.default,{dimColor:!0,color:"gray"}," ","(",k.file,":",k.line,":",k.column,")")):Vo.default.createElement(Q1.default,{key:t},Vo.default.createElement(Ic.default,{dimColor:!0},"- "),Vo.default.createElement(Ic.default,{dimColor:!0,bold:!0},t))})))};$f.default=tZ});var j7=ce(ec=>{"use strict";var nZ=ec&&ec.__createBinding||(Object.create?function(i,o,f,p){p===void 0&&(p=f),Object.defineProperty(i,p,{enumerable:!0,get:function(){return o[f]}})}:function(i,o,f,p){p===void 0&&(p=f),i[p]=o[f]}),rZ=ec&&ec.__setModuleDefault||(Object.create?function(i,o){Object.defineProperty(i,"default",{enumerable:!0,value:o})}:function(i,o){i.default=o}),iZ=ec&&ec.__importStar||function(i){if(i&&i.__esModule)return i;var o={};if(i!=null)for(var f in i)f!=="default"&&Object.hasOwnProperty.call(i,f)&&nZ(o,i,f);return rZ(o,i),o},uh=ec&&ec.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(ec,"__esModule",{value:!0});var oh=iZ(su()),U7=uh(ZD()),uZ=uh(v3()),oZ=uh(y3()),lZ=uh(_3()),sZ=uh(D3()),aZ=uh(P4()),fZ=uh(B7()),cZ=" ",dZ="",pZ="",A3=class extends oh.PureComponent{constructor(){super(...arguments);this.state={isFocusEnabled:!0,activeFocusId:void 0,focusables:[],error:void 0},this.rawModeEnabledCount=0,this.handleSetRawMode=o=>{let{stdin:f}=this.props;if(!this.isRawModeSupported())throw f===process.stdin?new Error(`Raw mode is not supported on the current process.stdin, which Ink uses as input stream by default. +Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`):new Error(`Raw mode is not supported on the stdin provided to Ink. +Read about how to prevent this error on https://github.com/vadimdemedes/ink/#israwmodesupported`);if(f.setEncoding("utf8"),o){this.rawModeEnabledCount===0&&(f.addListener("data",this.handleInput),f.resume(),f.setRawMode(!0)),this.rawModeEnabledCount++;return}--this.rawModeEnabledCount==0&&(f.setRawMode(!1),f.removeListener("data",this.handleInput),f.pause())},this.handleInput=o=>{o===""&&this.props.exitOnCtrlC&&this.handleExit(),o===pZ&&this.state.activeFocusId&&this.setState({activeFocusId:void 0}),this.state.isFocusEnabled&&this.state.focusables.length>0&&(o===cZ&&this.focusNext(),o===dZ&&this.focusPrevious())},this.handleExit=o=>{this.isRawModeSupported()&&this.handleSetRawMode(!1),this.props.onExit(o)},this.enableFocus=()=>{this.setState({isFocusEnabled:!0})},this.disableFocus=()=>{this.setState({isFocusEnabled:!1})},this.focusNext=()=>{this.setState(o=>{let f=o.focusables[0].id;return{activeFocusId:this.findNextFocusable(o)||f}})},this.focusPrevious=()=>{this.setState(o=>{let f=o.focusables[o.focusables.length-1].id;return{activeFocusId:this.findPreviousFocusable(o)||f}})},this.addFocusable=(o,{autoFocus:f})=>{this.setState(p=>{let E=p.activeFocusId;return!E&&f&&(E=o),{activeFocusId:E,focusables:[...p.focusables,{id:o,isActive:!0}]}})},this.removeFocusable=o=>{this.setState(f=>({activeFocusId:f.activeFocusId===o?void 0:f.activeFocusId,focusables:f.focusables.filter(p=>p.id!==o)}))},this.activateFocusable=o=>{this.setState(f=>({focusables:f.focusables.map(p=>p.id!==o?p:{id:o,isActive:!0})}))},this.deactivateFocusable=o=>{this.setState(f=>({activeFocusId:f.activeFocusId===o?void 0:f.activeFocusId,focusables:f.focusables.map(p=>p.id!==o?p:{id:o,isActive:!1})}))},this.findNextFocusable=o=>{let f=o.focusables.findIndex(p=>p.id===o.activeFocusId);for(let p=f+1;p{let f=o.focusables.findIndex(p=>p.id===o.activeFocusId);for(let p=f-1;p>=0;p--)if(o.focusables[p].isActive)return o.focusables[p].id}}static getDerivedStateFromError(o){return{error:o}}isRawModeSupported(){return this.props.stdin.isTTY}render(){return oh.default.createElement(uZ.default.Provider,{value:{exit:this.handleExit}},oh.default.createElement(oZ.default.Provider,{value:{stdin:this.props.stdin,setRawMode:this.handleSetRawMode,isRawModeSupported:this.isRawModeSupported(),internal_exitOnCtrlC:this.props.exitOnCtrlC}},oh.default.createElement(lZ.default.Provider,{value:{stdout:this.props.stdout,write:this.props.writeToStdout}},oh.default.createElement(sZ.default.Provider,{value:{stderr:this.props.stderr,write:this.props.writeToStderr}},oh.default.createElement(aZ.default.Provider,{value:{activeId:this.state.activeFocusId,add:this.addFocusable,remove:this.removeFocusable,activate:this.activateFocusable,deactivate:this.deactivateFocusable,enableFocus:this.enableFocus,disableFocus:this.disableFocus,focusNext:this.focusNext,focusPrevious:this.focusPrevious}},this.state.error?oh.default.createElement(fZ.default,{error:this.state.error}):this.props.children)))))}componentDidMount(){U7.default.hide(this.props.stdout)}componentWillUnmount(){U7.default.show(this.props.stdout),this.isRawModeSupported()&&this.handleSetRawMode(!1)}componentDidCatch(o){this.handleExit(o)}};ec.default=A3;A3.displayName="InternalApp"});var W7=ce(tc=>{"use strict";var hZ=tc&&tc.__createBinding||(Object.create?function(i,o,f,p){p===void 0&&(p=f),Object.defineProperty(i,p,{enumerable:!0,get:function(){return o[f]}})}:function(i,o,f,p){p===void 0&&(p=f),i[p]=o[f]}),vZ=tc&&tc.__setModuleDefault||(Object.create?function(i,o){Object.defineProperty(i,"default",{enumerable:!0,value:o})}:function(i,o){i.default=o}),mZ=tc&&tc.__importStar||function(i){if(i&&i.__esModule)return i;var o={};if(i!=null)for(var f in i)f!=="default"&&Object.hasOwnProperty.call(i,f)&&hZ(o,i,f);return vZ(o,i),o},nc=tc&&tc.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(tc,"__esModule",{value:!0});var yZ=nc(su()),z7=B5(),gZ=nc(Z5()),_Z=nc(GD()),EZ=nc(u9()),DZ=nc(l9()),U4=nc(DR()),wZ=nc(p7()),SZ=nc(JD()),TZ=nc(y7()),CZ=mZ(Lw()),xZ=nc(p3()),AZ=nc(j7()),pm=process.env.CI==="false"?!1:EZ.default,q7=()=>{},H7=class{constructor(o){this.resolveExitPromise=()=>{},this.rejectExitPromise=()=>{},this.unsubscribeExit=()=>{},this.onRender=()=>{if(this.isUnmounted)return;let{output:f,outputHeight:p,staticOutput:E}=wZ.default(this.rootNode,this.options.stdout.columns||80),t=E&&E!==` +`;if(this.options.debug){t&&(this.fullStaticOutput+=E),this.options.stdout.write(this.fullStaticOutput+f);return}if(pm){t&&this.options.stdout.write(E),this.lastOutput=f;return}if(t&&(this.fullStaticOutput+=E),p>=this.options.stdout.rows){this.options.stdout.write(_Z.default.clearTerminal+this.fullStaticOutput+f),this.lastOutput=f;return}t&&(this.log.clear(),this.options.stdout.write(E),this.log(f)),!t&&f!==this.lastOutput&&this.throttledLog(f),this.lastOutput=f},DZ.default(this),this.options=o,this.rootNode=CZ.createNode("ink-root"),this.rootNode.onRender=o.debug?this.onRender:z7.throttle(this.onRender,32,{leading:!0,trailing:!0}),this.rootNode.onImmediateRender=this.onRender,this.log=gZ.default.create(o.stdout),this.throttledLog=o.debug?this.log:z7.throttle(this.log,void 0,{leading:!0,trailing:!0}),this.isUnmounted=!1,this.lastOutput="",this.fullStaticOutput="",this.container=U4.default.createContainer(this.rootNode,!1,!1),this.unsubscribeExit=SZ.default(this.unmount,{alwaysLast:!1}),process.env.DEV==="true"&&U4.default.injectIntoDevTools({bundleType:0,version:"16.13.1",rendererPackageName:"ink"}),o.patchConsole&&this.patchConsole(),pm||(o.stdout.on("resize",this.onRender),this.unsubscribeResize=()=>{o.stdout.off("resize",this.onRender)})}render(o){let f=yZ.default.createElement(AZ.default,{stdin:this.options.stdin,stdout:this.options.stdout,stderr:this.options.stderr,writeToStdout:this.writeToStdout,writeToStderr:this.writeToStderr,exitOnCtrlC:this.options.exitOnCtrlC,onExit:this.unmount},o);U4.default.updateContainer(f,this.container,null,q7)}writeToStdout(o){if(!this.isUnmounted){if(this.options.debug){this.options.stdout.write(o+this.fullStaticOutput+this.lastOutput);return}if(pm){this.options.stdout.write(o);return}this.log.clear(),this.options.stdout.write(o),this.log(this.lastOutput)}}writeToStderr(o){if(!this.isUnmounted){if(this.options.debug){this.options.stderr.write(o),this.options.stdout.write(this.fullStaticOutput+this.lastOutput);return}if(pm){this.options.stderr.write(o);return}this.log.clear(),this.options.stderr.write(o),this.log(this.lastOutput)}}unmount(o){this.isUnmounted||(this.onRender(),this.unsubscribeExit(),typeof this.restoreConsole=="function"&&this.restoreConsole(),typeof this.unsubscribeResize=="function"&&this.unsubscribeResize(),pm?this.options.stdout.write(this.lastOutput+` +`):this.options.debug||this.log.done(),this.isUnmounted=!0,U4.default.updateContainer(null,this.container,null,q7),xZ.default.delete(this.options.stdout),o instanceof Error?this.rejectExitPromise(o):this.resolveExitPromise())}waitUntilExit(){return this.exitPromise||(this.exitPromise=new Promise((o,f)=>{this.resolveExitPromise=o,this.rejectExitPromise=f})),this.exitPromise}clear(){!pm&&!this.options.debug&&this.log.clear()}patchConsole(){this.options.debug||(this.restoreConsole=TZ.default((o,f)=>{o==="stdout"&&this.writeToStdout(f),o==="stderr"&&(f.startsWith("The above error occurred")||this.writeToStderr(f))}))}};tc.default=H7});var G7=ce(zg=>{"use strict";var V7=zg&&zg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(zg,"__esModule",{value:!0});var RZ=V7(W7()),j4=V7(p3()),OZ=require("stream"),NZ=(i,o)=>{let f=Object.assign({stdout:process.stdout,stdin:process.stdin,stderr:process.stderr,debug:!1,exitOnCtrlC:!0,patchConsole:!0},kZ(o)),p=MZ(f.stdout,()=>new RZ.default(f));return p.render(i),{rerender:p.render,unmount:()=>p.unmount(),waitUntilExit:p.waitUntilExit,cleanup:()=>j4.default.delete(f.stdout),clear:p.clear}};zg.default=NZ;var kZ=(i={})=>i instanceof OZ.Stream?{stdout:i,stdin:process.stdin}:i,MZ=(i,o)=>{let f;return j4.default.has(i)?f=j4.default.get(i):(f=o(),j4.default.set(i,f)),f}});var K7=ce(J1=>{"use strict";var LZ=J1&&J1.__createBinding||(Object.create?function(i,o,f,p){p===void 0&&(p=f),Object.defineProperty(i,p,{enumerable:!0,get:function(){return o[f]}})}:function(i,o,f,p){p===void 0&&(p=f),i[p]=o[f]}),FZ=J1&&J1.__setModuleDefault||(Object.create?function(i,o){Object.defineProperty(i,"default",{enumerable:!0,value:o})}:function(i,o){i.default=o}),bZ=J1&&J1.__importStar||function(i){if(i&&i.__esModule)return i;var o={};if(i!=null)for(var f in i)f!=="default"&&Object.hasOwnProperty.call(i,f)&&LZ(o,i,f);return FZ(o,i),o};Object.defineProperty(J1,"__esModule",{value:!0});var qg=bZ(su()),Y7=i=>{let{items:o,children:f,style:p}=i,[E,t]=qg.useState(0),k=qg.useMemo(()=>o.slice(E),[o,E]);qg.useLayoutEffect(()=>{t(o.length)},[o.length]);let L=k.map((C,U)=>f(C,E+U)),N=qg.useMemo(()=>Object.assign({position:"absolute",flexDirection:"column"},p),[p]);return qg.default.createElement("ink-box",{internal_static:!0,style:N},L)};Y7.displayName="Static";J1.default=Y7});var Q7=ce(Hg=>{"use strict";var PZ=Hg&&Hg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Hg,"__esModule",{value:!0});var IZ=PZ(su()),X7=({children:i,transform:o})=>i==null?null:IZ.default.createElement("ink-text",{style:{flexGrow:0,flexShrink:1,flexDirection:"row"},internal_transform:o},i);X7.displayName="Transform";Hg.default=X7});var Z7=ce(Wg=>{"use strict";var BZ=Wg&&Wg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Wg,"__esModule",{value:!0});var UZ=BZ(su()),J7=({count:i=1})=>UZ.default.createElement("ink-text",null,` +`.repeat(i));J7.displayName="Newline";Wg.default=J7});var tO=ce(Vg=>{"use strict";var $7=Vg&&Vg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Vg,"__esModule",{value:!0});var jZ=$7(su()),zZ=$7(B4()),eO=()=>jZ.default.createElement(zZ.default,{flexGrow:1});eO.displayName="Spacer";Vg.default=eO});var z4=ce(Gg=>{"use strict";var qZ=Gg&&Gg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Gg,"__esModule",{value:!0});var HZ=su(),WZ=qZ(y3()),VZ=()=>HZ.useContext(WZ.default);Gg.default=VZ});var rO=ce(Yg=>{"use strict";var GZ=Yg&&Yg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Yg,"__esModule",{value:!0});var nO=su(),YZ=GZ(z4()),KZ=(i,o={})=>{let{stdin:f,setRawMode:p,internal_exitOnCtrlC:E}=YZ.default();nO.useEffect(()=>{if(o.isActive!==!1)return p(!0),()=>{p(!1)}},[o.isActive,p]),nO.useEffect(()=>{if(o.isActive===!1)return;let t=k=>{let L=String(k),N={upArrow:L==="",downArrow:L==="",leftArrow:L==="",rightArrow:L==="",pageDown:L==="[6~",pageUp:L==="[5~",return:L==="\r",escape:L==="",ctrl:!1,shift:!1,tab:L===" "||L==="",backspace:L==="\b",delete:L==="\x7F"||L==="[3~",meta:!1};L<=""&&!N.return&&(L=String.fromCharCode(L.charCodeAt(0)+"a".charCodeAt(0)-1),N.ctrl=!0),L.startsWith("")&&(L=L.slice(1),N.meta=!0);let C=L>="A"&&L<="Z",U=L>="\u0410"&&L<="\u042F";L.length===1&&(C||U)&&(N.shift=!0),N.tab&&L==="[Z"&&(N.shift=!0),(N.tab||N.backspace||N.delete)&&(L=""),(!(L==="c"&&N.ctrl)||!E)&&i(L,N)};return f==null||f.on("data",t),()=>{f==null||f.off("data",t)}},[o.isActive,f,E,i])};Yg.default=KZ});var iO=ce(Kg=>{"use strict";var XZ=Kg&&Kg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Kg,"__esModule",{value:!0});var QZ=su(),JZ=XZ(v3()),ZZ=()=>QZ.useContext(JZ.default);Kg.default=ZZ});var uO=ce(Xg=>{"use strict";var $Z=Xg&&Xg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Xg,"__esModule",{value:!0});var e$=su(),t$=$Z(_3()),n$=()=>e$.useContext(t$.default);Xg.default=n$});var oO=ce(Qg=>{"use strict";var r$=Qg&&Qg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Qg,"__esModule",{value:!0});var i$=su(),u$=r$(D3()),o$=()=>i$.useContext(u$.default);Qg.default=o$});var sO=ce(Jg=>{"use strict";var lO=Jg&&Jg.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty(Jg,"__esModule",{value:!0});var Zg=su(),l$=lO(P4()),s$=lO(z4()),a$=({isActive:i=!0,autoFocus:o=!1}={})=>{let{isRawModeSupported:f,setRawMode:p}=s$.default(),{activeId:E,add:t,remove:k,activate:L,deactivate:N}=Zg.useContext(l$.default),C=Zg.useMemo(()=>Math.random().toString().slice(2,7),[]);return Zg.useEffect(()=>(t(C,{autoFocus:o}),()=>{k(C)}),[C,o]),Zg.useEffect(()=>{i?L(C):N(C)},[i,C]),Zg.useEffect(()=>{if(!(!f||!i))return p(!0),()=>{p(!1)}},[i]),{isFocused:Boolean(C)&&E===C}};Jg.default=a$});var aO=ce($g=>{"use strict";var f$=$g&&$g.__importDefault||function(i){return i&&i.__esModule?i:{default:i}};Object.defineProperty($g,"__esModule",{value:!0});var c$=su(),d$=f$(P4()),p$=()=>{let i=c$.useContext(d$.default);return{enableFocus:i.enableFocus,disableFocus:i.disableFocus,focusNext:i.focusNext,focusPrevious:i.focusPrevious}};$g.default=p$});var fO=ce(R3=>{"use strict";Object.defineProperty(R3,"__esModule",{value:!0});R3.default=i=>{var o,f,p,E;return{width:(f=(o=i.yogaNode)===null||o===void 0?void 0:o.getComputedWidth())!==null&&f!==void 0?f:0,height:(E=(p=i.yogaNode)===null||p===void 0?void 0:p.getComputedHeight())!==null&&E!==void 0?E:0}}});var lh=ce(ql=>{"use strict";Object.defineProperty(ql,"__esModule",{value:!0});var h$=G7();Object.defineProperty(ql,"render",{enumerable:!0,get:function(){return h$.default}});var v$=B4();Object.defineProperty(ql,"Box",{enumerable:!0,get:function(){return v$.default}});var m$=x3();Object.defineProperty(ql,"Text",{enumerable:!0,get:function(){return m$.default}});var y$=K7();Object.defineProperty(ql,"Static",{enumerable:!0,get:function(){return y$.default}});var g$=Q7();Object.defineProperty(ql,"Transform",{enumerable:!0,get:function(){return g$.default}});var _$=Z7();Object.defineProperty(ql,"Newline",{enumerable:!0,get:function(){return _$.default}});var E$=tO();Object.defineProperty(ql,"Spacer",{enumerable:!0,get:function(){return E$.default}});var D$=rO();Object.defineProperty(ql,"useInput",{enumerable:!0,get:function(){return D$.default}});var w$=iO();Object.defineProperty(ql,"useApp",{enumerable:!0,get:function(){return w$.default}});var S$=z4();Object.defineProperty(ql,"useStdin",{enumerable:!0,get:function(){return S$.default}});var T$=uO();Object.defineProperty(ql,"useStdout",{enumerable:!0,get:function(){return T$.default}});var C$=oO();Object.defineProperty(ql,"useStderr",{enumerable:!0,get:function(){return C$.default}});var x$=sO();Object.defineProperty(ql,"useFocus",{enumerable:!0,get:function(){return x$.default}});var A$=aO();Object.defineProperty(ql,"useFocusManager",{enumerable:!0,get:function(){return A$.default}});var R$=fO();Object.defineProperty(ql,"measureElement",{enumerable:!0,get:function(){return R$.default}})});var k$={};sS(k$,{default:()=>N$,versionUtils:()=>RD});var M3=Mi(require("@yarnpkg/core"));var X_=Mi(require("@yarnpkg/cli")),em=Mi(require("@yarnpkg/core")),Q_=Mi(require("@yarnpkg/core")),cd=Mi(require("clipanion"));var RD={};sS(RD,{Decision:()=>Nu,applyPrerelease:()=>v5,applyReleases:()=>ND,applyStrategy:()=>Y_,clearVersionFiles:()=>OD,fetchBase:()=>pK,fetchChangedFiles:()=>vK,fetchRoot:()=>hK,getUndecidedDependentWorkspaces:()=>Zy,getUndecidedWorkspaces:()=>K_,openVersionFile:()=>$v,requireMoreDecisions:()=>yK,resolveVersionFiles:()=>Jy,suggestStrategy:()=>MD,updateVersionFiles:()=>kD,validateReleaseDecision:()=>Zv});var Gi=Mi(require("@yarnpkg/core")),D0=Mi(require("@yarnpkg/fslib")),W1=Mi(require("@yarnpkg/parsers")),Zp=Mi(require("@yarnpkg/plugin-git")),Jv=Mi(require("clipanion")),h5=Mi(p5()),Fc=Mi(require("semver")),pK=Zp.gitUtils.fetchBase,hK=Zp.gitUtils.fetchRoot,vK=Zp.gitUtils.fetchChangedFiles,mK=/^(>=|[~^]|)(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$/,Nu;(function(k){k.UNDECIDED="undecided",k.DECLINE="decline",k.MAJOR="major",k.MINOR="minor",k.PATCH="patch",k.PRERELEASE="prerelease"})(Nu||(Nu={}));function Zv(i){let o=Fc.default.valid(i);return o||Gi.miscUtils.validateEnum((0,h5.default)(Nu,"UNDECIDED"),i)}async function Jy(i,{prerelease:o=null}={}){var t;let f=new Map,p=i.configuration.get("deferredVersionFolder");if(!D0.xfs.existsSync(p))return new Map;let E=await D0.xfs.readdirPromise(p);for(let k of E){if(!k.endsWith(".yml"))continue;let L=D0.ppath.join(p,k),N=await D0.xfs.readFilePromise(L,"utf8"),C=(0,W1.parseSyml)(N);for(let[U,q]of Object.entries(C.releases||{})){if(q===Nu.DECLINE)continue;let W=Gi.structUtils.parseIdent(U),ne=i.tryWorkspaceByIdent(W);if(ne===null)throw new Error(`Assertion failed: Expected a release definition file to only reference existing workspaces (${D0.ppath.basename(L)} references ${U})`);if(ne.manifest.version===null)throw new Error(`Assertion failed: Expected the workspace to have a version (${Gi.structUtils.prettyLocator(i.configuration,ne.anchoredLocator)})`);let m=(t=ne.manifest.raw.stableVersion)!=null?t:ne.manifest.version,we=f.get(ne),Se=Y_(m,Zv(q));if(Se===null)throw new Error(`Assertion failed: Expected ${m} to support being bumped via strategy ${q}`);let he=typeof we!="undefined"?Fc.default.gt(Se,we)?Se:we:Se;f.set(ne,he)}}return o&&(f=new Map([...f].map(([k,L])=>[k,v5(L,{current:k.manifest.version,prerelease:o})]))),f}async function OD(i){let o=i.configuration.get("deferredVersionFolder");!D0.xfs.existsSync(o)||await D0.xfs.removePromise(o)}async function kD(i){let o=i.configuration.get("deferredVersionFolder");if(!D0.xfs.existsSync(o))return;let f=await D0.xfs.readdirPromise(o);for(let p of f){if(!p.endsWith(".yml"))continue;let E=D0.ppath.join(o,p),t=await D0.xfs.readFilePromise(E,"utf8"),k=(0,W1.parseSyml)(t),L=k==null?void 0:k.releases;if(!!L){for(let N of Object.keys(L)){let C=Gi.structUtils.parseLocator(N);i.tryWorkspaceByLocator(C)===null&&delete k.releases[N]}await D0.xfs.changeFilePromise(E,(0,W1.stringifySyml)(new W1.stringifySyml.PreserveOrdering(k)))}}}async function $v(i,{allowEmpty:o=!1}={}){let f=i.configuration;if(f.projectCwd===null)throw new Jv.UsageError("This command can only be run from within a Yarn project");let p=await Zp.gitUtils.fetchRoot(f.projectCwd),E=p!==null?await Zp.gitUtils.fetchBase(p,{baseRefs:f.get("changesetBaseRefs")}):null,t=p!==null?await Zp.gitUtils.fetchChangedFiles(p,{base:E.hash,project:i}):[],k=f.get("deferredVersionFolder"),L=t.filter(ne=>D0.ppath.contains(k,ne)!==null);if(L.length>1)throw new Jv.UsageError(`Your current branch contains multiple versioning files; this isn't supported: +- ${L.map(ne=>D0.npath.fromPortablePath(ne)).join(` +- `)}`);let N=new Set(Gi.miscUtils.mapAndFilter(t,ne=>{let m=i.tryWorkspaceByFilePath(ne);return m===null?Gi.miscUtils.mapAndFilter.skip:m}));if(L.length===0&&N.size===0&&!o)return null;let C=L.length===1?L[0]:D0.ppath.join(k,`${Gi.hashUtils.makeHash(Math.random().toString()).slice(0,8)}.yml`),U=D0.xfs.existsSync(C)?await D0.xfs.readFilePromise(C,"utf8"):"{}",q=(0,W1.parseSyml)(U),W=new Map;for(let ne of q.declined||[]){let m=Gi.structUtils.parseIdent(ne),we=i.getWorkspaceByIdent(m);W.set(we,Nu.DECLINE)}for(let[ne,m]of Object.entries(q.releases||{})){let we=Gi.structUtils.parseIdent(ne),Se=i.getWorkspaceByIdent(we);W.set(Se,Zv(m))}return{project:i,root:p,baseHash:E!==null?E.hash:null,baseTitle:E!==null?E.title:null,changedFiles:new Set(t),changedWorkspaces:N,releaseRoots:new Set([...N].filter(ne=>ne.manifest.version!==null)),releases:W,async saveAll(){let ne={},m=[],we=[];for(let Se of i.workspaces){if(Se.manifest.version===null)continue;let he=Gi.structUtils.stringifyIdent(Se.locator),ge=W.get(Se);ge===Nu.DECLINE?m.push(he):typeof ge!="undefined"?ne[he]=Zv(ge):N.has(Se)&&we.push(he)}await D0.xfs.mkdirPromise(D0.ppath.dirname(C),{recursive:!0}),await D0.xfs.changeFilePromise(C,(0,W1.stringifySyml)(new W1.stringifySyml.PreserveOrdering({releases:Object.keys(ne).length>0?ne:void 0,declined:m.length>0?m:void 0,undecided:we.length>0?we:void 0})))}}}function yK(i){return K_(i).size>0||Zy(i).length>0}function K_(i){let o=new Set;for(let f of i.changedWorkspaces)f.manifest.version!==null&&(i.releases.has(f)||o.add(f));return o}function Zy(i,{include:o=new Set}={}){let f=[],p=new Map(Gi.miscUtils.mapAndFilter([...i.releases],([t,k])=>k===Nu.DECLINE?Gi.miscUtils.mapAndFilter.skip:[t.anchoredLocator.locatorHash,t])),E=new Map(Gi.miscUtils.mapAndFilter([...i.releases],([t,k])=>k!==Nu.DECLINE?Gi.miscUtils.mapAndFilter.skip:[t.anchoredLocator.locatorHash,t]));for(let t of i.project.workspaces)if(!(!o.has(t)&&(E.has(t.anchoredLocator.locatorHash)||p.has(t.anchoredLocator.locatorHash)))&&t.manifest.version!==null)for(let k of Gi.Manifest.hardDependencies)for(let L of t.manifest.getForScope(k).values()){let N=i.project.tryWorkspaceByDescriptor(L);N!==null&&p.has(N.anchoredLocator.locatorHash)&&f.push([t,N])}return f}function MD(i,o){let f=Fc.default.clean(o);for(let p of Object.values(Nu))if(p!==Nu.UNDECIDED&&p!==Nu.DECLINE&&Fc.default.inc(i,p)===f)return p;return null}function Y_(i,o){if(Fc.default.valid(o))return o;if(i===null)throw new Jv.UsageError(`Cannot apply the release strategy "${o}" unless the workspace already has a valid version`);if(!Fc.default.valid(i))throw new Jv.UsageError(`Cannot apply the release strategy "${o}" on a non-semver version (${i})`);let f=Fc.default.inc(i,o);if(f===null)throw new Jv.UsageError(`Cannot apply the release strategy "${o}" on the specified version (${i})`);return f}function ND(i,o,{report:f}){let p=new Map;for(let E of i.workspaces)for(let t of Gi.Manifest.allDependencies)for(let k of E.manifest[t].values()){let L=i.tryWorkspaceByDescriptor(k);if(L===null||!o.has(L))continue;Gi.miscUtils.getArrayWithDefault(p,L).push([E,t,k.identHash])}for(let[E,t]of o){let k=E.manifest.version;E.manifest.version=t,Fc.default.prerelease(t)===null?delete E.manifest.raw.stableVersion:E.manifest.raw.stableVersion||(E.manifest.raw.stableVersion=k);let L=E.manifest.name!==null?Gi.structUtils.stringifyIdent(E.manifest.name):null;f.reportInfo(Gi.MessageName.UNNAMED,`${Gi.structUtils.prettyLocator(i.configuration,E.anchoredLocator)}: Bumped to ${t}`),f.reportJson({cwd:D0.npath.fromPortablePath(E.cwd),ident:L,oldVersion:k,newVersion:t});let N=p.get(E);if(typeof N!="undefined")for(let[C,U,q]of N){let W=C.manifest[U].get(q);if(typeof W=="undefined")throw new Error("Assertion failed: The dependency should have existed");let ne=W.range,m=!1;if(ne.startsWith(Gi.WorkspaceResolver.protocol)&&(ne=ne.slice(Gi.WorkspaceResolver.protocol.length),m=!0,ne===E.relativeCwd))continue;let we=ne.match(mK);if(!we){f.reportWarning(Gi.MessageName.UNNAMED,`Couldn't auto-upgrade range ${ne} (in ${Gi.structUtils.prettyLocator(i.configuration,C.anchoredLocator)})`);continue}let Se=`${we[1]}${t}`;m&&(Se=`${Gi.WorkspaceResolver.protocol}${Se}`);let he=Gi.structUtils.makeDescriptor(W,Se);C.manifest[U].set(q,he)}}}var gK=new Map([["%n",{extract:i=>i.length>=1?[i[0],i.slice(1)]:null,generate:(i=0)=>`${i+1}`}]]);function v5(i,{current:o,prerelease:f}){let p=new Fc.default.SemVer(o),E=p.prerelease.slice(),t=[];p.prerelease=[],p.format()!==i&&(E.length=0);let k=!0,L=f.split(/\./g);for(let N of L){let C=gK.get(N);if(typeof C=="undefined")t.push(N),E[0]===N?E.shift():k=!1;else{let U=k?C.extract(E):null;U!==null&&typeof U[0]=="number"?(t.push(C.generate(U[0])),E=U[1]):(t.push(C.generate()),k=!1)}}return p.prerelease&&(p.prerelease=[]),`${i}-${t.join(".")}`}var $y=class extends X_.BaseCommand{constructor(){super(...arguments);this.all=cd.Option.Boolean("--all",!1,{description:"Apply the deferred version changes on all workspaces"});this.dryRun=cd.Option.Boolean("--dry-run",!1,{description:"Print the versions without actually generating the package archive"});this.prerelease=cd.Option.String("--prerelease",{description:"Add a prerelease identifier to new versions",tolerateBoolean:!0});this.recursive=cd.Option.Boolean("-R,--recursive",{description:"Release the transitive workspaces as well"});this.json=cd.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let o=await em.Configuration.find(this.context.cwd,this.context.plugins),{project:f,workspace:p}=await Q_.Project.find(o,this.context.cwd),E=await em.Cache.find(o);if(!p)throw new X_.WorkspaceRequiredError(f.cwd,this.context.cwd);return await f.restoreInstallState({restoreResolutions:!1}),(await Q_.StreamReport.start({configuration:o,json:this.json,stdout:this.context.stdout},async k=>{let L=this.prerelease?typeof this.prerelease!="boolean"?this.prerelease:"rc.%n":null,N=await Jy(f,{prerelease:L}),C=new Map;if(this.all)C=N;else{let U=this.recursive?p.getRecursiveWorkspaceDependencies():[p];for(let q of U){let W=N.get(q);typeof W!="undefined"&&C.set(q,W)}}if(C.size===0){let U=N.size>0?" Did you want to add --all?":"";k.reportWarning(em.MessageName.UNNAMED,`The current workspace doesn't seem to require a version bump.${U}`);return}ND(f,C,{report:k}),this.dryRun||(L||(this.all?await OD(f):await kD(f)),k.reportSeparator(),await f.install({cache:E,report:k}))})).exitCode()}};$y.paths=[["version","apply"]],$y.usage=cd.Command.Usage({category:"Release-related commands",description:"apply all the deferred version bumps at once",details:` + This command will apply the deferred version changes and remove their definitions from the repository. + + Note that if \`--prerelease\` is set, the given prerelease identifier (by default \`rc.%d\`) will be used on all new versions and the version definitions will be kept as-is. + + By default only the current workspace will be bumped, but you can configure this behavior by using one of: + + - \`--recursive\` to also apply the version bump on its dependencies + - \`--all\` to apply the version bump on all packages in the repository + + Note that this command will also update the \`workspace:\` references across all your local workspaces, thus ensuring that they keep referring to the same workspaces even after the version bump. + `,examples:[["Apply the version change to the local workspace","yarn version apply"],["Apply the version change to all the workspaces in the local workspace","yarn version apply --all"]]});var m5=$y;var e_=Mi(require("@yarnpkg/cli")),s0=Mi(require("@yarnpkg/core")),rc=Mi(require("@yarnpkg/fslib"));var cO=Mi(lh()),sh=Mi(su()),dO=(0,sh.memo)(({active:i})=>{let o=(0,sh.useMemo)(()=>i?"\u25C9":"\u25EF",[i]),f=(0,sh.useMemo)(()=>i?"green":"yellow",[i]);return sh.default.createElement(cO.Text,{color:f},o)});var yd=Mi(lh()),Js=Mi(su());var pO=Mi(lh()),q4=Mi(su());function hm({active:i},o,f){let{stdin:p}=(0,pO.useStdin)(),E=(0,q4.useCallback)((t,k)=>o(t,k),f);(0,q4.useEffect)(()=>{if(!(!i||!p))return p.on("keypress",E),()=>{p.off("keypress",E)}},[i,E,p])}var ah;(function(f){f.BEFORE="before",f.AFTER="after"})(ah||(ah={}));var hO=function({active:i},o,f){hm({active:i},(p,E)=>{E.name==="tab"&&(E.shift?o(ah.BEFORE):o(ah.AFTER))},f)};var H4=function(i,o,{active:f,minus:p,plus:E,set:t,loop:k=!0}){hm({active:f},(L,N)=>{let C=o.indexOf(i);switch(N.name){case p:{let U=C-1;if(k){t(o[(o.length+U)%o.length]);return}if(U<0)return;t(o[U])}break;case E:{let U=C+1;if(k){t(o[U%o.length]);return}if(U>=o.length)return;t(o[U])}break}},[o,i,E,t,k])};var O3=({active:i=!0,children:o=[],radius:f=10,size:p=1,loop:E=!0,onFocusRequest:t,willReachEnd:k})=>{let L=Se=>{if(Se.key===null)throw new Error("Expected all children to have a key");return Se.key},N=Js.default.Children.map(o,Se=>L(Se)),C=N[0],[U,q]=(0,Js.useState)(C),W=N.indexOf(U);(0,Js.useEffect)(()=>{N.includes(U)||q(C)},[o]),(0,Js.useEffect)(()=>{k&&W>=N.length-2&&k()},[W]),hO({active:i&&!!t},Se=>{t==null||t(Se)},[t]),H4(U,N,{active:i,minus:"up",plus:"down",set:q,loop:E});let ne=W-f,m=W+f;m>N.length&&(ne-=m-N.length,m=N.length),ne<0&&(m+=-ne,ne=0),m>=N.length&&(m=N.length-1);let we=[];for(let Se=ne;Se<=m;++Se){let he=N[Se],ge=i&&he===U;we.push(Js.default.createElement(yd.Box,{key:he,height:p},Js.default.createElement(yd.Box,{marginLeft:1,marginRight:1},Js.default.createElement(yd.Text,null,ge?Js.default.createElement(yd.Text,{color:"cyan",bold:!0},">"):" ")),Js.default.createElement(yd.Box,null,Js.default.cloneElement(o[Se],{active:ge}))))}return Js.default.createElement(yd.Box,{flexDirection:"column",width:"100%"},we)};var W4=Mi(lh()),k3=Mi(su());var vO=Mi(lh()),Z1=Mi(su()),mO=Mi(require("readline")),O$=Z1.default.createContext(null),yO=({children:i})=>{let{stdin:o,setRawMode:f}=(0,vO.useStdin)();(0,Z1.useEffect)(()=>{f&&f(!0),o&&(0,mO.emitKeypressEvents)(o)},[o,f]);let[p,E]=(0,Z1.useState)(new Map),t=(0,Z1.useMemo)(()=>({getAll:()=>p,get:k=>p.get(k),set:(k,L)=>E(new Map([...p,[k,L]]))}),[p,E]);return Z1.default.createElement(O$.Provider,{value:t,children:i})};async function gO(i,o,{stdin:f,stdout:p,stderr:E}={}){let t,k=N=>{let{exit:C}=(0,W4.useApp)();hm({active:!0},(U,q)=>{q.name==="return"&&(t=N,C())},[C,N])},{waitUntilExit:L}=(0,W4.render)(k3.default.createElement(yO,null,k3.default.createElement(i,Gf(E0({},o),{useSubmit:k}))),{stdin:f,stdout:p,stderr:E});return await L(),t}var fh=Mi(require("clipanion")),Dr=Mi(lh()),Tn=Mi(su()),V4=Mi(require("semver"));var t_=class extends e_.BaseCommand{constructor(){super(...arguments);this.interactive=fh.Option.Boolean("-i,--interactive",{description:"Open an interactive interface used to set version bumps"})}async execute(){return this.interactive?await this.executeInteractive():await this.executeStandard()}async executeInteractive(){let o=await s0.Configuration.find(this.context.cwd,this.context.plugins),{project:f,workspace:p}=await s0.Project.find(o,this.context.cwd);if(!p)throw new e_.WorkspaceRequiredError(f.cwd,this.context.cwd);await f.restoreInstallState();let E=await $v(f);if(E===null||E.releaseRoots.size===0)return 0;if(E.root===null)throw new fh.UsageError("This command can only be run on Git repositories");let t=()=>Tn.default.createElement(Dr.Box,{flexDirection:"row",paddingBottom:1},Tn.default.createElement(Dr.Box,{flexDirection:"column",width:60},Tn.default.createElement(Dr.Box,null,Tn.default.createElement(Dr.Text,null,"Press ",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},""),"/",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},"")," to select workspaces.")),Tn.default.createElement(Dr.Box,null,Tn.default.createElement(Dr.Text,null,"Press ",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},""),"/",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},"")," to select release strategies."))),Tn.default.createElement(Dr.Box,{flexDirection:"column"},Tn.default.createElement(Dr.Box,{marginLeft:1},Tn.default.createElement(Dr.Text,null,"Press ",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},"")," to save.")),Tn.default.createElement(Dr.Box,{marginLeft:1},Tn.default.createElement(Dr.Text,null,"Press ",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},"")," to abort.")))),k=({workspace:W,active:ne,decision:m,setDecision:we})=>{var ze;let Se=(ze=W.manifest.raw.stableVersion)!=null?ze:W.manifest.version;if(Se===null)throw new Error(`Assertion failed: The version should have been set (${s0.structUtils.prettyLocator(o,W.anchoredLocator)})`);if(V4.default.prerelease(Se)!==null)throw new Error(`Assertion failed: Prerelease identifiers shouldn't be found (${Se})`);let he=[Nu.UNDECIDED,Nu.DECLINE,Nu.PATCH,Nu.MINOR,Nu.MAJOR];H4(m,he,{active:ne,minus:"left",plus:"right",set:we});let ge=m===Nu.UNDECIDED?Tn.default.createElement(Dr.Text,{color:"yellow"},Se):m===Nu.DECLINE?Tn.default.createElement(Dr.Text,{color:"green"},Se):Tn.default.createElement(Dr.Text,null,Tn.default.createElement(Dr.Text,{color:"magenta"},Se)," \u2192 ",Tn.default.createElement(Dr.Text,{color:"green"},V4.default.valid(m)?m:V4.default.inc(Se,m)));return Tn.default.createElement(Dr.Box,{flexDirection:"column"},Tn.default.createElement(Dr.Box,null,Tn.default.createElement(Dr.Text,null,s0.structUtils.prettyLocator(o,W.anchoredLocator)," - ",ge)),Tn.default.createElement(Dr.Box,null,he.map(pe=>Tn.default.createElement(Dr.Box,{key:pe,paddingLeft:2},Tn.default.createElement(Dr.Text,null,Tn.default.createElement(dO,{active:pe===m})," ",pe)))))},L=W=>{let ne=new Set(E.releaseRoots),m=new Map([...W].filter(([we])=>ne.has(we)));for(;;){let we=Zy({project:E.project,releases:m}),Se=!1;if(we.length>0){for(let[he]of we)if(!ne.has(he)){ne.add(he),Se=!0;let ge=W.get(he);typeof ge!="undefined"&&m.set(he,ge)}}if(!Se)break}return{relevantWorkspaces:ne,relevantReleases:m}},N=()=>{let[W,ne]=(0,Tn.useState)(()=>new Map(E.releases)),m=(0,Tn.useCallback)((we,Se)=>{let he=new Map(W);Se!==Nu.UNDECIDED?he.set(we,Se):he.delete(we);let{relevantReleases:ge}=L(he);ne(ge)},[W,ne]);return[W,m]},C=({workspaces:W,releases:ne})=>{let m=[];m.push(`${W.size} total`);let we=0,Se=0;for(let he of W){let ge=ne.get(he);typeof ge=="undefined"?Se+=1:ge!==Nu.DECLINE&&(we+=1)}return m.push(`${we} release${we===1?"":"s"}`),m.push(`${Se} remaining`),Tn.default.createElement(Dr.Text,{color:"yellow"},m.join(", "))},q=await gO(({useSubmit:W})=>{let[ne,m]=N();W(ne);let{relevantWorkspaces:we}=L(ne),Se=new Set([...we].filter(pe=>!E.releaseRoots.has(pe))),[he,ge]=(0,Tn.useState)(0),ze=(0,Tn.useCallback)(pe=>{switch(pe){case ah.BEFORE:ge(he-1);break;case ah.AFTER:ge(he+1);break}},[he,ge]);return Tn.default.createElement(Dr.Box,{flexDirection:"column"},Tn.default.createElement(t,null),Tn.default.createElement(Dr.Box,null,Tn.default.createElement(Dr.Text,{wrap:"wrap"},"The following files have been modified in your local checkout.")),Tn.default.createElement(Dr.Box,{flexDirection:"column",marginTop:1,paddingLeft:2},[...E.changedFiles].map(pe=>Tn.default.createElement(Dr.Box,{key:pe},Tn.default.createElement(Dr.Text,null,Tn.default.createElement(Dr.Text,{color:"grey"},rc.npath.fromPortablePath(E.root)),rc.npath.sep,rc.npath.relative(rc.npath.fromPortablePath(E.root),rc.npath.fromPortablePath(pe)))))),E.releaseRoots.size>0&&Tn.default.createElement(Tn.default.Fragment,null,Tn.default.createElement(Dr.Box,{marginTop:1},Tn.default.createElement(Dr.Text,{wrap:"wrap"},"Because of those files having been modified, the following workspaces may need to be released again (note that private workspaces are also shown here, because even though they won't be published, releasing them will allow us to flag their dependents for potential re-release):")),Se.size>3?Tn.default.createElement(Dr.Box,{marginTop:1},Tn.default.createElement(C,{workspaces:E.releaseRoots,releases:ne})):null,Tn.default.createElement(Dr.Box,{marginTop:1,flexDirection:"column"},Tn.default.createElement(O3,{active:he%2==0,radius:1,size:2,onFocusRequest:ze},[...E.releaseRoots].map(pe=>Tn.default.createElement(k,{key:pe.cwd,workspace:pe,decision:ne.get(pe)||Nu.UNDECIDED,setDecision:Oe=>m(pe,Oe)}))))),Se.size>0?Tn.default.createElement(Tn.default.Fragment,null,Tn.default.createElement(Dr.Box,{marginTop:1},Tn.default.createElement(Dr.Text,{wrap:"wrap"},"The following workspaces depend on other workspaces that have been marked for release, and thus may need to be released as well:")),Tn.default.createElement(Dr.Box,null,Tn.default.createElement(Dr.Text,null,"(Press ",Tn.default.createElement(Dr.Text,{bold:!0,color:"cyanBright"},"")," to move the focus between the workspace groups.)")),Se.size>5?Tn.default.createElement(Dr.Box,{marginTop:1},Tn.default.createElement(C,{workspaces:Se,releases:ne})):null,Tn.default.createElement(Dr.Box,{marginTop:1,flexDirection:"column"},Tn.default.createElement(O3,{active:he%2==1,radius:2,size:2,onFocusRequest:ze},[...Se].map(pe=>Tn.default.createElement(k,{key:pe.cwd,workspace:pe,decision:ne.get(pe)||Nu.UNDECIDED,setDecision:Oe=>m(pe,Oe)}))))):null)},{versionFile:E},{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});if(typeof q=="undefined")return 1;E.releases.clear();for(let[W,ne]of q)E.releases.set(W,ne);await E.saveAll()}async executeStandard(){let o=await s0.Configuration.find(this.context.cwd,this.context.plugins),{project:f,workspace:p}=await s0.Project.find(o,this.context.cwd);if(!p)throw new e_.WorkspaceRequiredError(f.cwd,this.context.cwd);return await f.restoreInstallState(),(await s0.StreamReport.start({configuration:o,stdout:this.context.stdout},async t=>{let k=await $v(f);if(k===null||k.releaseRoots.size===0)return;if(k.root===null)throw new fh.UsageError("This command can only be run on Git repositories");if(t.reportInfo(s0.MessageName.UNNAMED,`Your PR was started right after ${s0.formatUtils.pretty(o,k.baseHash.slice(0,7),"yellow")} ${s0.formatUtils.pretty(o,k.baseTitle,"magenta")}`),k.changedFiles.size>0){t.reportInfo(s0.MessageName.UNNAMED,"You have changed the following files since then:"),t.reportSeparator();for(let q of k.changedFiles)t.reportInfo(null,`${s0.formatUtils.pretty(o,rc.npath.fromPortablePath(k.root),"gray")}${rc.npath.sep}${rc.npath.relative(rc.npath.fromPortablePath(k.root),rc.npath.fromPortablePath(q))}`)}let L=!1,N=!1,C=K_(k);if(C.size>0){L||t.reportSeparator();for(let q of C)t.reportError(s0.MessageName.UNNAMED,`${s0.structUtils.prettyLocator(o,q.anchoredLocator)} has been modified but doesn't have a release strategy attached`);L=!0}let U=Zy(k);for(let[q,W]of U)N||t.reportSeparator(),t.reportError(s0.MessageName.UNNAMED,`${s0.structUtils.prettyLocator(o,q.anchoredLocator)} doesn't have a release strategy attached, but depends on ${s0.structUtils.prettyWorkspace(o,W)} which is planned for release.`),N=!0;(L||N)&&(t.reportSeparator(),t.reportInfo(s0.MessageName.UNNAMED,"This command detected that at least some workspaces have received modifications without explicit instructions as to how they had to be released (if needed)."),t.reportInfo(s0.MessageName.UNNAMED,"To correct these errors, run `yarn version check --interactive` then follow the instructions."))})).exitCode()}};t_.paths=[["version","check"]],t_.usage=fh.Command.Usage({category:"Release-related commands",description:"check that all the relevant packages have been bumped",details:"\n **Warning:** This command currently requires Git.\n\n This command will check that all the packages covered by the files listed in argument have been properly bumped or declined to bump.\n\n In the case of a bump, the check will also cover transitive packages - meaning that should `Foo` be bumped, a package `Bar` depending on `Foo` will require a decision as to whether `Bar` will need to be bumped. This check doesn't cross packages that have declined to bump.\n\n In case no arguments are passed to the function, the list of modified files will be generated by comparing the HEAD against `master`.\n ",examples:[["Check whether the modified packages need a bump","yarn version check"]]});var _O=t_;var G4=Mi(require("@yarnpkg/cli")),Y4=Mi(require("@yarnpkg/core")),Bc=Mi(require("clipanion")),K4=Mi(require("semver"));var n_=class extends G4.BaseCommand{constructor(){super(...arguments);this.deferred=Bc.Option.Boolean("-d,--deferred",{description:"Prepare the version to be bumped during the next release cycle"});this.immediate=Bc.Option.Boolean("-i,--immediate",{description:"Bump the version immediately"});this.strategy=Bc.Option.String()}async execute(){let o=await Y4.Configuration.find(this.context.cwd,this.context.plugins),{project:f,workspace:p}=await Y4.Project.find(o,this.context.cwd);if(!p)throw new G4.WorkspaceRequiredError(f.cwd,this.context.cwd);let E=o.get("preferDeferredVersions");this.deferred&&(E=!0),this.immediate&&(E=!1);let t=K4.default.valid(this.strategy),k=this.strategy===Nu.DECLINE,L;if(t)if(p.manifest.version!==null){let C=MD(p.manifest.version,this.strategy);C!==null?L=C:L=this.strategy}else L=this.strategy;else{let C=p.manifest.version;if(!k){if(C===null)throw new Bc.UsageError("Can't bump the version if there wasn't a version to begin with - use 0.0.0 as initial version then run the command again.");if(typeof C!="string"||!K4.default.valid(C))throw new Bc.UsageError(`Can't bump the version (${C}) if it's not valid semver`)}L=Zv(this.strategy)}if(!E){let U=(await Jy(f)).get(p);if(typeof U!="undefined"&&L!==Nu.DECLINE){let q=Y_(p.manifest.version,L);if(K4.default.lt(q,U))throw new Bc.UsageError(`Can't bump the version to one that would be lower than the current deferred one (${U})`)}}let N=await $v(f,{allowEmpty:!0});return N.releases.set(p,L),await N.saveAll(),E?0:await this.cli.run(["version","apply"])}};n_.paths=[["version"]],n_.usage=Bc.Command.Usage({category:"Release-related commands",description:"apply a new version to the current package",details:"\n This command will bump the version number for the given package, following the specified strategy:\n\n - If `major`, the first number from the semver range will be increased (`X.0.0`).\n - If `minor`, the second number from the semver range will be increased (`0.X.0`).\n - If `patch`, the third number from the semver range will be increased (`0.0.X`).\n - If prefixed by `pre` (`premajor`, ...), a `-0` suffix will be set (`0.0.0-0`).\n - If `prerelease`, the suffix will be increased (`0.0.0-X`); the third number from the semver range will also be increased if there was no suffix in the previous version.\n - If `decline`, the nonce will be increased for `yarn version check` to pass without version bump.\n - If a valid semver range, it will be used as new version.\n - If unspecified, Yarn will ask you for guidance.\n\n For more information about the `--deferred` flag, consult our documentation (https://yarnpkg.com/features/release-workflow#deferred-versioning).\n ",examples:[["Immediately bump the version to the next major","yarn version major"],["Prepare the version to be bumped to the next major","yarn version major --deferred"]]});var EO=n_;var M$={configuration:{deferredVersionFolder:{description:"Folder where are stored the versioning files",type:M3.SettingsType.ABSOLUTE_PATH,default:"./.yarn/versions"},preferDeferredVersions:{description:"If true, running `yarn version` will assume the `--deferred` flag unless `--immediate` is set",type:M3.SettingsType.BOOLEAN,default:!1}},commands:[m5,_O,EO]},N$=M$;return k$;})(); +/* +object-assign +(c) Sindre Sorhus +@license MIT +*/ +/** + * @license + * Lodash + * Copyright OpenJS Foundation and other contributors + * Released under MIT license + * Based on Underscore.js 1.8.3 + * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + */ +/** @license React v0.0.0-experimental-51a3aa6af + * react-debug-tools.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.0.0-experimental-51a3aa6af + * react-is.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.0.0-experimental-51a3aa6af + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.18.0 + * scheduler-tracing.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.18.0 + * scheduler-tracing.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.18.0 + * scheduler.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.18.0 + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.24.0 + * react-reconciler.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v0.24.0 + * react-reconciler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v16.13.1 + * react.development.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +/** @license React v16.13.1 + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +return plugin; +} +}; diff --git a/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs new file mode 100644 index 0000000..911cd23 --- /dev/null +++ b/.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs @@ -0,0 +1,28 @@ +/* eslint-disable */ +//prettier-ignore +module.exports = { +name: "@yarnpkg/plugin-workspace-tools", +factory: function (require) { +var plugin=(()=>{var wr=Object.create,me=Object.defineProperty,Sr=Object.defineProperties,vr=Object.getOwnPropertyDescriptor,Hr=Object.getOwnPropertyDescriptors,$r=Object.getOwnPropertyNames,et=Object.getOwnPropertySymbols,kr=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty,Tr=Object.prototype.propertyIsEnumerable;var rt=(e,t,r)=>t in e?me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,B=(e,t)=>{for(var r in t||(t={}))tt.call(t,r)&&rt(e,r,t[r]);if(et)for(var r of et(t))Tr.call(t,r)&&rt(e,r,t[r]);return e},Q=(e,t)=>Sr(e,Hr(t)),Lr=e=>me(e,"__esModule",{value:!0});var K=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Or=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of $r(t))!tt.call(e,n)&&n!=="default"&&me(e,n,{get:()=>t[n],enumerable:!(r=vr(t,n))||r.enumerable});return e},X=e=>Nr(Lr(me(e!=null?wr(kr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var $e=K(te=>{"use strict";te.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;te.find=(e,t)=>e.nodes.find(r=>r.type===t);te.exceedsLimit=(e,t,r=1,n)=>n===!1||!te.isInteger(e)||!te.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=n;te.escapeNode=(e,t=0,r)=>{let n=e.nodes[t];!n||(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};te.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;te.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0==0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;te.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;te.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);te.flatten=(...e)=>{let t=[],r=n=>{for(let s=0;s{"use strict";var it=$e();at.exports=(e,t={})=>{let r=(n,s={})=>{let a=t.escapeInvalid&&it.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o="";if(n.value)return(a||i)&&it.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let h of n.nodes)o+=r(h);return o};return r(e)}});var ct=K((os,ot)=>{"use strict";ot.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var At=K((cs,ut)=>{"use strict";var lt=ct(),pe=(e,t,r)=>{if(lt(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(lt(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n=B({relaxZeros:!0},r);typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),a=String(n.shorthand),i=String(n.capture),o=String(n.wrap),h=e+":"+t+"="+s+a+i+o;if(pe.cache.hasOwnProperty(h))return pe.cache[h].result;let g=Math.min(e,t),f=Math.max(e,t);if(Math.abs(g-f)===1){let R=e+"|"+t;return n.capture?`(${R})`:n.wrap===!1?R:`(?:${R})`}let A=ft(e)||ft(t),p={min:e,max:t,a:g,b:f},k=[],y=[];if(A&&(p.isPadded=A,p.maxLen=String(p.max).length),g<0){let R=f<0?Math.abs(f):1;y=pt(R,Math.abs(g),p,n),g=p.a=0}return f>=0&&(k=pt(g,f,p,n)),p.negatives=y,p.positives=k,p.result=Ir(y,k,n),n.capture===!0?p.result=`(${p.result})`:n.wrap!==!1&&k.length+y.length>1&&(p.result=`(?:${p.result})`),pe.cache[h]=p,p.result};function Ir(e,t,r){let n=Pe(e,t,"-",!1,r)||[],s=Pe(t,e,"",!1,r)||[],a=Pe(e,t,"-?",!0,r)||[];return n.concat(a).concat(s).join("|")}function Mr(e,t){let r=1,n=1,s=ht(e,r),a=new Set([t]);for(;e<=s&&s<=t;)a.add(s),r+=1,s=ht(e,r);for(s=dt(t+1,n)-1;e1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+gt(o.count),i=g+1;continue}r.isPadded&&(A=Gr(g,r,n)),f.string=A+f.pattern+gt(f.count),a.push(f),i=g+1,o=f}return a}function Pe(e,t,r,n,s){let a=[];for(let i of e){let{string:o}=i;!n&&!mt(t,"string",o)&&a.push(r+o),n&&mt(t,"string",o)&&a.push(r+o)}return a}function Pr(e,t){let r=[];for(let n=0;nt?1:t>e?-1:0}function mt(e,t,r){return e.some(n=>n[t]===r)}function ht(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function dt(e,t){return e-e%Math.pow(10,t)}function gt(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function Dr(e,t,r){return`[${e}${t-e==1?"":"-"}${t}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Gr(e,t,r){if(!t.isPadded)return e;let n=Math.abs(t.maxLen-String(e).length),s=r.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}pe.cache={};pe.clearCache=()=>pe.cache={};ut.exports=pe});var Ge=K((us,Rt)=>{"use strict";var qr=require("util"),yt=At(),bt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Kr=e=>t=>e===!0?Number(t):String(t),De=e=>typeof e=="number"||typeof e=="string"&&e!=="",Re=e=>Number.isInteger(+e),Ue=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},Wr=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,jr=(e,t,r)=>{if(t>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?t-1:t,"0")}return r===!1?String(e):e},_t=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((i,o)=>io?1:0),e.positives.sort((i,o)=>io?1:0);let r=t.capture?"":"?:",n="",s="",a;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${r}${e.negatives.join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,t.wrap?`(${r}${a})`:a},Et=(e,t,r,n)=>{if(r)return yt(e,t,B({wrap:!1},n));let s=String.fromCharCode(e);if(e===t)return s;let a=String.fromCharCode(t);return`[${s}-${a}]`},xt=(e,t,r)=>{if(Array.isArray(e)){let n=r.wrap===!0,s=r.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return yt(e,t,r)},Ct=(...e)=>new RangeError("Invalid range arguments: "+qr.inspect(...e)),wt=(e,t,r)=>{if(r.strictRanges===!0)throw Ct([e,t]);return[]},Qr=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Xr=(e,t,r=1,n={})=>{let s=Number(e),a=Number(t);if(!Number.isInteger(s)||!Number.isInteger(a)){if(n.strictRanges===!0)throw Ct([e,t]);return[]}s===0&&(s=0),a===0&&(a=0);let i=s>a,o=String(e),h=String(t),g=String(r);r=Math.max(Math.abs(r),1);let f=Ue(o)||Ue(h)||Ue(g),A=f?Math.max(o.length,h.length,g.length):0,p=f===!1&&Wr(e,t,n)===!1,k=n.transform||Kr(p);if(n.toRegex&&r===1)return Et(_t(e,A),_t(t,A),!0,n);let y={negatives:[],positives:[]},R=T=>y[T<0?"negatives":"positives"].push(Math.abs(T)),_=[],x=0;for(;i?s>=a:s<=a;)n.toRegex===!0&&r>1?R(s):_.push(jr(k(s,x),A,p)),s=i?s-r:s+r,x++;return n.toRegex===!0?r>1?Fr(y,n):xt(_,null,B({wrap:!1},n)):_},Zr=(e,t,r=1,n={})=>{if(!Re(e)&&e.length>1||!Re(t)&&t.length>1)return wt(e,t,n);let s=n.transform||(p=>String.fromCharCode(p)),a=`${e}`.charCodeAt(0),i=`${t}`.charCodeAt(0),o=a>i,h=Math.min(a,i),g=Math.max(a,i);if(n.toRegex&&r===1)return Et(h,g,!1,n);let f=[],A=0;for(;o?a>=i:a<=i;)f.push(s(a,A)),a=o?a-r:a+r,A++;return n.toRegex===!0?xt(f,null,{wrap:!1,options:n}):f},Te=(e,t,r,n={})=>{if(t==null&&De(e))return[e];if(!De(e)||!De(t))return wt(e,t,n);if(typeof r=="function")return Te(e,t,1,{transform:r});if(bt(r))return Te(e,t,0,r);let s=B({},n);return s.capture===!0&&(s.wrap=!0),r=r||s.step||1,Re(r)?Re(e)&&Re(t)?Xr(e,t,r,s):Zr(e,t,Math.max(Math.abs(r),1),s):r!=null&&!bt(r)?Qr(r,s):Te(e,t,1,r)};Rt.exports=Te});var Ht=K((ls,St)=>{"use strict";var Yr=Ge(),vt=$e(),zr=(e,t={})=>{let r=(n,s={})=>{let a=vt.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o=a===!0||i===!0,h=t.escapeInvalid===!0?"\\":"",g="";if(n.isOpen===!0||n.isClose===!0)return h+n.value;if(n.type==="open")return o?h+n.value:"(";if(n.type==="close")return o?h+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":o?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let f=vt.reduce(n.nodes),A=Yr(...f,Q(B({},t),{wrap:!1,toRegex:!0}));if(A.length!==0)return f.length>1&&A.length>1?`(${A})`:A}if(n.nodes)for(let f of n.nodes)g+=r(f,n);return g};return r(e)};St.exports=zr});var Tt=K((ps,$t)=>{"use strict";var Vr=Ge(),kt=ke(),he=$e(),fe=(e="",t="",r=!1)=>{let n=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?he.flatten(t).map(s=>`{${s}}`):t;for(let s of e)if(Array.isArray(s))for(let a of s)n.push(fe(a,t,r));else for(let a of t)r===!0&&typeof a=="string"&&(a=`{${a}}`),n.push(Array.isArray(a)?fe(s,a,r):s+a);return he.flatten(n)},Jr=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,n=(s,a={})=>{s.queue=[];let i=a,o=a.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,o=i.queue;if(s.invalid||s.dollar){o.push(fe(o.pop(),kt(s,t)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){o.push(fe(o.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let A=he.reduce(s.nodes);if(he.exceedsLimit(...A,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=Vr(...A,t);p.length===0&&(p=kt(s,t)),o.push(fe(o.pop(),p)),s.nodes=[];return}let h=he.encloseBrace(s),g=s.queue,f=s;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,g=f.queue;for(let A=0;A{"use strict";Lt.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Pt=K((hs,Nt)=>{"use strict";var en=ke(),{MAX_LENGTH:It,CHAR_BACKSLASH:qe,CHAR_BACKTICK:tn,CHAR_COMMA:rn,CHAR_DOT:nn,CHAR_LEFT_PARENTHESES:sn,CHAR_RIGHT_PARENTHESES:an,CHAR_LEFT_CURLY_BRACE:on,CHAR_RIGHT_CURLY_BRACE:cn,CHAR_LEFT_SQUARE_BRACKET:Bt,CHAR_RIGHT_SQUARE_BRACKET:Mt,CHAR_DOUBLE_QUOTE:un,CHAR_SINGLE_QUOTE:ln,CHAR_NO_BREAK_SPACE:pn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:fn}=Ot(),hn=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},n=typeof r.maxLength=="number"?Math.min(It,r.maxLength):It;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},a=[s],i=s,o=s,h=0,g=e.length,f=0,A=0,p,k={},y=()=>e[f++],R=_=>{if(_.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&_.type==="text"){o.value+=_.value;return}return i.nodes.push(_),_.parent=i,_.prev=o,o=_,_};for(R({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let _=i.nodes.shift();i.nodes=[_,{type:"text",value:en(i)}]}R({type:"comma",value:p}),i.commas++;continue}if(p===nn&&A>0&&i.commas===0){let _=i.nodes;if(A===0||_.length===0){R({type:"text",value:p});continue}if(o.type==="dot"){if(i.range=[],o.value+=p,o.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,o.type="text";continue}i.ranges++,i.args=[];continue}if(o.type==="range"){_.pop();let x=_[_.length-1];x.value+=o.value+p,o=x,i.ranges--;continue}R({type:"dot",value:p});continue}R({type:"text",value:p})}do if(i=a.pop(),i.type!=="root"){i.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let _=a[a.length-1],x=_.nodes.indexOf(i);_.nodes.splice(x,1,...i.nodes)}while(a.length>0);return R({type:"eos"}),s};Nt.exports=hn});var Gt=K((ds,Dt)=>{"use strict";var Ut=ke(),dn=Ht(),gn=Tt(),mn=Pt(),V=(e,t={})=>{let r=[];if(Array.isArray(e))for(let n of e){let s=V.create(n,t);Array.isArray(s)?r.push(...s):r.push(s)}else r=[].concat(V.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};V.parse=(e,t={})=>mn(e,t);V.stringify=(e,t={})=>typeof e=="string"?Ut(V.parse(e,t),t):Ut(e,t);V.compile=(e,t={})=>(typeof e=="string"&&(e=V.parse(e,t)),dn(e,t));V.expand=(e,t={})=>{typeof e=="string"&&(e=V.parse(e,t));let r=gn(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};V.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?V.compile(e,t):V.expand(e,t);Dt.exports=V});var ye=K((gs,qt)=>{"use strict";var An=require("path"),ie="\\\\/",Kt=`[^${ie}]`,ce="\\.",Rn="\\+",yn="\\?",Le="\\/",bn="(?=.)",Wt="[^/]",Ke=`(?:${Le}|$)`,jt=`(?:^|${Le})`,We=`${ce}{1,2}${Ke}`,_n=`(?!${ce})`,En=`(?!${jt}${We})`,xn=`(?!${ce}{0,1}${Ke})`,Cn=`(?!${We})`,wn=`[^.${Le}]`,Sn=`${Wt}*?`,Ft={DOT_LITERAL:ce,PLUS_LITERAL:Rn,QMARK_LITERAL:yn,SLASH_LITERAL:Le,ONE_CHAR:bn,QMARK:Wt,END_ANCHOR:Ke,DOTS_SLASH:We,NO_DOT:_n,NO_DOTS:En,NO_DOT_SLASH:xn,NO_DOTS_SLASH:Cn,QMARK_NO_DOT:wn,STAR:Sn,START_ANCHOR:jt},vn=Q(B({},Ft),{SLASH_LITERAL:`[${ie}]`,QMARK:Kt,STAR:`${Kt}*?`,DOTS_SLASH:`${ce}{1,2}(?:[${ie}]|$)`,NO_DOT:`(?!${ce})`,NO_DOTS:`(?!(?:^|[${ie}])${ce}{1,2}(?:[${ie}]|$))`,NO_DOT_SLASH:`(?!${ce}{0,1}(?:[${ie}]|$))`,NO_DOTS_SLASH:`(?!${ce}{1,2}(?:[${ie}]|$))`,QMARK_NO_DOT:`[^.${ie}]`,START_ANCHOR:`(?:^|[${ie}])`,END_ANCHOR:`(?:[${ie}]|$)`}),Hn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Hn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:An.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?vn:Ft}}});var be=K(Z=>{"use strict";var $n=require("path"),kn=process.platform==="win32",{REGEX_BACKSLASH:Tn,REGEX_REMOVE_BACKSLASH:Ln,REGEX_SPECIAL_CHARS:On,REGEX_SPECIAL_CHARS_GLOBAL:Nn}=ye();Z.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Z.hasRegexChars=e=>On.test(e);Z.isRegexChar=e=>e.length===1&&Z.hasRegexChars(e);Z.escapeRegex=e=>e.replace(Nn,"\\$1");Z.toPosixSlashes=e=>e.replace(Tn,"/");Z.removeBackslashes=e=>e.replace(Ln,t=>t==="\\"?"":t);Z.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Z.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:kn===!0||$n.sep==="\\";Z.escapeLast=(e,t,r)=>{let n=e.lastIndexOf(t,r);return n===-1?e:e[n-1]==="\\"?Z.escapeLast(e,t,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Z.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};Z.wrapOutput=(e,t={},r={})=>{let n=r.contains?"":"^",s=r.contains?"":"$",a=`${n}(?:${e})${s}`;return t.negated===!0&&(a=`(?:^(?!${a}).*$)`),a}});var er=K((As,Qt)=>{"use strict";var Xt=be(),{CHAR_ASTERISK:je,CHAR_AT:In,CHAR_BACKWARD_SLASH:_e,CHAR_COMMA:Bn,CHAR_DOT:Fe,CHAR_EXCLAMATION_MARK:Qe,CHAR_FORWARD_SLASH:Zt,CHAR_LEFT_CURLY_BRACE:Xe,CHAR_LEFT_PARENTHESES:Ze,CHAR_LEFT_SQUARE_BRACKET:Mn,CHAR_PLUS:Pn,CHAR_QUESTION_MARK:Yt,CHAR_RIGHT_CURLY_BRACE:Dn,CHAR_RIGHT_PARENTHESES:zt,CHAR_RIGHT_SQUARE_BRACKET:Un}=ye(),Vt=e=>e===Zt||e===_e,Jt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Gn=(e,t)=>{let r=t||{},n=e.length-1,s=r.parts===!0||r.scanToEnd===!0,a=[],i=[],o=[],h=e,g=-1,f=0,A=0,p=!1,k=!1,y=!1,R=!1,_=!1,x=!1,T=!1,O=!1,W=!1,G=!1,ne=0,E,b,C={value:"",depth:0,isGlob:!1},M=()=>g>=n,l=()=>h.charCodeAt(g+1),H=()=>(E=b,h.charCodeAt(++g));for(;g0&&(j=h.slice(0,f),h=h.slice(f),A-=f),w&&y===!0&&A>0?(w=h.slice(0,A),c=h.slice(A)):y===!0?(w="",c=h):w=h,w&&w!==""&&w!=="/"&&w!==h&&Vt(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),r.unescape===!0&&(c&&(c=Xt.removeBackslashes(c)),w&&T===!0&&(w=Xt.removeBackslashes(w)));let u={prefix:j,input:e,start:f,base:w,glob:c,isBrace:p,isBracket:k,isGlob:y,isExtglob:R,isGlobstar:_,negated:O,negatedExtglob:W};if(r.tokens===!0&&(u.maxDepth=0,Vt(b)||i.push(C),u.tokens=i),r.parts===!0||r.tokens===!0){let I;for(let $=0;${"use strict";var Oe=ye(),J=be(),{MAX_LENGTH:Ne,POSIX_REGEX_SOURCE:qn,REGEX_NON_SPECIAL_CHARS:Kn,REGEX_SPECIAL_CHARS_BACKREF:Wn,REPLACEMENTS:rr}=Oe,jn=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch(n){return e.map(s=>J.escapeRegex(s)).join("..")}return r},de=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,nr=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=rr[e]||e;let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let a={type:"bos",value:"",output:r.prepend||""},i=[a],o=r.capture?"":"?:",h=J.isWindows(t),g=Oe.globChars(h),f=Oe.extglobChars(g),{DOT_LITERAL:A,PLUS_LITERAL:p,SLASH_LITERAL:k,ONE_CHAR:y,DOTS_SLASH:R,NO_DOT:_,NO_DOT_SLASH:x,NO_DOTS_SLASH:T,QMARK:O,QMARK_NO_DOT:W,STAR:G,START_ANCHOR:ne}=g,E=m=>`(${o}(?:(?!${ne}${m.dot?R:A}).)*?)`,b=r.dot?"":_,C=r.dot?O:W,M=r.bash===!0?E(r):G;r.capture&&(M=`(${M})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let l={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};e=J.removePrefix(e,l),s=e.length;let H=[],w=[],j=[],c=a,u,I=()=>l.index===s-1,$=l.peek=(m=1)=>e[l.index+m],ee=l.advance=()=>e[++l.index]||"",se=()=>e.slice(l.index+1),z=(m="",L=0)=>{l.consumed+=m,l.index+=L},Ce=m=>{l.output+=m.output!=null?m.output:m.value,z(m.value)},xr=()=>{let m=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)ee(),l.start++,m++;return m%2==0?!1:(l.negated=!0,l.start++,!0)},we=m=>{l[m]++,j.push(m)},ue=m=>{l[m]--,j.pop()},v=m=>{if(c.type==="globstar"){let L=l.braces>0&&(m.type==="comma"||m.type==="brace"),d=m.extglob===!0||H.length&&(m.type==="pipe"||m.type==="paren");m.type!=="slash"&&m.type!=="paren"&&!L&&!d&&(l.output=l.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=M,l.output+=c.output)}if(H.length&&m.type!=="paren"&&(H[H.length-1].inner+=m.value),(m.value||m.output)&&Ce(m),c&&c.type==="text"&&m.type==="text"){c.value+=m.value,c.output=(c.output||"")+m.value;return}m.prev=c,i.push(m),c=m},Se=(m,L)=>{let d=Q(B({},f[L]),{conditions:1,inner:""});d.prev=c,d.parens=l.parens,d.output=l.output;let S=(r.capture?"(":"")+d.open;we("parens"),v({type:m,value:L,output:l.output?"":y}),v({type:"paren",extglob:!0,value:ee(),output:S}),H.push(d)},Cr=m=>{let L=m.close+(r.capture?")":""),d;if(m.type==="negate"){let S=M;m.inner&&m.inner.length>1&&m.inner.includes("/")&&(S=E(r)),(S!==M||I()||/^\)+$/.test(se()))&&(L=m.close=`)$))${S}`),m.inner.includes("*")&&(d=se())&&/^\.[^\\/.]+$/.test(d)&&(L=m.close=`)${d})${S})`),m.prev.type==="bos"&&(l.negatedExtglob=!0)}v({type:"paren",extglob:!0,value:u,output:L}),ue("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let m=!1,L=e.replace(Wn,(d,S,P,F,q,Me)=>F==="\\"?(m=!0,d):F==="?"?S?S+F+(q?O.repeat(q.length):""):Me===0?C+(q?O.repeat(q.length):""):O.repeat(P.length):F==="."?A.repeat(P.length):F==="*"?S?S+F+(q?M:""):M:S?d:`\\${d}`);return m===!0&&(r.unescape===!0?L=L.replace(/\\/g,""):L=L.replace(/\\+/g,d=>d.length%2==0?"\\\\":d?"\\":"")),L===e&&r.contains===!0?(l.output=e,l):(l.output=J.wrapOutput(L,l,t),l)}for(;!I();){if(u=ee(),u==="\0")continue;if(u==="\\"){let d=$();if(d==="/"&&r.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",v({type:"text",value:u});continue}let S=/^\\+/.exec(se()),P=0;if(S&&S[0].length>2&&(P=S[0].length,l.index+=P,P%2!=0&&(u+="\\")),r.unescape===!0?u=ee():u+=ee(),l.brackets===0){v({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(r.posix!==!1&&u===":"){let d=c.value.slice(1);if(d.includes("[")&&(c.posix=!0,d.includes(":"))){let S=c.value.lastIndexOf("["),P=c.value.slice(0,S),F=c.value.slice(S+2),q=qn[F];if(q){c.value=P+q,l.backtrack=!0,ee(),!a.output&&i.indexOf(c)===1&&(a.output=y);continue}}}(u==="["&&$()!==":"||u==="-"&&$()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),r.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Ce({value:u});continue}if(l.quotes===1&&u!=='"'){u=J.escapeRegex(u),c.value+=u,Ce({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,r.keepQuotes===!0&&v({type:"text",value:u});continue}if(u==="("){we("parens"),v({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&r.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Cr(H.pop());continue}v({type:"paren",value:u,output:l.parens?")":"\\)"}),ue("parens");continue}if(u==="["){if(r.nobracket===!0||!se().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else we("brackets");v({type:"bracket",value:u});continue}if(u==="]"){if(r.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){v({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(de("opening","["));v({type:"text",value:u,output:`\\${u}`});continue}ue("brackets");let d=c.value.slice(1);if(c.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),c.value+=u,Ce({value:u}),r.literalBrackets===!1||J.hasRegexChars(d))continue;let S=J.escapeRegex(c.value);if(l.output=l.output.slice(0,-c.value.length),r.literalBrackets===!0){l.output+=S,c.value=S;continue}c.value=`(${o}${S}|${c.value})`,l.output+=c.value;continue}if(u==="{"&&r.nobrace!==!0){we("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),v(d);continue}if(u==="}"){let d=w[w.length-1];if(r.nobrace===!0||!d){v({type:"text",value:u,output:u});continue}let S=")";if(d.dots===!0){let P=i.slice(),F=[];for(let q=P.length-1;q>=0&&(i.pop(),P[q].type!=="brace");q--)P[q].type!=="dots"&&F.unshift(P[q].value);S=jn(F,r),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),F=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=S="\\}",l.output=P;for(let q of F)l.output+=q.output||q.value}v({type:"brace",value:u,output:S}),ue("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,v({type:"text",value:u});continue}if(u===","){let d=u,S=w[w.length-1];S&&j[j.length-1]==="braces"&&(S.comma=!0,d="|"),v({type:"comma",value:u,output:d});continue}if(u==="/"){if(c.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",i.pop(),c=a;continue}v({type:"slash",value:u,output:k});continue}if(u==="."){if(l.braces>0&&c.type==="dot"){c.value==="."&&(c.output=A);let d=w[w.length-1];c.type="dots",c.output+=u,c.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&c.type!=="bos"&&c.type!=="slash"){v({type:"text",value:u,output:A});continue}v({type:"dot",value:u,output:A});continue}if(u==="?"){if(!(c&&c.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("qmark",u);continue}if(c&&c.type==="paren"){let S=$(),P=u;if(S==="<"&&!J.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(S)||S==="<"&&!/<([!=]|\w+>)/.test(se()))&&(P=`\\${u}`),v({type:"text",value:u,output:P});continue}if(r.dot!==!0&&(c.type==="slash"||c.type==="bos")){v({type:"qmark",value:u,output:W});continue}v({type:"qmark",value:u,output:O});continue}if(u==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Se("negate",u);continue}if(r.nonegate!==!0&&l.index===0){xr();continue}}if(u==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("plus",u);continue}if(c&&c.value==="("||r.regex===!1){v({type:"plus",value:u,output:p});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||l.parens>0){v({type:"plus",value:u});continue}v({type:"plus",value:p});continue}if(u==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){v({type:"at",extglob:!0,value:u,output:""});continue}v({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Kn.exec(se());d&&(u+=d[0],l.index+=d[0].length),v({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=M,l.backtrack=!0,l.globstar=!0,z(u);continue}let m=se();if(r.noextglob!==!0&&/^\([^?]/.test(m)){Se("star",u);continue}if(c.type==="star"){if(r.noglobstar===!0){z(u);continue}let d=c.prev,S=d.prev,P=d.type==="slash"||d.type==="bos",F=S&&(S.type==="star"||S.type==="globstar");if(r.bash===!0&&(!P||m[0]&&m[0]!=="/")){v({type:"star",value:u,output:""});continue}let q=l.braces>0&&(d.type==="comma"||d.type==="brace"),Me=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!q&&!Me){v({type:"star",value:u,output:""});continue}for(;m.slice(0,3)==="/**";){let ve=e[l.index+4];if(ve&&ve!=="/")break;m=m.slice(3),z("/**",3)}if(d.type==="bos"&&I()){c.type="globstar",c.value+=u,c.output=E(r),l.output=c.output,l.globstar=!0,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!F&&I()){l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=E(r)+(r.strictSlashes?")":"|$)"),c.value+=u,l.globstar=!0,l.output+=d.output+c.output,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&m[0]==="/"){let ve=m[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=`${E(r)}${k}|${k}${ve})`,c.value+=u,l.output+=d.output+c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&m[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${k}|${E(r)}${k})`,l.output=c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-c.output.length),c.type="globstar",c.output=E(r),c.value+=u,l.output+=c.output,l.globstar=!0,z(u);continue}let L={type:"star",value:u,output:M};if(r.bash===!0){L.output=".*?",(c.type==="bos"||c.type==="slash")&&(L.output=b+L.output),v(L);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&r.regex===!0){L.output=u,v(L);continue}(l.index===l.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(l.output+=x,c.output+=x):r.dot===!0?(l.output+=T,c.output+=T):(l.output+=b,c.output+=b),$()!=="*"&&(l.output+=y,c.output+=y)),v(L)}for(;l.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=J.escapeLast(l.output,"["),ue("brackets")}for(;l.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=J.escapeLast(l.output,"("),ue("parens")}for(;l.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=J.escapeLast(l.output,"{"),ue("braces")}if(r.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&v({type:"maybe_slash",value:"",output:`${k}?`}),l.backtrack===!0){l.output="";for(let m of l.tokens)l.output+=m.output!=null?m.output:m.value,m.suffix&&(l.output+=m.suffix)}return l};nr.fastpaths=(e,t)=>{let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=rr[e]||e;let a=J.isWindows(t),{DOT_LITERAL:i,SLASH_LITERAL:o,ONE_CHAR:h,DOTS_SLASH:g,NO_DOT:f,NO_DOTS:A,NO_DOTS_SLASH:p,STAR:k,START_ANCHOR:y}=Oe.globChars(a),R=r.dot?A:f,_=r.dot?p:f,x=r.capture?"":"?:",T={negated:!1,prefix:""},O=r.bash===!0?".*?":k;r.capture&&(O=`(${O})`);let W=b=>b.noglobstar===!0?O:`(${x}(?:(?!${y}${b.dot?g:i}).)*?)`,G=b=>{switch(b){case"*":return`${R}${h}${O}`;case".*":return`${i}${h}${O}`;case"*.*":return`${R}${O}${i}${h}${O}`;case"*/*":return`${R}${O}${o}${h}${_}${O}`;case"**":return R+W(r);case"**/*":return`(?:${R}${W(r)}${o})?${_}${h}${O}`;case"**/*.*":return`(?:${R}${W(r)}${o})?${_}${O}${i}${h}${O}`;case"**/.*":return`(?:${R}${W(r)}${o})?${i}${h}${O}`;default:{let C=/^(.*?)\.(\w+)$/.exec(b);if(!C)return;let M=G(C[1]);return M?M+i+C[2]:void 0}}},ne=J.removePrefix(e,T),E=G(ne);return E&&r.strictSlashes!==!0&&(E+=`${o}?`),E};tr.exports=nr});var ir=K((ys,ar)=>{"use strict";var Fn=require("path"),Qn=er(),Ye=sr(),ze=be(),Xn=ye(),Zn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,t,r=!1)=>{if(Array.isArray(e)){let f=e.map(p=>D(p,t,r));return p=>{for(let k of f){let y=k(p);if(y)return y}return!1}}let n=Zn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=t||{},a=ze.isWindows(t),i=n?D.compileRe(e,t):D.makeRe(e,t,!1,!0),o=i.state;delete i.state;let h=()=>!1;if(s.ignore){let f=Q(B({},t),{ignore:null,onMatch:null,onResult:null});h=D(s.ignore,f,r)}let g=(f,A=!1)=>{let{isMatch:p,match:k,output:y}=D.test(f,i,t,{glob:e,posix:a}),R={glob:e,state:o,regex:i,posix:a,input:f,output:y,match:k,isMatch:p};return typeof s.onResult=="function"&&s.onResult(R),p===!1?(R.isMatch=!1,A?R:!1):h(f)?(typeof s.onIgnore=="function"&&s.onIgnore(R),R.isMatch=!1,A?R:!1):(typeof s.onMatch=="function"&&s.onMatch(R),A?R:!0)};return r&&(g.state=o),g};D.test=(e,t,r,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let a=r||{},i=a.format||(s?ze.toPosixSlashes:null),o=e===n,h=o&&i?i(e):e;return o===!1&&(h=i?i(e):e,o=h===n),(o===!1||a.capture===!0)&&(a.matchBase===!0||a.basename===!0?o=D.matchBase(e,t,r,s):o=t.exec(h)),{isMatch:Boolean(o),match:o,output:h}};D.matchBase=(e,t,r,n=ze.isWindows(r))=>(t instanceof RegExp?t:D.makeRe(t,r)).test(Fn.basename(e));D.isMatch=(e,t,r)=>D(t,r)(e);D.parse=(e,t)=>Array.isArray(e)?e.map(r=>D.parse(r,t)):Ye(e,Q(B({},t),{fastpaths:!1}));D.scan=(e,t)=>Qn(e,t);D.compileRe=(e,t,r=!1,n=!1)=>{if(r===!0)return e.output;let s=t||{},a=s.contains?"":"^",i=s.contains?"":"$",o=`${a}(?:${e.output})${i}`;e&&e.negated===!0&&(o=`^(?!${o}).*$`);let h=D.toRegex(o,t);return n===!0&&(h.state=e),h};D.makeRe=(e,t={},r=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ye.fastpaths(e,t)),s.output||(s=Ye(e,t)),D.compileRe(s,t,r,n)};D.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};D.constants=Xn;ar.exports=D});var cr=K((bs,or)=>{"use strict";or.exports=ir()});var hr=K((_s,ur)=>{"use strict";var lr=require("util"),pr=Gt(),oe=cr(),Ve=be(),fr=e=>e===""||e==="./",N=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let n=new Set,s=new Set,a=new Set,i=0,o=f=>{a.add(f.output),r&&r.onResult&&r.onResult(f)};for(let f=0;f!n.has(f));if(r&&g.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(f=>f.replace(/\\/g,"")):t}return g};N.match=N;N.matcher=(e,t)=>oe(e,t);N.isMatch=(e,t,r)=>oe(t,r)(e);N.any=N.isMatch;N.not=(e,t,r={})=>{t=[].concat(t).map(String);let n=new Set,s=[],a=o=>{r.onResult&&r.onResult(o),s.push(o.output)},i=N(e,t,Q(B({},r),{onResult:a}));for(let o of s)i.includes(o)||n.add(o);return[...n]};N.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);if(Array.isArray(t))return t.some(n=>N.contains(e,n,r));if(typeof t=="string"){if(fr(e)||fr(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return N.isMatch(e,t,Q(B({},r),{contains:!0}))};N.matchKeys=(e,t,r)=>{if(!Ve.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),t,r),s={};for(let a of n)s[a]=e[a];return s};N.some=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(n.some(i=>a(i)))return!0}return!1};N.every=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(!n.every(i=>a(i)))return!1}return!0};N.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);return[].concat(t).every(n=>oe(n,r)(e))};N.capture=(e,t,r)=>{let n=Ve.isWindows(r),a=oe.makeRe(String(e),Q(B({},r),{capture:!0})).exec(n?Ve.toPosixSlashes(t):t);if(a)return a.slice(1).map(i=>i===void 0?"":i)};N.makeRe=(...e)=>oe.makeRe(...e);N.scan=(...e)=>oe.scan(...e);N.parse=(e,t)=>{let r=[];for(let n of[].concat(e||[]))for(let s of pr(String(n),t))r.push(oe.parse(s,t));return r};N.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:pr(e,t)};N.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,Q(B({},t),{expand:!0}))};ur.exports=N});var gr=K((Es,dr)=>{"use strict";dr.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ar=K((xs,Je)=>{"use strict";var Yn=gr(),mr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,n=()=>{r--,t.length>0&&t.shift()()},s=(o,h,...g)=>{r++;let f=Yn(o,...g);h(f),f.then(n,n)},a=(o,h,...g)=>{rnew Promise(g=>a(o,g,...h));return Object.defineProperties(i,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),i};Je.exports=mr;Je.exports.default=mr});var Vn={};Or(Vn,{default:()=>es});var He=X(require("@yarnpkg/cli")),ae=X(require("@yarnpkg/core")),nt=X(require("@yarnpkg/core")),le=X(require("clipanion")),Ae=class extends He.BaseCommand{constructor(){super(...arguments);this.json=le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=le.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=le.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=le.Option.Rest()}async execute(){let t=await ae.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ae.Project.find(t,this.context.cwd),s=await ae.Cache.find(t);await r.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(r.workspaces);else if(this.workspaces.length===0){if(!n)throw new He.WorkspaceRequiredError(r.cwd,this.context.cwd);a=new Set([n])}else a=new Set(this.workspaces.map(o=>r.getWorkspaceByIdent(nt.structUtils.parseIdent(o))));for(let o of a)for(let h of this.production?["dependencies"]:ae.Manifest.hardDependencies)for(let g of o.manifest.getForScope(h).values()){let f=r.tryWorkspaceByDescriptor(g);f!==null&&a.add(f)}for(let o of r.workspaces)a.has(o)?this.production&&o.manifest.devDependencies.clear():(o.manifest.installConfig=o.manifest.installConfig||{},o.manifest.installConfig.selfReferences=!1,o.manifest.dependencies.clear(),o.manifest.devDependencies.clear(),o.manifest.peerDependencies.clear(),o.manifest.scripts.clear());return(await ae.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async o=>{await r.install({cache:s,report:o,persistProject:!1})})).exitCode()}};Ae.paths=[["workspaces","focus"]],Ae.usage=le.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var st=Ae;var Ie=X(require("@yarnpkg/cli")),ge=X(require("@yarnpkg/core")),Ee=X(require("@yarnpkg/core")),Y=X(require("@yarnpkg/core")),Rr=X(require("@yarnpkg/plugin-git")),U=X(require("clipanion")),Be=X(hr()),yr=X(require("os")),br=X(Ar()),re=X(require("typanion")),xe=class extends Ie.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:re.isOneOf([re.isEnum(["unlimited"]),re.applyCascade(re.isNumber(),[re.isInteger(),re.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!n)throw new Ie.WorkspaceRequiredError(r.cwd,this.context.cwd);await r.restoreInstallState();let s=this.cli.process([this.commandName,...this.args]),a=s.path.length===1&&s.path[0]==="run"&&typeof s.scriptName!="undefined"?s.scriptName:null;if(s.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let i=this.all?r.topLevelWorkspace:n,o=this.since?Array.from(await Rr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:r})):[i,...this.from.length>0?i.getRecursiveWorkspaceChildren():[]],h=E=>Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.from),g=this.from.length>0?o.filter(h):o,f=new Set([...g,...g.map(E=>[...this.recursive?this.since?E.getRecursiveWorkspaceDependents():E.getRecursiveWorkspaceDependencies():E.getRecursiveWorkspaceChildren()]).flat()]),A=[],p=!1;if(a==null?void 0:a.includes(":")){for(let E of r.workspaces)if(E.manifest.scripts.has(a)&&(p=!p,p===!1))break}for(let E of f)a&&!E.manifest.scripts.has(a)&&!p&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(E)).has(a)||a===process.env.npm_lifecycle_event&&E.cwd===n.cwd||this.include.length>0&&!Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.include)||this.exclude.length>0&&Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.exclude)||this.publicOnly&&E.manifest.private===!0||A.push(E);let k=this.parallel?this.jobs==="unlimited"?Infinity:Number(this.jobs)||Math.max(1,(0,yr.cpus)().length/2):1,y=k===1?!1:this.parallel,R=y?this.interlaced:!0,_=(0,br.default)(k),x=new Map,T=new Set,O=0,W=null,G=!1,ne=await Ee.StreamReport.start({configuration:t,stdout:this.context.stdout},async E=>{let b=async(C,{commandIndex:M})=>{if(G)return-1;!y&&this.verbose&&M>1&&E.reportSeparator();let l=zn(C,{configuration:t,verbose:this.verbose,commandIndex:M}),[H,w]=_r(E,{prefix:l,interlaced:R}),[j,c]=_r(E,{prefix:l,interlaced:R});try{this.verbose&&E.reportInfo(null,`${l} Process started`);let u=Date.now(),I=await this.cli.run([this.commandName,...this.args],{cwd:C.cwd,stdout:H,stderr:j})||0;H.end(),j.end(),await w,await c;let $=Date.now();if(this.verbose){let ee=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,$-u,Y.formatUtils.Type.DURATION)}`:"";E.reportInfo(null,`${l} Process exited (exit code ${I})${ee}`)}return I===130&&(G=!0,W=I),I}catch(u){throw H.end(),j.end(),await w,await c,u}};for(let C of A)x.set(C.anchoredLocator.locatorHash,C);for(;x.size>0&&!E.hasErrors();){let C=[];for(let[H,w]of x){if(T.has(w.anchoredDescriptor.descriptorHash))continue;let j=!0;if(this.topological||this.topologicalDev){let c=this.topologicalDev?new Map([...w.manifest.dependencies,...w.manifest.devDependencies]):w.manifest.dependencies;for(let u of c.values()){let I=r.tryWorkspaceByDescriptor(u);if(j=I===null||!x.has(I.anchoredLocator.locatorHash),!j)break}}if(!!j&&(T.add(w.anchoredDescriptor.descriptorHash),C.push(_(async()=>{let c=await b(w,{commandIndex:++O});return x.delete(H),T.delete(w.anchoredDescriptor.descriptorHash),c})),!y))break}if(C.length===0){let H=Array.from(x.values()).map(w=>Y.structUtils.prettyLocator(t,w.anchoredLocator)).join(", ");E.reportError(Ee.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${H})`);return}let l=(await Promise.all(C)).find(H=>H!==0);W===null&&(W=typeof l!="undefined"?1:W),(this.topological||this.topologicalDev)&&typeof l!="undefined"&&E.reportError(Ee.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return W!==null?W:ne.exitCode()}};xe.paths=[["workspaces","foreach"]],xe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});var Er=xe;function _r(e,{prefix:t,interlaced:r}){let n=e.createStreamReporter(t),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let a=new Promise(o=>{n.on("finish",()=>{o(s.active)})});if(r)return[s,a];let i=new Y.miscUtils.BufferStream;return i.pipe(s,{end:!1}),i.on("finish",()=>{s.end()}),[i,a]}function zn(e,{configuration:t,commandIndex:r,verbose:n}){if(!n)return null;let s=Y.structUtils.convertToIdent(e.locator),i=`[${Y.structUtils.stringifyIdent(s)}]:`,o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],h=o[r%o.length];return Y.formatUtils.pretty(t,i,h)}var Jn={commands:[st,Er]},es=Jn;return Vn;})(); +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ +return plugin; +} +}; diff --git a/.yarn/releases/yarn-3.2.3.cjs b/.yarn/releases/yarn-3.2.3.cjs new file mode 100755 index 0000000..12bde03 --- /dev/null +++ b/.yarn/releases/yarn-3.2.3.cjs @@ -0,0 +1,783 @@ +#!/usr/bin/env node +/* eslint-disable */ +//prettier-ignore +(()=>{var age=Object.create,Uh=Object.defineProperty,Age=Object.defineProperties,lge=Object.getOwnPropertyDescriptor,cge=Object.getOwnPropertyDescriptors,uge=Object.getOwnPropertyNames,RE=Object.getOwnPropertySymbols,gge=Object.getPrototypeOf,tQ=Object.prototype.hasOwnProperty,HO=Object.prototype.propertyIsEnumerable;var jO=(r,e,t)=>e in r?Uh(r,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):r[e]=t,N=(r,e)=>{for(var t in e||(e={}))tQ.call(e,t)&&jO(r,t,e[t]);if(RE)for(var t of RE(e))HO.call(e,t)&&jO(r,t,e[t]);return r},te=(r,e)=>Age(r,cge(e)),fge=r=>Uh(r,"__esModule",{value:!0});var Or=(r,e)=>{var t={};for(var i in r)tQ.call(r,i)&&e.indexOf(i)<0&&(t[i]=r[i]);if(r!=null&&RE)for(var i of RE(r))e.indexOf(i)<0&&HO.call(r,i)&&(t[i]=r[i]);return t},hge=(r,e)=>()=>(r&&(e=r(r=0)),e),w=(r,e)=>()=>(e||r((e={exports:{}}).exports,e),e.exports),ft=(r,e)=>{for(var t in e)Uh(r,t,{get:e[t],enumerable:!0})},pge=(r,e,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of uge(e))!tQ.call(r,i)&&i!=="default"&&Uh(r,i,{get:()=>e[i],enumerable:!(t=lge(e,i))||t.enumerable});return r},ge=r=>pge(fge(Uh(r!=null?age(gge(r)):{},"default",r&&r.__esModule&&"default"in r?{get:()=>r.default,enumerable:!0}:{value:r,enumerable:!0})),r);var hM=w((s7e,cM)=>{cM.exports=uM;uM.sync=Dge;var gM=require("fs");function Rge(r,e){var t=e.pathExt!==void 0?e.pathExt:process.env.PATHEXT;if(!t||(t=t.split(";"),t.indexOf("")!==-1))return!0;for(var i=0;i{pM.exports=dM;dM.sync=Fge;var CM=require("fs");function dM(r,e,t){CM.stat(r,function(i,n){t(i,i?!1:mM(n,e))})}function Fge(r,e){return mM(CM.statSync(r),e)}function mM(r,e){return r.isFile()&&Nge(r,e)}function Nge(r,e){var t=r.mode,i=r.uid,n=r.gid,s=e.uid!==void 0?e.uid:process.getuid&&process.getuid(),o=e.gid!==void 0?e.gid:process.getgid&&process.getgid(),a=parseInt("100",8),l=parseInt("010",8),c=parseInt("001",8),u=a|l,g=t&c||t&l&&n===o||t&a&&i===s||t&u&&s===0;return g}});var yM=w((A7e,IM)=>{var a7e=require("fs"),_E;process.platform==="win32"||global.TESTING_WINDOWS?_E=hM():_E=EM();IM.exports=mQ;mQ.sync=Lge;function mQ(r,e,t){if(typeof e=="function"&&(t=e,e={}),!t){if(typeof Promise!="function")throw new TypeError("callback not provided");return new Promise(function(i,n){mQ(r,e||{},function(s,o){s?n(s):i(o)})})}_E(r,e||{},function(i,n){i&&(i.code==="EACCES"||e&&e.ignoreErrors)&&(i=null,n=!1),t(i,n)})}function Lge(r,e){try{return _E.sync(r,e||{})}catch(t){if(e&&e.ignoreErrors||t.code==="EACCES")return!1;throw t}}});var kM=w((l7e,wM)=>{var Zu=process.platform==="win32"||process.env.OSTYPE==="cygwin"||process.env.OSTYPE==="msys",BM=require("path"),Tge=Zu?";":":",bM=yM(),QM=r=>Object.assign(new Error(`not found: ${r}`),{code:"ENOENT"}),SM=(r,e)=>{let t=e.colon||Tge,i=r.match(/\//)||Zu&&r.match(/\\/)?[""]:[...Zu?[process.cwd()]:[],...(e.path||process.env.PATH||"").split(t)],n=Zu?e.pathExt||process.env.PATHEXT||".EXE;.CMD;.BAT;.COM":"",s=Zu?n.split(t):[""];return Zu&&r.indexOf(".")!==-1&&s[0]!==""&&s.unshift(""),{pathEnv:i,pathExt:s,pathExtExe:n}},vM=(r,e,t)=>{typeof e=="function"&&(t=e,e={}),e||(e={});let{pathEnv:i,pathExt:n,pathExtExe:s}=SM(r,e),o=[],a=c=>new Promise((u,g)=>{if(c===i.length)return e.all&&o.length?u(o):g(QM(r));let f=i[c],h=/^".*"$/.test(f)?f.slice(1,-1):f,p=BM.join(h,r),m=!h&&/^\.[\\\/]/.test(r)?r.slice(0,2)+p:p;u(l(m,c,0))}),l=(c,u,g)=>new Promise((f,h)=>{if(g===n.length)return f(a(u+1));let p=n[g];bM(c+p,{pathExt:s},(m,y)=>{if(!m&&y)if(e.all)o.push(c+p);else return f(c+p);return f(l(c,u,g+1))})});return t?a(0).then(c=>t(null,c),t):a(0)},Oge=(r,e)=>{e=e||{};let{pathEnv:t,pathExt:i,pathExtExe:n}=SM(r,e),s=[];for(let o=0;o{"use strict";var xM=(r={})=>{let e=r.env||process.env;return(r.platform||process.platform)!=="win32"?"PATH":Object.keys(e).reverse().find(i=>i.toUpperCase()==="PATH")||"Path"};EQ.exports=xM;EQ.exports.default=xM});var NM=w((u7e,DM)=>{"use strict";var RM=require("path"),Mge=kM(),Uge=PM();function FM(r,e){let t=r.options.env||process.env,i=process.cwd(),n=r.options.cwd!=null,s=n&&process.chdir!==void 0&&!process.chdir.disabled;if(s)try{process.chdir(r.options.cwd)}catch(a){}let o;try{o=Mge.sync(r.command,{path:t[Uge({env:t})],pathExt:e?RM.delimiter:void 0})}catch(a){}finally{s&&process.chdir(i)}return o&&(o=RM.resolve(n?r.options.cwd:"",o)),o}function Kge(r){return FM(r)||FM(r,!0)}DM.exports=Kge});var LM=w((g7e,IQ)=>{"use strict";var yQ=/([()\][%!^"`<>&|;, *?])/g;function Hge(r){return r=r.replace(yQ,"^$1"),r}function jge(r,e){return r=`${r}`,r=r.replace(/(\\*)"/g,'$1$1\\"'),r=r.replace(/(\\*)$/,"$1$1"),r=`"${r}"`,r=r.replace(yQ,"^$1"),e&&(r=r.replace(yQ,"^$1")),r}IQ.exports.command=Hge;IQ.exports.argument=jge});var OM=w((f7e,TM)=>{"use strict";TM.exports=/^#!(.*)/});var UM=w((h7e,MM)=>{"use strict";var Gge=OM();MM.exports=(r="")=>{let e=r.match(Gge);if(!e)return null;let[t,i]=e[0].replace(/#! ?/,"").split(" "),n=t.split("/").pop();return n==="env"?i:i?`${n} ${i}`:n}});var HM=w((p7e,KM)=>{"use strict";var wQ=require("fs"),Yge=UM();function qge(r){let e=150,t=Buffer.alloc(e),i;try{i=wQ.openSync(r,"r"),wQ.readSync(i,t,0,e,0),wQ.closeSync(i)}catch(n){}return Yge(t.toString())}KM.exports=qge});var qM=w((d7e,jM)=>{"use strict";var Jge=require("path"),GM=NM(),YM=LM(),Wge=HM(),zge=process.platform==="win32",_ge=/\.(?:com|exe)$/i,Vge=/node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;function Xge(r){r.file=GM(r);let e=r.file&&Wge(r.file);return e?(r.args.unshift(r.file),r.command=e,GM(r)):r.file}function Zge(r){if(!zge)return r;let e=Xge(r),t=!_ge.test(e);if(r.options.forceShell||t){let i=Vge.test(e);r.command=Jge.normalize(r.command),r.command=YM.command(r.command),r.args=r.args.map(s=>YM.argument(s,i));let n=[r.command].concat(r.args).join(" ");r.args=["/d","/s","/c",`"${n}"`],r.command=process.env.comspec||"cmd.exe",r.options.windowsVerbatimArguments=!0}return r}function $ge(r,e,t){e&&!Array.isArray(e)&&(t=e,e=null),e=e?e.slice(0):[],t=Object.assign({},t);let i={command:r,args:e,options:t,file:void 0,original:{command:r,args:e}};return t.shell?i:Zge(i)}jM.exports=$ge});var zM=w((C7e,JM)=>{"use strict";var BQ=process.platform==="win32";function bQ(r,e){return Object.assign(new Error(`${e} ${r.command} ENOENT`),{code:"ENOENT",errno:"ENOENT",syscall:`${e} ${r.command}`,path:r.command,spawnargs:r.args})}function efe(r,e){if(!BQ)return;let t=r.emit;r.emit=function(i,n){if(i==="exit"){let s=WM(n,e,"spawn");if(s)return t.call(r,"error",s)}return t.apply(r,arguments)}}function WM(r,e){return BQ&&r===1&&!e.file?bQ(e.original,"spawn"):null}function tfe(r,e){return BQ&&r===1&&!e.file?bQ(e.original,"spawnSync"):null}JM.exports={hookChildProcess:efe,verifyENOENT:WM,verifyENOENTSync:tfe,notFoundError:bQ}});var vQ=w((m7e,$u)=>{"use strict";var _M=require("child_process"),QQ=qM(),SQ=zM();function VM(r,e,t){let i=QQ(r,e,t),n=_M.spawn(i.command,i.args,i.options);return SQ.hookChildProcess(n,i),n}function rfe(r,e,t){let i=QQ(r,e,t),n=_M.spawnSync(i.command,i.args,i.options);return n.error=n.error||SQ.verifyENOENTSync(n.status,i),n}$u.exports=VM;$u.exports.spawn=VM;$u.exports.sync=rfe;$u.exports._parse=QQ;$u.exports._enoent=SQ});var ZM=w((E7e,XM)=>{"use strict";function ife(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function uc(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,uc)}ife(uc,Error);uc.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g>",le=me(">>",!1),fe=">&",gt=me(">&",!1),Ht=">",Mt=me(">",!1),Ei="<<<",jt=me("<<<",!1),Qr="<&",Oi=me("<&",!1),$s="<",Hn=me("<",!1),jn=function(C){return{type:"argument",segments:[].concat(...C)}},Sr=function(C){return C},Gn="$'",fs=me("$'",!1),Qa="'",RA=me("'",!1),Lu=function(C){return[{type:"text",text:C}]},hs='""',FA=me('""',!1),Sa=function(){return{type:"text",text:""}},Tu='"',NA=me('"',!1),LA=function(C){return C},vr=function(C){return{type:"arithmetic",arithmetic:C,quoted:!0}},_l=function(C){return{type:"shell",shell:C,quoted:!0}},Ou=function(C){return te(N({type:"variable"},C),{quoted:!0})},Po=function(C){return{type:"text",text:C}},Mu=function(C){return{type:"arithmetic",arithmetic:C,quoted:!1}},vh=function(C){return{type:"shell",shell:C,quoted:!1}},kh=function(C){return te(N({type:"variable"},C),{quoted:!1})},Dr=function(C){return{type:"glob",pattern:C}},Ae=/^[^']/,Do=_e(["'"],!0,!1),Yn=function(C){return C.join("")},Uu=/^[^$"]/,St=_e(["$",'"'],!0,!1),Vl=`\\ +`,qn=me(`\\ +`,!1),ps=function(){return""},ds="\\",pt=me("\\",!1),Ro=/^[\\$"`]/,lt=_e(["\\","$",'"',"`"],!1,!1),mn=function(C){return C},S="\\a",Tt=me("\\a",!1),Ku=function(){return"a"},Xl="\\b",xh=me("\\b",!1),Ph=function(){return"\b"},Dh=/^[Ee]/,Rh=_e(["E","e"],!1,!1),Fh=function(){return""},j="\\f",wt=me("\\f",!1),TA=function(){return"\f"},$i="\\n",Zl=me("\\n",!1),$e=function(){return` +`},va="\\r",Hu=me("\\r",!1),wE=function(){return"\r"},Nh="\\t",BE=me("\\t",!1),gr=function(){return" "},Jn="\\v",$l=me("\\v",!1),Lh=function(){return"\v"},eo=/^[\\'"?]/,ka=_e(["\\","'",'"',"?"],!1,!1),En=function(C){return String.fromCharCode(parseInt(C,16))},Oe="\\x",ju=me("\\x",!1),ec="\\u",to=me("\\u",!1),tc="\\U",OA=me("\\U",!1),Gu=function(C){return String.fromCodePoint(parseInt(C,16))},Yu=/^[0-7]/,xa=_e([["0","7"]],!1,!1),Pa=/^[0-9a-fA-f]/,nt=_e([["0","9"],["a","f"],["A","f"]],!1,!1),Fo=ot(),MA="-",rc=me("-",!1),ro="+",ic=me("+",!1),bE=".",Th=me(".",!1),qu=function(C,Q,F){return{type:"number",value:(C==="-"?-1:1)*parseFloat(Q.join("")+"."+F.join(""))}},Oh=function(C,Q){return{type:"number",value:(C==="-"?-1:1)*parseInt(Q.join(""))}},QE=function(C){return N({type:"variable"},C)},nc=function(C){return{type:"variable",name:C}},SE=function(C){return C},Ju="*",UA=me("*",!1),Tr="/",vE=me("/",!1),io=function(C,Q,F){return{type:Q==="*"?"multiplication":"division",right:F}},no=function(C,Q){return Q.reduce((F,K)=>N({left:F},K),C)},Wu=function(C,Q,F){return{type:Q==="+"?"addition":"subtraction",right:F}},KA="$((",R=me("$((",!1),G="))",Ce=me("))",!1),je=function(C){return C},Te="$(",Xe=me("$(",!1),Et=function(C){return C},Rt="${",Wn=me("${",!1),Mb=":-",fO=me(":-",!1),hO=function(C,Q){return{name:C,defaultValue:Q}},Ub=":-}",pO=me(":-}",!1),dO=function(C){return{name:C,defaultValue:[]}},Kb=":+",CO=me(":+",!1),mO=function(C,Q){return{name:C,alternativeValue:Q}},Hb=":+}",EO=me(":+}",!1),IO=function(C){return{name:C,alternativeValue:[]}},jb=function(C){return{name:C}},yO="$",wO=me("$",!1),BO=function(C){return e.isGlobPattern(C)},bO=function(C){return C},Gb=/^[a-zA-Z0-9_]/,Yb=_e([["a","z"],["A","Z"],["0","9"],"_"],!1,!1),qb=function(){return M()},Jb=/^[$@*?#a-zA-Z0-9_\-]/,Wb=_e(["$","@","*","?","#",["a","z"],["A","Z"],["0","9"],"_","-"],!1,!1),QO=/^[(){}<>$|&; \t"']/,zu=_e(["(",")","{","}","<",">","$","|","&",";"," "," ",'"',"'"],!1,!1),zb=/^[<>&; \t"']/,_b=_e(["<",">","&",";"," "," ",'"',"'"],!1,!1),kE=/^[ \t]/,xE=_e([" "," "],!1,!1),B=0,Ke=0,HA=[{line:1,column:1}],d=0,E=[],I=0,D;if("startRule"in e){if(!(e.startRule in i))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');n=i[e.startRule]}function M(){return r.substring(Ke,B)}function _(){return yt(Ke,B)}function ie(C,Q){throw Q=Q!==void 0?Q:yt(Ke,B),Mi([ut(C)],r.substring(Ke,B),Q)}function we(C,Q){throw Q=Q!==void 0?Q:yt(Ke,B),zn(C,Q)}function me(C,Q){return{type:"literal",text:C,ignoreCase:Q}}function _e(C,Q,F){return{type:"class",parts:C,inverted:Q,ignoreCase:F}}function ot(){return{type:"any"}}function Bt(){return{type:"end"}}function ut(C){return{type:"other",description:C}}function st(C){var Q=HA[C],F;if(Q)return Q;for(F=C-1;!HA[F];)F--;for(Q=HA[F],Q={line:Q.line,column:Q.column};Fd&&(d=B,E=[]),E.push(C))}function zn(C,Q){return new uc(C,null,null,Q)}function Mi(C,Q,F){return new uc(uc.buildMessage(C,Q),C,Q,F)}function jA(){var C,Q;return C=B,Q=Yr(),Q===t&&(Q=null),Q!==t&&(Ke=C,Q=s(Q)),C=Q,C}function Yr(){var C,Q,F,K,ue;if(C=B,Q=qr(),Q!==t){for(F=[],K=qe();K!==t;)F.push(K),K=qe();F!==t?(K=Da(),K!==t?(ue=Cs(),ue===t&&(ue=null),ue!==t?(Ke=C,Q=o(Q,K,ue),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)}else B=C,C=t;if(C===t)if(C=B,Q=qr(),Q!==t){for(F=[],K=qe();K!==t;)F.push(K),K=qe();F!==t?(K=Da(),K===t&&(K=null),K!==t?(Ke=C,Q=a(Q,K),C=Q):(B=C,C=t)):(B=C,C=t)}else B=C,C=t;return C}function Cs(){var C,Q,F,K,ue;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t)if(F=Yr(),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();K!==t?(Ke=C,Q=l(F),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t;return C}function Da(){var C;return r.charCodeAt(B)===59?(C=c,B++):(C=t,I===0&&ke(u)),C===t&&(r.charCodeAt(B)===38?(C=g,B++):(C=t,I===0&&ke(f))),C}function qr(){var C,Q,F;return C=B,Q=SO(),Q!==t?(F=Yue(),F===t&&(F=null),F!==t?(Ke=C,Q=h(Q,F),C=Q):(B=C,C=t)):(B=C,C=t),C}function Yue(){var C,Q,F,K,ue,De,Ct;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t)if(F=que(),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();if(K!==t)if(ue=qr(),ue!==t){for(De=[],Ct=qe();Ct!==t;)De.push(Ct),Ct=qe();De!==t?(Ke=C,Q=p(F,ue),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t;return C}function que(){var C;return r.substr(B,2)===m?(C=m,B+=2):(C=t,I===0&&ke(y)),C===t&&(r.substr(B,2)===b?(C=b,B+=2):(C=t,I===0&&ke(v))),C}function SO(){var C,Q,F;return C=B,Q=zue(),Q!==t?(F=Jue(),F===t&&(F=null),F!==t?(Ke=C,Q=k(Q,F),C=Q):(B=C,C=t)):(B=C,C=t),C}function Jue(){var C,Q,F,K,ue,De,Ct;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t)if(F=Wue(),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();if(K!==t)if(ue=SO(),ue!==t){for(De=[],Ct=qe();Ct!==t;)De.push(Ct),Ct=qe();De!==t?(Ke=C,Q=T(F,ue),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t;return C}function Wue(){var C;return r.substr(B,2)===Y?(C=Y,B+=2):(C=t,I===0&&ke(q)),C===t&&(r.charCodeAt(B)===124?(C=$,B++):(C=t,I===0&&ke(z))),C}function PE(){var C,Q,F,K,ue,De;if(C=B,Q=MO(),Q!==t)if(r.charCodeAt(B)===61?(F=ne,B++):(F=t,I===0&&ke(ee)),F!==t)if(K=xO(),K!==t){for(ue=[],De=qe();De!==t;)ue.push(De),De=qe();ue!==t?(Ke=C,Q=A(Q,K),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t;else B=C,C=t;if(C===t)if(C=B,Q=MO(),Q!==t)if(r.charCodeAt(B)===61?(F=ne,B++):(F=t,I===0&&ke(ee)),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();K!==t?(Ke=C,Q=oe(Q),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t;return C}function zue(){var C,Q,F,K,ue,De,Ct,bt,$r,Ii,ms;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t)if(r.charCodeAt(B)===40?(F=ce,B++):(F=t,I===0&&ke(Z)),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();if(K!==t)if(ue=Yr(),ue!==t){for(De=[],Ct=qe();Ct!==t;)De.push(Ct),Ct=qe();if(De!==t)if(r.charCodeAt(B)===41?(Ct=O,B++):(Ct=t,I===0&&ke(L)),Ct!==t){for(bt=[],$r=qe();$r!==t;)bt.push($r),$r=qe();if(bt!==t){for($r=[],Ii=Mh();Ii!==t;)$r.push(Ii),Ii=Mh();if($r!==t){for(Ii=[],ms=qe();ms!==t;)Ii.push(ms),ms=qe();Ii!==t?(Ke=C,Q=de(ue,$r),C=Q):(B=C,C=t)}else B=C,C=t}else B=C,C=t}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t;if(C===t){for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t)if(r.charCodeAt(B)===123?(F=Be,B++):(F=t,I===0&&ke(Ge)),F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();if(K!==t)if(ue=Yr(),ue!==t){for(De=[],Ct=qe();Ct!==t;)De.push(Ct),Ct=qe();if(De!==t)if(r.charCodeAt(B)===125?(Ct=re,B++):(Ct=t,I===0&&ke(se)),Ct!==t){for(bt=[],$r=qe();$r!==t;)bt.push($r),$r=qe();if(bt!==t){for($r=[],Ii=Mh();Ii!==t;)$r.push(Ii),Ii=Mh();if($r!==t){for(Ii=[],ms=qe();ms!==t;)Ii.push(ms),ms=qe();Ii!==t?(Ke=C,Q=be(ue,$r),C=Q):(B=C,C=t)}else B=C,C=t}else B=C,C=t}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t}else B=C,C=t;else B=C,C=t;if(C===t){for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t){for(F=[],K=PE();K!==t;)F.push(K),K=PE();if(F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();if(K!==t){if(ue=[],De=kO(),De!==t)for(;De!==t;)ue.push(De),De=kO();else ue=t;if(ue!==t){for(De=[],Ct=qe();Ct!==t;)De.push(Ct),Ct=qe();De!==t?(Ke=C,Q=he(F,ue),C=Q):(B=C,C=t)}else B=C,C=t}else B=C,C=t}else B=C,C=t}else B=C,C=t;if(C===t){for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t){if(F=[],K=PE(),K!==t)for(;K!==t;)F.push(K),K=PE();else F=t;if(F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();K!==t?(Ke=C,Q=Fe(F),C=Q):(B=C,C=t)}else B=C,C=t}else B=C,C=t}}}return C}function vO(){var C,Q,F,K,ue;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t){if(F=[],K=DE(),K!==t)for(;K!==t;)F.push(K),K=DE();else F=t;if(F!==t){for(K=[],ue=qe();ue!==t;)K.push(ue),ue=qe();K!==t?(Ke=C,Q=Ue(F),C=Q):(B=C,C=t)}else B=C,C=t}else B=C,C=t;return C}function kO(){var C,Q,F;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();if(Q!==t?(F=Mh(),F!==t?(Ke=C,Q=xe(F),C=Q):(B=C,C=t)):(B=C,C=t),C===t){for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();Q!==t?(F=DE(),F!==t?(Ke=C,Q=xe(F),C=Q):(B=C,C=t)):(B=C,C=t)}return C}function Mh(){var C,Q,F,K,ue;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();return Q!==t?(ve.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(pe)),F===t&&(F=null),F!==t?(K=_ue(),K!==t?(ue=DE(),ue!==t?(Ke=C,Q=V(F,K,ue),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C}function _ue(){var C;return r.substr(B,2)===Qe?(C=Qe,B+=2):(C=t,I===0&&ke(le)),C===t&&(r.substr(B,2)===fe?(C=fe,B+=2):(C=t,I===0&&ke(gt)),C===t&&(r.charCodeAt(B)===62?(C=Ht,B++):(C=t,I===0&&ke(Mt)),C===t&&(r.substr(B,3)===Ei?(C=Ei,B+=3):(C=t,I===0&&ke(jt)),C===t&&(r.substr(B,2)===Qr?(C=Qr,B+=2):(C=t,I===0&&ke(Oi)),C===t&&(r.charCodeAt(B)===60?(C=$s,B++):(C=t,I===0&&ke(Hn))))))),C}function DE(){var C,Q,F;for(C=B,Q=[],F=qe();F!==t;)Q.push(F),F=qe();return Q!==t?(F=xO(),F!==t?(Ke=C,Q=xe(F),C=Q):(B=C,C=t)):(B=C,C=t),C}function xO(){var C,Q,F;if(C=B,Q=[],F=PO(),F!==t)for(;F!==t;)Q.push(F),F=PO();else Q=t;return Q!==t&&(Ke=C,Q=jn(Q)),C=Q,C}function PO(){var C,Q;return C=B,Q=Vue(),Q!==t&&(Ke=C,Q=Sr(Q)),C=Q,C===t&&(C=B,Q=Xue(),Q!==t&&(Ke=C,Q=Sr(Q)),C=Q,C===t&&(C=B,Q=Zue(),Q!==t&&(Ke=C,Q=Sr(Q)),C=Q,C===t&&(C=B,Q=$ue(),Q!==t&&(Ke=C,Q=Sr(Q)),C=Q))),C}function Vue(){var C,Q,F,K;return C=B,r.substr(B,2)===Gn?(Q=Gn,B+=2):(Q=t,I===0&&ke(fs)),Q!==t?(F=rge(),F!==t?(r.charCodeAt(B)===39?(K=Qa,B++):(K=t,I===0&&ke(RA)),K!==t?(Ke=C,Q=Lu(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C}function Xue(){var C,Q,F,K;return C=B,r.charCodeAt(B)===39?(Q=Qa,B++):(Q=t,I===0&&ke(RA)),Q!==t?(F=ege(),F!==t?(r.charCodeAt(B)===39?(K=Qa,B++):(K=t,I===0&&ke(RA)),K!==t?(Ke=C,Q=Lu(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C}function Zue(){var C,Q,F,K;if(C=B,r.substr(B,2)===hs?(Q=hs,B+=2):(Q=t,I===0&&ke(FA)),Q!==t&&(Ke=C,Q=Sa()),C=Q,C===t)if(C=B,r.charCodeAt(B)===34?(Q=Tu,B++):(Q=t,I===0&&ke(NA)),Q!==t){for(F=[],K=DO();K!==t;)F.push(K),K=DO();F!==t?(r.charCodeAt(B)===34?(K=Tu,B++):(K=t,I===0&&ke(NA)),K!==t?(Ke=C,Q=LA(F),C=Q):(B=C,C=t)):(B=C,C=t)}else B=C,C=t;return C}function $ue(){var C,Q,F;if(C=B,Q=[],F=RO(),F!==t)for(;F!==t;)Q.push(F),F=RO();else Q=t;return Q!==t&&(Ke=C,Q=LA(Q)),C=Q,C}function DO(){var C,Q;return C=B,Q=TO(),Q!==t&&(Ke=C,Q=vr(Q)),C=Q,C===t&&(C=B,Q=OO(),Q!==t&&(Ke=C,Q=_l(Q)),C=Q,C===t&&(C=B,Q=$b(),Q!==t&&(Ke=C,Q=Ou(Q)),C=Q,C===t&&(C=B,Q=tge(),Q!==t&&(Ke=C,Q=Po(Q)),C=Q))),C}function RO(){var C,Q;return C=B,Q=TO(),Q!==t&&(Ke=C,Q=Mu(Q)),C=Q,C===t&&(C=B,Q=OO(),Q!==t&&(Ke=C,Q=vh(Q)),C=Q,C===t&&(C=B,Q=$b(),Q!==t&&(Ke=C,Q=kh(Q)),C=Q,C===t&&(C=B,Q=sge(),Q!==t&&(Ke=C,Q=Dr(Q)),C=Q,C===t&&(C=B,Q=nge(),Q!==t&&(Ke=C,Q=Po(Q)),C=Q)))),C}function ege(){var C,Q,F;for(C=B,Q=[],Ae.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Do));F!==t;)Q.push(F),Ae.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Do));return Q!==t&&(Ke=C,Q=Yn(Q)),C=Q,C}function tge(){var C,Q,F;if(C=B,Q=[],F=FO(),F===t&&(Uu.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(St))),F!==t)for(;F!==t;)Q.push(F),F=FO(),F===t&&(Uu.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(St)));else Q=t;return Q!==t&&(Ke=C,Q=Yn(Q)),C=Q,C}function FO(){var C,Q,F;return C=B,r.substr(B,2)===Vl?(Q=Vl,B+=2):(Q=t,I===0&&ke(qn)),Q!==t&&(Ke=C,Q=ps()),C=Q,C===t&&(C=B,r.charCodeAt(B)===92?(Q=ds,B++):(Q=t,I===0&&ke(pt)),Q!==t?(Ro.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(lt)),F!==t?(Ke=C,Q=mn(F),C=Q):(B=C,C=t)):(B=C,C=t)),C}function rge(){var C,Q,F;for(C=B,Q=[],F=NO(),F===t&&(Ae.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Do)));F!==t;)Q.push(F),F=NO(),F===t&&(Ae.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Do)));return Q!==t&&(Ke=C,Q=Yn(Q)),C=Q,C}function NO(){var C,Q,F;return C=B,r.substr(B,2)===S?(Q=S,B+=2):(Q=t,I===0&&ke(Tt)),Q!==t&&(Ke=C,Q=Ku()),C=Q,C===t&&(C=B,r.substr(B,2)===Xl?(Q=Xl,B+=2):(Q=t,I===0&&ke(xh)),Q!==t&&(Ke=C,Q=Ph()),C=Q,C===t&&(C=B,r.charCodeAt(B)===92?(Q=ds,B++):(Q=t,I===0&&ke(pt)),Q!==t?(Dh.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Rh)),F!==t?(Ke=C,Q=Fh(),C=Q):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===j?(Q=j,B+=2):(Q=t,I===0&&ke(wt)),Q!==t&&(Ke=C,Q=TA()),C=Q,C===t&&(C=B,r.substr(B,2)===$i?(Q=$i,B+=2):(Q=t,I===0&&ke(Zl)),Q!==t&&(Ke=C,Q=$e()),C=Q,C===t&&(C=B,r.substr(B,2)===va?(Q=va,B+=2):(Q=t,I===0&&ke(Hu)),Q!==t&&(Ke=C,Q=wE()),C=Q,C===t&&(C=B,r.substr(B,2)===Nh?(Q=Nh,B+=2):(Q=t,I===0&&ke(BE)),Q!==t&&(Ke=C,Q=gr()),C=Q,C===t&&(C=B,r.substr(B,2)===Jn?(Q=Jn,B+=2):(Q=t,I===0&&ke($l)),Q!==t&&(Ke=C,Q=Lh()),C=Q,C===t&&(C=B,r.charCodeAt(B)===92?(Q=ds,B++):(Q=t,I===0&&ke(pt)),Q!==t?(eo.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(ka)),F!==t?(Ke=C,Q=mn(F),C=Q):(B=C,C=t)):(B=C,C=t),C===t&&(C=ige()))))))))),C}function ige(){var C,Q,F,K,ue,De,Ct,bt,$r,Ii,ms,eQ;return C=B,r.charCodeAt(B)===92?(Q=ds,B++):(Q=t,I===0&&ke(pt)),Q!==t?(F=Vb(),F!==t?(Ke=C,Q=En(F),C=Q):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===Oe?(Q=Oe,B+=2):(Q=t,I===0&&ke(ju)),Q!==t?(F=B,K=B,ue=Vb(),ue!==t?(De=_n(),De!==t?(ue=[ue,De],K=ue):(B=K,K=t)):(B=K,K=t),K===t&&(K=Vb()),K!==t?F=r.substring(F,B):F=K,F!==t?(Ke=C,Q=En(F),C=Q):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===ec?(Q=ec,B+=2):(Q=t,I===0&&ke(to)),Q!==t?(F=B,K=B,ue=_n(),ue!==t?(De=_n(),De!==t?(Ct=_n(),Ct!==t?(bt=_n(),bt!==t?(ue=[ue,De,Ct,bt],K=ue):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t),K!==t?F=r.substring(F,B):F=K,F!==t?(Ke=C,Q=En(F),C=Q):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===tc?(Q=tc,B+=2):(Q=t,I===0&&ke(OA)),Q!==t?(F=B,K=B,ue=_n(),ue!==t?(De=_n(),De!==t?(Ct=_n(),Ct!==t?(bt=_n(),bt!==t?($r=_n(),$r!==t?(Ii=_n(),Ii!==t?(ms=_n(),ms!==t?(eQ=_n(),eQ!==t?(ue=[ue,De,Ct,bt,$r,Ii,ms,eQ],K=ue):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t)):(B=K,K=t),K!==t?F=r.substring(F,B):F=K,F!==t?(Ke=C,Q=Gu(F),C=Q):(B=C,C=t)):(B=C,C=t)))),C}function Vb(){var C;return Yu.test(r.charAt(B))?(C=r.charAt(B),B++):(C=t,I===0&&ke(xa)),C}function _n(){var C;return Pa.test(r.charAt(B))?(C=r.charAt(B),B++):(C=t,I===0&&ke(nt)),C}function nge(){var C,Q,F,K,ue;if(C=B,Q=[],F=B,r.charCodeAt(B)===92?(K=ds,B++):(K=t,I===0&&ke(pt)),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t),F===t&&(F=B,K=B,I++,ue=UO(),I--,ue===t?K=void 0:(B=K,K=t),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t)),F!==t)for(;F!==t;)Q.push(F),F=B,r.charCodeAt(B)===92?(K=ds,B++):(K=t,I===0&&ke(pt)),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t),F===t&&(F=B,K=B,I++,ue=UO(),I--,ue===t?K=void 0:(B=K,K=t),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t));else Q=t;return Q!==t&&(Ke=C,Q=Yn(Q)),C=Q,C}function Xb(){var C,Q,F,K,ue,De;if(C=B,r.charCodeAt(B)===45?(Q=MA,B++):(Q=t,I===0&&ke(rc)),Q===t&&(r.charCodeAt(B)===43?(Q=ro,B++):(Q=t,I===0&&ke(ic))),Q===t&&(Q=null),Q!==t){if(F=[],ve.test(r.charAt(B))?(K=r.charAt(B),B++):(K=t,I===0&&ke(pe)),K!==t)for(;K!==t;)F.push(K),ve.test(r.charAt(B))?(K=r.charAt(B),B++):(K=t,I===0&&ke(pe));else F=t;if(F!==t)if(r.charCodeAt(B)===46?(K=bE,B++):(K=t,I===0&&ke(Th)),K!==t){if(ue=[],ve.test(r.charAt(B))?(De=r.charAt(B),B++):(De=t,I===0&&ke(pe)),De!==t)for(;De!==t;)ue.push(De),ve.test(r.charAt(B))?(De=r.charAt(B),B++):(De=t,I===0&&ke(pe));else ue=t;ue!==t?(Ke=C,Q=qu(Q,F,ue),C=Q):(B=C,C=t)}else B=C,C=t;else B=C,C=t}else B=C,C=t;if(C===t){if(C=B,r.charCodeAt(B)===45?(Q=MA,B++):(Q=t,I===0&&ke(rc)),Q===t&&(r.charCodeAt(B)===43?(Q=ro,B++):(Q=t,I===0&&ke(ic))),Q===t&&(Q=null),Q!==t){if(F=[],ve.test(r.charAt(B))?(K=r.charAt(B),B++):(K=t,I===0&&ke(pe)),K!==t)for(;K!==t;)F.push(K),ve.test(r.charAt(B))?(K=r.charAt(B),B++):(K=t,I===0&&ke(pe));else F=t;F!==t?(Ke=C,Q=Oh(Q,F),C=Q):(B=C,C=t)}else B=C,C=t;if(C===t&&(C=B,Q=$b(),Q!==t&&(Ke=C,Q=QE(Q)),C=Q,C===t&&(C=B,Q=sc(),Q!==t&&(Ke=C,Q=nc(Q)),C=Q,C===t)))if(C=B,r.charCodeAt(B)===40?(Q=ce,B++):(Q=t,I===0&&ke(Z)),Q!==t){for(F=[],K=qe();K!==t;)F.push(K),K=qe();if(F!==t)if(K=LO(),K!==t){for(ue=[],De=qe();De!==t;)ue.push(De),De=qe();ue!==t?(r.charCodeAt(B)===41?(De=O,B++):(De=t,I===0&&ke(L)),De!==t?(Ke=C,Q=SE(K),C=Q):(B=C,C=t)):(B=C,C=t)}else B=C,C=t;else B=C,C=t}else B=C,C=t}return C}function Zb(){var C,Q,F,K,ue,De,Ct,bt;if(C=B,Q=Xb(),Q!==t){for(F=[],K=B,ue=[],De=qe();De!==t;)ue.push(De),De=qe();if(ue!==t)if(r.charCodeAt(B)===42?(De=Ju,B++):(De=t,I===0&&ke(UA)),De===t&&(r.charCodeAt(B)===47?(De=Tr,B++):(De=t,I===0&&ke(vE))),De!==t){for(Ct=[],bt=qe();bt!==t;)Ct.push(bt),bt=qe();Ct!==t?(bt=Xb(),bt!==t?(Ke=K,ue=io(Q,De,bt),K=ue):(B=K,K=t)):(B=K,K=t)}else B=K,K=t;else B=K,K=t;for(;K!==t;){for(F.push(K),K=B,ue=[],De=qe();De!==t;)ue.push(De),De=qe();if(ue!==t)if(r.charCodeAt(B)===42?(De=Ju,B++):(De=t,I===0&&ke(UA)),De===t&&(r.charCodeAt(B)===47?(De=Tr,B++):(De=t,I===0&&ke(vE))),De!==t){for(Ct=[],bt=qe();bt!==t;)Ct.push(bt),bt=qe();Ct!==t?(bt=Xb(),bt!==t?(Ke=K,ue=io(Q,De,bt),K=ue):(B=K,K=t)):(B=K,K=t)}else B=K,K=t;else B=K,K=t}F!==t?(Ke=C,Q=no(Q,F),C=Q):(B=C,C=t)}else B=C,C=t;return C}function LO(){var C,Q,F,K,ue,De,Ct,bt;if(C=B,Q=Zb(),Q!==t){for(F=[],K=B,ue=[],De=qe();De!==t;)ue.push(De),De=qe();if(ue!==t)if(r.charCodeAt(B)===43?(De=ro,B++):(De=t,I===0&&ke(ic)),De===t&&(r.charCodeAt(B)===45?(De=MA,B++):(De=t,I===0&&ke(rc))),De!==t){for(Ct=[],bt=qe();bt!==t;)Ct.push(bt),bt=qe();Ct!==t?(bt=Zb(),bt!==t?(Ke=K,ue=Wu(Q,De,bt),K=ue):(B=K,K=t)):(B=K,K=t)}else B=K,K=t;else B=K,K=t;for(;K!==t;){for(F.push(K),K=B,ue=[],De=qe();De!==t;)ue.push(De),De=qe();if(ue!==t)if(r.charCodeAt(B)===43?(De=ro,B++):(De=t,I===0&&ke(ic)),De===t&&(r.charCodeAt(B)===45?(De=MA,B++):(De=t,I===0&&ke(rc))),De!==t){for(Ct=[],bt=qe();bt!==t;)Ct.push(bt),bt=qe();Ct!==t?(bt=Zb(),bt!==t?(Ke=K,ue=Wu(Q,De,bt),K=ue):(B=K,K=t)):(B=K,K=t)}else B=K,K=t;else B=K,K=t}F!==t?(Ke=C,Q=no(Q,F),C=Q):(B=C,C=t)}else B=C,C=t;return C}function TO(){var C,Q,F,K,ue,De;if(C=B,r.substr(B,3)===KA?(Q=KA,B+=3):(Q=t,I===0&&ke(R)),Q!==t){for(F=[],K=qe();K!==t;)F.push(K),K=qe();if(F!==t)if(K=LO(),K!==t){for(ue=[],De=qe();De!==t;)ue.push(De),De=qe();ue!==t?(r.substr(B,2)===G?(De=G,B+=2):(De=t,I===0&&ke(Ce)),De!==t?(Ke=C,Q=je(K),C=Q):(B=C,C=t)):(B=C,C=t)}else B=C,C=t;else B=C,C=t}else B=C,C=t;return C}function OO(){var C,Q,F,K;return C=B,r.substr(B,2)===Te?(Q=Te,B+=2):(Q=t,I===0&&ke(Xe)),Q!==t?(F=Yr(),F!==t?(r.charCodeAt(B)===41?(K=O,B++):(K=t,I===0&&ke(L)),K!==t?(Ke=C,Q=Et(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C}function $b(){var C,Q,F,K,ue,De;return C=B,r.substr(B,2)===Rt?(Q=Rt,B+=2):(Q=t,I===0&&ke(Wn)),Q!==t?(F=sc(),F!==t?(r.substr(B,2)===Mb?(K=Mb,B+=2):(K=t,I===0&&ke(fO)),K!==t?(ue=vO(),ue!==t?(r.charCodeAt(B)===125?(De=re,B++):(De=t,I===0&&ke(se)),De!==t?(Ke=C,Q=hO(F,ue),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===Rt?(Q=Rt,B+=2):(Q=t,I===0&&ke(Wn)),Q!==t?(F=sc(),F!==t?(r.substr(B,3)===Ub?(K=Ub,B+=3):(K=t,I===0&&ke(pO)),K!==t?(Ke=C,Q=dO(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===Rt?(Q=Rt,B+=2):(Q=t,I===0&&ke(Wn)),Q!==t?(F=sc(),F!==t?(r.substr(B,2)===Kb?(K=Kb,B+=2):(K=t,I===0&&ke(CO)),K!==t?(ue=vO(),ue!==t?(r.charCodeAt(B)===125?(De=re,B++):(De=t,I===0&&ke(se)),De!==t?(Ke=C,Q=mO(F,ue),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===Rt?(Q=Rt,B+=2):(Q=t,I===0&&ke(Wn)),Q!==t?(F=sc(),F!==t?(r.substr(B,3)===Hb?(K=Hb,B+=3):(K=t,I===0&&ke(EO)),K!==t?(Ke=C,Q=IO(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.substr(B,2)===Rt?(Q=Rt,B+=2):(Q=t,I===0&&ke(Wn)),Q!==t?(F=sc(),F!==t?(r.charCodeAt(B)===125?(K=re,B++):(K=t,I===0&&ke(se)),K!==t?(Ke=C,Q=jb(F),C=Q):(B=C,C=t)):(B=C,C=t)):(B=C,C=t),C===t&&(C=B,r.charCodeAt(B)===36?(Q=yO,B++):(Q=t,I===0&&ke(wO)),Q!==t?(F=sc(),F!==t?(Ke=C,Q=jb(F),C=Q):(B=C,C=t)):(B=C,C=t)))))),C}function sge(){var C,Q,F;return C=B,Q=oge(),Q!==t?(Ke=B,F=BO(Q),F?F=void 0:F=t,F!==t?(Ke=C,Q=bO(Q),C=Q):(B=C,C=t)):(B=C,C=t),C}function oge(){var C,Q,F,K,ue;if(C=B,Q=[],F=B,K=B,I++,ue=KO(),I--,ue===t?K=void 0:(B=K,K=t),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t),F!==t)for(;F!==t;)Q.push(F),F=B,K=B,I++,ue=KO(),I--,ue===t?K=void 0:(B=K,K=t),K!==t?(r.length>B?(ue=r.charAt(B),B++):(ue=t,I===0&&ke(Fo)),ue!==t?(Ke=F,K=mn(ue),F=K):(B=F,F=t)):(B=F,F=t);else Q=t;return Q!==t&&(Ke=C,Q=Yn(Q)),C=Q,C}function MO(){var C,Q,F;if(C=B,Q=[],Gb.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Yb)),F!==t)for(;F!==t;)Q.push(F),Gb.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Yb));else Q=t;return Q!==t&&(Ke=C,Q=qb()),C=Q,C}function sc(){var C,Q,F;if(C=B,Q=[],Jb.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Wb)),F!==t)for(;F!==t;)Q.push(F),Jb.test(r.charAt(B))?(F=r.charAt(B),B++):(F=t,I===0&&ke(Wb));else Q=t;return Q!==t&&(Ke=C,Q=qb()),C=Q,C}function UO(){var C;return QO.test(r.charAt(B))?(C=r.charAt(B),B++):(C=t,I===0&&ke(zu)),C}function KO(){var C;return zb.test(r.charAt(B))?(C=r.charAt(B),B++):(C=t,I===0&&ke(_b)),C}function qe(){var C,Q;if(C=[],kE.test(r.charAt(B))?(Q=r.charAt(B),B++):(Q=t,I===0&&ke(xE)),Q!==t)for(;Q!==t;)C.push(Q),kE.test(r.charAt(B))?(Q=r.charAt(B),B++):(Q=t,I===0&&ke(xE));else C=t;return C}if(D=n(),D!==t&&B===r.length)return D;throw D!==t&&B{"use strict";function sfe(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function fc(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,fc)}sfe(fc,Error);fc.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;gY&&(Y=v,q=[]),q.push(pe))}function se(pe,V){return new fc(pe,null,null,V)}function be(pe,V,Qe){return new fc(fc.buildMessage(pe,V),pe,V,Qe)}function he(){var pe,V,Qe,le;return pe=v,V=Fe(),V!==t?(r.charCodeAt(v)===47?(Qe=s,v++):(Qe=t,$===0&&re(o)),Qe!==t?(le=Fe(),le!==t?(k=pe,V=a(V,le),pe=V):(v=pe,pe=t)):(v=pe,pe=t)):(v=pe,pe=t),pe===t&&(pe=v,V=Fe(),V!==t&&(k=pe,V=l(V)),pe=V),pe}function Fe(){var pe,V,Qe,le;return pe=v,V=Ue(),V!==t?(r.charCodeAt(v)===64?(Qe=c,v++):(Qe=t,$===0&&re(u)),Qe!==t?(le=ve(),le!==t?(k=pe,V=g(V,le),pe=V):(v=pe,pe=t)):(v=pe,pe=t)):(v=pe,pe=t),pe===t&&(pe=v,V=Ue(),V!==t&&(k=pe,V=f(V)),pe=V),pe}function Ue(){var pe,V,Qe,le,fe;return pe=v,r.charCodeAt(v)===64?(V=c,v++):(V=t,$===0&&re(u)),V!==t?(Qe=xe(),Qe!==t?(r.charCodeAt(v)===47?(le=s,v++):(le=t,$===0&&re(o)),le!==t?(fe=xe(),fe!==t?(k=pe,V=h(),pe=V):(v=pe,pe=t)):(v=pe,pe=t)):(v=pe,pe=t)):(v=pe,pe=t),pe===t&&(pe=v,V=xe(),V!==t&&(k=pe,V=h()),pe=V),pe}function xe(){var pe,V,Qe;if(pe=v,V=[],p.test(r.charAt(v))?(Qe=r.charAt(v),v++):(Qe=t,$===0&&re(m)),Qe!==t)for(;Qe!==t;)V.push(Qe),p.test(r.charAt(v))?(Qe=r.charAt(v),v++):(Qe=t,$===0&&re(m));else V=t;return V!==t&&(k=pe,V=h()),pe=V,pe}function ve(){var pe,V,Qe;if(pe=v,V=[],y.test(r.charAt(v))?(Qe=r.charAt(v),v++):(Qe=t,$===0&&re(b)),Qe!==t)for(;Qe!==t;)V.push(Qe),y.test(r.charAt(v))?(Qe=r.charAt(v),v++):(Qe=t,$===0&&re(b));else V=t;return V!==t&&(k=pe,V=h()),pe=V,pe}if(z=n(),z!==t&&v===r.length)return z;throw z!==t&&v{"use strict";function i1(r){return typeof r=="undefined"||r===null}function afe(r){return typeof r=="object"&&r!==null}function Afe(r){return Array.isArray(r)?r:i1(r)?[]:[r]}function lfe(r,e){var t,i,n,s;if(e)for(s=Object.keys(e),t=0,i=s.length;t{"use strict";function tp(r,e){Error.call(this),this.name="YAMLException",this.reason=r,this.mark=e,this.message=(this.reason||"(unknown reason)")+(this.mark?" "+this.mark.toString():""),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack||""}tp.prototype=Object.create(Error.prototype);tp.prototype.constructor=tp;tp.prototype.toString=function(e){var t=this.name+": ";return t+=this.reason||"(unknown reason)",!e&&this.mark&&(t+=" "+this.mark.toString()),t};n1.exports=tp});var a1=w((O7e,s1)=>{"use strict";var o1=pc();function FQ(r,e,t,i,n){this.name=r,this.buffer=e,this.position=t,this.line=i,this.column=n}FQ.prototype.getSnippet=function(e,t){var i,n,s,o,a;if(!this.buffer)return null;for(e=e||4,t=t||75,i="",n=this.position;n>0&&`\0\r +\x85\u2028\u2029`.indexOf(this.buffer.charAt(n-1))===-1;)if(n-=1,this.position-n>t/2-1){i=" ... ",n+=5;break}for(s="",o=this.position;ot/2-1){s=" ... ",o-=5;break}return a=this.buffer.slice(n,o),o1.repeat(" ",e)+i+a+s+` +`+o1.repeat(" ",e+this.position-n+i.length)+"^"};FQ.prototype.toString=function(e){var t,i="";return this.name&&(i+='in "'+this.name+'" '),i+="at line "+(this.line+1)+", column "+(this.column+1),e||(t=this.getSnippet(),t&&(i+=`: +`+t)),i};s1.exports=FQ});var ci=w((M7e,A1)=>{"use strict";var l1=rg(),gfe=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],ffe=["scalar","sequence","mapping"];function hfe(r){var e={};return r!==null&&Object.keys(r).forEach(function(t){r[t].forEach(function(i){e[String(i)]=t})}),e}function pfe(r,e){if(e=e||{},Object.keys(e).forEach(function(t){if(gfe.indexOf(t)===-1)throw new l1('Unknown option "'+t+'" is met in definition of "'+r+'" YAML type.')}),this.tag=r,this.kind=e.kind||null,this.resolve=e.resolve||function(){return!0},this.construct=e.construct||function(t){return t},this.instanceOf=e.instanceOf||null,this.predicate=e.predicate||null,this.represent=e.represent||null,this.defaultStyle=e.defaultStyle||null,this.styleAliases=hfe(e.styleAliases||null),ffe.indexOf(this.kind)===-1)throw new l1('Unknown kind "'+this.kind+'" is specified for "'+r+'" YAML type.')}A1.exports=pfe});var dc=w((U7e,c1)=>{"use strict";var u1=pc(),rI=rg(),dfe=ci();function NQ(r,e,t){var i=[];return r.include.forEach(function(n){t=NQ(n,e,t)}),r[e].forEach(function(n){t.forEach(function(s,o){s.tag===n.tag&&s.kind===n.kind&&i.push(o)}),t.push(n)}),t.filter(function(n,s){return i.indexOf(s)===-1})}function Cfe(){var r={scalar:{},sequence:{},mapping:{},fallback:{}},e,t;function i(n){r[n.kind][n.tag]=r.fallback[n.tag]=n}for(e=0,t=arguments.length;e{"use strict";var mfe=ci();g1.exports=new mfe("tag:yaml.org,2002:str",{kind:"scalar",construct:function(r){return r!==null?r:""}})});var p1=w((H7e,h1)=>{"use strict";var Efe=ci();h1.exports=new Efe("tag:yaml.org,2002:seq",{kind:"sequence",construct:function(r){return r!==null?r:[]}})});var C1=w((j7e,d1)=>{"use strict";var Ife=ci();d1.exports=new Ife("tag:yaml.org,2002:map",{kind:"mapping",construct:function(r){return r!==null?r:{}}})});var iI=w((G7e,m1)=>{"use strict";var yfe=dc();m1.exports=new yfe({explicit:[f1(),p1(),C1()]})});var I1=w((Y7e,E1)=>{"use strict";var wfe=ci();function Bfe(r){if(r===null)return!0;var e=r.length;return e===1&&r==="~"||e===4&&(r==="null"||r==="Null"||r==="NULL")}function bfe(){return null}function Qfe(r){return r===null}E1.exports=new wfe("tag:yaml.org,2002:null",{kind:"scalar",resolve:Bfe,construct:bfe,predicate:Qfe,represent:{canonical:function(){return"~"},lowercase:function(){return"null"},uppercase:function(){return"NULL"},camelcase:function(){return"Null"}},defaultStyle:"lowercase"})});var w1=w((q7e,y1)=>{"use strict";var Sfe=ci();function vfe(r){if(r===null)return!1;var e=r.length;return e===4&&(r==="true"||r==="True"||r==="TRUE")||e===5&&(r==="false"||r==="False"||r==="FALSE")}function kfe(r){return r==="true"||r==="True"||r==="TRUE"}function xfe(r){return Object.prototype.toString.call(r)==="[object Boolean]"}y1.exports=new Sfe("tag:yaml.org,2002:bool",{kind:"scalar",resolve:vfe,construct:kfe,predicate:xfe,represent:{lowercase:function(r){return r?"true":"false"},uppercase:function(r){return r?"TRUE":"FALSE"},camelcase:function(r){return r?"True":"False"}},defaultStyle:"lowercase"})});var b1=w((J7e,B1)=>{"use strict";var Pfe=pc(),Dfe=ci();function Rfe(r){return 48<=r&&r<=57||65<=r&&r<=70||97<=r&&r<=102}function Ffe(r){return 48<=r&&r<=55}function Nfe(r){return 48<=r&&r<=57}function Lfe(r){if(r===null)return!1;var e=r.length,t=0,i=!1,n;if(!e)return!1;if(n=r[t],(n==="-"||n==="+")&&(n=r[++t]),n==="0"){if(t+1===e)return!0;if(n=r[++t],n==="b"){for(t++;t=0?"0b"+r.toString(2):"-0b"+r.toString(2).slice(1)},octal:function(r){return r>=0?"0"+r.toString(8):"-0"+r.toString(8).slice(1)},decimal:function(r){return r.toString(10)},hexadecimal:function(r){return r>=0?"0x"+r.toString(16).toUpperCase():"-0x"+r.toString(16).toUpperCase().slice(1)}},defaultStyle:"decimal",styleAliases:{binary:[2,"bin"],octal:[8,"oct"],decimal:[10,"dec"],hexadecimal:[16,"hex"]}})});var v1=w((W7e,Q1)=>{"use strict";var S1=pc(),Mfe=ci(),Ufe=new RegExp("^(?:[-+]?(?:0|[1-9][0-9_]*)(?:\\.[0-9_]*)?(?:[eE][-+]?[0-9]+)?|\\.[0-9_]+(?:[eE][-+]?[0-9]+)?|[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]*|[-+]?\\.(?:inf|Inf|INF)|\\.(?:nan|NaN|NAN))$");function Kfe(r){return!(r===null||!Ufe.test(r)||r[r.length-1]==="_")}function Hfe(r){var e,t,i,n;return e=r.replace(/_/g,"").toLowerCase(),t=e[0]==="-"?-1:1,n=[],"+-".indexOf(e[0])>=0&&(e=e.slice(1)),e===".inf"?t===1?Number.POSITIVE_INFINITY:Number.NEGATIVE_INFINITY:e===".nan"?NaN:e.indexOf(":")>=0?(e.split(":").forEach(function(s){n.unshift(parseFloat(s,10))}),e=0,i=1,n.forEach(function(s){e+=s*i,i*=60}),t*e):t*parseFloat(e,10)}var jfe=/^[-+]?[0-9]+e/;function Gfe(r,e){var t;if(isNaN(r))switch(e){case"lowercase":return".nan";case"uppercase":return".NAN";case"camelcase":return".NaN"}else if(Number.POSITIVE_INFINITY===r)switch(e){case"lowercase":return".inf";case"uppercase":return".INF";case"camelcase":return".Inf"}else if(Number.NEGATIVE_INFINITY===r)switch(e){case"lowercase":return"-.inf";case"uppercase":return"-.INF";case"camelcase":return"-.Inf"}else if(S1.isNegativeZero(r))return"-0.0";return t=r.toString(10),jfe.test(t)?t.replace("e",".e"):t}function Yfe(r){return Object.prototype.toString.call(r)==="[object Number]"&&(r%1!=0||S1.isNegativeZero(r))}Q1.exports=new Mfe("tag:yaml.org,2002:float",{kind:"scalar",resolve:Kfe,construct:Hfe,predicate:Yfe,represent:Gfe,defaultStyle:"lowercase"})});var LQ=w((z7e,k1)=>{"use strict";var qfe=dc();k1.exports=new qfe({include:[iI()],implicit:[I1(),w1(),b1(),v1()]})});var TQ=w((_7e,x1)=>{"use strict";var Jfe=dc();x1.exports=new Jfe({include:[LQ()]})});var F1=w((V7e,P1)=>{"use strict";var Wfe=ci(),D1=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])$"),R1=new RegExp("^([0-9][0-9][0-9][0-9])-([0-9][0-9]?)-([0-9][0-9]?)(?:[Tt]|[ \\t]+)([0-9][0-9]?):([0-9][0-9]):([0-9][0-9])(?:\\.([0-9]*))?(?:[ \\t]*(Z|([-+])([0-9][0-9]?)(?::([0-9][0-9]))?))?$");function zfe(r){return r===null?!1:D1.exec(r)!==null||R1.exec(r)!==null}function _fe(r){var e,t,i,n,s,o,a,l=0,c=null,u,g,f;if(e=D1.exec(r),e===null&&(e=R1.exec(r)),e===null)throw new Error("Date resolve error");if(t=+e[1],i=+e[2]-1,n=+e[3],!e[4])return new Date(Date.UTC(t,i,n));if(s=+e[4],o=+e[5],a=+e[6],e[7]){for(l=e[7].slice(0,3);l.length<3;)l+="0";l=+l}return e[9]&&(u=+e[10],g=+(e[11]||0),c=(u*60+g)*6e4,e[9]==="-"&&(c=-c)),f=new Date(Date.UTC(t,i,n,s,o,a,l)),c&&f.setTime(f.getTime()-c),f}function Vfe(r){return r.toISOString()}P1.exports=new Wfe("tag:yaml.org,2002:timestamp",{kind:"scalar",resolve:zfe,construct:_fe,instanceOf:Date,represent:Vfe})});var L1=w((X7e,N1)=>{"use strict";var Xfe=ci();function Zfe(r){return r==="<<"||r===null}N1.exports=new Xfe("tag:yaml.org,2002:merge",{kind:"scalar",resolve:Zfe})});var M1=w((Z7e,T1)=>{"use strict";var Cc;try{O1=require,Cc=O1("buffer").Buffer}catch(r){}var O1,$fe=ci(),OQ=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= +\r`;function ehe(r){if(r===null)return!1;var e,t,i=0,n=r.length,s=OQ;for(t=0;t64)){if(e<0)return!1;i+=6}return i%8==0}function the(r){var e,t,i=r.replace(/[\r\n=]/g,""),n=i.length,s=OQ,o=0,a=[];for(e=0;e>16&255),a.push(o>>8&255),a.push(o&255)),o=o<<6|s.indexOf(i.charAt(e));return t=n%4*6,t===0?(a.push(o>>16&255),a.push(o>>8&255),a.push(o&255)):t===18?(a.push(o>>10&255),a.push(o>>2&255)):t===12&&a.push(o>>4&255),Cc?Cc.from?Cc.from(a):new Cc(a):a}function rhe(r){var e="",t=0,i,n,s=r.length,o=OQ;for(i=0;i>18&63],e+=o[t>>12&63],e+=o[t>>6&63],e+=o[t&63]),t=(t<<8)+r[i];return n=s%3,n===0?(e+=o[t>>18&63],e+=o[t>>12&63],e+=o[t>>6&63],e+=o[t&63]):n===2?(e+=o[t>>10&63],e+=o[t>>4&63],e+=o[t<<2&63],e+=o[64]):n===1&&(e+=o[t>>2&63],e+=o[t<<4&63],e+=o[64],e+=o[64]),e}function ihe(r){return Cc&&Cc.isBuffer(r)}T1.exports=new $fe("tag:yaml.org,2002:binary",{kind:"scalar",resolve:ehe,construct:the,predicate:ihe,represent:rhe})});var K1=w(($7e,U1)=>{"use strict";var nhe=ci(),she=Object.prototype.hasOwnProperty,ohe=Object.prototype.toString;function ahe(r){if(r===null)return!0;var e=[],t,i,n,s,o,a=r;for(t=0,i=a.length;t{"use strict";var lhe=ci(),che=Object.prototype.toString;function uhe(r){if(r===null)return!0;var e,t,i,n,s,o=r;for(s=new Array(o.length),e=0,t=o.length;e{"use strict";var fhe=ci(),hhe=Object.prototype.hasOwnProperty;function phe(r){if(r===null)return!0;var e,t=r;for(e in t)if(hhe.call(t,e)&&t[e]!==null)return!1;return!0}function dhe(r){return r!==null?r:{}}G1.exports=new fhe("tag:yaml.org,2002:set",{kind:"mapping",resolve:phe,construct:dhe})});var ng=w((rXe,q1)=>{"use strict";var Che=dc();q1.exports=new Che({include:[TQ()],implicit:[F1(),L1()],explicit:[M1(),K1(),j1(),Y1()]})});var W1=w((iXe,J1)=>{"use strict";var mhe=ci();function Ehe(){return!0}function Ihe(){}function yhe(){return""}function whe(r){return typeof r=="undefined"}J1.exports=new mhe("tag:yaml.org,2002:js/undefined",{kind:"scalar",resolve:Ehe,construct:Ihe,predicate:whe,represent:yhe})});var _1=w((nXe,z1)=>{"use strict";var Bhe=ci();function bhe(r){if(r===null||r.length===0)return!1;var e=r,t=/\/([gim]*)$/.exec(r),i="";return!(e[0]==="/"&&(t&&(i=t[1]),i.length>3||e[e.length-i.length-1]!=="/"))}function Qhe(r){var e=r,t=/\/([gim]*)$/.exec(r),i="";return e[0]==="/"&&(t&&(i=t[1]),e=e.slice(1,e.length-i.length-1)),new RegExp(e,i)}function She(r){var e="/"+r.source+"/";return r.global&&(e+="g"),r.multiline&&(e+="m"),r.ignoreCase&&(e+="i"),e}function vhe(r){return Object.prototype.toString.call(r)==="[object RegExp]"}z1.exports=new Bhe("tag:yaml.org,2002:js/regexp",{kind:"scalar",resolve:bhe,construct:Qhe,predicate:vhe,represent:She})});var Z1=w((sXe,V1)=>{"use strict";var nI;try{X1=require,nI=X1("esprima")}catch(r){typeof window!="undefined"&&(nI=window.esprima)}var X1,khe=ci();function xhe(r){if(r===null)return!1;try{var e="("+r+")",t=nI.parse(e,{range:!0});return!(t.type!=="Program"||t.body.length!==1||t.body[0].type!=="ExpressionStatement"||t.body[0].expression.type!=="ArrowFunctionExpression"&&t.body[0].expression.type!=="FunctionExpression")}catch(i){return!1}}function Phe(r){var e="("+r+")",t=nI.parse(e,{range:!0}),i=[],n;if(t.type!=="Program"||t.body.length!==1||t.body[0].type!=="ExpressionStatement"||t.body[0].expression.type!=="ArrowFunctionExpression"&&t.body[0].expression.type!=="FunctionExpression")throw new Error("Failed to resolve function");return t.body[0].expression.params.forEach(function(s){i.push(s.name)}),n=t.body[0].expression.body.range,t.body[0].expression.body.type==="BlockStatement"?new Function(i,e.slice(n[0]+1,n[1]-1)):new Function(i,"return "+e.slice(n[0],n[1]))}function Dhe(r){return r.toString()}function Rhe(r){return Object.prototype.toString.call(r)==="[object Function]"}V1.exports=new khe("tag:yaml.org,2002:js/function",{kind:"scalar",resolve:xhe,construct:Phe,predicate:Rhe,represent:Dhe})});var rp=w((oXe,$1)=>{"use strict";var eU=dc();$1.exports=eU.DEFAULT=new eU({include:[ng()],explicit:[W1(),_1(),Z1()]})});var EU=w((aXe,ip)=>{"use strict";var Ma=pc(),tU=rg(),Fhe=a1(),rU=ng(),Nhe=rp(),WA=Object.prototype.hasOwnProperty,sI=1,iU=2,nU=3,oI=4,MQ=1,Lhe=2,sU=3,The=/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x84\x86-\x9F\uFFFE\uFFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/,Ohe=/[\x85\u2028\u2029]/,Mhe=/[,\[\]\{\}]/,oU=/^(?:!|!!|![a-z\-]+!)$/i,aU=/^(?:!|[^,\[\]\{\}])(?:%[0-9a-f]{2}|[0-9a-z\-#;\/\?:@&=\+\$,_\.!~\*'\(\)\[\]])*$/i;function AU(r){return Object.prototype.toString.call(r)}function Oo(r){return r===10||r===13}function mc(r){return r===9||r===32}function yn(r){return r===9||r===32||r===10||r===13}function sg(r){return r===44||r===91||r===93||r===123||r===125}function Uhe(r){var e;return 48<=r&&r<=57?r-48:(e=r|32,97<=e&&e<=102?e-97+10:-1)}function Khe(r){return r===120?2:r===117?4:r===85?8:0}function Hhe(r){return 48<=r&&r<=57?r-48:-1}function lU(r){return r===48?"\0":r===97?"\x07":r===98?"\b":r===116||r===9?" ":r===110?` +`:r===118?"\v":r===102?"\f":r===114?"\r":r===101?"":r===32?" ":r===34?'"':r===47?"/":r===92?"\\":r===78?"\x85":r===95?"\xA0":r===76?"\u2028":r===80?"\u2029":""}function jhe(r){return r<=65535?String.fromCharCode(r):String.fromCharCode((r-65536>>10)+55296,(r-65536&1023)+56320)}var cU=new Array(256),uU=new Array(256);for(var og=0;og<256;og++)cU[og]=lU(og)?1:0,uU[og]=lU(og);function Ghe(r,e){this.input=r,this.filename=e.filename||null,this.schema=e.schema||Nhe,this.onWarning=e.onWarning||null,this.legacy=e.legacy||!1,this.json=e.json||!1,this.listener=e.listener||null,this.implicitTypes=this.schema.compiledImplicit,this.typeMap=this.schema.compiledTypeMap,this.length=r.length,this.position=0,this.line=0,this.lineStart=0,this.lineIndent=0,this.documents=[]}function gU(r,e){return new tU(e,new Fhe(r.filename,r.input,r.position,r.line,r.position-r.lineStart))}function dt(r,e){throw gU(r,e)}function aI(r,e){r.onWarning&&r.onWarning.call(null,gU(r,e))}var fU={YAML:function(e,t,i){var n,s,o;e.version!==null&&dt(e,"duplication of %YAML directive"),i.length!==1&&dt(e,"YAML directive accepts exactly one argument"),n=/^([0-9]+)\.([0-9]+)$/.exec(i[0]),n===null&&dt(e,"ill-formed argument of the YAML directive"),s=parseInt(n[1],10),o=parseInt(n[2],10),s!==1&&dt(e,"unacceptable YAML version of the document"),e.version=i[0],e.checkLineBreaks=o<2,o!==1&&o!==2&&aI(e,"unsupported YAML version of the document")},TAG:function(e,t,i){var n,s;i.length!==2&&dt(e,"TAG directive accepts exactly two arguments"),n=i[0],s=i[1],oU.test(n)||dt(e,"ill-formed tag handle (first argument) of the TAG directive"),WA.call(e.tagMap,n)&&dt(e,'there is a previously declared suffix for "'+n+'" tag handle'),aU.test(s)||dt(e,"ill-formed tag prefix (second argument) of the TAG directive"),e.tagMap[n]=s}};function zA(r,e,t,i){var n,s,o,a;if(e1&&(r.result+=Ma.repeat(` +`,e-1))}function Yhe(r,e,t){var i,n,s,o,a,l,c,u,g=r.kind,f=r.result,h;if(h=r.input.charCodeAt(r.position),yn(h)||sg(h)||h===35||h===38||h===42||h===33||h===124||h===62||h===39||h===34||h===37||h===64||h===96||(h===63||h===45)&&(n=r.input.charCodeAt(r.position+1),yn(n)||t&&sg(n)))return!1;for(r.kind="scalar",r.result="",s=o=r.position,a=!1;h!==0;){if(h===58){if(n=r.input.charCodeAt(r.position+1),yn(n)||t&&sg(n))break}else if(h===35){if(i=r.input.charCodeAt(r.position-1),yn(i))break}else{if(r.position===r.lineStart&&AI(r)||t&&sg(h))break;if(Oo(h))if(l=r.line,c=r.lineStart,u=r.lineIndent,ei(r,!1,-1),r.lineIndent>=e){a=!0,h=r.input.charCodeAt(r.position);continue}else{r.position=o,r.line=l,r.lineStart=c,r.lineIndent=u;break}}a&&(zA(r,s,o,!1),KQ(r,r.line-l),s=o=r.position,a=!1),mc(h)||(o=r.position+1),h=r.input.charCodeAt(++r.position)}return zA(r,s,o,!1),r.result?!0:(r.kind=g,r.result=f,!1)}function qhe(r,e){var t,i,n;if(t=r.input.charCodeAt(r.position),t!==39)return!1;for(r.kind="scalar",r.result="",r.position++,i=n=r.position;(t=r.input.charCodeAt(r.position))!==0;)if(t===39)if(zA(r,i,r.position,!0),t=r.input.charCodeAt(++r.position),t===39)i=r.position,r.position++,n=r.position;else return!0;else Oo(t)?(zA(r,i,n,!0),KQ(r,ei(r,!1,e)),i=n=r.position):r.position===r.lineStart&&AI(r)?dt(r,"unexpected end of the document within a single quoted scalar"):(r.position++,n=r.position);dt(r,"unexpected end of the stream within a single quoted scalar")}function Jhe(r,e){var t,i,n,s,o,a;if(a=r.input.charCodeAt(r.position),a!==34)return!1;for(r.kind="scalar",r.result="",r.position++,t=i=r.position;(a=r.input.charCodeAt(r.position))!==0;){if(a===34)return zA(r,t,r.position,!0),r.position++,!0;if(a===92){if(zA(r,t,r.position,!0),a=r.input.charCodeAt(++r.position),Oo(a))ei(r,!1,e);else if(a<256&&cU[a])r.result+=uU[a],r.position++;else if((o=Khe(a))>0){for(n=o,s=0;n>0;n--)a=r.input.charCodeAt(++r.position),(o=Uhe(a))>=0?s=(s<<4)+o:dt(r,"expected hexadecimal character");r.result+=jhe(s),r.position++}else dt(r,"unknown escape sequence");t=i=r.position}else Oo(a)?(zA(r,t,i,!0),KQ(r,ei(r,!1,e)),t=i=r.position):r.position===r.lineStart&&AI(r)?dt(r,"unexpected end of the document within a double quoted scalar"):(r.position++,i=r.position)}dt(r,"unexpected end of the stream within a double quoted scalar")}function Whe(r,e){var t=!0,i,n=r.tag,s,o=r.anchor,a,l,c,u,g,f={},h,p,m,y;if(y=r.input.charCodeAt(r.position),y===91)l=93,g=!1,s=[];else if(y===123)l=125,g=!0,s={};else return!1;for(r.anchor!==null&&(r.anchorMap[r.anchor]=s),y=r.input.charCodeAt(++r.position);y!==0;){if(ei(r,!0,e),y=r.input.charCodeAt(r.position),y===l)return r.position++,r.tag=n,r.anchor=o,r.kind=g?"mapping":"sequence",r.result=s,!0;t||dt(r,"missed comma between flow collection entries"),p=h=m=null,c=u=!1,y===63&&(a=r.input.charCodeAt(r.position+1),yn(a)&&(c=u=!0,r.position++,ei(r,!0,e))),i=r.line,Ag(r,e,sI,!1,!0),p=r.tag,h=r.result,ei(r,!0,e),y=r.input.charCodeAt(r.position),(u||r.line===i)&&y===58&&(c=!0,y=r.input.charCodeAt(++r.position),ei(r,!0,e),Ag(r,e,sI,!1,!0),m=r.result),g?ag(r,s,f,p,h,m):c?s.push(ag(r,null,f,p,h,m)):s.push(h),ei(r,!0,e),y=r.input.charCodeAt(r.position),y===44?(t=!0,y=r.input.charCodeAt(++r.position)):t=!1}dt(r,"unexpected end of the stream within a flow collection")}function zhe(r,e){var t,i,n=MQ,s=!1,o=!1,a=e,l=0,c=!1,u,g;if(g=r.input.charCodeAt(r.position),g===124)i=!1;else if(g===62)i=!0;else return!1;for(r.kind="scalar",r.result="";g!==0;)if(g=r.input.charCodeAt(++r.position),g===43||g===45)MQ===n?n=g===43?sU:Lhe:dt(r,"repeat of a chomping mode identifier");else if((u=Hhe(g))>=0)u===0?dt(r,"bad explicit indentation width of a block scalar; it cannot be less than one"):o?dt(r,"repeat of an indentation width identifier"):(a=e+u-1,o=!0);else break;if(mc(g)){do g=r.input.charCodeAt(++r.position);while(mc(g));if(g===35)do g=r.input.charCodeAt(++r.position);while(!Oo(g)&&g!==0)}for(;g!==0;){for(UQ(r),r.lineIndent=0,g=r.input.charCodeAt(r.position);(!o||r.lineIndenta&&(a=r.lineIndent),Oo(g)){l++;continue}if(r.lineIndente)&&l!==0)dt(r,"bad indentation of a sequence entry");else if(r.lineIndente)&&(Ag(r,e,oI,!0,n)&&(p?f=r.result:h=r.result),p||(ag(r,c,u,g,f,h,s,o),g=f=h=null),ei(r,!0,-1),y=r.input.charCodeAt(r.position)),r.lineIndent>e&&y!==0)dt(r,"bad indentation of a mapping entry");else if(r.lineIndente?l=1:r.lineIndent===e?l=0:r.lineIndente?l=1:r.lineIndent===e?l=0:r.lineIndent tag; it should be "scalar", not "'+r.kind+'"'),g=0,f=r.implicitTypes.length;g tag; it should be "'+h.kind+'", not "'+r.kind+'"'),h.resolve(r.result)?(r.result=h.construct(r.result),r.anchor!==null&&(r.anchorMap[r.anchor]=r.result)):dt(r,"cannot resolve a node with !<"+r.tag+"> explicit tag")):dt(r,"unknown tag !<"+r.tag+">");return r.listener!==null&&r.listener("close",r),r.tag!==null||r.anchor!==null||u}function $he(r){var e=r.position,t,i,n,s=!1,o;for(r.version=null,r.checkLineBreaks=r.legacy,r.tagMap={},r.anchorMap={};(o=r.input.charCodeAt(r.position))!==0&&(ei(r,!0,-1),o=r.input.charCodeAt(r.position),!(r.lineIndent>0||o!==37));){for(s=!0,o=r.input.charCodeAt(++r.position),t=r.position;o!==0&&!yn(o);)o=r.input.charCodeAt(++r.position);for(i=r.input.slice(t,r.position),n=[],i.length<1&&dt(r,"directive name must not be less than one character in length");o!==0;){for(;mc(o);)o=r.input.charCodeAt(++r.position);if(o===35){do o=r.input.charCodeAt(++r.position);while(o!==0&&!Oo(o));break}if(Oo(o))break;for(t=r.position;o!==0&&!yn(o);)o=r.input.charCodeAt(++r.position);n.push(r.input.slice(t,r.position))}o!==0&&UQ(r),WA.call(fU,i)?fU[i](r,i,n):aI(r,'unknown document directive "'+i+'"')}if(ei(r,!0,-1),r.lineIndent===0&&r.input.charCodeAt(r.position)===45&&r.input.charCodeAt(r.position+1)===45&&r.input.charCodeAt(r.position+2)===45?(r.position+=3,ei(r,!0,-1)):s&&dt(r,"directives end mark is expected"),Ag(r,r.lineIndent-1,oI,!1,!0),ei(r,!0,-1),r.checkLineBreaks&&Ohe.test(r.input.slice(e,r.position))&&aI(r,"non-ASCII line breaks are interpreted as content"),r.documents.push(r.result),r.position===r.lineStart&&AI(r)){r.input.charCodeAt(r.position)===46&&(r.position+=3,ei(r,!0,-1));return}if(r.position{"use strict";var np=pc(),sp=rg(),rpe=rp(),ipe=ng(),IU=Object.prototype.toString,yU=Object.prototype.hasOwnProperty,npe=9,op=10,spe=13,ope=32,ape=33,Ape=34,wU=35,lpe=37,cpe=38,upe=39,gpe=42,BU=44,fpe=45,bU=58,hpe=61,ppe=62,dpe=63,Cpe=64,QU=91,SU=93,mpe=96,vU=123,Epe=124,kU=125,Ui={};Ui[0]="\\0";Ui[7]="\\a";Ui[8]="\\b";Ui[9]="\\t";Ui[10]="\\n";Ui[11]="\\v";Ui[12]="\\f";Ui[13]="\\r";Ui[27]="\\e";Ui[34]='\\"';Ui[92]="\\\\";Ui[133]="\\N";Ui[160]="\\_";Ui[8232]="\\L";Ui[8233]="\\P";var Ipe=["y","Y","yes","Yes","YES","on","On","ON","n","N","no","No","NO","off","Off","OFF"];function ype(r,e){var t,i,n,s,o,a,l;if(e===null)return{};for(t={},i=Object.keys(e),n=0,s=i.length;n0?r.charCodeAt(s-1):null,f=f&&DU(o,a)}else{for(s=0;si&&r[g+1]!==" ",g=s);else if(!lg(o))return lI;a=s>0?r.charCodeAt(s-1):null,f=f&&DU(o,a)}c=c||u&&s-g-1>i&&r[g+1]!==" "}return!l&&!c?f&&!n(r)?FU:NU:t>9&&RU(r)?lI:c?TU:LU}function xpe(r,e,t,i){r.dump=function(){if(e.length===0)return"''";if(!r.noCompatMode&&Ipe.indexOf(e)!==-1)return"'"+e+"'";var n=r.indent*Math.max(1,t),s=r.lineWidth===-1?-1:Math.max(Math.min(r.lineWidth,40),r.lineWidth-n),o=i||r.flowLevel>-1&&t>=r.flowLevel;function a(l){return Bpe(r,l)}switch(Spe(e,o,r.indent,s,a)){case FU:return e;case NU:return"'"+e.replace(/'/g,"''")+"'";case LU:return"|"+OU(e,r.indent)+MU(PU(e,n));case TU:return">"+OU(e,r.indent)+MU(PU(vpe(e,s),n));case lI:return'"'+kpe(e,s)+'"';default:throw new sp("impossible error: invalid scalar style")}}()}function OU(r,e){var t=RU(r)?String(e):"",i=r[r.length-1]===` +`,n=i&&(r[r.length-2]===` +`||r===` +`),s=n?"+":i?"":"-";return t+s+` +`}function MU(r){return r[r.length-1]===` +`?r.slice(0,-1):r}function vpe(r,e){for(var t=/(\n+)([^\n]*)/g,i=function(){var c=r.indexOf(` +`);return c=c!==-1?c:r.length,t.lastIndex=c,UU(r.slice(0,c),e)}(),n=r[0]===` +`||r[0]===" ",s,o;o=t.exec(r);){var a=o[1],l=o[2];s=l[0]===" ",i+=a+(!n&&!s&&l!==""?` +`:"")+UU(l,e),n=s}return i}function UU(r,e){if(r===""||r[0]===" ")return r;for(var t=/ [^ ]/g,i,n=0,s,o=0,a=0,l="";i=t.exec(r);)a=i.index,a-n>e&&(s=o>n?o:a,l+=` +`+r.slice(n,s),n=s+1),o=a;return l+=` +`,r.length-n>e&&o>n?l+=r.slice(n,o)+` +`+r.slice(o+1):l+=r.slice(n),l.slice(1)}function kpe(r){for(var e="",t,i,n,s=0;s=55296&&t<=56319&&(i=r.charCodeAt(s+1),i>=56320&&i<=57343)){e+=xU((t-55296)*1024+i-56320+65536),s++;continue}n=Ui[t],e+=!n&&lg(t)?r[s]:n||xU(t)}return e}function Ppe(r,e,t){var i="",n=r.tag,s,o;for(s=0,o=t.length;s1024&&(u+="? "),u+=r.dump+(r.condenseFlow?'"':"")+":"+(r.condenseFlow?"":" "),!!Ec(r,e,c,!1,!1)&&(u+=r.dump,i+=u));r.tag=n,r.dump="{"+i+"}"}function Fpe(r,e,t,i){var n="",s=r.tag,o=Object.keys(t),a,l,c,u,g,f;if(r.sortKeys===!0)o.sort();else if(typeof r.sortKeys=="function")o.sort(r.sortKeys);else if(r.sortKeys)throw new sp("sortKeys must be a boolean or a function");for(a=0,l=o.length;a1024,g&&(r.dump&&op===r.dump.charCodeAt(0)?f+="?":f+="? "),f+=r.dump,g&&(f+=jQ(r,e)),!!Ec(r,e+1,u,!0,g)&&(r.dump&&op===r.dump.charCodeAt(0)?f+=":":f+=": ",f+=r.dump,n+=f));r.tag=s,r.dump=n||"{}"}function KU(r,e,t){var i,n,s,o,a,l;for(n=t?r.explicitTypes:r.implicitTypes,s=0,o=n.length;s tag resolver accepts not "'+l+'" style');r.dump=i}return!0}return!1}function Ec(r,e,t,i,n,s){r.tag=null,r.dump=t,KU(r,t,!1)||KU(r,t,!0);var o=IU.call(r.dump);i&&(i=r.flowLevel<0||r.flowLevel>e);var a=o==="[object Object]"||o==="[object Array]",l,c;if(a&&(l=r.duplicates.indexOf(t),c=l!==-1),(r.tag!==null&&r.tag!=="?"||c||r.indent!==2&&e>0)&&(n=!1),c&&r.usedDuplicates[l])r.dump="*ref_"+l;else{if(a&&c&&!r.usedDuplicates[l]&&(r.usedDuplicates[l]=!0),o==="[object Object]")i&&Object.keys(r.dump).length!==0?(Fpe(r,e,r.dump,n),c&&(r.dump="&ref_"+l+r.dump)):(Rpe(r,e,r.dump),c&&(r.dump="&ref_"+l+" "+r.dump));else if(o==="[object Array]"){var u=r.noArrayIndent&&e>0?e-1:e;i&&r.dump.length!==0?(Dpe(r,u,r.dump,n),c&&(r.dump="&ref_"+l+r.dump)):(Ppe(r,u,r.dump),c&&(r.dump="&ref_"+l+" "+r.dump))}else if(o==="[object String]")r.tag!=="?"&&xpe(r,r.dump,e,s);else{if(r.skipInvalid)return!1;throw new sp("unacceptable kind of an object to dump "+o)}r.tag!==null&&r.tag!=="?"&&(r.dump="!<"+r.tag+"> "+r.dump)}return!0}function Npe(r,e){var t=[],i=[],n,s;for(YQ(r,t,i),n=0,s=i.length;n{"use strict";var cI=EU(),GU=jU();function uI(r){return function(){throw new Error("Function "+r+" is deprecated and cannot be used.")}}Mr.exports.Type=ci();Mr.exports.Schema=dc();Mr.exports.FAILSAFE_SCHEMA=iI();Mr.exports.JSON_SCHEMA=LQ();Mr.exports.CORE_SCHEMA=TQ();Mr.exports.DEFAULT_SAFE_SCHEMA=ng();Mr.exports.DEFAULT_FULL_SCHEMA=rp();Mr.exports.load=cI.load;Mr.exports.loadAll=cI.loadAll;Mr.exports.safeLoad=cI.safeLoad;Mr.exports.safeLoadAll=cI.safeLoadAll;Mr.exports.dump=GU.dump;Mr.exports.safeDump=GU.safeDump;Mr.exports.YAMLException=rg();Mr.exports.MINIMAL_SCHEMA=iI();Mr.exports.SAFE_SCHEMA=ng();Mr.exports.DEFAULT_SCHEMA=rp();Mr.exports.scan=uI("scan");Mr.exports.parse=uI("parse");Mr.exports.compose=uI("compose");Mr.exports.addConstructor=uI("addConstructor")});var JU=w((cXe,qU)=>{"use strict";var Tpe=YU();qU.exports=Tpe});var zU=w((uXe,WU)=>{"use strict";function Ope(r,e){function t(){this.constructor=r}t.prototype=e.prototype,r.prototype=new t}function Ic(r,e,t,i){this.message=r,this.expected=e,this.found=t,this.location=i,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,Ic)}Ope(Ic,Error);Ic.buildMessage=function(r,e){var t={literal:function(c){return'"'+n(c.text)+'"'},class:function(c){var u="",g;for(g=0;g0){for(g=1,f=1;g({[je]:Ce})))},Y=function(R){return R},q=function(R){return R},$=eo("correct indentation"),z=" ",ne=gr(" ",!1),ee=function(R){return R.length===KA*Wu},A=function(R){return R.length===(KA+1)*Wu},oe=function(){return KA++,!0},ce=function(){return KA--,!0},Z=function(){return Hu()},O=eo("pseudostring"),L=/^[^\r\n\t ?:,\][{}#&*!|>'"%@`\-]/,de=Jn(["\r",` +`," "," ","?",":",",","]","[","{","}","#","&","*","!","|",">","'",'"',"%","@","`","-"],!0,!1),Be=/^[^\r\n\t ,\][{}:#"']/,Ge=Jn(["\r",` +`," "," ",",","]","[","{","}",":","#",'"',"'"],!0,!1),re=function(){return Hu().replace(/^ *| *$/g,"")},se="--",be=gr("--",!1),he=/^[a-zA-Z\/0-9]/,Fe=Jn([["a","z"],["A","Z"],"/",["0","9"]],!1,!1),Ue=/^[^\r\n\t :,]/,xe=Jn(["\r",` +`," "," ",":",","],!0,!1),ve="null",pe=gr("null",!1),V=function(){return null},Qe="true",le=gr("true",!1),fe=function(){return!0},gt="false",Ht=gr("false",!1),Mt=function(){return!1},Ei=eo("string"),jt='"',Qr=gr('"',!1),Oi=function(){return""},$s=function(R){return R},Hn=function(R){return R.join("")},jn=/^[^"\\\0-\x1F\x7F]/,Sr=Jn(['"',"\\",["\0",""],"\x7F"],!0,!1),Gn='\\"',fs=gr('\\"',!1),Qa=function(){return'"'},RA="\\\\",Lu=gr("\\\\",!1),hs=function(){return"\\"},FA="\\/",Sa=gr("\\/",!1),Tu=function(){return"/"},NA="\\b",LA=gr("\\b",!1),vr=function(){return"\b"},_l="\\f",Ou=gr("\\f",!1),Po=function(){return"\f"},Mu="\\n",vh=gr("\\n",!1),kh=function(){return` +`},Dr="\\r",Ae=gr("\\r",!1),Do=function(){return"\r"},Yn="\\t",Uu=gr("\\t",!1),St=function(){return" "},Vl="\\u",qn=gr("\\u",!1),ps=function(R,G,Ce,je){return String.fromCharCode(parseInt(`0x${R}${G}${Ce}${je}`))},ds=/^[0-9a-fA-F]/,pt=Jn([["0","9"],["a","f"],["A","F"]],!1,!1),Ro=eo("blank space"),lt=/^[ \t]/,mn=Jn([" "," "],!1,!1),S=eo("white space"),Tt=/^[ \t\n\r]/,Ku=Jn([" "," ",` +`,"\r"],!1,!1),Xl=`\r +`,xh=gr(`\r +`,!1),Ph=` +`,Dh=gr(` +`,!1),Rh="\r",Fh=gr("\r",!1),j=0,wt=0,TA=[{line:1,column:1}],$i=0,Zl=[],$e=0,va;if("startRule"in e){if(!(e.startRule in i))throw new Error(`Can't start parsing from rule "`+e.startRule+'".');n=i[e.startRule]}function Hu(){return r.substring(wt,j)}function wE(){return En(wt,j)}function Nh(R,G){throw G=G!==void 0?G:En(wt,j),ec([eo(R)],r.substring(wt,j),G)}function BE(R,G){throw G=G!==void 0?G:En(wt,j),ju(R,G)}function gr(R,G){return{type:"literal",text:R,ignoreCase:G}}function Jn(R,G,Ce){return{type:"class",parts:R,inverted:G,ignoreCase:Ce}}function $l(){return{type:"any"}}function Lh(){return{type:"end"}}function eo(R){return{type:"other",description:R}}function ka(R){var G=TA[R],Ce;if(G)return G;for(Ce=R-1;!TA[Ce];)Ce--;for(G=TA[Ce],G={line:G.line,column:G.column};Ce$i&&($i=j,Zl=[]),Zl.push(R))}function ju(R,G){return new Ic(R,null,null,G)}function ec(R,G,Ce){return new Ic(Ic.buildMessage(R,G),R,G,Ce)}function to(){var R;return R=Gu(),R}function tc(){var R,G,Ce;for(R=j,G=[],Ce=OA();Ce!==t;)G.push(Ce),Ce=OA();return G!==t&&(wt=R,G=s(G)),R=G,R}function OA(){var R,G,Ce,je,Te;return R=j,G=Pa(),G!==t?(r.charCodeAt(j)===45?(Ce=o,j++):(Ce=t,$e===0&&Oe(a)),Ce!==t?(je=Tr(),je!==t?(Te=xa(),Te!==t?(wt=R,G=l(Te),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t),R}function Gu(){var R,G,Ce;for(R=j,G=[],Ce=Yu();Ce!==t;)G.push(Ce),Ce=Yu();return G!==t&&(wt=R,G=c(G)),R=G,R}function Yu(){var R,G,Ce,je,Te,Xe,Et,Rt,Wn;if(R=j,G=Tr(),G===t&&(G=null),G!==t){if(Ce=j,r.charCodeAt(j)===35?(je=u,j++):(je=t,$e===0&&Oe(g)),je!==t){if(Te=[],Xe=j,Et=j,$e++,Rt=no(),$e--,Rt===t?Et=void 0:(j=Et,Et=t),Et!==t?(r.length>j?(Rt=r.charAt(j),j++):(Rt=t,$e===0&&Oe(f)),Rt!==t?(Et=[Et,Rt],Xe=Et):(j=Xe,Xe=t)):(j=Xe,Xe=t),Xe!==t)for(;Xe!==t;)Te.push(Xe),Xe=j,Et=j,$e++,Rt=no(),$e--,Rt===t?Et=void 0:(j=Et,Et=t),Et!==t?(r.length>j?(Rt=r.charAt(j),j++):(Rt=t,$e===0&&Oe(f)),Rt!==t?(Et=[Et,Rt],Xe=Et):(j=Xe,Xe=t)):(j=Xe,Xe=t);else Te=t;Te!==t?(je=[je,Te],Ce=je):(j=Ce,Ce=t)}else j=Ce,Ce=t;if(Ce===t&&(Ce=null),Ce!==t){if(je=[],Te=io(),Te!==t)for(;Te!==t;)je.push(Te),Te=io();else je=t;je!==t?(wt=R,G=h(),R=G):(j=R,R=t)}else j=R,R=t}else j=R,R=t;if(R===t&&(R=j,G=Pa(),G!==t?(Ce=rc(),Ce!==t?(je=Tr(),je===t&&(je=null),je!==t?(r.charCodeAt(j)===58?(Te=p,j++):(Te=t,$e===0&&Oe(m)),Te!==t?(Xe=Tr(),Xe===t&&(Xe=null),Xe!==t?(Et=xa(),Et!==t?(wt=R,G=y(Ce,Et),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t),R===t&&(R=j,G=Pa(),G!==t?(Ce=ro(),Ce!==t?(je=Tr(),je===t&&(je=null),je!==t?(r.charCodeAt(j)===58?(Te=p,j++):(Te=t,$e===0&&Oe(m)),Te!==t?(Xe=Tr(),Xe===t&&(Xe=null),Xe!==t?(Et=xa(),Et!==t?(wt=R,G=y(Ce,Et),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t),R===t))){if(R=j,G=Pa(),G!==t)if(Ce=ro(),Ce!==t)if(je=Tr(),je!==t)if(Te=bE(),Te!==t){if(Xe=[],Et=io(),Et!==t)for(;Et!==t;)Xe.push(Et),Et=io();else Xe=t;Xe!==t?(wt=R,G=y(Ce,Te),R=G):(j=R,R=t)}else j=R,R=t;else j=R,R=t;else j=R,R=t;else j=R,R=t;if(R===t)if(R=j,G=Pa(),G!==t)if(Ce=ro(),Ce!==t){if(je=[],Te=j,Xe=Tr(),Xe===t&&(Xe=null),Xe!==t?(r.charCodeAt(j)===44?(Et=b,j++):(Et=t,$e===0&&Oe(v)),Et!==t?(Rt=Tr(),Rt===t&&(Rt=null),Rt!==t?(Wn=ro(),Wn!==t?(wt=Te,Xe=k(Ce,Wn),Te=Xe):(j=Te,Te=t)):(j=Te,Te=t)):(j=Te,Te=t)):(j=Te,Te=t),Te!==t)for(;Te!==t;)je.push(Te),Te=j,Xe=Tr(),Xe===t&&(Xe=null),Xe!==t?(r.charCodeAt(j)===44?(Et=b,j++):(Et=t,$e===0&&Oe(v)),Et!==t?(Rt=Tr(),Rt===t&&(Rt=null),Rt!==t?(Wn=ro(),Wn!==t?(wt=Te,Xe=k(Ce,Wn),Te=Xe):(j=Te,Te=t)):(j=Te,Te=t)):(j=Te,Te=t)):(j=Te,Te=t);else je=t;je!==t?(Te=Tr(),Te===t&&(Te=null),Te!==t?(r.charCodeAt(j)===58?(Xe=p,j++):(Xe=t,$e===0&&Oe(m)),Xe!==t?(Et=Tr(),Et===t&&(Et=null),Et!==t?(Rt=xa(),Rt!==t?(wt=R,G=T(Ce,je,Rt),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)}else j=R,R=t;else j=R,R=t}return R}function xa(){var R,G,Ce,je,Te,Xe,Et;if(R=j,G=j,$e++,Ce=j,je=no(),je!==t?(Te=nt(),Te!==t?(r.charCodeAt(j)===45?(Xe=o,j++):(Xe=t,$e===0&&Oe(a)),Xe!==t?(Et=Tr(),Et!==t?(je=[je,Te,Xe,Et],Ce=je):(j=Ce,Ce=t)):(j=Ce,Ce=t)):(j=Ce,Ce=t)):(j=Ce,Ce=t),$e--,Ce!==t?(j=G,G=void 0):G=t,G!==t?(Ce=io(),Ce!==t?(je=Fo(),je!==t?(Te=tc(),Te!==t?(Xe=MA(),Xe!==t?(wt=R,G=Y(Te),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t),R===t&&(R=j,G=no(),G!==t?(Ce=Fo(),Ce!==t?(je=Gu(),je!==t?(Te=MA(),Te!==t?(wt=R,G=Y(je),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t),R===t))if(R=j,G=ic(),G!==t){if(Ce=[],je=io(),je!==t)for(;je!==t;)Ce.push(je),je=io();else Ce=t;Ce!==t?(wt=R,G=q(G),R=G):(j=R,R=t)}else j=R,R=t;return R}function Pa(){var R,G,Ce;for($e++,R=j,G=[],r.charCodeAt(j)===32?(Ce=z,j++):(Ce=t,$e===0&&Oe(ne));Ce!==t;)G.push(Ce),r.charCodeAt(j)===32?(Ce=z,j++):(Ce=t,$e===0&&Oe(ne));return G!==t?(wt=j,Ce=ee(G),Ce?Ce=void 0:Ce=t,Ce!==t?(G=[G,Ce],R=G):(j=R,R=t)):(j=R,R=t),$e--,R===t&&(G=t,$e===0&&Oe($)),R}function nt(){var R,G,Ce;for(R=j,G=[],r.charCodeAt(j)===32?(Ce=z,j++):(Ce=t,$e===0&&Oe(ne));Ce!==t;)G.push(Ce),r.charCodeAt(j)===32?(Ce=z,j++):(Ce=t,$e===0&&Oe(ne));return G!==t?(wt=j,Ce=A(G),Ce?Ce=void 0:Ce=t,Ce!==t?(G=[G,Ce],R=G):(j=R,R=t)):(j=R,R=t),R}function Fo(){var R;return wt=j,R=oe(),R?R=void 0:R=t,R}function MA(){var R;return wt=j,R=ce(),R?R=void 0:R=t,R}function rc(){var R;return R=nc(),R===t&&(R=Th()),R}function ro(){var R,G,Ce;if(R=nc(),R===t){if(R=j,G=[],Ce=qu(),Ce!==t)for(;Ce!==t;)G.push(Ce),Ce=qu();else G=t;G!==t&&(wt=R,G=Z()),R=G}return R}function ic(){var R;return R=Oh(),R===t&&(R=QE(),R===t&&(R=nc(),R===t&&(R=Th()))),R}function bE(){var R;return R=Oh(),R===t&&(R=nc(),R===t&&(R=qu())),R}function Th(){var R,G,Ce,je,Te,Xe;if($e++,R=j,L.test(r.charAt(j))?(G=r.charAt(j),j++):(G=t,$e===0&&Oe(de)),G!==t){for(Ce=[],je=j,Te=Tr(),Te===t&&(Te=null),Te!==t?(Be.test(r.charAt(j))?(Xe=r.charAt(j),j++):(Xe=t,$e===0&&Oe(Ge)),Xe!==t?(Te=[Te,Xe],je=Te):(j=je,je=t)):(j=je,je=t);je!==t;)Ce.push(je),je=j,Te=Tr(),Te===t&&(Te=null),Te!==t?(Be.test(r.charAt(j))?(Xe=r.charAt(j),j++):(Xe=t,$e===0&&Oe(Ge)),Xe!==t?(Te=[Te,Xe],je=Te):(j=je,je=t)):(j=je,je=t);Ce!==t?(wt=R,G=re(),R=G):(j=R,R=t)}else j=R,R=t;return $e--,R===t&&(G=t,$e===0&&Oe(O)),R}function qu(){var R,G,Ce,je,Te;if(R=j,r.substr(j,2)===se?(G=se,j+=2):(G=t,$e===0&&Oe(be)),G===t&&(G=null),G!==t)if(he.test(r.charAt(j))?(Ce=r.charAt(j),j++):(Ce=t,$e===0&&Oe(Fe)),Ce!==t){for(je=[],Ue.test(r.charAt(j))?(Te=r.charAt(j),j++):(Te=t,$e===0&&Oe(xe));Te!==t;)je.push(Te),Ue.test(r.charAt(j))?(Te=r.charAt(j),j++):(Te=t,$e===0&&Oe(xe));je!==t?(wt=R,G=re(),R=G):(j=R,R=t)}else j=R,R=t;else j=R,R=t;return R}function Oh(){var R,G;return R=j,r.substr(j,4)===ve?(G=ve,j+=4):(G=t,$e===0&&Oe(pe)),G!==t&&(wt=R,G=V()),R=G,R}function QE(){var R,G;return R=j,r.substr(j,4)===Qe?(G=Qe,j+=4):(G=t,$e===0&&Oe(le)),G!==t&&(wt=R,G=fe()),R=G,R===t&&(R=j,r.substr(j,5)===gt?(G=gt,j+=5):(G=t,$e===0&&Oe(Ht)),G!==t&&(wt=R,G=Mt()),R=G),R}function nc(){var R,G,Ce,je;return $e++,R=j,r.charCodeAt(j)===34?(G=jt,j++):(G=t,$e===0&&Oe(Qr)),G!==t?(r.charCodeAt(j)===34?(Ce=jt,j++):(Ce=t,$e===0&&Oe(Qr)),Ce!==t?(wt=R,G=Oi(),R=G):(j=R,R=t)):(j=R,R=t),R===t&&(R=j,r.charCodeAt(j)===34?(G=jt,j++):(G=t,$e===0&&Oe(Qr)),G!==t?(Ce=SE(),Ce!==t?(r.charCodeAt(j)===34?(je=jt,j++):(je=t,$e===0&&Oe(Qr)),je!==t?(wt=R,G=$s(Ce),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)),$e--,R===t&&(G=t,$e===0&&Oe(Ei)),R}function SE(){var R,G,Ce;if(R=j,G=[],Ce=Ju(),Ce!==t)for(;Ce!==t;)G.push(Ce),Ce=Ju();else G=t;return G!==t&&(wt=R,G=Hn(G)),R=G,R}function Ju(){var R,G,Ce,je,Te,Xe;return jn.test(r.charAt(j))?(R=r.charAt(j),j++):(R=t,$e===0&&Oe(Sr)),R===t&&(R=j,r.substr(j,2)===Gn?(G=Gn,j+=2):(G=t,$e===0&&Oe(fs)),G!==t&&(wt=R,G=Qa()),R=G,R===t&&(R=j,r.substr(j,2)===RA?(G=RA,j+=2):(G=t,$e===0&&Oe(Lu)),G!==t&&(wt=R,G=hs()),R=G,R===t&&(R=j,r.substr(j,2)===FA?(G=FA,j+=2):(G=t,$e===0&&Oe(Sa)),G!==t&&(wt=R,G=Tu()),R=G,R===t&&(R=j,r.substr(j,2)===NA?(G=NA,j+=2):(G=t,$e===0&&Oe(LA)),G!==t&&(wt=R,G=vr()),R=G,R===t&&(R=j,r.substr(j,2)===_l?(G=_l,j+=2):(G=t,$e===0&&Oe(Ou)),G!==t&&(wt=R,G=Po()),R=G,R===t&&(R=j,r.substr(j,2)===Mu?(G=Mu,j+=2):(G=t,$e===0&&Oe(vh)),G!==t&&(wt=R,G=kh()),R=G,R===t&&(R=j,r.substr(j,2)===Dr?(G=Dr,j+=2):(G=t,$e===0&&Oe(Ae)),G!==t&&(wt=R,G=Do()),R=G,R===t&&(R=j,r.substr(j,2)===Yn?(G=Yn,j+=2):(G=t,$e===0&&Oe(Uu)),G!==t&&(wt=R,G=St()),R=G,R===t&&(R=j,r.substr(j,2)===Vl?(G=Vl,j+=2):(G=t,$e===0&&Oe(qn)),G!==t?(Ce=UA(),Ce!==t?(je=UA(),je!==t?(Te=UA(),Te!==t?(Xe=UA(),Xe!==t?(wt=R,G=ps(Ce,je,Te,Xe),R=G):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)):(j=R,R=t)))))))))),R}function UA(){var R;return ds.test(r.charAt(j))?(R=r.charAt(j),j++):(R=t,$e===0&&Oe(pt)),R}function Tr(){var R,G;if($e++,R=[],lt.test(r.charAt(j))?(G=r.charAt(j),j++):(G=t,$e===0&&Oe(mn)),G!==t)for(;G!==t;)R.push(G),lt.test(r.charAt(j))?(G=r.charAt(j),j++):(G=t,$e===0&&Oe(mn));else R=t;return $e--,R===t&&(G=t,$e===0&&Oe(Ro)),R}function vE(){var R,G;if($e++,R=[],Tt.test(r.charAt(j))?(G=r.charAt(j),j++):(G=t,$e===0&&Oe(Ku)),G!==t)for(;G!==t;)R.push(G),Tt.test(r.charAt(j))?(G=r.charAt(j),j++):(G=t,$e===0&&Oe(Ku));else R=t;return $e--,R===t&&(G=t,$e===0&&Oe(S)),R}function io(){var R,G,Ce,je,Te,Xe;if(R=j,G=no(),G!==t){for(Ce=[],je=j,Te=Tr(),Te===t&&(Te=null),Te!==t?(Xe=no(),Xe!==t?(Te=[Te,Xe],je=Te):(j=je,je=t)):(j=je,je=t);je!==t;)Ce.push(je),je=j,Te=Tr(),Te===t&&(Te=null),Te!==t?(Xe=no(),Xe!==t?(Te=[Te,Xe],je=Te):(j=je,je=t)):(j=je,je=t);Ce!==t?(G=[G,Ce],R=G):(j=R,R=t)}else j=R,R=t;return R}function no(){var R;return r.substr(j,2)===Xl?(R=Xl,j+=2):(R=t,$e===0&&Oe(xh)),R===t&&(r.charCodeAt(j)===10?(R=Ph,j++):(R=t,$e===0&&Oe(Dh)),R===t&&(r.charCodeAt(j)===13?(R=Rh,j++):(R=t,$e===0&&Oe(Fh)))),R}let Wu=2,KA=0;if(va=n(),va!==t&&j===r.length)return va;throw va!==t&&j{"use strict";var Gpe=r=>{let e=!1,t=!1,i=!1;for(let n=0;n{if(!(typeof r=="string"||Array.isArray(r)))throw new TypeError("Expected the input to be `string | string[]`");e=Object.assign({pascalCase:!1},e);let t=n=>e.pascalCase?n.charAt(0).toUpperCase()+n.slice(1):n;return Array.isArray(r)?r=r.map(n=>n.trim()).filter(n=>n.length).join("-"):r=r.trim(),r.length===0?"":r.length===1?e.pascalCase?r.toUpperCase():r.toLowerCase():(r!==r.toLowerCase()&&(r=Gpe(r)),r=r.replace(/^[_.\- ]+/,"").toLowerCase().replace(/[_.\- ]+(\w|$)/g,(n,s)=>s.toUpperCase()).replace(/\d+(\w|$)/g,n=>n.toUpperCase()),t(r))};WQ.exports=$U;WQ.exports.default=$U});var rK=w((CXe,tK)=>{tK.exports=[{name:"AppVeyor",constant:"APPVEYOR",env:"APPVEYOR",pr:"APPVEYOR_PULL_REQUEST_NUMBER"},{name:"Azure Pipelines",constant:"AZURE_PIPELINES",env:"SYSTEM_TEAMFOUNDATIONCOLLECTIONURI",pr:"SYSTEM_PULLREQUEST_PULLREQUESTID"},{name:"Appcircle",constant:"APPCIRCLE",env:"AC_APPCIRCLE"},{name:"Bamboo",constant:"BAMBOO",env:"bamboo_planKey"},{name:"Bitbucket Pipelines",constant:"BITBUCKET",env:"BITBUCKET_COMMIT",pr:"BITBUCKET_PR_ID"},{name:"Bitrise",constant:"BITRISE",env:"BITRISE_IO",pr:"BITRISE_PULL_REQUEST"},{name:"Buddy",constant:"BUDDY",env:"BUDDY_WORKSPACE_ID",pr:"BUDDY_EXECUTION_PULL_REQUEST_ID"},{name:"Buildkite",constant:"BUILDKITE",env:"BUILDKITE",pr:{env:"BUILDKITE_PULL_REQUEST",ne:"false"}},{name:"CircleCI",constant:"CIRCLE",env:"CIRCLECI",pr:"CIRCLE_PULL_REQUEST"},{name:"Cirrus CI",constant:"CIRRUS",env:"CIRRUS_CI",pr:"CIRRUS_PR"},{name:"AWS CodeBuild",constant:"CODEBUILD",env:"CODEBUILD_BUILD_ARN"},{name:"Codefresh",constant:"CODEFRESH",env:"CF_BUILD_ID",pr:{any:["CF_PULL_REQUEST_NUMBER","CF_PULL_REQUEST_ID"]}},{name:"Codeship",constant:"CODESHIP",env:{CI_NAME:"codeship"}},{name:"Drone",constant:"DRONE",env:"DRONE",pr:{DRONE_BUILD_EVENT:"pull_request"}},{name:"dsari",constant:"DSARI",env:"DSARI"},{name:"GitHub Actions",constant:"GITHUB_ACTIONS",env:"GITHUB_ACTIONS",pr:{GITHUB_EVENT_NAME:"pull_request"}},{name:"GitLab CI",constant:"GITLAB",env:"GITLAB_CI",pr:"CI_MERGE_REQUEST_ID"},{name:"GoCD",constant:"GOCD",env:"GO_PIPELINE_LABEL"},{name:"LayerCI",constant:"LAYERCI",env:"LAYERCI",pr:"LAYERCI_PULL_REQUEST"},{name:"Hudson",constant:"HUDSON",env:"HUDSON_URL"},{name:"Jenkins",constant:"JENKINS",env:["JENKINS_URL","BUILD_ID"],pr:{any:["ghprbPullId","CHANGE_ID"]}},{name:"Magnum CI",constant:"MAGNUM",env:"MAGNUM"},{name:"Netlify CI",constant:"NETLIFY",env:"NETLIFY",pr:{env:"PULL_REQUEST",ne:"false"}},{name:"Nevercode",constant:"NEVERCODE",env:"NEVERCODE",pr:{env:"NEVERCODE_PULL_REQUEST",ne:"false"}},{name:"Render",constant:"RENDER",env:"RENDER",pr:{IS_PULL_REQUEST:"true"}},{name:"Sail CI",constant:"SAIL",env:"SAILCI",pr:"SAIL_PULL_REQUEST_NUMBER"},{name:"Semaphore",constant:"SEMAPHORE",env:"SEMAPHORE",pr:"PULL_REQUEST_NUMBER"},{name:"Screwdriver",constant:"SCREWDRIVER",env:"SCREWDRIVER",pr:{env:"SD_PULL_REQUEST",ne:"false"}},{name:"Shippable",constant:"SHIPPABLE",env:"SHIPPABLE",pr:{IS_PULL_REQUEST:"true"}},{name:"Solano CI",constant:"SOLANO",env:"TDDIUM",pr:"TDDIUM_PR_ID"},{name:"Strider CD",constant:"STRIDER",env:"STRIDER"},{name:"TaskCluster",constant:"TASKCLUSTER",env:["TASK_ID","RUN_ID"]},{name:"TeamCity",constant:"TEAMCITY",env:"TEAMCITY_VERSION"},{name:"Travis CI",constant:"TRAVIS",env:"TRAVIS",pr:{env:"TRAVIS_PULL_REQUEST",ne:"false"}},{name:"Vercel",constant:"VERCEL",env:"NOW_BUILDER"},{name:"Visual Studio App Center",constant:"APPCENTER",env:"APPCENTER_BUILD_ID"}]});var yc=w(Zn=>{"use strict";var iK=rK(),Mo=process.env;Object.defineProperty(Zn,"_vendors",{value:iK.map(function(r){return r.constant})});Zn.name=null;Zn.isPR=null;iK.forEach(function(r){let t=(Array.isArray(r.env)?r.env:[r.env]).every(function(i){return nK(i)});if(Zn[r.constant]=t,t)switch(Zn.name=r.name,typeof r.pr){case"string":Zn.isPR=!!Mo[r.pr];break;case"object":"env"in r.pr?Zn.isPR=r.pr.env in Mo&&Mo[r.pr.env]!==r.pr.ne:"any"in r.pr?Zn.isPR=r.pr.any.some(function(i){return!!Mo[i]}):Zn.isPR=nK(r.pr);break;default:Zn.isPR=null}});Zn.isCI=!!(Mo.CI||Mo.CONTINUOUS_INTEGRATION||Mo.BUILD_NUMBER||Mo.RUN_ID||Zn.name);function nK(r){return typeof r=="string"?!!Mo[r]:Object.keys(r).every(function(e){return Mo[e]===r[e]})}});var ug={};ft(ug,{KeyRelationship:()=>bc,applyCascade:()=>hp,base64RegExp:()=>lK,colorStringAlphaRegExp:()=>AK,colorStringRegExp:()=>aK,computeKey:()=>_A,getPrintable:()=>ti,hasExactLength:()=>hK,hasForbiddenKeys:()=>yde,hasKeyRelationship:()=>tS,hasMaxLength:()=>nde,hasMinLength:()=>ide,hasMutuallyExclusiveKeys:()=>wde,hasRequiredKeys:()=>Ide,hasUniqueItems:()=>sde,isArray:()=>_pe,isAtLeast:()=>Ade,isAtMost:()=>lde,isBase64:()=>mde,isBoolean:()=>Jpe,isDate:()=>zpe,isDict:()=>Xpe,isEnum:()=>nn,isHexColor:()=>Cde,isISO8601:()=>dde,isInExclusiveRange:()=>ude,isInInclusiveRange:()=>cde,isInstanceOf:()=>$pe,isInteger:()=>gde,isJSON:()=>Ede,isLiteral:()=>Ype,isLowerCase:()=>fde,isNegative:()=>ode,isNullable:()=>rde,isNumber:()=>Wpe,isObject:()=>Zpe,isOneOf:()=>ede,isOptional:()=>tde,isPositive:()=>ade,isString:()=>fp,isTuple:()=>Vpe,isUUID4:()=>pde,isUnknown:()=>fK,isUpperCase:()=>hde,iso8601RegExp:()=>eS,makeCoercionFn:()=>Bc,makeSetter:()=>gK,makeTrait:()=>uK,makeValidator:()=>vt,matchesRegExp:()=>pp,plural:()=>pI,pushError:()=>mt,simpleKeyRegExp:()=>oK,uuid4RegExp:()=>cK});function vt({test:r}){return uK(r)()}function ti(r){return r===null?"null":r===void 0?"undefined":r===""?"an empty string":JSON.stringify(r)}function _A(r,e){var t,i,n;return typeof e=="number"?`${(t=r==null?void 0:r.p)!==null&&t!==void 0?t:"."}[${e}]`:oK.test(e)?`${(i=r==null?void 0:r.p)!==null&&i!==void 0?i:""}.${e}`:`${(n=r==null?void 0:r.p)!==null&&n!==void 0?n:"."}[${JSON.stringify(e)}]`}function Bc(r,e){return t=>{let i=r[e];return r[e]=t,Bc(r,e).bind(null,i)}}function gK(r,e){return t=>{r[e]=t}}function pI(r,e,t){return r===1?e:t}function mt({errors:r,p:e}={},t){return r==null||r.push(`${e!=null?e:"."}: ${t}`),!1}function Ype(r){return vt({test:(e,t)=>e!==r?mt(t,`Expected a literal (got ${ti(r)})`):!0})}function nn(r){let e=Array.isArray(r)?r:Object.values(r),t=new Set(e);return vt({test:(i,n)=>t.has(i)?!0:mt(n,`Expected a valid enumeration value (got ${ti(i)})`)})}var oK,aK,AK,lK,cK,eS,uK,fK,fp,qpe,Jpe,Wpe,zpe,_pe,Vpe,Xpe,Zpe,$pe,ede,hp,tde,rde,ide,nde,hK,sde,ode,ade,Ade,lde,cde,ude,gde,pp,fde,hde,pde,dde,Cde,mde,Ede,Ide,yde,wde,bc,Bde,tS,ws=hge(()=>{oK=/^[a-zA-Z_][a-zA-Z0-9_]*$/,aK=/^#[0-9a-f]{6}$/i,AK=/^#[0-9a-f]{6}([0-9a-f]{2})?$/i,lK=/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/,cK=/^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89aAbB][a-f0-9]{3}-[a-f0-9]{12}$/i,eS=/^(?:[1-9]\d{3}(-?)(?:(?:0[1-9]|1[0-2])\1(?:0[1-9]|1\d|2[0-8])|(?:0[13-9]|1[0-2])\1(?:29|30)|(?:0[13578]|1[02])(?:\1)31|00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[0-5]))|(?:[1-9]\d(?:0[48]|[2468][048]|[13579][26])|(?:[2468][048]|[13579][26])00)(?:(-?)02(?:\2)29|-?366))T(?:[01]\d|2[0-3])(:?)[0-5]\d(?:\3[0-5]\d)?(?:Z|[+-][01]\d(?:\3[0-5]\d)?)$/,uK=r=>()=>r;fK=()=>vt({test:(r,e)=>!0});fp=()=>vt({test:(r,e)=>typeof r!="string"?mt(e,`Expected a string (got ${ti(r)})`):!0});qpe=new Map([["true",!0],["True",!0],["1",!0],[1,!0],["false",!1],["False",!1],["0",!1],[0,!1]]),Jpe=()=>vt({test:(r,e)=>{var t;if(typeof r!="boolean"){if(typeof(e==null?void 0:e.coercions)!="undefined"){if(typeof(e==null?void 0:e.coercion)=="undefined")return mt(e,"Unbound coercion result");let i=qpe.get(r);if(typeof i!="undefined")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return mt(e,`Expected a boolean (got ${ti(r)})`)}return!0}}),Wpe=()=>vt({test:(r,e)=>{var t;if(typeof r!="number"){if(typeof(e==null?void 0:e.coercions)!="undefined"){if(typeof(e==null?void 0:e.coercion)=="undefined")return mt(e,"Unbound coercion result");let i;if(typeof r=="string"){let n;try{n=JSON.parse(r)}catch(s){}if(typeof n=="number")if(JSON.stringify(n)===r)i=n;else return mt(e,`Received a number that can't be safely represented by the runtime (${r})`)}if(typeof i!="undefined")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return mt(e,`Expected a number (got ${ti(r)})`)}return!0}}),zpe=()=>vt({test:(r,e)=>{var t;if(!(r instanceof Date)){if(typeof(e==null?void 0:e.coercions)!="undefined"){if(typeof(e==null?void 0:e.coercion)=="undefined")return mt(e,"Unbound coercion result");let i;if(typeof r=="string"&&eS.test(r))i=new Date(r);else{let n;if(typeof r=="string"){let s;try{s=JSON.parse(r)}catch(o){}typeof s=="number"&&(n=s)}else typeof r=="number"&&(n=r);if(typeof n!="undefined")if(Number.isSafeInteger(n)||!Number.isSafeInteger(n*1e3))i=new Date(n*1e3);else return mt(e,`Received a timestamp that can't be safely represented by the runtime (${r})`)}if(typeof i!="undefined")return e.coercions.push([(t=e.p)!==null&&t!==void 0?t:".",e.coercion.bind(null,i)]),!0}return mt(e,`Expected a date (got ${ti(r)})`)}return!0}}),_pe=(r,{delimiter:e}={})=>vt({test:(t,i)=>{var n;if(typeof t=="string"&&typeof e!="undefined"&&typeof(i==null?void 0:i.coercions)!="undefined"){if(typeof(i==null?void 0:i.coercion)=="undefined")return mt(i,"Unbound coercion result");t=t.split(e),i.coercions.push([(n=i.p)!==null&&n!==void 0?n:".",i.coercion.bind(null,t)])}if(!Array.isArray(t))return mt(i,`Expected an array (got ${ti(t)})`);let s=!0;for(let o=0,a=t.length;o{let t=hK(r.length);return vt({test:(i,n)=>{var s;if(typeof i=="string"&&typeof e!="undefined"&&typeof(n==null?void 0:n.coercions)!="undefined"){if(typeof(n==null?void 0:n.coercion)=="undefined")return mt(n,"Unbound coercion result");i=i.split(e),n.coercions.push([(s=n.p)!==null&&s!==void 0?s:".",n.coercion.bind(null,i)])}if(!Array.isArray(i))return mt(n,`Expected a tuple (got ${ti(i)})`);let o=t(i,Object.assign({},n));for(let a=0,l=i.length;avt({test:(t,i)=>{if(typeof t!="object"||t===null)return mt(i,`Expected an object (got ${ti(t)})`);let n=Object.keys(t),s=!0;for(let o=0,a=n.length;o{let t=Object.keys(r);return vt({test:(i,n)=>{if(typeof i!="object"||i===null)return mt(n,`Expected an object (got ${ti(i)})`);let s=new Set([...t,...Object.keys(i)]),o={},a=!0;for(let l of s){if(l==="constructor"||l==="__proto__")a=mt(Object.assign(Object.assign({},n),{p:_A(n,l)}),"Unsafe property name");else{let c=Object.prototype.hasOwnProperty.call(r,l)?r[l]:void 0,u=Object.prototype.hasOwnProperty.call(i,l)?i[l]:void 0;typeof c!="undefined"?a=c(u,Object.assign(Object.assign({},n),{p:_A(n,l),coercion:Bc(i,l)}))&&a:e===null?a=mt(Object.assign(Object.assign({},n),{p:_A(n,l)}),`Extraneous property (got ${ti(u)})`):Object.defineProperty(o,l,{enumerable:!0,get:()=>u,set:gK(i,l)})}if(!a&&(n==null?void 0:n.errors)==null)break}return e!==null&&(a||(n==null?void 0:n.errors)!=null)&&(a=e(o,n)&&a),a}})},$pe=r=>vt({test:(e,t)=>e instanceof r?!0:mt(t,`Expected an instance of ${r.name} (got ${ti(e)})`)}),ede=(r,{exclusive:e=!1}={})=>vt({test:(t,i)=>{var n,s,o;let a=[],l=typeof(i==null?void 0:i.errors)!="undefined"?[]:void 0;for(let c=0,u=r.length;c1?mt(i,`Expected to match exactly a single predicate (matched ${a.join(", ")})`):(o=i==null?void 0:i.errors)===null||o===void 0||o.push(...l),!1}}),hp=(r,e)=>vt({test:(t,i)=>{var n,s;let o={value:t},a=typeof(i==null?void 0:i.coercions)!="undefined"?Bc(o,"value"):void 0,l=typeof(i==null?void 0:i.coercions)!="undefined"?[]:void 0;if(!r(t,Object.assign(Object.assign({},i),{coercion:a,coercions:l})))return!1;let c=[];if(typeof l!="undefined")for(let[,u]of l)c.push(u());try{if(typeof(i==null?void 0:i.coercions)!="undefined"){if(o.value!==t){if(typeof(i==null?void 0:i.coercion)=="undefined")return mt(i,"Unbound coercion result");i.coercions.push([(n=i.p)!==null&&n!==void 0?n:".",i.coercion.bind(null,o.value)])}(s=i==null?void 0:i.coercions)===null||s===void 0||s.push(...l)}return e.every(u=>u(o.value,i))}finally{for(let u of c)u()}}}),tde=r=>vt({test:(e,t)=>typeof e=="undefined"?!0:r(e,t)}),rde=r=>vt({test:(e,t)=>e===null?!0:r(e,t)}),ide=r=>vt({test:(e,t)=>e.length>=r?!0:mt(t,`Expected to have a length of at least ${r} elements (got ${e.length})`)}),nde=r=>vt({test:(e,t)=>e.length<=r?!0:mt(t,`Expected to have a length of at most ${r} elements (got ${e.length})`)}),hK=r=>vt({test:(e,t)=>e.length!==r?mt(t,`Expected to have a length of exactly ${r} elements (got ${e.length})`):!0}),sde=({map:r}={})=>vt({test:(e,t)=>{let i=new Set,n=new Set;for(let s=0,o=e.length;svt({test:(r,e)=>r<=0?!0:mt(e,`Expected to be negative (got ${r})`)}),ade=()=>vt({test:(r,e)=>r>=0?!0:mt(e,`Expected to be positive (got ${r})`)}),Ade=r=>vt({test:(e,t)=>e>=r?!0:mt(t,`Expected to be at least ${r} (got ${e})`)}),lde=r=>vt({test:(e,t)=>e<=r?!0:mt(t,`Expected to be at most ${r} (got ${e})`)}),cde=(r,e)=>vt({test:(t,i)=>t>=r&&t<=e?!0:mt(i,`Expected to be in the [${r}; ${e}] range (got ${t})`)}),ude=(r,e)=>vt({test:(t,i)=>t>=r&&tvt({test:(e,t)=>e!==Math.round(e)?mt(t,`Expected to be an integer (got ${e})`):Number.isSafeInteger(e)?!0:mt(t,`Expected to be a safe integer (got ${e})`)}),pp=r=>vt({test:(e,t)=>r.test(e)?!0:mt(t,`Expected to match the pattern ${r.toString()} (got ${ti(e)})`)}),fde=()=>vt({test:(r,e)=>r!==r.toLowerCase()?mt(e,`Expected to be all-lowercase (got ${r})`):!0}),hde=()=>vt({test:(r,e)=>r!==r.toUpperCase()?mt(e,`Expected to be all-uppercase (got ${r})`):!0}),pde=()=>vt({test:(r,e)=>cK.test(r)?!0:mt(e,`Expected to be a valid UUID v4 (got ${ti(r)})`)}),dde=()=>vt({test:(r,e)=>eS.test(r)?!1:mt(e,`Expected to be a valid ISO 8601 date string (got ${ti(r)})`)}),Cde=({alpha:r=!1})=>vt({test:(e,t)=>(r?aK.test(e):AK.test(e))?!0:mt(t,`Expected to be a valid hexadecimal color string (got ${ti(e)})`)}),mde=()=>vt({test:(r,e)=>lK.test(r)?!0:mt(e,`Expected to be a valid base 64 string (got ${ti(r)})`)}),Ede=(r=fK())=>vt({test:(e,t)=>{let i;try{i=JSON.parse(e)}catch(n){return mt(t,`Expected to be a valid JSON string (got ${ti(e)})`)}return r(i,t)}}),Ide=r=>{let e=new Set(r);return vt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)||s.push(o);return s.length>0?mt(i,`Missing required ${pI(s.length,"property","properties")} ${s.map(o=>`"${o}"`).join(", ")}`):!0}})},yde=r=>{let e=new Set(r);return vt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)&&s.push(o);return s.length>0?mt(i,`Forbidden ${pI(s.length,"property","properties")} ${s.map(o=>`"${o}"`).join(", ")}`):!0}})},wde=r=>{let e=new Set(r);return vt({test:(t,i)=>{let n=new Set(Object.keys(t)),s=[];for(let o of e)n.has(o)&&s.push(o);return s.length>1?mt(i,`Mutually exclusive properties ${s.map(o=>`"${o}"`).join(", ")}`):!0}})};(function(r){r.Forbids="Forbids",r.Requires="Requires"})(bc||(bc={}));Bde={[bc.Forbids]:{expect:!1,message:"forbids using"},[bc.Requires]:{expect:!0,message:"requires using"}},tS=(r,e,t,{ignore:i=[]}={})=>{let n=new Set(i),s=new Set(t),o=Bde[e];return vt({test:(a,l)=>{let c=new Set(Object.keys(a));if(!c.has(r)||n.has(a[r]))return!0;let u=[];for(let g of s)(c.has(g)&&!n.has(a[g]))!==o.expect&&u.push(g);return u.length>=1?mt(l,`Property "${r}" ${o.message} ${pI(u.length,"property","properties")} ${u.map(g=>`"${g}"`).join(", ")}`):!0}})}});var FK=w((mZe,RK)=>{"use strict";RK.exports=(r,...e)=>new Promise(t=>{t(r(...e))})});var fg=w((EZe,AS)=>{"use strict";var Kde=FK(),NK=r=>{if(r<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let e=[],t=0,i=()=>{t--,e.length>0&&e.shift()()},n=(a,l,...c)=>{t++;let u=Kde(a,...c);l(u),u.then(i,i)},s=(a,l,...c)=>{tnew Promise(c=>s(a,c,...l));return Object.defineProperties(o,{activeCount:{get:()=>t},pendingCount:{get:()=>e.length}}),o};AS.exports=NK;AS.exports.default=NK});var Ep=w((yZe,LK)=>{var Hde="2.0.0",jde=256,Gde=Number.MAX_SAFE_INTEGER||9007199254740991,Yde=16;LK.exports={SEMVER_SPEC_VERSION:Hde,MAX_LENGTH:jde,MAX_SAFE_INTEGER:Gde,MAX_SAFE_COMPONENT_LENGTH:Yde}});var Ip=w((wZe,TK)=>{var qde=typeof process=="object"&&process.env&&process.env.NODE_DEBUG&&/\bsemver\b/i.test(process.env.NODE_DEBUG)?(...r)=>console.error("SEMVER",...r):()=>{};TK.exports=qde});var Qc=w((XA,OK)=>{var{MAX_SAFE_COMPONENT_LENGTH:lS}=Ep(),Jde=Ip();XA=OK.exports={};var Wde=XA.re=[],rt=XA.src=[],it=XA.t={},zde=0,kt=(r,e,t)=>{let i=zde++;Jde(i,e),it[r]=i,rt[i]=e,Wde[i]=new RegExp(e,t?"g":void 0)};kt("NUMERICIDENTIFIER","0|[1-9]\\d*");kt("NUMERICIDENTIFIERLOOSE","[0-9]+");kt("NONNUMERICIDENTIFIER","\\d*[a-zA-Z-][a-zA-Z0-9-]*");kt("MAINVERSION",`(${rt[it.NUMERICIDENTIFIER]})\\.(${rt[it.NUMERICIDENTIFIER]})\\.(${rt[it.NUMERICIDENTIFIER]})`);kt("MAINVERSIONLOOSE",`(${rt[it.NUMERICIDENTIFIERLOOSE]})\\.(${rt[it.NUMERICIDENTIFIERLOOSE]})\\.(${rt[it.NUMERICIDENTIFIERLOOSE]})`);kt("PRERELEASEIDENTIFIER",`(?:${rt[it.NUMERICIDENTIFIER]}|${rt[it.NONNUMERICIDENTIFIER]})`);kt("PRERELEASEIDENTIFIERLOOSE",`(?:${rt[it.NUMERICIDENTIFIERLOOSE]}|${rt[it.NONNUMERICIDENTIFIER]})`);kt("PRERELEASE",`(?:-(${rt[it.PRERELEASEIDENTIFIER]}(?:\\.${rt[it.PRERELEASEIDENTIFIER]})*))`);kt("PRERELEASELOOSE",`(?:-?(${rt[it.PRERELEASEIDENTIFIERLOOSE]}(?:\\.${rt[it.PRERELEASEIDENTIFIERLOOSE]})*))`);kt("BUILDIDENTIFIER","[0-9A-Za-z-]+");kt("BUILD",`(?:\\+(${rt[it.BUILDIDENTIFIER]}(?:\\.${rt[it.BUILDIDENTIFIER]})*))`);kt("FULLPLAIN",`v?${rt[it.MAINVERSION]}${rt[it.PRERELEASE]}?${rt[it.BUILD]}?`);kt("FULL",`^${rt[it.FULLPLAIN]}$`);kt("LOOSEPLAIN",`[v=\\s]*${rt[it.MAINVERSIONLOOSE]}${rt[it.PRERELEASELOOSE]}?${rt[it.BUILD]}?`);kt("LOOSE",`^${rt[it.LOOSEPLAIN]}$`);kt("GTLT","((?:<|>)?=?)");kt("XRANGEIDENTIFIERLOOSE",`${rt[it.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);kt("XRANGEIDENTIFIER",`${rt[it.NUMERICIDENTIFIER]}|x|X|\\*`);kt("XRANGEPLAIN",`[v=\\s]*(${rt[it.XRANGEIDENTIFIER]})(?:\\.(${rt[it.XRANGEIDENTIFIER]})(?:\\.(${rt[it.XRANGEIDENTIFIER]})(?:${rt[it.PRERELEASE]})?${rt[it.BUILD]}?)?)?`);kt("XRANGEPLAINLOOSE",`[v=\\s]*(${rt[it.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rt[it.XRANGEIDENTIFIERLOOSE]})(?:\\.(${rt[it.XRANGEIDENTIFIERLOOSE]})(?:${rt[it.PRERELEASELOOSE]})?${rt[it.BUILD]}?)?)?`);kt("XRANGE",`^${rt[it.GTLT]}\\s*${rt[it.XRANGEPLAIN]}$`);kt("XRANGELOOSE",`^${rt[it.GTLT]}\\s*${rt[it.XRANGEPLAINLOOSE]}$`);kt("COERCE",`(^|[^\\d])(\\d{1,${lS}})(?:\\.(\\d{1,${lS}}))?(?:\\.(\\d{1,${lS}}))?(?:$|[^\\d])`);kt("COERCERTL",rt[it.COERCE],!0);kt("LONETILDE","(?:~>?)");kt("TILDETRIM",`(\\s*)${rt[it.LONETILDE]}\\s+`,!0);XA.tildeTrimReplace="$1~";kt("TILDE",`^${rt[it.LONETILDE]}${rt[it.XRANGEPLAIN]}$`);kt("TILDELOOSE",`^${rt[it.LONETILDE]}${rt[it.XRANGEPLAINLOOSE]}$`);kt("LONECARET","(?:\\^)");kt("CARETTRIM",`(\\s*)${rt[it.LONECARET]}\\s+`,!0);XA.caretTrimReplace="$1^";kt("CARET",`^${rt[it.LONECARET]}${rt[it.XRANGEPLAIN]}$`);kt("CARETLOOSE",`^${rt[it.LONECARET]}${rt[it.XRANGEPLAINLOOSE]}$`);kt("COMPARATORLOOSE",`^${rt[it.GTLT]}\\s*(${rt[it.LOOSEPLAIN]})$|^$`);kt("COMPARATOR",`^${rt[it.GTLT]}\\s*(${rt[it.FULLPLAIN]})$|^$`);kt("COMPARATORTRIM",`(\\s*)${rt[it.GTLT]}\\s*(${rt[it.LOOSEPLAIN]}|${rt[it.XRANGEPLAIN]})`,!0);XA.comparatorTrimReplace="$1$2$3";kt("HYPHENRANGE",`^\\s*(${rt[it.XRANGEPLAIN]})\\s+-\\s+(${rt[it.XRANGEPLAIN]})\\s*$`);kt("HYPHENRANGELOOSE",`^\\s*(${rt[it.XRANGEPLAINLOOSE]})\\s+-\\s+(${rt[it.XRANGEPLAINLOOSE]})\\s*$`);kt("STAR","(<|>)?=?\\s*\\*");kt("GTE0","^\\s*>=\\s*0.0.0\\s*$");kt("GTE0PRE","^\\s*>=\\s*0.0.0-0\\s*$")});var yp=w((BZe,MK)=>{var _de=["includePrerelease","loose","rtl"],Vde=r=>r?typeof r!="object"?{loose:!0}:_de.filter(e=>r[e]).reduce((e,t)=>(e[t]=!0,e),{}):{};MK.exports=Vde});var wI=w((bZe,UK)=>{var KK=/^[0-9]+$/,HK=(r,e)=>{let t=KK.test(r),i=KK.test(e);return t&&i&&(r=+r,e=+e),r===e?0:t&&!i?-1:i&&!t?1:rHK(e,r);UK.exports={compareIdentifiers:HK,rcompareIdentifiers:Xde}});var Hi=w((QZe,jK)=>{var BI=Ip(),{MAX_LENGTH:GK,MAX_SAFE_INTEGER:bI}=Ep(),{re:YK,t:qK}=Qc(),Zde=yp(),{compareIdentifiers:wp}=wI(),bs=class{constructor(e,t){if(t=Zde(t),e instanceof bs){if(e.loose===!!t.loose&&e.includePrerelease===!!t.includePrerelease)return e;e=e.version}else if(typeof e!="string")throw new TypeError(`Invalid Version: ${e}`);if(e.length>GK)throw new TypeError(`version is longer than ${GK} characters`);BI("SemVer",e,t),this.options=t,this.loose=!!t.loose,this.includePrerelease=!!t.includePrerelease;let i=e.trim().match(t.loose?YK[qK.LOOSE]:YK[qK.FULL]);if(!i)throw new TypeError(`Invalid Version: ${e}`);if(this.raw=e,this.major=+i[1],this.minor=+i[2],this.patch=+i[3],this.major>bI||this.major<0)throw new TypeError("Invalid major version");if(this.minor>bI||this.minor<0)throw new TypeError("Invalid minor version");if(this.patch>bI||this.patch<0)throw new TypeError("Invalid patch version");i[4]?this.prerelease=i[4].split(".").map(n=>{if(/^[0-9]+$/.test(n)){let s=+n;if(s>=0&&s=0;)typeof this.prerelease[i]=="number"&&(this.prerelease[i]++,i=-2);i===-1&&this.prerelease.push(0)}t&&(this.prerelease[0]===t?isNaN(this.prerelease[1])&&(this.prerelease=[t,0]):this.prerelease=[t,0]);break;default:throw new Error(`invalid increment argument: ${e}`)}return this.format(),this.raw=this.version,this}};jK.exports=bs});var Sc=w((SZe,JK)=>{var{MAX_LENGTH:$de}=Ep(),{re:WK,t:zK}=Qc(),_K=Hi(),eCe=yp(),tCe=(r,e)=>{if(e=eCe(e),r instanceof _K)return r;if(typeof r!="string"||r.length>$de||!(e.loose?WK[zK.LOOSE]:WK[zK.FULL]).test(r))return null;try{return new _K(r,e)}catch(i){return null}};JK.exports=tCe});var XK=w((vZe,VK)=>{var rCe=Sc(),iCe=(r,e)=>{let t=rCe(r,e);return t?t.version:null};VK.exports=iCe});var $K=w((kZe,ZK)=>{var nCe=Sc(),sCe=(r,e)=>{let t=nCe(r.trim().replace(/^[=v]+/,""),e);return t?t.version:null};ZK.exports=sCe});var t2=w((xZe,e2)=>{var oCe=Hi(),aCe=(r,e,t,i)=>{typeof t=="string"&&(i=t,t=void 0);try{return new oCe(r,t).inc(e,i).version}catch(n){return null}};e2.exports=aCe});var Qs=w((PZe,r2)=>{var i2=Hi(),ACe=(r,e,t)=>new i2(r,t).compare(new i2(e,t));r2.exports=ACe});var QI=w((DZe,n2)=>{var lCe=Qs(),cCe=(r,e,t)=>lCe(r,e,t)===0;n2.exports=cCe});var a2=w((RZe,s2)=>{var o2=Sc(),uCe=QI(),gCe=(r,e)=>{if(uCe(r,e))return null;{let t=o2(r),i=o2(e),n=t.prerelease.length||i.prerelease.length,s=n?"pre":"",o=n?"prerelease":"";for(let a in t)if((a==="major"||a==="minor"||a==="patch")&&t[a]!==i[a])return s+a;return o}};s2.exports=gCe});var l2=w((FZe,A2)=>{var fCe=Hi(),hCe=(r,e)=>new fCe(r,e).major;A2.exports=hCe});var u2=w((NZe,c2)=>{var pCe=Hi(),dCe=(r,e)=>new pCe(r,e).minor;c2.exports=dCe});var f2=w((LZe,g2)=>{var CCe=Hi(),mCe=(r,e)=>new CCe(r,e).patch;g2.exports=mCe});var p2=w((TZe,h2)=>{var ECe=Sc(),ICe=(r,e)=>{let t=ECe(r,e);return t&&t.prerelease.length?t.prerelease:null};h2.exports=ICe});var C2=w((OZe,d2)=>{var yCe=Qs(),wCe=(r,e,t)=>yCe(e,r,t);d2.exports=wCe});var E2=w((MZe,m2)=>{var BCe=Qs(),bCe=(r,e)=>BCe(r,e,!0);m2.exports=bCe});var SI=w((UZe,I2)=>{var y2=Hi(),QCe=(r,e,t)=>{let i=new y2(r,t),n=new y2(e,t);return i.compare(n)||i.compareBuild(n)};I2.exports=QCe});var B2=w((KZe,w2)=>{var SCe=SI(),vCe=(r,e)=>r.sort((t,i)=>SCe(t,i,e));w2.exports=vCe});var Q2=w((HZe,b2)=>{var kCe=SI(),xCe=(r,e)=>r.sort((t,i)=>kCe(i,t,e));b2.exports=xCe});var Bp=w((jZe,S2)=>{var PCe=Qs(),DCe=(r,e,t)=>PCe(r,e,t)>0;S2.exports=DCe});var vI=w((GZe,v2)=>{var RCe=Qs(),FCe=(r,e,t)=>RCe(r,e,t)<0;v2.exports=FCe});var cS=w((YZe,k2)=>{var NCe=Qs(),LCe=(r,e,t)=>NCe(r,e,t)!==0;k2.exports=LCe});var kI=w((qZe,x2)=>{var TCe=Qs(),OCe=(r,e,t)=>TCe(r,e,t)>=0;x2.exports=OCe});var xI=w((JZe,P2)=>{var MCe=Qs(),UCe=(r,e,t)=>MCe(r,e,t)<=0;P2.exports=UCe});var uS=w((WZe,D2)=>{var KCe=QI(),HCe=cS(),jCe=Bp(),GCe=kI(),YCe=vI(),qCe=xI(),JCe=(r,e,t,i)=>{switch(e){case"===":return typeof r=="object"&&(r=r.version),typeof t=="object"&&(t=t.version),r===t;case"!==":return typeof r=="object"&&(r=r.version),typeof t=="object"&&(t=t.version),r!==t;case"":case"=":case"==":return KCe(r,t,i);case"!=":return HCe(r,t,i);case">":return jCe(r,t,i);case">=":return GCe(r,t,i);case"<":return YCe(r,t,i);case"<=":return qCe(r,t,i);default:throw new TypeError(`Invalid operator: ${e}`)}};D2.exports=JCe});var F2=w((zZe,R2)=>{var WCe=Hi(),zCe=Sc(),{re:PI,t:DI}=Qc(),_Ce=(r,e)=>{if(r instanceof WCe)return r;if(typeof r=="number"&&(r=String(r)),typeof r!="string")return null;e=e||{};let t=null;if(!e.rtl)t=r.match(PI[DI.COERCE]);else{let i;for(;(i=PI[DI.COERCERTL].exec(r))&&(!t||t.index+t[0].length!==r.length);)(!t||i.index+i[0].length!==t.index+t[0].length)&&(t=i),PI[DI.COERCERTL].lastIndex=i.index+i[1].length+i[2].length;PI[DI.COERCERTL].lastIndex=-1}return t===null?null:zCe(`${t[2]}.${t[3]||"0"}.${t[4]||"0"}`,e)};R2.exports=_Ce});var L2=w((_Ze,N2)=>{"use strict";N2.exports=function(r){r.prototype[Symbol.iterator]=function*(){for(let e=this.head;e;e=e.next)yield e.value}}});var bp=w((VZe,T2)=>{"use strict";T2.exports=Gt;Gt.Node=vc;Gt.create=Gt;function Gt(r){var e=this;if(e instanceof Gt||(e=new Gt),e.tail=null,e.head=null,e.length=0,r&&typeof r.forEach=="function")r.forEach(function(n){e.push(n)});else if(arguments.length>0)for(var t=0,i=arguments.length;t1)t=e;else if(this.head)i=this.head.next,t=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;i!==null;n++)t=r(t,i.value,n),i=i.next;return t};Gt.prototype.reduceReverse=function(r,e){var t,i=this.tail;if(arguments.length>1)t=e;else if(this.tail)i=this.tail.prev,t=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=this.length-1;i!==null;n--)t=r(t,i.value,n),i=i.prev;return t};Gt.prototype.toArray=function(){for(var r=new Array(this.length),e=0,t=this.head;t!==null;e++)r[e]=t.value,t=t.next;return r};Gt.prototype.toArrayReverse=function(){for(var r=new Array(this.length),e=0,t=this.tail;t!==null;e++)r[e]=t.value,t=t.prev;return r};Gt.prototype.slice=function(r,e){e=e||this.length,e<0&&(e+=this.length),r=r||0,r<0&&(r+=this.length);var t=new Gt;if(ethis.length&&(e=this.length);for(var i=0,n=this.head;n!==null&&ithis.length&&(e=this.length);for(var i=this.length,n=this.tail;n!==null&&i>e;i--)n=n.prev;for(;n!==null&&i>r;i--,n=n.prev)t.push(n.value);return t};Gt.prototype.splice=function(r,e,...t){r>this.length&&(r=this.length-1),r<0&&(r=this.length+r);for(var i=0,n=this.head;n!==null&&i{"use strict";var $Ce=bp(),kc=Symbol("max"),Ha=Symbol("length"),hg=Symbol("lengthCalculator"),Qp=Symbol("allowStale"),xc=Symbol("maxAge"),ja=Symbol("dispose"),M2=Symbol("noDisposeOnSet"),yi=Symbol("lruList"),Ao=Symbol("cache"),U2=Symbol("updateAgeOnGet"),gS=()=>1,K2=class{constructor(e){if(typeof e=="number"&&(e={max:e}),e||(e={}),e.max&&(typeof e.max!="number"||e.max<0))throw new TypeError("max must be a non-negative number");let t=this[kc]=e.max||Infinity,i=e.length||gS;if(this[hg]=typeof i!="function"?gS:i,this[Qp]=e.stale||!1,e.maxAge&&typeof e.maxAge!="number")throw new TypeError("maxAge must be a number");this[xc]=e.maxAge||0,this[ja]=e.dispose,this[M2]=e.noDisposeOnSet||!1,this[U2]=e.updateAgeOnGet||!1,this.reset()}set max(e){if(typeof e!="number"||e<0)throw new TypeError("max must be a non-negative number");this[kc]=e||Infinity,Sp(this)}get max(){return this[kc]}set allowStale(e){this[Qp]=!!e}get allowStale(){return this[Qp]}set maxAge(e){if(typeof e!="number")throw new TypeError("maxAge must be a non-negative number");this[xc]=e,Sp(this)}get maxAge(){return this[xc]}set lengthCalculator(e){typeof e!="function"&&(e=gS),e!==this[hg]&&(this[hg]=e,this[Ha]=0,this[yi].forEach(t=>{t.length=this[hg](t.value,t.key),this[Ha]+=t.length})),Sp(this)}get lengthCalculator(){return this[hg]}get length(){return this[Ha]}get itemCount(){return this[yi].length}rforEach(e,t){t=t||this;for(let i=this[yi].tail;i!==null;){let n=i.prev;j2(this,e,i,t),i=n}}forEach(e,t){t=t||this;for(let i=this[yi].head;i!==null;){let n=i.next;j2(this,e,i,t),i=n}}keys(){return this[yi].toArray().map(e=>e.key)}values(){return this[yi].toArray().map(e=>e.value)}reset(){this[ja]&&this[yi]&&this[yi].length&&this[yi].forEach(e=>this[ja](e.key,e.value)),this[Ao]=new Map,this[yi]=new $Ce,this[Ha]=0}dump(){return this[yi].map(e=>RI(this,e)?!1:{k:e.key,v:e.value,e:e.now+(e.maxAge||0)}).toArray().filter(e=>e)}dumpLru(){return this[yi]}set(e,t,i){if(i=i||this[xc],i&&typeof i!="number")throw new TypeError("maxAge must be a number");let n=i?Date.now():0,s=this[hg](t,e);if(this[Ao].has(e)){if(s>this[kc])return pg(this,this[Ao].get(e)),!1;let l=this[Ao].get(e).value;return this[ja]&&(this[M2]||this[ja](e,l.value)),l.now=n,l.maxAge=i,l.value=t,this[Ha]+=s-l.length,l.length=s,this.get(e),Sp(this),!0}let o=new H2(e,t,s,n,i);return o.length>this[kc]?(this[ja]&&this[ja](e,t),!1):(this[Ha]+=o.length,this[yi].unshift(o),this[Ao].set(e,this[yi].head),Sp(this),!0)}has(e){if(!this[Ao].has(e))return!1;let t=this[Ao].get(e).value;return!RI(this,t)}get(e){return fS(this,e,!0)}peek(e){return fS(this,e,!1)}pop(){let e=this[yi].tail;return e?(pg(this,e),e.value):null}del(e){pg(this,this[Ao].get(e))}load(e){this.reset();let t=Date.now();for(let i=e.length-1;i>=0;i--){let n=e[i],s=n.e||0;if(s===0)this.set(n.k,n.v);else{let o=s-t;o>0&&this.set(n.k,n.v,o)}}}prune(){this[Ao].forEach((e,t)=>fS(this,t,!1))}},fS=(r,e,t)=>{let i=r[Ao].get(e);if(i){let n=i.value;if(RI(r,n)){if(pg(r,i),!r[Qp])return}else t&&(r[U2]&&(i.value.now=Date.now()),r[yi].unshiftNode(i));return n.value}},RI=(r,e)=>{if(!e||!e.maxAge&&!r[xc])return!1;let t=Date.now()-e.now;return e.maxAge?t>e.maxAge:r[xc]&&t>r[xc]},Sp=r=>{if(r[Ha]>r[kc])for(let e=r[yi].tail;r[Ha]>r[kc]&&e!==null;){let t=e.prev;pg(r,e),e=t}},pg=(r,e)=>{if(e){let t=e.value;r[ja]&&r[ja](t.key,t.value),r[Ha]-=t.length,r[Ao].delete(t.key),r[yi].removeNode(e)}},H2=class{constructor(e,t,i,n,s){this.key=e,this.value=t,this.length=i,this.now=n,this.maxAge=s||0}},j2=(r,e,t,i)=>{let n=t.value;RI(r,n)&&(pg(r,t),r[Qp]||(n=void 0)),n&&e.call(i,n.value,n.key,r)};O2.exports=K2});var Ss=w((ZZe,Y2)=>{var dg=class{constructor(e,t){if(t=eme(t),e instanceof dg)return e.loose===!!t.loose&&e.includePrerelease===!!t.includePrerelease?e:new dg(e.raw,t);if(e instanceof hS)return this.raw=e.value,this.set=[[e]],this.format(),this;if(this.options=t,this.loose=!!t.loose,this.includePrerelease=!!t.includePrerelease,this.raw=e,this.set=e.split(/\s*\|\|\s*/).map(i=>this.parseRange(i.trim())).filter(i=>i.length),!this.set.length)throw new TypeError(`Invalid SemVer Range: ${e}`);if(this.set.length>1){let i=this.set[0];if(this.set=this.set.filter(n=>!J2(n[0])),this.set.length===0)this.set=[i];else if(this.set.length>1){for(let n of this.set)if(n.length===1&&sme(n[0])){this.set=[n];break}}}this.format()}format(){return this.range=this.set.map(e=>e.join(" ").trim()).join("||").trim(),this.range}toString(){return this.range}parseRange(e){e=e.trim();let i=`parseRange:${Object.keys(this.options).join(",")}:${e}`,n=q2.get(i);if(n)return n;let s=this.options.loose,o=s?ji[xi.HYPHENRANGELOOSE]:ji[xi.HYPHENRANGE];e=e.replace(o,Ame(this.options.includePrerelease)),zr("hyphen replace",e),e=e.replace(ji[xi.COMPARATORTRIM],rme),zr("comparator trim",e,ji[xi.COMPARATORTRIM]),e=e.replace(ji[xi.TILDETRIM],ime),e=e.replace(ji[xi.CARETTRIM],nme),e=e.split(/\s+/).join(" ");let a=s?ji[xi.COMPARATORLOOSE]:ji[xi.COMPARATOR],l=e.split(" ").map(f=>ome(f,this.options)).join(" ").split(/\s+/).map(f=>ame(f,this.options)).filter(this.options.loose?f=>!!f.match(a):()=>!0).map(f=>new hS(f,this.options)),c=l.length,u=new Map;for(let f of l){if(J2(f))return[f];u.set(f.value,f)}u.size>1&&u.has("")&&u.delete("");let g=[...u.values()];return q2.set(i,g),g}intersects(e,t){if(!(e instanceof dg))throw new TypeError("a Range is required");return this.set.some(i=>W2(i,t)&&e.set.some(n=>W2(n,t)&&i.every(s=>n.every(o=>s.intersects(o,t)))))}test(e){if(!e)return!1;if(typeof e=="string")try{e=new tme(e,this.options)}catch(t){return!1}for(let t=0;tr.value==="<0.0.0-0",sme=r=>r.value==="",W2=(r,e)=>{let t=!0,i=r.slice(),n=i.pop();for(;t&&i.length;)t=i.every(s=>n.intersects(s,e)),n=i.pop();return t},ome=(r,e)=>(zr("comp",r,e),r=gme(r,e),zr("caret",r),r=ume(r,e),zr("tildes",r),r=fme(r,e),zr("xrange",r),r=hme(r,e),zr("stars",r),r),on=r=>!r||r.toLowerCase()==="x"||r==="*",ume=(r,e)=>r.trim().split(/\s+/).map(t=>pme(t,e)).join(" "),pme=(r,e)=>{let t=e.loose?ji[xi.TILDELOOSE]:ji[xi.TILDE];return r.replace(t,(i,n,s,o,a)=>{zr("tilde",r,i,n,s,o,a);let l;return on(n)?l="":on(s)?l=`>=${n}.0.0 <${+n+1}.0.0-0`:on(o)?l=`>=${n}.${s}.0 <${n}.${+s+1}.0-0`:a?(zr("replaceTilde pr",a),l=`>=${n}.${s}.${o}-${a} <${n}.${+s+1}.0-0`):l=`>=${n}.${s}.${o} <${n}.${+s+1}.0-0`,zr("tilde return",l),l})},gme=(r,e)=>r.trim().split(/\s+/).map(t=>dme(t,e)).join(" "),dme=(r,e)=>{zr("caret",r,e);let t=e.loose?ji[xi.CARETLOOSE]:ji[xi.CARET],i=e.includePrerelease?"-0":"";return r.replace(t,(n,s,o,a,l)=>{zr("caret",r,n,s,o,a,l);let c;return on(s)?c="":on(o)?c=`>=${s}.0.0${i} <${+s+1}.0.0-0`:on(a)?s==="0"?c=`>=${s}.${o}.0${i} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.0${i} <${+s+1}.0.0-0`:l?(zr("replaceCaret pr",l),s==="0"?o==="0"?c=`>=${s}.${o}.${a}-${l} <${s}.${o}.${+a+1}-0`:c=`>=${s}.${o}.${a}-${l} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.${a}-${l} <${+s+1}.0.0-0`):(zr("no pr"),s==="0"?o==="0"?c=`>=${s}.${o}.${a}${i} <${s}.${o}.${+a+1}-0`:c=`>=${s}.${o}.${a}${i} <${s}.${+o+1}.0-0`:c=`>=${s}.${o}.${a} <${+s+1}.0.0-0`),zr("caret return",c),c})},fme=(r,e)=>(zr("replaceXRanges",r,e),r.split(/\s+/).map(t=>Cme(t,e)).join(" ")),Cme=(r,e)=>{r=r.trim();let t=e.loose?ji[xi.XRANGELOOSE]:ji[xi.XRANGE];return r.replace(t,(i,n,s,o,a,l)=>{zr("xRange",r,i,n,s,o,a,l);let c=on(s),u=c||on(o),g=u||on(a),f=g;return n==="="&&f&&(n=""),l=e.includePrerelease?"-0":"",c?n===">"||n==="<"?i="<0.0.0-0":i="*":n&&f?(u&&(o=0),a=0,n===">"?(n=">=",u?(s=+s+1,o=0,a=0):(o=+o+1,a=0)):n==="<="&&(n="<",u?s=+s+1:o=+o+1),n==="<"&&(l="-0"),i=`${n+s}.${o}.${a}${l}`):u?i=`>=${s}.0.0${l} <${+s+1}.0.0-0`:g&&(i=`>=${s}.${o}.0${l} <${s}.${+o+1}.0-0`),zr("xRange return",i),i})},hme=(r,e)=>(zr("replaceStars",r,e),r.trim().replace(ji[xi.STAR],"")),ame=(r,e)=>(zr("replaceGTE0",r,e),r.trim().replace(ji[e.includePrerelease?xi.GTE0PRE:xi.GTE0],"")),Ame=r=>(e,t,i,n,s,o,a,l,c,u,g,f,h)=>(on(i)?t="":on(n)?t=`>=${i}.0.0${r?"-0":""}`:on(s)?t=`>=${i}.${n}.0${r?"-0":""}`:o?t=`>=${t}`:t=`>=${t}${r?"-0":""}`,on(c)?l="":on(u)?l=`<${+c+1}.0.0-0`:on(g)?l=`<${c}.${+u+1}.0-0`:f?l=`<=${c}.${u}.${g}-${f}`:r?l=`<${c}.${u}.${+g+1}-0`:l=`<=${l}`,`${t} ${l}`.trim()),lme=(r,e,t)=>{for(let i=0;i0){let n=r[i].semver;if(n.major===e.major&&n.minor===e.minor&&n.patch===e.patch)return!0}return!1}return!0}});var vp=w(($Ze,z2)=>{var kp=Symbol("SemVer ANY"),xp=class{static get ANY(){return kp}constructor(e,t){if(t=mme(t),e instanceof xp){if(e.loose===!!t.loose)return e;e=e.value}dS("comparator",e,t),this.options=t,this.loose=!!t.loose,this.parse(e),this.semver===kp?this.value="":this.value=this.operator+this.semver.version,dS("comp",this)}parse(e){let t=this.options.loose?_2[V2.COMPARATORLOOSE]:_2[V2.COMPARATOR],i=e.match(t);if(!i)throw new TypeError(`Invalid comparator: ${e}`);this.operator=i[1]!==void 0?i[1]:"",this.operator==="="&&(this.operator=""),i[2]?this.semver=new X2(i[2],this.options.loose):this.semver=kp}toString(){return this.value}test(e){if(dS("Comparator.test",e,this.options.loose),this.semver===kp||e===kp)return!0;if(typeof e=="string")try{e=new X2(e,this.options)}catch(t){return!1}return pS(e,this.operator,this.semver,this.options)}intersects(e,t){if(!(e instanceof xp))throw new TypeError("a Comparator is required");if((!t||typeof t!="object")&&(t={loose:!!t,includePrerelease:!1}),this.operator==="")return this.value===""?!0:new Z2(e.value,t).test(this.value);if(e.operator==="")return e.value===""?!0:new Z2(this.value,t).test(e.semver);let i=(this.operator===">="||this.operator===">")&&(e.operator===">="||e.operator===">"),n=(this.operator==="<="||this.operator==="<")&&(e.operator==="<="||e.operator==="<"),s=this.semver.version===e.semver.version,o=(this.operator===">="||this.operator==="<=")&&(e.operator===">="||e.operator==="<="),a=pS(this.semver,"<",e.semver,t)&&(this.operator===">="||this.operator===">")&&(e.operator==="<="||e.operator==="<"),l=pS(this.semver,">",e.semver,t)&&(this.operator==="<="||this.operator==="<")&&(e.operator===">="||e.operator===">");return i||n||s&&o||a||l}};z2.exports=xp;var mme=yp(),{re:_2,t:V2}=Qc(),pS=uS(),dS=Ip(),X2=Hi(),Z2=Ss()});var Pp=w((e$e,$2)=>{var Eme=Ss(),Ime=(r,e,t)=>{try{e=new Eme(e,t)}catch(i){return!1}return e.test(r)};$2.exports=Ime});var tH=w((t$e,eH)=>{var yme=Ss(),wme=(r,e)=>new yme(r,e).set.map(t=>t.map(i=>i.value).join(" ").trim().split(" "));eH.exports=wme});var iH=w((r$e,rH)=>{var Bme=Hi(),bme=Ss(),Qme=(r,e,t)=>{let i=null,n=null,s=null;try{s=new bme(e,t)}catch(o){return null}return r.forEach(o=>{s.test(o)&&(!i||n.compare(o)===-1)&&(i=o,n=new Bme(i,t))}),i};rH.exports=Qme});var sH=w((i$e,nH)=>{var Sme=Hi(),vme=Ss(),kme=(r,e,t)=>{let i=null,n=null,s=null;try{s=new vme(e,t)}catch(o){return null}return r.forEach(o=>{s.test(o)&&(!i||n.compare(o)===1)&&(i=o,n=new Sme(i,t))}),i};nH.exports=kme});var AH=w((n$e,oH)=>{var CS=Hi(),xme=Ss(),aH=Bp(),Pme=(r,e)=>{r=new xme(r,e);let t=new CS("0.0.0");if(r.test(t)||(t=new CS("0.0.0-0"),r.test(t)))return t;t=null;for(let i=0;i{let a=new CS(o.semver.version);switch(o.operator){case">":a.prerelease.length===0?a.patch++:a.prerelease.push(0),a.raw=a.format();case"":case">=":(!s||aH(a,s))&&(s=a);break;case"<":case"<=":break;default:throw new Error(`Unexpected operation: ${o.operator}`)}}),s&&(!t||aH(t,s))&&(t=s)}return t&&r.test(t)?t:null};oH.exports=Pme});var cH=w((s$e,lH)=>{var Dme=Ss(),Rme=(r,e)=>{try{return new Dme(r,e).range||"*"}catch(t){return null}};lH.exports=Rme});var FI=w((o$e,uH)=>{var Fme=Hi(),gH=vp(),{ANY:Nme}=gH,Lme=Ss(),Tme=Pp(),fH=Bp(),hH=vI(),Ome=xI(),Mme=kI(),Ume=(r,e,t,i)=>{r=new Fme(r,i),e=new Lme(e,i);let n,s,o,a,l;switch(t){case">":n=fH,s=Ome,o=hH,a=">",l=">=";break;case"<":n=hH,s=Mme,o=fH,a="<",l="<=";break;default:throw new TypeError('Must provide a hilo val of "<" or ">"')}if(Tme(r,e,i))return!1;for(let c=0;c{h.semver===Nme&&(h=new gH(">=0.0.0")),g=g||h,f=f||h,n(h.semver,g.semver,i)?g=h:o(h.semver,f.semver,i)&&(f=h)}),g.operator===a||g.operator===l||(!f.operator||f.operator===a)&&s(r,f.semver))return!1;if(f.operator===l&&o(r,f.semver))return!1}return!0};uH.exports=Ume});var dH=w((a$e,pH)=>{var Kme=FI(),Hme=(r,e,t)=>Kme(r,e,">",t);pH.exports=Hme});var mH=w((A$e,CH)=>{var jme=FI(),Gme=(r,e,t)=>jme(r,e,"<",t);CH.exports=Gme});var yH=w((l$e,EH)=>{var IH=Ss(),Yme=(r,e,t)=>(r=new IH(r,t),e=new IH(e,t),r.intersects(e));EH.exports=Yme});var BH=w((c$e,wH)=>{var qme=Pp(),Jme=Qs();wH.exports=(r,e,t)=>{let i=[],n=null,s=null,o=r.sort((u,g)=>Jme(u,g,t));for(let u of o)qme(u,e,t)?(s=u,n||(n=u)):(s&&i.push([n,s]),s=null,n=null);n&&i.push([n,null]);let a=[];for(let[u,g]of i)u===g?a.push(u):!g&&u===o[0]?a.push("*"):g?u===o[0]?a.push(`<=${g}`):a.push(`${u} - ${g}`):a.push(`>=${u}`);let l=a.join(" || "),c=typeof e.raw=="string"?e.raw:String(e);return l.length{var QH=Ss(),NI=vp(),{ANY:mS}=NI,Dp=Pp(),ES=Qs(),zme=(r,e,t={})=>{if(r===e)return!0;r=new QH(r,t),e=new QH(e,t);let i=!1;e:for(let n of r.set){for(let s of e.set){let o=Wme(n,s,t);if(i=i||o!==null,o)continue e}if(i)return!1}return!0},Wme=(r,e,t)=>{if(r===e)return!0;if(r.length===1&&r[0].semver===mS){if(e.length===1&&e[0].semver===mS)return!0;t.includePrerelease?r=[new NI(">=0.0.0-0")]:r=[new NI(">=0.0.0")]}if(e.length===1&&e[0].semver===mS){if(t.includePrerelease)return!0;e=[new NI(">=0.0.0")]}let i=new Set,n,s;for(let h of r)h.operator===">"||h.operator===">="?n=SH(n,h,t):h.operator==="<"||h.operator==="<="?s=vH(s,h,t):i.add(h.semver);if(i.size>1)return null;let o;if(n&&s){if(o=ES(n.semver,s.semver,t),o>0)return null;if(o===0&&(n.operator!==">="||s.operator!=="<="))return null}for(let h of i){if(n&&!Dp(h,String(n),t)||s&&!Dp(h,String(s),t))return null;for(let p of e)if(!Dp(h,String(p),t))return!1;return!0}let a,l,c,u,g=s&&!t.includePrerelease&&s.semver.prerelease.length?s.semver:!1,f=n&&!t.includePrerelease&&n.semver.prerelease.length?n.semver:!1;g&&g.prerelease.length===1&&s.operator==="<"&&g.prerelease[0]===0&&(g=!1);for(let h of e){if(u=u||h.operator===">"||h.operator===">=",c=c||h.operator==="<"||h.operator==="<=",n){if(f&&h.semver.prerelease&&h.semver.prerelease.length&&h.semver.major===f.major&&h.semver.minor===f.minor&&h.semver.patch===f.patch&&(f=!1),h.operator===">"||h.operator===">="){if(a=SH(n,h,t),a===h&&a!==n)return!1}else if(n.operator===">="&&!Dp(n.semver,String(h),t))return!1}if(s){if(g&&h.semver.prerelease&&h.semver.prerelease.length&&h.semver.major===g.major&&h.semver.minor===g.minor&&h.semver.patch===g.patch&&(g=!1),h.operator==="<"||h.operator==="<="){if(l=vH(s,h,t),l===h&&l!==s)return!1}else if(s.operator==="<="&&!Dp(s.semver,String(h),t))return!1}if(!h.operator&&(s||n)&&o!==0)return!1}return!(n&&c&&!s&&o!==0||s&&u&&!n&&o!==0||f||g)},SH=(r,e,t)=>{if(!r)return e;let i=ES(r.semver,e.semver,t);return i>0?r:i<0||e.operator===">"&&r.operator===">="?e:r},vH=(r,e,t)=>{if(!r)return e;let i=ES(r.semver,e.semver,t);return i<0?r:i>0||e.operator==="<"&&r.operator==="<="?e:r};bH.exports=zme});var ri=w((g$e,xH)=>{var IS=Qc();xH.exports={re:IS.re,src:IS.src,tokens:IS.t,SEMVER_SPEC_VERSION:Ep().SEMVER_SPEC_VERSION,SemVer:Hi(),compareIdentifiers:wI().compareIdentifiers,rcompareIdentifiers:wI().rcompareIdentifiers,parse:Sc(),valid:XK(),clean:$K(),inc:t2(),diff:a2(),major:l2(),minor:u2(),patch:f2(),prerelease:p2(),compare:Qs(),rcompare:C2(),compareLoose:E2(),compareBuild:SI(),sort:B2(),rsort:Q2(),gt:Bp(),lt:vI(),eq:QI(),neq:cS(),gte:kI(),lte:xI(),cmp:uS(),coerce:F2(),Comparator:vp(),Range:Ss(),satisfies:Pp(),toComparators:tH(),maxSatisfying:iH(),minSatisfying:sH(),minVersion:AH(),validRange:cH(),outside:FI(),gtr:dH(),ltr:mH(),intersects:yH(),simplifyRange:BH(),subset:kH()}});var yS=w(LI=>{"use strict";Object.defineProperty(LI,"__esModule",{value:!0});LI.VERSION=void 0;LI.VERSION="9.1.0"});var Yt=w((exports,module)=>{"use strict";var __spreadArray=exports&&exports.__spreadArray||function(r,e,t){if(t||arguments.length===2)for(var i=0,n=e.length,s;i{(function(r,e){typeof define=="function"&&define.amd?define([],e):typeof TI=="object"&&TI.exports?TI.exports=e():r.regexpToAst=e()})(typeof self!="undefined"?self:PH,function(){function r(){}r.prototype.saveState=function(){return{idx:this.idx,input:this.input,groupIdx:this.groupIdx}},r.prototype.restoreState=function(p){this.idx=p.idx,this.input=p.input,this.groupIdx=p.groupIdx},r.prototype.pattern=function(p){this.idx=0,this.input=p,this.groupIdx=0,this.consumeChar("/");var m=this.disjunction();this.consumeChar("/");for(var y={type:"Flags",loc:{begin:this.idx,end:p.length},global:!1,ignoreCase:!1,multiLine:!1,unicode:!1,sticky:!1};this.isRegExpFlag();)switch(this.popChar()){case"g":o(y,"global");break;case"i":o(y,"ignoreCase");break;case"m":o(y,"multiLine");break;case"u":o(y,"unicode");break;case"y":o(y,"sticky");break}if(this.idx!==this.input.length)throw Error("Redundant input: "+this.input.substring(this.idx));return{type:"Pattern",flags:y,value:m,loc:this.loc(0)}},r.prototype.disjunction=function(){var p=[],m=this.idx;for(p.push(this.alternative());this.peekChar()==="|";)this.consumeChar("|"),p.push(this.alternative());return{type:"Disjunction",value:p,loc:this.loc(m)}},r.prototype.alternative=function(){for(var p=[],m=this.idx;this.isTerm();)p.push(this.term());return{type:"Alternative",value:p,loc:this.loc(m)}},r.prototype.term=function(){return this.isAssertion()?this.assertion():this.atom()},r.prototype.assertion=function(){var p=this.idx;switch(this.popChar()){case"^":return{type:"StartAnchor",loc:this.loc(p)};case"$":return{type:"EndAnchor",loc:this.loc(p)};case"\\":switch(this.popChar()){case"b":return{type:"WordBoundary",loc:this.loc(p)};case"B":return{type:"NonWordBoundary",loc:this.loc(p)}}throw Error("Invalid Assertion Escape");case"(":this.consumeChar("?");var m;switch(this.popChar()){case"=":m="Lookahead";break;case"!":m="NegativeLookahead";break}a(m);var y=this.disjunction();return this.consumeChar(")"),{type:m,value:y,loc:this.loc(p)}}l()},r.prototype.quantifier=function(p){var m,y=this.idx;switch(this.popChar()){case"*":m={atLeast:0,atMost:Infinity};break;case"+":m={atLeast:1,atMost:Infinity};break;case"?":m={atLeast:0,atMost:1};break;case"{":var b=this.integerIncludingZero();switch(this.popChar()){case"}":m={atLeast:b,atMost:b};break;case",":var v;this.isDigit()?(v=this.integerIncludingZero(),m={atLeast:b,atMost:v}):m={atLeast:b,atMost:Infinity},this.consumeChar("}");break}if(p===!0&&m===void 0)return;a(m);break}if(!(p===!0&&m===void 0))return a(m),this.peekChar(0)==="?"?(this.consumeChar("?"),m.greedy=!1):m.greedy=!0,m.type="Quantifier",m.loc=this.loc(y),m},r.prototype.atom=function(){var p,m=this.idx;switch(this.peekChar()){case".":p=this.dotAll();break;case"\\":p=this.atomEscape();break;case"[":p=this.characterClass();break;case"(":p=this.group();break}return p===void 0&&this.isPatternCharacter()&&(p=this.patternCharacter()),a(p),p.loc=this.loc(m),this.isQuantifier()&&(p.quantifier=this.quantifier()),p},r.prototype.dotAll=function(){return this.consumeChar("."),{type:"Set",complement:!0,value:[n(` +`),n("\r"),n("\u2028"),n("\u2029")]}},r.prototype.atomEscape=function(){switch(this.consumeChar("\\"),this.peekChar()){case"1":case"2":case"3":case"4":case"5":case"6":case"7":case"8":case"9":return this.decimalEscapeAtom();case"d":case"D":case"s":case"S":case"w":case"W":return this.characterClassEscape();case"f":case"n":case"r":case"t":case"v":return this.controlEscapeAtom();case"c":return this.controlLetterEscapeAtom();case"0":return this.nulCharacterAtom();case"x":return this.hexEscapeSequenceAtom();case"u":return this.regExpUnicodeEscapeSequenceAtom();default:return this.identityEscapeAtom()}},r.prototype.decimalEscapeAtom=function(){var p=this.positiveInteger();return{type:"GroupBackReference",value:p}},r.prototype.characterClassEscape=function(){var p,m=!1;switch(this.popChar()){case"d":p=u;break;case"D":p=u,m=!0;break;case"s":p=f;break;case"S":p=f,m=!0;break;case"w":p=g;break;case"W":p=g,m=!0;break}return a(p),{type:"Set",value:p,complement:m}},r.prototype.controlEscapeAtom=function(){var p;switch(this.popChar()){case"f":p=n("\f");break;case"n":p=n(` +`);break;case"r":p=n("\r");break;case"t":p=n(" ");break;case"v":p=n("\v");break}return a(p),{type:"Character",value:p}},r.prototype.controlLetterEscapeAtom=function(){this.consumeChar("c");var p=this.popChar();if(/[a-zA-Z]/.test(p)===!1)throw Error("Invalid ");var m=p.toUpperCase().charCodeAt(0)-64;return{type:"Character",value:m}},r.prototype.nulCharacterAtom=function(){return this.consumeChar("0"),{type:"Character",value:n("\0")}},r.prototype.hexEscapeSequenceAtom=function(){return this.consumeChar("x"),this.parseHexDigits(2)},r.prototype.regExpUnicodeEscapeSequenceAtom=function(){return this.consumeChar("u"),this.parseHexDigits(4)},r.prototype.identityEscapeAtom=function(){var p=this.popChar();return{type:"Character",value:n(p)}},r.prototype.classPatternCharacterAtom=function(){switch(this.peekChar()){case` +`:case"\r":case"\u2028":case"\u2029":case"\\":case"]":throw Error("TBD");default:var p=this.popChar();return{type:"Character",value:n(p)}}},r.prototype.characterClass=function(){var p=[],m=!1;for(this.consumeChar("["),this.peekChar(0)==="^"&&(this.consumeChar("^"),m=!0);this.isClassAtom();){var y=this.classAtom(),b=y.type==="Character";if(b&&this.isRangeDash()){this.consumeChar("-");var v=this.classAtom(),k=v.type==="Character";if(k){if(v.value=this.input.length)throw Error("Unexpected end of input");this.idx++},r.prototype.loc=function(p){return{begin:p,end:this.idx}};var e=/[0-9a-fA-F]/,t=/[0-9]/,i=/[1-9]/;function n(p){return p.charCodeAt(0)}function s(p,m){p.length!==void 0?p.forEach(function(y){m.push(y)}):m.push(p)}function o(p,m){if(p[m]===!0)throw"duplicate flag "+m;p[m]=!0}function a(p){if(p===void 0)throw Error("Internal Error - Should never get here!")}function l(){throw Error("Internal Error - Should never get here!")}var c,u=[];for(c=n("0");c<=n("9");c++)u.push(c);var g=[n("_")].concat(u);for(c=n("a");c<=n("z");c++)g.push(c);for(c=n("A");c<=n("Z");c++)g.push(c);var f=[n(" "),n("\f"),n(` +`),n("\r"),n(" "),n("\v"),n(" "),n("\xA0"),n("\u1680"),n("\u2000"),n("\u2001"),n("\u2002"),n("\u2003"),n("\u2004"),n("\u2005"),n("\u2006"),n("\u2007"),n("\u2008"),n("\u2009"),n("\u200A"),n("\u2028"),n("\u2029"),n("\u202F"),n("\u205F"),n("\u3000"),n("\uFEFF")];function h(){}return h.prototype.visitChildren=function(p){for(var m in p){var y=p[m];p.hasOwnProperty(m)&&(y.type!==void 0?this.visit(y):Array.isArray(y)&&y.forEach(function(b){this.visit(b)},this))}},h.prototype.visit=function(p){switch(p.type){case"Pattern":this.visitPattern(p);break;case"Flags":this.visitFlags(p);break;case"Disjunction":this.visitDisjunction(p);break;case"Alternative":this.visitAlternative(p);break;case"StartAnchor":this.visitStartAnchor(p);break;case"EndAnchor":this.visitEndAnchor(p);break;case"WordBoundary":this.visitWordBoundary(p);break;case"NonWordBoundary":this.visitNonWordBoundary(p);break;case"Lookahead":this.visitLookahead(p);break;case"NegativeLookahead":this.visitNegativeLookahead(p);break;case"Character":this.visitCharacter(p);break;case"Set":this.visitSet(p);break;case"Group":this.visitGroup(p);break;case"GroupBackReference":this.visitGroupBackReference(p);break;case"Quantifier":this.visitQuantifier(p);break}this.visitChildren(p)},h.prototype.visitPattern=function(p){},h.prototype.visitFlags=function(p){},h.prototype.visitDisjunction=function(p){},h.prototype.visitAlternative=function(p){},h.prototype.visitStartAnchor=function(p){},h.prototype.visitEndAnchor=function(p){},h.prototype.visitWordBoundary=function(p){},h.prototype.visitNonWordBoundary=function(p){},h.prototype.visitLookahead=function(p){},h.prototype.visitNegativeLookahead=function(p){},h.prototype.visitCharacter=function(p){},h.prototype.visitSet=function(p){},h.prototype.visitGroup=function(p){},h.prototype.visitGroupBackReference=function(p){},h.prototype.visitQuantifier=function(p){},{RegExpParser:r,BaseRegExpVisitor:h,VERSION:"0.5.0"}})});var UI=w(Cg=>{"use strict";Object.defineProperty(Cg,"__esModule",{value:!0});Cg.clearRegExpParserCache=Cg.getRegExpAst=void 0;var _me=OI(),MI={},Vme=new _me.RegExpParser;function Xme(r){var e=r.toString();if(MI.hasOwnProperty(e))return MI[e];var t=Vme.pattern(e);return MI[e]=t,t}Cg.getRegExpAst=Xme;function Zme(){MI={}}Cg.clearRegExpParserCache=Zme});var LH=w(Bn=>{"use strict";var $me=Bn&&Bn.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Bn,"__esModule",{value:!0});Bn.canMatchCharCode=Bn.firstCharOptimizedIndices=Bn.getOptimizedStartCodesIndices=Bn.failedOptimizationPrefixMsg=void 0;var DH=OI(),vs=Yt(),RH=UI(),Ga=wS(),FH="Complement Sets are not supported for first char optimization";Bn.failedOptimizationPrefixMsg=`Unable to use "first char" lexer optimizations: +`;function eEe(r,e){e===void 0&&(e=!1);try{var t=(0,RH.getRegExpAst)(r),i=KI(t.value,{},t.flags.ignoreCase);return i}catch(s){if(s.message===FH)e&&(0,vs.PRINT_WARNING)(""+Bn.failedOptimizationPrefixMsg+(" Unable to optimize: < "+r.toString()+` > +`)+` Complement Sets cannot be automatically optimized. + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#COMPLEMENT for details.`);else{var n="";e&&(n=` + This will disable the lexer's first char optimizations. + See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#REGEXP_PARSING for details.`),(0,vs.PRINT_ERROR)(Bn.failedOptimizationPrefixMsg+` +`+(" Failed parsing: < "+r.toString()+` > +`)+(" Using the regexp-to-ast library version: "+DH.VERSION+` +`)+" Please open an issue at: https://github.com/bd82/regexp-to-ast/issues"+n)}}return[]}Bn.getOptimizedStartCodesIndices=eEe;function KI(r,e,t){switch(r.type){case"Disjunction":for(var i=0;i=Ga.minOptimizationVal)for(var f=u.from>=Ga.minOptimizationVal?u.from:Ga.minOptimizationVal,h=u.to,p=(0,Ga.charCodeToOptimizedIndex)(f),m=(0,Ga.charCodeToOptimizedIndex)(h),y=p;y<=m;y++)e[y]=y}}});break;case"Group":KI(o.value,e,t);break;default:throw Error("Non Exhaustive Match")}var a=o.quantifier!==void 0&&o.quantifier.atLeast===0;if(o.type==="Group"&&BS(o)===!1||o.type!=="Group"&&a===!1)break}break;default:throw Error("non exhaustive match!")}return(0,vs.values)(e)}Bn.firstCharOptimizedIndices=KI;function HI(r,e,t){var i=(0,Ga.charCodeToOptimizedIndex)(r);e[i]=i,t===!0&&tEe(r,e)}function tEe(r,e){var t=String.fromCharCode(r),i=t.toUpperCase();if(i!==t){var n=(0,Ga.charCodeToOptimizedIndex)(i.charCodeAt(0));e[n]=n}else{var s=t.toLowerCase();if(s!==t){var n=(0,Ga.charCodeToOptimizedIndex)(s.charCodeAt(0));e[n]=n}}}function NH(r,e){return(0,vs.find)(r.value,function(t){if(typeof t=="number")return(0,vs.contains)(e,t);var i=t;return(0,vs.find)(e,function(n){return i.from<=n&&n<=i.to})!==void 0})}function BS(r){return r.quantifier&&r.quantifier.atLeast===0?!0:r.value?(0,vs.isArray)(r.value)?(0,vs.every)(r.value,BS):BS(r.value):!1}var rEe=function(r){$me(e,r);function e(t){var i=r.call(this)||this;return i.targetCharCodes=t,i.found=!1,i}return e.prototype.visitChildren=function(t){if(this.found!==!0){switch(t.type){case"Lookahead":this.visitLookahead(t);return;case"NegativeLookahead":this.visitNegativeLookahead(t);return}r.prototype.visitChildren.call(this,t)}},e.prototype.visitCharacter=function(t){(0,vs.contains)(this.targetCharCodes,t.value)&&(this.found=!0)},e.prototype.visitSet=function(t){t.complement?NH(t,this.targetCharCodes)===void 0&&(this.found=!0):NH(t,this.targetCharCodes)!==void 0&&(this.found=!0)},e}(DH.BaseRegExpVisitor);function iEe(r,e){if(e instanceof RegExp){var t=(0,RH.getRegExpAst)(e),i=new rEe(r);return i.visit(t),i.found}else return(0,vs.find)(e,function(n){return(0,vs.contains)(r,n.charCodeAt(0))})!==void 0}Bn.canMatchCharCode=iEe});var wS=w(Ze=>{"use strict";var TH=Ze&&Ze.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Ze,"__esModule",{value:!0});Ze.charCodeToOptimizedIndex=Ze.minOptimizationVal=Ze.buildLineBreakIssueMessage=Ze.LineTerminatorOptimizedTester=Ze.isShortPattern=Ze.isCustomPattern=Ze.cloneEmptyGroups=Ze.performWarningRuntimeChecks=Ze.performRuntimeChecks=Ze.addStickyFlag=Ze.addStartOfInput=Ze.findUnreachablePatterns=Ze.findModesThatDoNotExist=Ze.findInvalidGroupType=Ze.findDuplicatePatterns=Ze.findUnsupportedFlags=Ze.findStartOfInputAnchor=Ze.findEmptyMatchRegExps=Ze.findEndOfInputAnchor=Ze.findInvalidPatterns=Ze.findMissingPatterns=Ze.validatePatterns=Ze.analyzeTokenTypes=Ze.enableSticky=Ze.disableSticky=Ze.SUPPORT_STICKY=Ze.MODES=Ze.DEFAULT_MODE=void 0;var OH=OI(),Ar=Rp(),Ne=Yt(),mg=LH(),MH=UI(),Ko="PATTERN";Ze.DEFAULT_MODE="defaultMode";Ze.MODES="modes";Ze.SUPPORT_STICKY=typeof new RegExp("(?:)").sticky=="boolean";function nEe(){Ze.SUPPORT_STICKY=!1}Ze.disableSticky=nEe;function sEe(){Ze.SUPPORT_STICKY=!0}Ze.enableSticky=sEe;function aEe(r,e){e=(0,Ne.defaults)(e,{useSticky:Ze.SUPPORT_STICKY,debug:!1,safeMode:!1,positionTracking:"full",lineTerminatorCharacters:["\r",` +`],tracer:function(v,k){return k()}});var t=e.tracer;t("initCharCodeToOptimizedIndexMap",function(){oEe()});var i;t("Reject Lexer.NA",function(){i=(0,Ne.reject)(r,function(v){return v[Ko]===Ar.Lexer.NA})});var n=!1,s;t("Transform Patterns",function(){n=!1,s=(0,Ne.map)(i,function(v){var k=v[Ko];if((0,Ne.isRegExp)(k)){var T=k.source;return T.length===1&&T!=="^"&&T!=="$"&&T!=="."&&!k.ignoreCase?T:T.length===2&&T[0]==="\\"&&!(0,Ne.contains)(["d","D","s","S","t","r","n","t","0","c","b","B","f","v","w","W"],T[1])?T[1]:e.useSticky?QS(k):bS(k)}else{if((0,Ne.isFunction)(k))return n=!0,{exec:k};if((0,Ne.has)(k,"exec"))return n=!0,k;if(typeof k=="string"){if(k.length===1)return k;var Y=k.replace(/[\\^$.*+?()[\]{}|]/g,"\\$&"),q=new RegExp(Y);return e.useSticky?QS(q):bS(q)}else throw Error("non exhaustive match")}})});var o,a,l,c,u;t("misc mapping",function(){o=(0,Ne.map)(i,function(v){return v.tokenTypeIdx}),a=(0,Ne.map)(i,function(v){var k=v.GROUP;if(k!==Ar.Lexer.SKIPPED){if((0,Ne.isString)(k))return k;if((0,Ne.isUndefined)(k))return!1;throw Error("non exhaustive match")}}),l=(0,Ne.map)(i,function(v){var k=v.LONGER_ALT;if(k){var T=(0,Ne.isArray)(k)?(0,Ne.map)(k,function(Y){return(0,Ne.indexOf)(i,Y)}):[(0,Ne.indexOf)(i,k)];return T}}),c=(0,Ne.map)(i,function(v){return v.PUSH_MODE}),u=(0,Ne.map)(i,function(v){return(0,Ne.has)(v,"POP_MODE")})});var g;t("Line Terminator Handling",function(){var v=HH(e.lineTerminatorCharacters);g=(0,Ne.map)(i,function(k){return!1}),e.positionTracking!=="onlyOffset"&&(g=(0,Ne.map)(i,function(k){if((0,Ne.has)(k,"LINE_BREAKS"))return k.LINE_BREAKS;if(KH(k,v)===!1)return(0,mg.canMatchCharCode)(v,k.PATTERN)}))});var f,h,p,m;t("Misc Mapping #2",function(){f=(0,Ne.map)(i,SS),h=(0,Ne.map)(s,UH),p=(0,Ne.reduce)(i,function(v,k){var T=k.GROUP;return(0,Ne.isString)(T)&&T!==Ar.Lexer.SKIPPED&&(v[T]=[]),v},{}),m=(0,Ne.map)(s,function(v,k){return{pattern:s[k],longerAlt:l[k],canLineTerminator:g[k],isCustom:f[k],short:h[k],group:a[k],push:c[k],pop:u[k],tokenTypeIdx:o[k],tokenType:i[k]}})});var y=!0,b=[];return e.safeMode||t("First Char Optimization",function(){b=(0,Ne.reduce)(i,function(v,k,T){if(typeof k.PATTERN=="string"){var Y=k.PATTERN.charCodeAt(0),q=kS(Y);vS(v,q,m[T])}else if((0,Ne.isArray)(k.START_CHARS_HINT)){var $;(0,Ne.forEach)(k.START_CHARS_HINT,function(ne){var ee=typeof ne=="string"?ne.charCodeAt(0):ne,A=kS(ee);$!==A&&($=A,vS(v,A,m[T]))})}else if((0,Ne.isRegExp)(k.PATTERN))if(k.PATTERN.unicode)y=!1,e.ensureOptimizations&&(0,Ne.PRINT_ERROR)(""+mg.failedOptimizationPrefixMsg+(" Unable to analyze < "+k.PATTERN.toString()+` > pattern. +`)+` The regexp unicode flag is not currently supported by the regexp-to-ast library. + This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNICODE_OPTIMIZE`);else{var z=(0,mg.getOptimizedStartCodesIndices)(k.PATTERN,e.ensureOptimizations);(0,Ne.isEmpty)(z)&&(y=!1),(0,Ne.forEach)(z,function(ne){vS(v,ne,m[T])})}else e.ensureOptimizations&&(0,Ne.PRINT_ERROR)(""+mg.failedOptimizationPrefixMsg+(" TokenType: <"+k.name+`> is using a custom token pattern without providing parameter. +`)+` This will disable the lexer's first char optimizations. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_OPTIMIZE`),y=!1;return v},[])}),t("ArrayPacking",function(){b=(0,Ne.packArray)(b)}),{emptyGroups:p,patternIdxToConfig:m,charCodeToPatternIdxToConfig:b,hasCustom:n,canBeOptimized:y}}Ze.analyzeTokenTypes=aEe;function lEe(r,e){var t=[],i=jH(r);t=t.concat(i.errors);var n=GH(i.valid),s=n.valid;return t=t.concat(n.errors),t=t.concat(AEe(s)),t=t.concat(YH(s)),t=t.concat(qH(s,e)),t=t.concat(JH(s)),t}Ze.validatePatterns=lEe;function AEe(r){var e=[],t=(0,Ne.filter)(r,function(i){return(0,Ne.isRegExp)(i[Ko])});return e=e.concat(WH(t)),e=e.concat(_H(t)),e=e.concat(VH(t)),e=e.concat(XH(t)),e=e.concat(zH(t)),e}function jH(r){var e=(0,Ne.filter)(r,function(n){return!(0,Ne.has)(n,Ko)}),t=(0,Ne.map)(e,function(n){return{message:"Token Type: ->"+n.name+"<- missing static 'PATTERN' property",type:Ar.LexerDefinitionErrorType.MISSING_PATTERN,tokenTypes:[n]}}),i=(0,Ne.difference)(r,e);return{errors:t,valid:i}}Ze.findMissingPatterns=jH;function GH(r){var e=(0,Ne.filter)(r,function(n){var s=n[Ko];return!(0,Ne.isRegExp)(s)&&!(0,Ne.isFunction)(s)&&!(0,Ne.has)(s,"exec")&&!(0,Ne.isString)(s)}),t=(0,Ne.map)(e,function(n){return{message:"Token Type: ->"+n.name+"<- static 'PATTERN' can only be a RegExp, a Function matching the {CustomPatternMatcherFunc} type or an Object matching the {ICustomPattern} interface.",type:Ar.LexerDefinitionErrorType.INVALID_PATTERN,tokenTypes:[n]}}),i=(0,Ne.difference)(r,e);return{errors:t,valid:i}}Ze.findInvalidPatterns=GH;var cEe=/[^\\][\$]/;function WH(r){var e=function(n){TH(s,n);function s(){var o=n!==null&&n.apply(this,arguments)||this;return o.found=!1,o}return s.prototype.visitEndAnchor=function(o){this.found=!0},s}(OH.BaseRegExpVisitor),t=(0,Ne.filter)(r,function(n){var s=n[Ko];try{var o=(0,MH.getRegExpAst)(s),a=new e;return a.visit(o),a.found}catch(l){return cEe.test(s.source)}}),i=(0,Ne.map)(t,function(n){return{message:`Unexpected RegExp Anchor Error: + Token Type: ->`+n.name+`<- static 'PATTERN' cannot contain end of input anchor '$' + See chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Ar.LexerDefinitionErrorType.EOI_ANCHOR_FOUND,tokenTypes:[n]}});return i}Ze.findEndOfInputAnchor=WH;function zH(r){var e=(0,Ne.filter)(r,function(i){var n=i[Ko];return n.test("")}),t=(0,Ne.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'PATTERN' must not match an empty string",type:Ar.LexerDefinitionErrorType.EMPTY_MATCH_PATTERN,tokenTypes:[i]}});return t}Ze.findEmptyMatchRegExps=zH;var uEe=/[^\\[][\^]|^\^/;function _H(r){var e=function(n){TH(s,n);function s(){var o=n!==null&&n.apply(this,arguments)||this;return o.found=!1,o}return s.prototype.visitStartAnchor=function(o){this.found=!0},s}(OH.BaseRegExpVisitor),t=(0,Ne.filter)(r,function(n){var s=n[Ko];try{var o=(0,MH.getRegExpAst)(s),a=new e;return a.visit(o),a.found}catch(l){return uEe.test(s.source)}}),i=(0,Ne.map)(t,function(n){return{message:`Unexpected RegExp Anchor Error: + Token Type: ->`+n.name+`<- static 'PATTERN' cannot contain start of input anchor '^' + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#ANCHORS for details.`,type:Ar.LexerDefinitionErrorType.SOI_ANCHOR_FOUND,tokenTypes:[n]}});return i}Ze.findStartOfInputAnchor=_H;function VH(r){var e=(0,Ne.filter)(r,function(i){var n=i[Ko];return n instanceof RegExp&&(n.multiline||n.global)}),t=(0,Ne.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'PATTERN' may NOT contain global('g') or multiline('m')",type:Ar.LexerDefinitionErrorType.UNSUPPORTED_FLAGS_FOUND,tokenTypes:[i]}});return t}Ze.findUnsupportedFlags=VH;function XH(r){var e=[],t=(0,Ne.map)(r,function(s){return(0,Ne.reduce)(r,function(o,a){return s.PATTERN.source===a.PATTERN.source&&!(0,Ne.contains)(e,a)&&a.PATTERN!==Ar.Lexer.NA&&(e.push(a),o.push(a)),o},[])});t=(0,Ne.compact)(t);var i=(0,Ne.filter)(t,function(s){return s.length>1}),n=(0,Ne.map)(i,function(s){var o=(0,Ne.map)(s,function(l){return l.name}),a=(0,Ne.first)(s).PATTERN;return{message:"The same RegExp pattern ->"+a+"<-"+("has been used in all of the following Token Types: "+o.join(", ")+" <-"),type:Ar.LexerDefinitionErrorType.DUPLICATE_PATTERNS_FOUND,tokenTypes:s}});return n}Ze.findDuplicatePatterns=XH;function YH(r){var e=(0,Ne.filter)(r,function(i){if(!(0,Ne.has)(i,"GROUP"))return!1;var n=i.GROUP;return n!==Ar.Lexer.SKIPPED&&n!==Ar.Lexer.NA&&!(0,Ne.isString)(n)}),t=(0,Ne.map)(e,function(i){return{message:"Token Type: ->"+i.name+"<- static 'GROUP' can only be Lexer.SKIPPED/Lexer.NA/A String",type:Ar.LexerDefinitionErrorType.INVALID_GROUP_TYPE_FOUND,tokenTypes:[i]}});return t}Ze.findInvalidGroupType=YH;function qH(r,e){var t=(0,Ne.filter)(r,function(n){return n.PUSH_MODE!==void 0&&!(0,Ne.contains)(e,n.PUSH_MODE)}),i=(0,Ne.map)(t,function(n){var s="Token Type: ->"+n.name+"<- static 'PUSH_MODE' value cannot refer to a Lexer Mode ->"+n.PUSH_MODE+"<-which does not exist";return{message:s,type:Ar.LexerDefinitionErrorType.PUSH_MODE_DOES_NOT_EXIST,tokenTypes:[n]}});return i}Ze.findModesThatDoNotExist=qH;function JH(r){var e=[],t=(0,Ne.reduce)(r,function(i,n,s){var o=n.PATTERN;return o===Ar.Lexer.NA||((0,Ne.isString)(o)?i.push({str:o,idx:s,tokenType:n}):(0,Ne.isRegExp)(o)&&fEe(o)&&i.push({str:o.source,idx:s,tokenType:n})),i},[]);return(0,Ne.forEach)(r,function(i,n){(0,Ne.forEach)(t,function(s){var o=s.str,a=s.idx,l=s.tokenType;if(n"+i.name+"<-")+`in the lexer's definition. +See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#UNREACHABLE`;e.push({message:c,type:Ar.LexerDefinitionErrorType.UNREACHABLE_PATTERN,tokenTypes:[i,l]})}})}),e}Ze.findUnreachablePatterns=JH;function gEe(r,e){if((0,Ne.isRegExp)(e)){var t=e.exec(r);return t!==null&&t.index===0}else{if((0,Ne.isFunction)(e))return e(r,0,[],{});if((0,Ne.has)(e,"exec"))return e.exec(r,0,[],{});if(typeof e=="string")return e===r;throw Error("non exhaustive match")}}function fEe(r){var e=[".","\\","[","]","|","^","$","(",")","?","*","+","{"];return(0,Ne.find)(e,function(t){return r.source.indexOf(t)!==-1})===void 0}function bS(r){var e=r.ignoreCase?"i":"";return new RegExp("^(?:"+r.source+")",e)}Ze.addStartOfInput=bS;function QS(r){var e=r.ignoreCase?"iy":"y";return new RegExp(""+r.source,e)}Ze.addStickyFlag=QS;function hEe(r,e,t){var i=[];return(0,Ne.has)(r,Ze.DEFAULT_MODE)||i.push({message:"A MultiMode Lexer cannot be initialized without a <"+Ze.DEFAULT_MODE+`> property in its definition +`,type:Ar.LexerDefinitionErrorType.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE}),(0,Ne.has)(r,Ze.MODES)||i.push({message:"A MultiMode Lexer cannot be initialized without a <"+Ze.MODES+`> property in its definition +`,type:Ar.LexerDefinitionErrorType.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY}),(0,Ne.has)(r,Ze.MODES)&&(0,Ne.has)(r,Ze.DEFAULT_MODE)&&!(0,Ne.has)(r.modes,r.defaultMode)&&i.push({message:"A MultiMode Lexer cannot be initialized with a "+Ze.DEFAULT_MODE+": <"+r.defaultMode+`>which does not exist +`,type:Ar.LexerDefinitionErrorType.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST}),(0,Ne.has)(r,Ze.MODES)&&(0,Ne.forEach)(r.modes,function(n,s){(0,Ne.forEach)(n,function(o,a){(0,Ne.isUndefined)(o)&&i.push({message:"A Lexer cannot be initialized using an undefined Token Type. Mode:"+("<"+s+"> at index: <"+a+`> +`),type:Ar.LexerDefinitionErrorType.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED})})}),i}Ze.performRuntimeChecks=hEe;function pEe(r,e,t){var i=[],n=!1,s=(0,Ne.compact)((0,Ne.flatten)((0,Ne.mapValues)(r.modes,function(l){return l}))),o=(0,Ne.reject)(s,function(l){return l[Ko]===Ar.Lexer.NA}),a=HH(t);return e&&(0,Ne.forEach)(o,function(l){var c=KH(l,a);if(c!==!1){var u=ZH(l,c),g={message:u,type:c.issue,tokenType:l};i.push(g)}else(0,Ne.has)(l,"LINE_BREAKS")?l.LINE_BREAKS===!0&&(n=!0):(0,mg.canMatchCharCode)(a,l.PATTERN)&&(n=!0)}),e&&!n&&i.push({message:`Warning: No LINE_BREAKS Found. + This Lexer has been defined to track line and column information, + But none of the Token Types can be identified as matching a line terminator. + See https://chevrotain.io/docs/guide/resolving_lexer_errors.html#LINE_BREAKS + for details.`,type:Ar.LexerDefinitionErrorType.NO_LINE_BREAKS_FLAGS}),i}Ze.performWarningRuntimeChecks=pEe;function dEe(r){var e={},t=(0,Ne.keys)(r);return(0,Ne.forEach)(t,function(i){var n=r[i];if((0,Ne.isArray)(n))e[i]=[];else throw Error("non exhaustive match")}),e}Ze.cloneEmptyGroups=dEe;function SS(r){var e=r.PATTERN;if((0,Ne.isRegExp)(e))return!1;if((0,Ne.isFunction)(e))return!0;if((0,Ne.has)(e,"exec"))return!0;if((0,Ne.isString)(e))return!1;throw Error("non exhaustive match")}Ze.isCustomPattern=SS;function UH(r){return(0,Ne.isString)(r)&&r.length===1?r.charCodeAt(0):!1}Ze.isShortPattern=UH;Ze.LineTerminatorOptimizedTester={test:function(r){for(var e=r.length,t=this.lastIndex;t Token Type +`)+(" Root cause: "+e.errMsg+`. +`)+" For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#IDENTIFY_TERMINATOR";if(e.issue===Ar.LexerDefinitionErrorType.CUSTOM_LINE_BREAK)return`Warning: A Custom Token Pattern should specify the option. +`+(" The problem is in the <"+r.name+`> Token Type +`)+" For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#CUSTOM_LINE_BREAK";throw Error("non exhaustive match")}Ze.buildLineBreakIssueMessage=ZH;function HH(r){var e=(0,Ne.map)(r,function(t){return(0,Ne.isString)(t)&&t.length>0?t.charCodeAt(0):t});return e}function vS(r,e,t){r[e]===void 0?r[e]=[t]:r[e].push(t)}Ze.minOptimizationVal=256;var jI=[];function kS(r){return r255?255+~~(r/255):r}}});var Eg=w(Ft=>{"use strict";Object.defineProperty(Ft,"__esModule",{value:!0});Ft.isTokenType=Ft.hasExtendingTokensTypesMapProperty=Ft.hasExtendingTokensTypesProperty=Ft.hasCategoriesProperty=Ft.hasShortKeyProperty=Ft.singleAssignCategoriesToksMap=Ft.assignCategoriesMapProp=Ft.assignCategoriesTokensProp=Ft.assignTokenDefaultProps=Ft.expandCategories=Ft.augmentTokenTypes=Ft.tokenIdxToClass=Ft.tokenShortNameIdx=Ft.tokenStructuredMatcherNoCategories=Ft.tokenStructuredMatcher=void 0;var ii=Yt();function CEe(r,e){var t=r.tokenTypeIdx;return t===e.tokenTypeIdx?!0:e.isParent===!0&&e.categoryMatchesMap[t]===!0}Ft.tokenStructuredMatcher=CEe;function mEe(r,e){return r.tokenTypeIdx===e.tokenTypeIdx}Ft.tokenStructuredMatcherNoCategories=mEe;Ft.tokenShortNameIdx=1;Ft.tokenIdxToClass={};function EEe(r){var e=$H(r);ej(e),rj(e),tj(e),(0,ii.forEach)(e,function(t){t.isParent=t.categoryMatches.length>0})}Ft.augmentTokenTypes=EEe;function $H(r){for(var e=(0,ii.cloneArr)(r),t=r,i=!0;i;){t=(0,ii.compact)((0,ii.flatten)((0,ii.map)(t,function(s){return s.CATEGORIES})));var n=(0,ii.difference)(t,e);e=e.concat(n),(0,ii.isEmpty)(n)?i=!1:t=n}return e}Ft.expandCategories=$H;function ej(r){(0,ii.forEach)(r,function(e){ij(e)||(Ft.tokenIdxToClass[Ft.tokenShortNameIdx]=e,e.tokenTypeIdx=Ft.tokenShortNameIdx++),xS(e)&&!(0,ii.isArray)(e.CATEGORIES)&&(e.CATEGORIES=[e.CATEGORIES]),xS(e)||(e.CATEGORIES=[]),nj(e)||(e.categoryMatches=[]),sj(e)||(e.categoryMatchesMap={})})}Ft.assignTokenDefaultProps=ej;function tj(r){(0,ii.forEach)(r,function(e){e.categoryMatches=[],(0,ii.forEach)(e.categoryMatchesMap,function(t,i){e.categoryMatches.push(Ft.tokenIdxToClass[i].tokenTypeIdx)})})}Ft.assignCategoriesTokensProp=tj;function rj(r){(0,ii.forEach)(r,function(e){PS([],e)})}Ft.assignCategoriesMapProp=rj;function PS(r,e){(0,ii.forEach)(r,function(t){e.categoryMatchesMap[t.tokenTypeIdx]=!0}),(0,ii.forEach)(e.CATEGORIES,function(t){var i=r.concat(e);(0,ii.contains)(i,t)||PS(i,t)})}Ft.singleAssignCategoriesToksMap=PS;function ij(r){return(0,ii.has)(r,"tokenTypeIdx")}Ft.hasShortKeyProperty=ij;function xS(r){return(0,ii.has)(r,"CATEGORIES")}Ft.hasCategoriesProperty=xS;function nj(r){return(0,ii.has)(r,"categoryMatches")}Ft.hasExtendingTokensTypesProperty=nj;function sj(r){return(0,ii.has)(r,"categoryMatchesMap")}Ft.hasExtendingTokensTypesMapProperty=sj;function IEe(r){return(0,ii.has)(r,"tokenTypeIdx")}Ft.isTokenType=IEe});var DS=w(GI=>{"use strict";Object.defineProperty(GI,"__esModule",{value:!0});GI.defaultLexerErrorProvider=void 0;GI.defaultLexerErrorProvider={buildUnableToPopLexerModeMessage:function(r){return"Unable to pop Lexer Mode after encountering Token ->"+r.image+"<- The Mode Stack is empty"},buildUnexpectedCharactersMessage:function(r,e,t,i,n){return"unexpected character: ->"+r.charAt(e)+"<- at offset: "+e+","+(" skipped "+t+" characters.")}}});var Rp=w(Pc=>{"use strict";Object.defineProperty(Pc,"__esModule",{value:!0});Pc.Lexer=Pc.LexerDefinitionErrorType=void 0;var lo=wS(),lr=Yt(),yEe=Eg(),wEe=DS(),BEe=UI(),bEe;(function(r){r[r.MISSING_PATTERN=0]="MISSING_PATTERN",r[r.INVALID_PATTERN=1]="INVALID_PATTERN",r[r.EOI_ANCHOR_FOUND=2]="EOI_ANCHOR_FOUND",r[r.UNSUPPORTED_FLAGS_FOUND=3]="UNSUPPORTED_FLAGS_FOUND",r[r.DUPLICATE_PATTERNS_FOUND=4]="DUPLICATE_PATTERNS_FOUND",r[r.INVALID_GROUP_TYPE_FOUND=5]="INVALID_GROUP_TYPE_FOUND",r[r.PUSH_MODE_DOES_NOT_EXIST=6]="PUSH_MODE_DOES_NOT_EXIST",r[r.MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE=7]="MULTI_MODE_LEXER_WITHOUT_DEFAULT_MODE",r[r.MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY=8]="MULTI_MODE_LEXER_WITHOUT_MODES_PROPERTY",r[r.MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST=9]="MULTI_MODE_LEXER_DEFAULT_MODE_VALUE_DOES_NOT_EXIST",r[r.LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED=10]="LEXER_DEFINITION_CANNOT_CONTAIN_UNDEFINED",r[r.SOI_ANCHOR_FOUND=11]="SOI_ANCHOR_FOUND",r[r.EMPTY_MATCH_PATTERN=12]="EMPTY_MATCH_PATTERN",r[r.NO_LINE_BREAKS_FLAGS=13]="NO_LINE_BREAKS_FLAGS",r[r.UNREACHABLE_PATTERN=14]="UNREACHABLE_PATTERN",r[r.IDENTIFY_TERMINATOR=15]="IDENTIFY_TERMINATOR",r[r.CUSTOM_LINE_BREAK=16]="CUSTOM_LINE_BREAK"})(bEe=Pc.LexerDefinitionErrorType||(Pc.LexerDefinitionErrorType={}));var Fp={deferDefinitionErrorsHandling:!1,positionTracking:"full",lineTerminatorsPattern:/\n|\r\n?/g,lineTerminatorCharacters:[` +`,"\r"],ensureOptimizations:!1,safeMode:!1,errorMessageProvider:wEe.defaultLexerErrorProvider,traceInitPerf:!1,skipValidations:!1};Object.freeze(Fp);var QEe=function(){function r(e,t){var i=this;if(t===void 0&&(t=Fp),this.lexerDefinition=e,this.lexerDefinitionErrors=[],this.lexerDefinitionWarning=[],this.patternIdxToConfig={},this.charCodeToPatternIdxToConfig={},this.modes=[],this.emptyGroups={},this.config=void 0,this.trackStartLines=!0,this.trackEndLines=!0,this.hasCustom=!1,this.canModeBeOptimized={},typeof t=="boolean")throw Error(`The second argument to the Lexer constructor is now an ILexerConfig Object. +a boolean 2nd argument is no longer supported`);this.config=(0,lr.merge)(Fp,t);var n=this.config.traceInitPerf;n===!0?(this.traceInitMaxIdent=Infinity,this.traceInitPerf=!0):typeof n=="number"&&(this.traceInitMaxIdent=n,this.traceInitPerf=!0),this.traceInitIndent=-1,this.TRACE_INIT("Lexer Constructor",function(){var s,o=!0;i.TRACE_INIT("Lexer Config handling",function(){if(i.config.lineTerminatorsPattern===Fp.lineTerminatorsPattern)i.config.lineTerminatorsPattern=lo.LineTerminatorOptimizedTester;else if(i.config.lineTerminatorCharacters===Fp.lineTerminatorCharacters)throw Error(`Error: Missing property on the Lexer config. + For details See: https://chevrotain.io/docs/guide/resolving_lexer_errors.html#MISSING_LINE_TERM_CHARS`);if(t.safeMode&&t.ensureOptimizations)throw Error('"safeMode" and "ensureOptimizations" flags are mutually exclusive.');i.trackStartLines=/full|onlyStart/i.test(i.config.positionTracking),i.trackEndLines=/full/i.test(i.config.positionTracking),(0,lr.isArray)(e)?(s={modes:{}},s.modes[lo.DEFAULT_MODE]=(0,lr.cloneArr)(e),s[lo.DEFAULT_MODE]=lo.DEFAULT_MODE):(o=!1,s=(0,lr.cloneObj)(e))}),i.config.skipValidations===!1&&(i.TRACE_INIT("performRuntimeChecks",function(){i.lexerDefinitionErrors=i.lexerDefinitionErrors.concat((0,lo.performRuntimeChecks)(s,i.trackStartLines,i.config.lineTerminatorCharacters))}),i.TRACE_INIT("performWarningRuntimeChecks",function(){i.lexerDefinitionWarning=i.lexerDefinitionWarning.concat((0,lo.performWarningRuntimeChecks)(s,i.trackStartLines,i.config.lineTerminatorCharacters))})),s.modes=s.modes?s.modes:{},(0,lr.forEach)(s.modes,function(u,g){s.modes[g]=(0,lr.reject)(u,function(f){return(0,lr.isUndefined)(f)})});var a=(0,lr.keys)(s.modes);if((0,lr.forEach)(s.modes,function(u,g){i.TRACE_INIT("Mode: <"+g+"> processing",function(){if(i.modes.push(g),i.config.skipValidations===!1&&i.TRACE_INIT("validatePatterns",function(){i.lexerDefinitionErrors=i.lexerDefinitionErrors.concat((0,lo.validatePatterns)(u,a))}),(0,lr.isEmpty)(i.lexerDefinitionErrors)){(0,yEe.augmentTokenTypes)(u);var f;i.TRACE_INIT("analyzeTokenTypes",function(){f=(0,lo.analyzeTokenTypes)(u,{lineTerminatorCharacters:i.config.lineTerminatorCharacters,positionTracking:t.positionTracking,ensureOptimizations:t.ensureOptimizations,safeMode:t.safeMode,tracer:i.TRACE_INIT.bind(i)})}),i.patternIdxToConfig[g]=f.patternIdxToConfig,i.charCodeToPatternIdxToConfig[g]=f.charCodeToPatternIdxToConfig,i.emptyGroups=(0,lr.merge)(i.emptyGroups,f.emptyGroups),i.hasCustom=f.hasCustom||i.hasCustom,i.canModeBeOptimized[g]=f.canBeOptimized}})}),i.defaultMode=s.defaultMode,!(0,lr.isEmpty)(i.lexerDefinitionErrors)&&!i.config.deferDefinitionErrorsHandling){var l=(0,lr.map)(i.lexerDefinitionErrors,function(u){return u.message}),c=l.join(`----------------------- +`);throw new Error(`Errors detected in definition of Lexer: +`+c)}(0,lr.forEach)(i.lexerDefinitionWarning,function(u){(0,lr.PRINT_WARNING)(u.message)}),i.TRACE_INIT("Choosing sub-methods implementations",function(){if(lo.SUPPORT_STICKY?(i.chopInput=lr.IDENTITY,i.match=i.matchWithTest):(i.updateLastIndex=lr.NOOP,i.match=i.matchWithExec),o&&(i.handleModes=lr.NOOP),i.trackStartLines===!1&&(i.computeNewColumn=lr.IDENTITY),i.trackEndLines===!1&&(i.updateTokenEndLineColumnLocation=lr.NOOP),/full/i.test(i.config.positionTracking))i.createTokenInstance=i.createFullToken;else if(/onlyStart/i.test(i.config.positionTracking))i.createTokenInstance=i.createStartOnlyToken;else if(/onlyOffset/i.test(i.config.positionTracking))i.createTokenInstance=i.createOffsetOnlyToken;else throw Error('Invalid config option: "'+i.config.positionTracking+'"');i.hasCustom?(i.addToken=i.addTokenUsingPush,i.handlePayload=i.handlePayloadWithCustom):(i.addToken=i.addTokenUsingMemberAccess,i.handlePayload=i.handlePayloadNoCustom)}),i.TRACE_INIT("Failed Optimization Warnings",function(){var u=(0,lr.reduce)(i.canModeBeOptimized,function(g,f,h){return f===!1&&g.push(h),g},[]);if(t.ensureOptimizations&&!(0,lr.isEmpty)(u))throw Error("Lexer Modes: < "+u.join(", ")+` > cannot be optimized. + Disable the "ensureOptimizations" lexer config flag to silently ignore this and run the lexer in an un-optimized mode. + Or inspect the console log for details on how to resolve these issues.`)}),i.TRACE_INIT("clearRegExpParserCache",function(){(0,BEe.clearRegExpParserCache)()}),i.TRACE_INIT("toFastProperties",function(){(0,lr.toFastProperties)(i)})})}return r.prototype.tokenize=function(e,t){if(t===void 0&&(t=this.defaultMode),!(0,lr.isEmpty)(this.lexerDefinitionErrors)){var i=(0,lr.map)(this.lexerDefinitionErrors,function(o){return o.message}),n=i.join(`----------------------- +`);throw new Error(`Unable to Tokenize because Errors detected in definition of Lexer: +`+n)}var s=this.tokenizeInternal(e,t);return s},r.prototype.tokenizeInternal=function(e,t){var i=this,n,s,o,a,l,c,u,g,f,h,p,m,y,b,v,k,T=e,Y=T.length,q=0,$=0,z=this.hasCustom?0:Math.floor(e.length/10),ne=new Array(z),ee=[],A=this.trackStartLines?1:void 0,oe=this.trackStartLines?1:void 0,ce=(0,lo.cloneEmptyGroups)(this.emptyGroups),Z=this.trackStartLines,O=this.config.lineTerminatorsPattern,L=0,de=[],Be=[],Ge=[],re=[];Object.freeze(re);var se=void 0;function be(){return de}function he(Sr){var Gn=(0,lo.charCodeToOptimizedIndex)(Sr),fs=Be[Gn];return fs===void 0?re:fs}var Fe=function(Sr){if(Ge.length===1&&Sr.tokenType.PUSH_MODE===void 0){var Gn=i.config.errorMessageProvider.buildUnableToPopLexerModeMessage(Sr);ee.push({offset:Sr.startOffset,line:Sr.startLine!==void 0?Sr.startLine:void 0,column:Sr.startColumn!==void 0?Sr.startColumn:void 0,length:Sr.image.length,message:Gn})}else{Ge.pop();var fs=(0,lr.last)(Ge);de=i.patternIdxToConfig[fs],Be=i.charCodeToPatternIdxToConfig[fs],L=de.length;var Qa=i.canModeBeOptimized[fs]&&i.config.safeMode===!1;Be&&Qa?se=he:se=be}};function Ue(Sr){Ge.push(Sr),Be=this.charCodeToPatternIdxToConfig[Sr],de=this.patternIdxToConfig[Sr],L=de.length,L=de.length;var Gn=this.canModeBeOptimized[Sr]&&this.config.safeMode===!1;Be&&Gn?se=he:se=be}Ue.call(this,t);for(var xe;qc.length){c=a,u=g,xe=gt;break}}}break}}if(c!==null){if(f=c.length,h=xe.group,h!==void 0&&(p=xe.tokenTypeIdx,m=this.createTokenInstance(c,q,p,xe.tokenType,A,oe,f),this.handlePayload(m,u),h===!1?$=this.addToken(ne,$,m):ce[h].push(m)),e=this.chopInput(e,f),q=q+f,oe=this.computeNewColumn(oe,f),Z===!0&&xe.canLineTerminator===!0){var Mt=0,Ei=void 0,jt=void 0;O.lastIndex=0;do Ei=O.test(c),Ei===!0&&(jt=O.lastIndex-1,Mt++);while(Ei===!0);Mt!==0&&(A=A+Mt,oe=f-jt,this.updateTokenEndLineColumnLocation(m,h,jt,Mt,A,oe,f))}this.handleModes(xe,Fe,Ue,m)}else{for(var Qr=q,Oi=A,$s=oe,Hn=!1;!Hn&&q <"+e+">");var n=(0,lr.timer)(t),s=n.time,o=n.value,a=s>10?console.warn:console.log;return this.traceInitIndent time: "+s+"ms"),this.traceInitIndent--,o}else return t()},r.SKIPPED="This marks a skipped Token pattern, this means each token identified by it willbe consumed and then thrown into oblivion, this can be used to for example to completely ignore whitespace.",r.NA=/NOT_APPLICABLE/,r}();Pc.Lexer=QEe});var ZA=w(Pi=>{"use strict";Object.defineProperty(Pi,"__esModule",{value:!0});Pi.tokenMatcher=Pi.createTokenInstance=Pi.EOF=Pi.createToken=Pi.hasTokenLabel=Pi.tokenName=Pi.tokenLabel=void 0;var co=Yt(),SEe=Rp(),RS=Eg();function vEe(r){return oj(r)?r.LABEL:r.name}Pi.tokenLabel=vEe;function kEe(r){return r.name}Pi.tokenName=kEe;function oj(r){return(0,co.isString)(r.LABEL)&&r.LABEL!==""}Pi.hasTokenLabel=oj;var xEe="parent",aj="categories",Aj="label",lj="group",cj="push_mode",uj="pop_mode",gj="longer_alt",fj="line_breaks",hj="start_chars_hint";function pj(r){return PEe(r)}Pi.createToken=pj;function PEe(r){var e=r.pattern,t={};if(t.name=r.name,(0,co.isUndefined)(e)||(t.PATTERN=e),(0,co.has)(r,xEe))throw`The parent property is no longer supported. +See: https://github.com/chevrotain/chevrotain/issues/564#issuecomment-349062346 for details.`;return(0,co.has)(r,aj)&&(t.CATEGORIES=r[aj]),(0,RS.augmentTokenTypes)([t]),(0,co.has)(r,Aj)&&(t.LABEL=r[Aj]),(0,co.has)(r,lj)&&(t.GROUP=r[lj]),(0,co.has)(r,uj)&&(t.POP_MODE=r[uj]),(0,co.has)(r,cj)&&(t.PUSH_MODE=r[cj]),(0,co.has)(r,gj)&&(t.LONGER_ALT=r[gj]),(0,co.has)(r,fj)&&(t.LINE_BREAKS=r[fj]),(0,co.has)(r,hj)&&(t.START_CHARS_HINT=r[hj]),t}Pi.EOF=pj({name:"EOF",pattern:SEe.Lexer.NA});(0,RS.augmentTokenTypes)([Pi.EOF]);function DEe(r,e,t,i,n,s,o,a){return{image:e,startOffset:t,endOffset:i,startLine:n,endLine:s,startColumn:o,endColumn:a,tokenTypeIdx:r.tokenTypeIdx,tokenType:r}}Pi.createTokenInstance=DEe;function REe(r,e){return(0,RS.tokenStructuredMatcher)(r,e)}Pi.tokenMatcher=REe});var bn=w(Vt=>{"use strict";var Ya=Vt&&Vt.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Vt,"__esModule",{value:!0});Vt.serializeProduction=Vt.serializeGrammar=Vt.Terminal=Vt.Alternation=Vt.RepetitionWithSeparator=Vt.Repetition=Vt.RepetitionMandatoryWithSeparator=Vt.RepetitionMandatory=Vt.Option=Vt.Alternative=Vt.Rule=Vt.NonTerminal=Vt.AbstractProduction=void 0;var fr=Yt(),FEe=ZA(),Ho=function(){function r(e){this._definition=e}return Object.defineProperty(r.prototype,"definition",{get:function(){return this._definition},set:function(e){this._definition=e},enumerable:!1,configurable:!0}),r.prototype.accept=function(e){e.visit(this),(0,fr.forEach)(this.definition,function(t){t.accept(e)})},r}();Vt.AbstractProduction=Ho;var dj=function(r){Ya(e,r);function e(t){var i=r.call(this,[])||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return Object.defineProperty(e.prototype,"definition",{get:function(){return this.referencedRule!==void 0?this.referencedRule.definition:[]},set:function(t){},enumerable:!1,configurable:!0}),e.prototype.accept=function(t){t.visit(this)},e}(Ho);Vt.NonTerminal=dj;var Cj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.orgText="",(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.Rule=Cj;var mj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.ignoreAmbiguities=!1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.Alternative=mj;var Ej=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.Option=Ej;var Ij=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.RepetitionMandatory=Ij;var yj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.RepetitionMandatoryWithSeparator=yj;var wj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.Repetition=wj;var Bj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return e}(Ho);Vt.RepetitionWithSeparator=Bj;var bj=function(r){Ya(e,r);function e(t){var i=r.call(this,t.definition)||this;return i.idx=1,i.ignoreAmbiguities=!1,i.hasPredicates=!1,(0,fr.assign)(i,(0,fr.pick)(t,function(n){return n!==void 0})),i}return Object.defineProperty(e.prototype,"definition",{get:function(){return this._definition},set:function(t){this._definition=t},enumerable:!1,configurable:!0}),e}(Ho);Vt.Alternation=bj;var YI=function(){function r(e){this.idx=1,(0,fr.assign)(this,(0,fr.pick)(e,function(t){return t!==void 0}))}return r.prototype.accept=function(e){e.visit(this)},r}();Vt.Terminal=YI;function NEe(r){return(0,fr.map)(r,Np)}Vt.serializeGrammar=NEe;function Np(r){function e(s){return(0,fr.map)(s,Np)}if(r instanceof dj){var t={type:"NonTerminal",name:r.nonTerminalName,idx:r.idx};return(0,fr.isString)(r.label)&&(t.label=r.label),t}else{if(r instanceof mj)return{type:"Alternative",definition:e(r.definition)};if(r instanceof Ej)return{type:"Option",idx:r.idx,definition:e(r.definition)};if(r instanceof Ij)return{type:"RepetitionMandatory",idx:r.idx,definition:e(r.definition)};if(r instanceof yj)return{type:"RepetitionMandatoryWithSeparator",idx:r.idx,separator:Np(new YI({terminalType:r.separator})),definition:e(r.definition)};if(r instanceof Bj)return{type:"RepetitionWithSeparator",idx:r.idx,separator:Np(new YI({terminalType:r.separator})),definition:e(r.definition)};if(r instanceof wj)return{type:"Repetition",idx:r.idx,definition:e(r.definition)};if(r instanceof bj)return{type:"Alternation",idx:r.idx,definition:e(r.definition)};if(r instanceof YI){var i={type:"Terminal",name:r.terminalType.name,label:(0,FEe.tokenLabel)(r.terminalType),idx:r.idx};(0,fr.isString)(r.label)&&(i.terminalLabel=r.label);var n=r.terminalType.PATTERN;return r.terminalType.PATTERN&&(i.pattern=(0,fr.isRegExp)(n)?n.source:n),i}else{if(r instanceof Cj)return{type:"Rule",name:r.name,orgText:r.orgText,definition:e(r.definition)};throw Error("non exhaustive match")}}}Vt.serializeProduction=Np});var JI=w(qI=>{"use strict";Object.defineProperty(qI,"__esModule",{value:!0});qI.RestWalker=void 0;var FS=Yt(),Qn=bn(),LEe=function(){function r(){}return r.prototype.walk=function(e,t){var i=this;t===void 0&&(t=[]),(0,FS.forEach)(e.definition,function(n,s){var o=(0,FS.drop)(e.definition,s+1);if(n instanceof Qn.NonTerminal)i.walkProdRef(n,o,t);else if(n instanceof Qn.Terminal)i.walkTerminal(n,o,t);else if(n instanceof Qn.Alternative)i.walkFlat(n,o,t);else if(n instanceof Qn.Option)i.walkOption(n,o,t);else if(n instanceof Qn.RepetitionMandatory)i.walkAtLeastOne(n,o,t);else if(n instanceof Qn.RepetitionMandatoryWithSeparator)i.walkAtLeastOneSep(n,o,t);else if(n instanceof Qn.RepetitionWithSeparator)i.walkManySep(n,o,t);else if(n instanceof Qn.Repetition)i.walkMany(n,o,t);else if(n instanceof Qn.Alternation)i.walkOr(n,o,t);else throw Error("non exhaustive match")})},r.prototype.walkTerminal=function(e,t,i){},r.prototype.walkProdRef=function(e,t,i){},r.prototype.walkFlat=function(e,t,i){var n=t.concat(i);this.walk(e,n)},r.prototype.walkOption=function(e,t,i){var n=t.concat(i);this.walk(e,n)},r.prototype.walkAtLeastOne=function(e,t,i){var n=[new Qn.Option({definition:e.definition})].concat(t,i);this.walk(e,n)},r.prototype.walkAtLeastOneSep=function(e,t,i){var n=Qj(e,t,i);this.walk(e,n)},r.prototype.walkMany=function(e,t,i){var n=[new Qn.Option({definition:e.definition})].concat(t,i);this.walk(e,n)},r.prototype.walkManySep=function(e,t,i){var n=Qj(e,t,i);this.walk(e,n)},r.prototype.walkOr=function(e,t,i){var n=this,s=t.concat(i);(0,FS.forEach)(e.definition,function(o){var a=new Qn.Alternative({definition:[o]});n.walk(a,s)})},r}();qI.RestWalker=LEe;function Qj(r,e,t){var i=[new Qn.Option({definition:[new Qn.Terminal({terminalType:r.separator})].concat(r.definition)})],n=i.concat(e,t);return n}});var Ig=w(WI=>{"use strict";Object.defineProperty(WI,"__esModule",{value:!0});WI.GAstVisitor=void 0;var jo=bn(),TEe=function(){function r(){}return r.prototype.visit=function(e){var t=e;switch(t.constructor){case jo.NonTerminal:return this.visitNonTerminal(t);case jo.Alternative:return this.visitAlternative(t);case jo.Option:return this.visitOption(t);case jo.RepetitionMandatory:return this.visitRepetitionMandatory(t);case jo.RepetitionMandatoryWithSeparator:return this.visitRepetitionMandatoryWithSeparator(t);case jo.RepetitionWithSeparator:return this.visitRepetitionWithSeparator(t);case jo.Repetition:return this.visitRepetition(t);case jo.Alternation:return this.visitAlternation(t);case jo.Terminal:return this.visitTerminal(t);case jo.Rule:return this.visitRule(t);default:throw Error("non exhaustive match")}},r.prototype.visitNonTerminal=function(e){},r.prototype.visitAlternative=function(e){},r.prototype.visitOption=function(e){},r.prototype.visitRepetition=function(e){},r.prototype.visitRepetitionMandatory=function(e){},r.prototype.visitRepetitionMandatoryWithSeparator=function(e){},r.prototype.visitRepetitionWithSeparator=function(e){},r.prototype.visitAlternation=function(e){},r.prototype.visitTerminal=function(e){},r.prototype.visitRule=function(e){},r}();WI.GAstVisitor=TEe});var Tp=w(Gi=>{"use strict";var OEe=Gi&&Gi.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Gi,"__esModule",{value:!0});Gi.collectMethods=Gi.DslMethodsCollectorVisitor=Gi.getProductionDslName=Gi.isBranchingProd=Gi.isOptionalProd=Gi.isSequenceProd=void 0;var Lp=Yt(),kr=bn(),MEe=Ig();function UEe(r){return r instanceof kr.Alternative||r instanceof kr.Option||r instanceof kr.Repetition||r instanceof kr.RepetitionMandatory||r instanceof kr.RepetitionMandatoryWithSeparator||r instanceof kr.RepetitionWithSeparator||r instanceof kr.Terminal||r instanceof kr.Rule}Gi.isSequenceProd=UEe;function NS(r,e){e===void 0&&(e=[]);var t=r instanceof kr.Option||r instanceof kr.Repetition||r instanceof kr.RepetitionWithSeparator;return t?!0:r instanceof kr.Alternation?(0,Lp.some)(r.definition,function(i){return NS(i,e)}):r instanceof kr.NonTerminal&&(0,Lp.contains)(e,r)?!1:r instanceof kr.AbstractProduction?(r instanceof kr.NonTerminal&&e.push(r),(0,Lp.every)(r.definition,function(i){return NS(i,e)})):!1}Gi.isOptionalProd=NS;function KEe(r){return r instanceof kr.Alternation}Gi.isBranchingProd=KEe;function HEe(r){if(r instanceof kr.NonTerminal)return"SUBRULE";if(r instanceof kr.Option)return"OPTION";if(r instanceof kr.Alternation)return"OR";if(r instanceof kr.RepetitionMandatory)return"AT_LEAST_ONE";if(r instanceof kr.RepetitionMandatoryWithSeparator)return"AT_LEAST_ONE_SEP";if(r instanceof kr.RepetitionWithSeparator)return"MANY_SEP";if(r instanceof kr.Repetition)return"MANY";if(r instanceof kr.Terminal)return"CONSUME";throw Error("non exhaustive match")}Gi.getProductionDslName=HEe;var Sj=function(r){OEe(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.separator="-",t.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]},t}return e.prototype.reset=function(){this.dslMethods={option:[],alternation:[],repetition:[],repetitionWithSeparator:[],repetitionMandatory:[],repetitionMandatoryWithSeparator:[]}},e.prototype.visitTerminal=function(t){var i=t.terminalType.name+this.separator+"Terminal";(0,Lp.has)(this.dslMethods,i)||(this.dslMethods[i]=[]),this.dslMethods[i].push(t)},e.prototype.visitNonTerminal=function(t){var i=t.nonTerminalName+this.separator+"Terminal";(0,Lp.has)(this.dslMethods,i)||(this.dslMethods[i]=[]),this.dslMethods[i].push(t)},e.prototype.visitOption=function(t){this.dslMethods.option.push(t)},e.prototype.visitRepetitionWithSeparator=function(t){this.dslMethods.repetitionWithSeparator.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.dslMethods.repetitionMandatory.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.dslMethods.repetitionMandatoryWithSeparator.push(t)},e.prototype.visitRepetition=function(t){this.dslMethods.repetition.push(t)},e.prototype.visitAlternation=function(t){this.dslMethods.alternation.push(t)},e}(MEe.GAstVisitor);Gi.DslMethodsCollectorVisitor=Sj;var zI=new Sj;function jEe(r){zI.reset(),r.accept(zI);var e=zI.dslMethods;return zI.reset(),e}Gi.collectMethods=jEe});var TS=w(Go=>{"use strict";Object.defineProperty(Go,"__esModule",{value:!0});Go.firstForTerminal=Go.firstForBranching=Go.firstForSequence=Go.first=void 0;var _I=Yt(),vj=bn(),LS=Tp();function VI(r){if(r instanceof vj.NonTerminal)return VI(r.referencedRule);if(r instanceof vj.Terminal)return Pj(r);if((0,LS.isSequenceProd)(r))return kj(r);if((0,LS.isBranchingProd)(r))return xj(r);throw Error("non exhaustive match")}Go.first=VI;function kj(r){for(var e=[],t=r.definition,i=0,n=t.length>i,s,o=!0;n&&o;)s=t[i],o=(0,LS.isOptionalProd)(s),e=e.concat(VI(s)),i=i+1,n=t.length>i;return(0,_I.uniq)(e)}Go.firstForSequence=kj;function xj(r){var e=(0,_I.map)(r.definition,function(t){return VI(t)});return(0,_I.uniq)((0,_I.flatten)(e))}Go.firstForBranching=xj;function Pj(r){return[r.terminalType]}Go.firstForTerminal=Pj});var OS=w(XI=>{"use strict";Object.defineProperty(XI,"__esModule",{value:!0});XI.IN=void 0;XI.IN="_~IN~_"});var Lj=w(ks=>{"use strict";var GEe=ks&&ks.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(ks,"__esModule",{value:!0});ks.buildInProdFollowPrefix=ks.buildBetweenProdsFollowPrefix=ks.computeAllProdsFollows=ks.ResyncFollowsWalker=void 0;var YEe=JI(),qEe=TS(),Dj=Yt(),Rj=OS(),JEe=bn(),Nj=function(r){GEe(e,r);function e(t){var i=r.call(this)||this;return i.topProd=t,i.follows={},i}return e.prototype.startWalking=function(){return this.walk(this.topProd),this.follows},e.prototype.walkTerminal=function(t,i,n){},e.prototype.walkProdRef=function(t,i,n){var s=Fj(t.referencedRule,t.idx)+this.topProd.name,o=i.concat(n),a=new JEe.Alternative({definition:o}),l=(0,qEe.first)(a);this.follows[s]=l},e}(YEe.RestWalker);ks.ResyncFollowsWalker=Nj;function WEe(r){var e={};return(0,Dj.forEach)(r,function(t){var i=new Nj(t).startWalking();(0,Dj.assign)(e,i)}),e}ks.computeAllProdsFollows=WEe;function Fj(r,e){return r.name+e+Rj.IN}ks.buildBetweenProdsFollowPrefix=Fj;function zEe(r){var e=r.terminalType.name;return e+r.idx+Rj.IN}ks.buildInProdFollowPrefix=zEe});var Op=w(qa=>{"use strict";Object.defineProperty(qa,"__esModule",{value:!0});qa.defaultGrammarValidatorErrorProvider=qa.defaultGrammarResolverErrorProvider=qa.defaultParserErrorProvider=void 0;var yg=ZA(),_Ee=Yt(),uo=Yt(),MS=bn(),Tj=Tp();qa.defaultParserErrorProvider={buildMismatchTokenMessage:function(r){var e=r.expected,t=r.actual,i=r.previous,n=r.ruleName,s=(0,yg.hasTokenLabel)(e),o=s?"--> "+(0,yg.tokenLabel)(e)+" <--":"token of type --> "+e.name+" <--",a="Expecting "+o+" but found --> '"+t.image+"' <--";return a},buildNotAllInputParsedMessage:function(r){var e=r.firstRedundant,t=r.ruleName;return"Redundant input, expecting EOF but found: "+e.image},buildNoViableAltMessage:function(r){var e=r.expectedPathsPerAlt,t=r.actual,i=r.previous,n=r.customUserDescription,s=r.ruleName,o="Expecting: ",a=(0,uo.first)(t).image,l=` +but found: '`+a+"'";if(n)return o+n+l;var c=(0,uo.reduce)(e,function(h,p){return h.concat(p)},[]),u=(0,uo.map)(c,function(h){return"["+(0,uo.map)(h,function(p){return(0,yg.tokenLabel)(p)}).join(", ")+"]"}),g=(0,uo.map)(u,function(h,p){return" "+(p+1)+". "+h}),f=`one of these possible Token sequences: +`+g.join(` +`);return o+f+l},buildEarlyExitMessage:function(r){var e=r.expectedIterationPaths,t=r.actual,i=r.customUserDescription,n=r.ruleName,s="Expecting: ",o=(0,uo.first)(t).image,a=` +but found: '`+o+"'";if(i)return s+i+a;var l=(0,uo.map)(e,function(u){return"["+(0,uo.map)(u,function(g){return(0,yg.tokenLabel)(g)}).join(",")+"]"}),c=`expecting at least one iteration which starts with one of these possible Token sequences:: + `+("<"+l.join(" ,")+">");return s+c+a}};Object.freeze(qa.defaultParserErrorProvider);qa.defaultGrammarResolverErrorProvider={buildRuleNotFoundError:function(r,e){var t="Invalid grammar, reference to a rule which is not defined: ->"+e.nonTerminalName+`<- +inside top level rule: ->`+r.name+"<-";return t}};qa.defaultGrammarValidatorErrorProvider={buildDuplicateFoundError:function(r,e){function t(u){return u instanceof MS.Terminal?u.terminalType.name:u instanceof MS.NonTerminal?u.nonTerminalName:""}var i=r.name,n=(0,uo.first)(e),s=n.idx,o=(0,Tj.getProductionDslName)(n),a=t(n),l=s>0,c="->"+o+(l?s:"")+"<- "+(a?"with argument: ->"+a+"<-":"")+` + appears more than once (`+e.length+" times) in the top level rule: ->"+i+`<-. + For further details see: https://chevrotain.io/docs/FAQ.html#NUMERICAL_SUFFIXES + `;return c=c.replace(/[ \t]+/g," "),c=c.replace(/\s\s+/g,` +`),c},buildNamespaceConflictError:function(r){var e=`Namespace conflict found in grammar. +`+("The grammar has both a Terminal(Token) and a Non-Terminal(Rule) named: <"+r.name+`>. +`)+`To resolve this make sure each Terminal and Non-Terminal names are unique +This is easy to accomplish by using the convention that Terminal names start with an uppercase letter +and Non-Terminal names start with a lower case letter.`;return e},buildAlternationPrefixAmbiguityError:function(r){var e=(0,uo.map)(r.prefixPath,function(n){return(0,yg.tokenLabel)(n)}).join(", "),t=r.alternation.idx===0?"":r.alternation.idx,i="Ambiguous alternatives: <"+r.ambiguityIndices.join(" ,")+`> due to common lookahead prefix +`+("in inside <"+r.topLevelRule.name+`> Rule, +`)+("<"+e+`> may appears as a prefix path in all these alternatives. +`)+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#COMMON_PREFIX +For Further details.`;return i},buildAlternationAmbiguityError:function(r){var e=(0,uo.map)(r.prefixPath,function(n){return(0,yg.tokenLabel)(n)}).join(", "),t=r.alternation.idx===0?"":r.alternation.idx,i="Ambiguous Alternatives Detected: <"+r.ambiguityIndices.join(" ,")+"> in "+(" inside <"+r.topLevelRule.name+`> Rule, +`)+("<"+e+`> may appears as a prefix path in all these alternatives. +`);return i=i+`See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#AMBIGUOUS_ALTERNATIVES +For Further details.`,i},buildEmptyRepetitionError:function(r){var e=(0,Tj.getProductionDslName)(r.repetition);r.repetition.idx!==0&&(e+=r.repetition.idx);var t="The repetition <"+e+"> within Rule <"+r.topLevelRule.name+`> can never consume any tokens. +This could lead to an infinite loop.`;return t},buildTokenNameError:function(r){return"deprecated"},buildEmptyAlternationError:function(r){var e="Ambiguous empty alternative: <"+(r.emptyChoiceIdx+1)+">"+(" in inside <"+r.topLevelRule.name+`> Rule. +`)+"Only the last alternative may be an empty alternative.";return e},buildTooManyAlternativesError:function(r){var e=`An Alternation cannot have more than 256 alternatives: +`+(" inside <"+r.topLevelRule.name+`> Rule. + has `+(r.alternation.definition.length+1)+" alternatives.");return e},buildLeftRecursionError:function(r){var e=r.topLevelRule.name,t=_Ee.map(r.leftRecursionPath,function(s){return s.name}),i=e+" --> "+t.concat([e]).join(" --> "),n=`Left Recursion found in grammar. +`+("rule: <"+e+`> can be invoked from itself (directly or indirectly) +`)+(`without consuming any Tokens. The grammar path that causes this is: + `+i+` +`)+` To fix this refactor your grammar to remove the left recursion. +see: https://en.wikipedia.org/wiki/LL_parser#Left_Factoring.`;return n},buildInvalidRuleNameError:function(r){return"deprecated"},buildDuplicateRuleNameError:function(r){var e;r.topLevelRule instanceof MS.Rule?e=r.topLevelRule.name:e=r.topLevelRule;var t="Duplicate definition, rule: ->"+e+"<- is already defined in the grammar: ->"+r.grammarName+"<-";return t}}});var Uj=w($A=>{"use strict";var VEe=$A&&$A.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty($A,"__esModule",{value:!0});$A.GastRefResolverVisitor=$A.resolveGrammar=void 0;var XEe=es(),Oj=Yt(),ZEe=Ig();function $Ee(r,e){var t=new Mj(r,e);return t.resolveRefs(),t.errors}$A.resolveGrammar=$Ee;var Mj=function(r){VEe(e,r);function e(t,i){var n=r.call(this)||this;return n.nameToTopRule=t,n.errMsgProvider=i,n.errors=[],n}return e.prototype.resolveRefs=function(){var t=this;(0,Oj.forEach)((0,Oj.values)(this.nameToTopRule),function(i){t.currTopLevel=i,i.accept(t)})},e.prototype.visitNonTerminal=function(t){var i=this.nameToTopRule[t.nonTerminalName];if(i)t.referencedRule=i;else{var n=this.errMsgProvider.buildRuleNotFoundError(this.currTopLevel,t);this.errors.push({message:n,type:XEe.ParserDefinitionErrorType.UNRESOLVED_SUBRULE_REF,ruleName:this.currTopLevel.name,unresolvedRefName:t.nonTerminalName})}},e}(ZEe.GAstVisitor);$A.GastRefResolverVisitor=Mj});var Up=w(Ur=>{"use strict";var Dc=Ur&&Ur.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Ur,"__esModule",{value:!0});Ur.nextPossibleTokensAfter=Ur.possiblePathsFrom=Ur.NextTerminalAfterAtLeastOneSepWalker=Ur.NextTerminalAfterAtLeastOneWalker=Ur.NextTerminalAfterManySepWalker=Ur.NextTerminalAfterManyWalker=Ur.AbstractNextTerminalAfterProductionWalker=Ur.NextAfterTokenWalker=Ur.AbstractNextPossibleTokensWalker=void 0;var Kj=JI(),Ut=Yt(),eIe=TS(),Dt=bn(),Hj=function(r){Dc(e,r);function e(t,i){var n=r.call(this)||this;return n.topProd=t,n.path=i,n.possibleTokTypes=[],n.nextProductionName="",n.nextProductionOccurrence=0,n.found=!1,n.isAtEndOfPath=!1,n}return e.prototype.startWalking=function(){if(this.found=!1,this.path.ruleStack[0]!==this.topProd.name)throw Error("The path does not start with the walker's top Rule!");return this.ruleStack=(0,Ut.cloneArr)(this.path.ruleStack).reverse(),this.occurrenceStack=(0,Ut.cloneArr)(this.path.occurrenceStack).reverse(),this.ruleStack.pop(),this.occurrenceStack.pop(),this.updateExpectedNext(),this.walk(this.topProd),this.possibleTokTypes},e.prototype.walk=function(t,i){i===void 0&&(i=[]),this.found||r.prototype.walk.call(this,t,i)},e.prototype.walkProdRef=function(t,i,n){if(t.referencedRule.name===this.nextProductionName&&t.idx===this.nextProductionOccurrence){var s=i.concat(n);this.updateExpectedNext(),this.walk(t.referencedRule,s)}},e.prototype.updateExpectedNext=function(){(0,Ut.isEmpty)(this.ruleStack)?(this.nextProductionName="",this.nextProductionOccurrence=0,this.isAtEndOfPath=!0):(this.nextProductionName=this.ruleStack.pop(),this.nextProductionOccurrence=this.occurrenceStack.pop())},e}(Kj.RestWalker);Ur.AbstractNextPossibleTokensWalker=Hj;var tIe=function(r){Dc(e,r);function e(t,i){var n=r.call(this,t,i)||this;return n.path=i,n.nextTerminalName="",n.nextTerminalOccurrence=0,n.nextTerminalName=n.path.lastTok.name,n.nextTerminalOccurrence=n.path.lastTokOccurrence,n}return e.prototype.walkTerminal=function(t,i,n){if(this.isAtEndOfPath&&t.terminalType.name===this.nextTerminalName&&t.idx===this.nextTerminalOccurrence&&!this.found){var s=i.concat(n),o=new Dt.Alternative({definition:s});this.possibleTokTypes=(0,eIe.first)(o),this.found=!0}},e}(Hj);Ur.NextAfterTokenWalker=tIe;var Mp=function(r){Dc(e,r);function e(t,i){var n=r.call(this)||this;return n.topRule=t,n.occurrence=i,n.result={token:void 0,occurrence:void 0,isEndOfRule:void 0},n}return e.prototype.startWalking=function(){return this.walk(this.topRule),this.result},e}(Kj.RestWalker);Ur.AbstractNextTerminalAfterProductionWalker=Mp;var rIe=function(r){Dc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkMany=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Ut.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof Dt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkMany.call(this,t,i,n)},e}(Mp);Ur.NextTerminalAfterManyWalker=rIe;var iIe=function(r){Dc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkManySep=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Ut.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof Dt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkManySep.call(this,t,i,n)},e}(Mp);Ur.NextTerminalAfterManySepWalker=iIe;var nIe=function(r){Dc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkAtLeastOne=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Ut.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof Dt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkAtLeastOne.call(this,t,i,n)},e}(Mp);Ur.NextTerminalAfterAtLeastOneWalker=nIe;var sIe=function(r){Dc(e,r);function e(){return r!==null&&r.apply(this,arguments)||this}return e.prototype.walkAtLeastOneSep=function(t,i,n){if(t.idx===this.occurrence){var s=(0,Ut.first)(i.concat(n));this.result.isEndOfRule=s===void 0,s instanceof Dt.Terminal&&(this.result.token=s.terminalType,this.result.occurrence=s.idx)}else r.prototype.walkAtLeastOneSep.call(this,t,i,n)},e}(Mp);Ur.NextTerminalAfterAtLeastOneSepWalker=sIe;function jj(r,e,t){t===void 0&&(t=[]),t=(0,Ut.cloneArr)(t);var i=[],n=0;function s(c){return c.concat((0,Ut.drop)(r,n+1))}function o(c){var u=jj(s(c),e,t);return i.concat(u)}for(;t.length=0;ce--){var Z=b.definition[ce],O={idx:p,def:Z.definition.concat((0,Ut.drop)(h)),ruleStack:m,occurrenceStack:y};g.push(O),g.push(o)}else if(b instanceof Dt.Alternative)g.push({idx:p,def:b.definition.concat((0,Ut.drop)(h)),ruleStack:m,occurrenceStack:y});else if(b instanceof Dt.Rule)g.push(oIe(b,p,m,y));else throw Error("non exhaustive match")}}return u}Ur.nextPossibleTokensAfter=aIe;function oIe(r,e,t,i){var n=(0,Ut.cloneArr)(t);n.push(r.name);var s=(0,Ut.cloneArr)(i);return s.push(1),{idx:e,def:r.definition,ruleStack:n,occurrenceStack:s}}});var Kp=w(tr=>{"use strict";var Gj=tr&&tr.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(tr,"__esModule",{value:!0});tr.areTokenCategoriesNotUsed=tr.isStrictPrefixOfPath=tr.containsPath=tr.getLookaheadPathsForOptionalProd=tr.getLookaheadPathsForOr=tr.lookAheadSequenceFromAlternatives=tr.buildSingleAlternativeLookaheadFunction=tr.buildAlternativesLookAheadFunc=tr.buildLookaheadFuncForOptionalProd=tr.buildLookaheadFuncForOr=tr.getProdType=tr.PROD_TYPE=void 0;var cr=Yt(),Yj=Up(),AIe=JI(),ZI=Eg(),el=bn(),lIe=Ig(),ui;(function(r){r[r.OPTION=0]="OPTION",r[r.REPETITION=1]="REPETITION",r[r.REPETITION_MANDATORY=2]="REPETITION_MANDATORY",r[r.REPETITION_MANDATORY_WITH_SEPARATOR=3]="REPETITION_MANDATORY_WITH_SEPARATOR",r[r.REPETITION_WITH_SEPARATOR=4]="REPETITION_WITH_SEPARATOR",r[r.ALTERNATION=5]="ALTERNATION"})(ui=tr.PROD_TYPE||(tr.PROD_TYPE={}));function cIe(r){if(r instanceof el.Option)return ui.OPTION;if(r instanceof el.Repetition)return ui.REPETITION;if(r instanceof el.RepetitionMandatory)return ui.REPETITION_MANDATORY;if(r instanceof el.RepetitionMandatoryWithSeparator)return ui.REPETITION_MANDATORY_WITH_SEPARATOR;if(r instanceof el.RepetitionWithSeparator)return ui.REPETITION_WITH_SEPARATOR;if(r instanceof el.Alternation)return ui.ALTERNATION;throw Error("non exhaustive match")}tr.getProdType=cIe;function uIe(r,e,t,i,n,s){var o=qj(r,e,t),a=US(o)?ZI.tokenStructuredMatcherNoCategories:ZI.tokenStructuredMatcher;return s(o,i,a,n)}tr.buildLookaheadFuncForOr=uIe;function gIe(r,e,t,i,n,s){var o=Jj(r,e,n,t),a=US(o)?ZI.tokenStructuredMatcherNoCategories:ZI.tokenStructuredMatcher;return s(o[0],a,i)}tr.buildLookaheadFuncForOptionalProd=gIe;function fIe(r,e,t,i){var n=r.length,s=(0,cr.every)(r,function(l){return(0,cr.every)(l,function(c){return c.length===1})});if(e)return function(l){for(var c=(0,cr.map)(l,function(k){return k.GATE}),u=0;u{"use strict";var jS=Xt&&Xt.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Xt,"__esModule",{value:!0});Xt.checkPrefixAlternativesAmbiguities=Xt.validateSomeNonEmptyLookaheadPath=Xt.validateTooManyAlts=Xt.RepetionCollector=Xt.validateAmbiguousAlternationAlternatives=Xt.validateEmptyOrAlternative=Xt.getFirstNoneTerminal=Xt.validateNoLeftRecursion=Xt.validateRuleIsOverridden=Xt.validateRuleDoesNotAlreadyExist=Xt.OccurrenceValidationCollector=Xt.identifyProductionForDuplicates=Xt.validateGrammar=void 0;var nr=Yt(),xr=Yt(),Yo=es(),GS=Tp(),wg=Kp(),mIe=Up(),go=bn(),YS=Ig();function yIe(r,e,t,i,n){var s=nr.map(r,function(h){return EIe(h,i)}),o=nr.map(r,function(h){return qS(h,h,i)}),a=[],l=[],c=[];(0,xr.every)(o,xr.isEmpty)&&(a=(0,xr.map)(r,function(h){return Xj(h,i)}),l=(0,xr.map)(r,function(h){return Zj(h,e,i)}),c=eG(r,e,i));var u=IIe(r,t,i),g=(0,xr.map)(r,function(h){return $j(h,i)}),f=(0,xr.map)(r,function(h){return Vj(h,r,n,i)});return nr.flatten(s.concat(c,o,a,l,u,g,f))}Xt.validateGrammar=yIe;function EIe(r,e){var t=new iG;r.accept(t);var i=t.allProductions,n=nr.groupBy(i,tG),s=nr.pick(n,function(a){return a.length>1}),o=nr.map(nr.values(s),function(a){var l=nr.first(a),c=e.buildDuplicateFoundError(r,a),u=(0,GS.getProductionDslName)(l),g={message:c,type:Yo.ParserDefinitionErrorType.DUPLICATE_PRODUCTIONS,ruleName:r.name,dslName:u,occurrence:l.idx},f=rG(l);return f&&(g.parameter=f),g});return o}function tG(r){return(0,GS.getProductionDslName)(r)+"_#_"+r.idx+"_#_"+rG(r)}Xt.identifyProductionForDuplicates=tG;function rG(r){return r instanceof go.Terminal?r.terminalType.name:r instanceof go.NonTerminal?r.nonTerminalName:""}var iG=function(r){jS(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.allProductions=[],t}return e.prototype.visitNonTerminal=function(t){this.allProductions.push(t)},e.prototype.visitOption=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetition=function(t){this.allProductions.push(t)},e.prototype.visitAlternation=function(t){this.allProductions.push(t)},e.prototype.visitTerminal=function(t){this.allProductions.push(t)},e}(YS.GAstVisitor);Xt.OccurrenceValidationCollector=iG;function Vj(r,e,t,i){var n=[],s=(0,xr.reduce)(e,function(a,l){return l.name===r.name?a+1:a},0);if(s>1){var o=i.buildDuplicateRuleNameError({topLevelRule:r,grammarName:t});n.push({message:o,type:Yo.ParserDefinitionErrorType.DUPLICATE_RULE_NAME,ruleName:r.name})}return n}Xt.validateRuleDoesNotAlreadyExist=Vj;function wIe(r,e,t){var i=[],n;return nr.contains(e,r)||(n="Invalid rule override, rule: ->"+r+"<- cannot be overridden in the grammar: ->"+t+"<-as it is not defined in any of the super grammars ",i.push({message:n,type:Yo.ParserDefinitionErrorType.INVALID_RULE_OVERRIDE,ruleName:r})),i}Xt.validateRuleIsOverridden=wIe;function qS(r,e,t,i){i===void 0&&(i=[]);var n=[],s=Hp(e.definition);if(nr.isEmpty(s))return[];var o=r.name,a=nr.contains(s,r);a&&n.push({message:t.buildLeftRecursionError({topLevelRule:r,leftRecursionPath:i}),type:Yo.ParserDefinitionErrorType.LEFT_RECURSION,ruleName:o});var l=nr.difference(s,i.concat([r])),c=nr.map(l,function(u){var g=nr.cloneArr(i);return g.push(u),qS(r,u,t,g)});return n.concat(nr.flatten(c))}Xt.validateNoLeftRecursion=qS;function Hp(r){var e=[];if(nr.isEmpty(r))return e;var t=nr.first(r);if(t instanceof go.NonTerminal)e.push(t.referencedRule);else if(t instanceof go.Alternative||t instanceof go.Option||t instanceof go.RepetitionMandatory||t instanceof go.RepetitionMandatoryWithSeparator||t instanceof go.RepetitionWithSeparator||t instanceof go.Repetition)e=e.concat(Hp(t.definition));else if(t instanceof go.Alternation)e=nr.flatten(nr.map(t.definition,function(o){return Hp(o.definition)}));else if(!(t instanceof go.Terminal))throw Error("non exhaustive match");var i=(0,GS.isOptionalProd)(t),n=r.length>1;if(i&&n){var s=nr.drop(r);return e.concat(Hp(s))}else return e}Xt.getFirstNoneTerminal=Hp;var JS=function(r){jS(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.alternations=[],t}return e.prototype.visitAlternation=function(t){this.alternations.push(t)},e}(YS.GAstVisitor);function Xj(r,e){var t=new JS;r.accept(t);var i=t.alternations,n=nr.reduce(i,function(s,o){var a=nr.dropRight(o.definition),l=nr.map(a,function(c,u){var g=(0,mIe.nextPossibleTokensAfter)([c],[],null,1);return nr.isEmpty(g)?{message:e.buildEmptyAlternationError({topLevelRule:r,alternation:o,emptyChoiceIdx:u}),type:Yo.ParserDefinitionErrorType.NONE_LAST_EMPTY_ALT,ruleName:r.name,occurrence:o.idx,alternative:u+1}:null});return s.concat(nr.compact(l))},[]);return n}Xt.validateEmptyOrAlternative=Xj;function Zj(r,e,t){var i=new JS;r.accept(i);var n=i.alternations;n=(0,xr.reject)(n,function(o){return o.ignoreAmbiguities===!0});var s=nr.reduce(n,function(o,a){var l=a.idx,c=a.maxLookahead||e,u=(0,wg.getLookaheadPathsForOr)(l,r,c,a),g=BIe(u,a,r,t),f=nG(u,a,r,t);return o.concat(g,f)},[]);return s}Xt.validateAmbiguousAlternationAlternatives=Zj;var sG=function(r){jS(e,r);function e(){var t=r!==null&&r.apply(this,arguments)||this;return t.allProductions=[],t}return e.prototype.visitRepetitionWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatory=function(t){this.allProductions.push(t)},e.prototype.visitRepetitionMandatoryWithSeparator=function(t){this.allProductions.push(t)},e.prototype.visitRepetition=function(t){this.allProductions.push(t)},e}(YS.GAstVisitor);Xt.RepetionCollector=sG;function $j(r,e){var t=new JS;r.accept(t);var i=t.alternations,n=nr.reduce(i,function(s,o){return o.definition.length>255&&s.push({message:e.buildTooManyAlternativesError({topLevelRule:r,alternation:o}),type:Yo.ParserDefinitionErrorType.TOO_MANY_ALTS,ruleName:r.name,occurrence:o.idx}),s},[]);return n}Xt.validateTooManyAlts=$j;function eG(r,e,t){var i=[];return(0,xr.forEach)(r,function(n){var s=new sG;n.accept(s);var o=s.allProductions;(0,xr.forEach)(o,function(a){var l=(0,wg.getProdType)(a),c=a.maxLookahead||e,u=a.idx,g=(0,wg.getLookaheadPathsForOptionalProd)(u,n,l,c),f=g[0];if((0,xr.isEmpty)((0,xr.flatten)(f))){var h=t.buildEmptyRepetitionError({topLevelRule:n,repetition:a});i.push({message:h,type:Yo.ParserDefinitionErrorType.NO_NON_EMPTY_LOOKAHEAD,ruleName:n.name})}})}),i}Xt.validateSomeNonEmptyLookaheadPath=eG;function BIe(r,e,t,i){var n=[],s=(0,xr.reduce)(r,function(a,l,c){return e.definition[c].ignoreAmbiguities===!0||(0,xr.forEach)(l,function(u){var g=[c];(0,xr.forEach)(r,function(f,h){c!==h&&(0,wg.containsPath)(f,u)&&e.definition[h].ignoreAmbiguities!==!0&&g.push(h)}),g.length>1&&!(0,wg.containsPath)(n,u)&&(n.push(u),a.push({alts:g,path:u}))}),a},[]),o=nr.map(s,function(a){var l=(0,xr.map)(a.alts,function(u){return u+1}),c=i.buildAlternationAmbiguityError({topLevelRule:t,alternation:e,ambiguityIndices:l,prefixPath:a.path});return{message:c,type:Yo.ParserDefinitionErrorType.AMBIGUOUS_ALTS,ruleName:t.name,occurrence:e.idx,alternatives:[a.alts]}});return o}function nG(r,e,t,i){var n=[],s=(0,xr.reduce)(r,function(o,a,l){var c=(0,xr.map)(a,function(u){return{idx:l,path:u}});return o.concat(c)},[]);return(0,xr.forEach)(s,function(o){var a=e.definition[o.idx];if(a.ignoreAmbiguities!==!0){var l=o.idx,c=o.path,u=(0,xr.findAll)(s,function(f){return e.definition[f.idx].ignoreAmbiguities!==!0&&f.idx{"use strict";Object.defineProperty(Bg,"__esModule",{value:!0});Bg.validateGrammar=Bg.resolveGrammar=void 0;var zS=Yt(),bIe=Uj(),QIe=WS(),oG=Op();function SIe(r){r=(0,zS.defaults)(r,{errMsgProvider:oG.defaultGrammarResolverErrorProvider});var e={};return(0,zS.forEach)(r.rules,function(t){e[t.name]=t}),(0,bIe.resolveGrammar)(e,r.errMsgProvider)}Bg.resolveGrammar=SIe;function vIe(r){return r=(0,zS.defaults)(r,{errMsgProvider:oG.defaultGrammarValidatorErrorProvider}),(0,QIe.validateGrammar)(r.rules,r.maxLookahead,r.tokenTypes,r.errMsgProvider,r.grammarName)}Bg.validateGrammar=vIe});var bg=w(Sn=>{"use strict";var jp=Sn&&Sn.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Sn,"__esModule",{value:!0});Sn.EarlyExitException=Sn.NotAllInputParsedException=Sn.NoViableAltException=Sn.MismatchedTokenException=Sn.isRecognitionException=void 0;var kIe=Yt(),AG="MismatchedTokenException",lG="NoViableAltException",cG="EarlyExitException",uG="NotAllInputParsedException",gG=[AG,lG,cG,uG];Object.freeze(gG);function xIe(r){return(0,kIe.contains)(gG,r.name)}Sn.isRecognitionException=xIe;var $I=function(r){jp(e,r);function e(t,i){var n=this.constructor,s=r.call(this,t)||this;return s.token=i,s.resyncedTokens=[],Object.setPrototypeOf(s,n.prototype),Error.captureStackTrace&&Error.captureStackTrace(s,s.constructor),s}return e}(Error),PIe=function(r){jp(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=AG,s}return e}($I);Sn.MismatchedTokenException=PIe;var DIe=function(r){jp(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=lG,s}return e}($I);Sn.NoViableAltException=DIe;var RIe=function(r){jp(e,r);function e(t,i){var n=r.call(this,t,i)||this;return n.name=uG,n}return e}($I);Sn.NotAllInputParsedException=RIe;var FIe=function(r){jp(e,r);function e(t,i,n){var s=r.call(this,t,i)||this;return s.previousToken=n,s.name=cG,s}return e}($I);Sn.EarlyExitException=FIe});var VS=w(Yi=>{"use strict";Object.defineProperty(Yi,"__esModule",{value:!0});Yi.attemptInRepetitionRecovery=Yi.Recoverable=Yi.InRuleRecoveryException=Yi.IN_RULE_RECOVERY_EXCEPTION=Yi.EOF_FOLLOW_KEY=void 0;var ey=ZA(),xs=Yt(),NIe=bg(),LIe=OS(),TIe=es();Yi.EOF_FOLLOW_KEY={};Yi.IN_RULE_RECOVERY_EXCEPTION="InRuleRecoveryException";function _S(r){this.name=Yi.IN_RULE_RECOVERY_EXCEPTION,this.message=r}Yi.InRuleRecoveryException=_S;_S.prototype=Error.prototype;var OIe=function(){function r(){}return r.prototype.initRecoverable=function(e){this.firstAfterRepMap={},this.resyncFollows={},this.recoveryEnabled=(0,xs.has)(e,"recoveryEnabled")?e.recoveryEnabled:TIe.DEFAULT_PARSER_CONFIG.recoveryEnabled,this.recoveryEnabled&&(this.attemptInRepetitionRecovery=fG)},r.prototype.getTokenToInsert=function(e){var t=(0,ey.createTokenInstance)(e,"",NaN,NaN,NaN,NaN,NaN,NaN);return t.isInsertedInRecovery=!0,t},r.prototype.canTokenTypeBeInsertedInRecovery=function(e){return!0},r.prototype.tryInRepetitionRecovery=function(e,t,i,n){for(var s=this,o=this.findReSyncTokenType(),a=this.exportLexerState(),l=[],c=!1,u=this.LA(1),g=this.LA(1),f=function(){var h=s.LA(0),p=s.errorMessageProvider.buildMismatchTokenMessage({expected:n,actual:u,previous:h,ruleName:s.getCurrRuleFullName()}),m=new NIe.MismatchedTokenException(p,u,s.LA(0));m.resyncedTokens=(0,xs.dropRight)(l),s.SAVE_ERROR(m)};!c;)if(this.tokenMatcher(g,n)){f();return}else if(i.call(this)){f(),e.apply(this,t);return}else this.tokenMatcher(g,o)?c=!0:(g=this.SKIP_TOKEN(),this.addToResyncTokens(g,l));this.importLexerState(a)},r.prototype.shouldInRepetitionRecoveryBeTried=function(e,t,i){return!(i===!1||e===void 0||t===void 0||this.tokenMatcher(this.LA(1),e)||this.isBackTracking()||this.canPerformInRuleRecovery(e,this.getFollowsForInRuleRecovery(e,t)))},r.prototype.getFollowsForInRuleRecovery=function(e,t){var i=this.getCurrentGrammarPath(e,t),n=this.getNextPossibleTokenTypes(i);return n},r.prototype.tryInRuleRecovery=function(e,t){if(this.canRecoverWithSingleTokenInsertion(e,t)){var i=this.getTokenToInsert(e);return i}if(this.canRecoverWithSingleTokenDeletion(e)){var n=this.SKIP_TOKEN();return this.consumeToken(),n}throw new _S("sad sad panda")},r.prototype.canPerformInRuleRecovery=function(e,t){return this.canRecoverWithSingleTokenInsertion(e,t)||this.canRecoverWithSingleTokenDeletion(e)},r.prototype.canRecoverWithSingleTokenInsertion=function(e,t){var i=this;if(!this.canTokenTypeBeInsertedInRecovery(e)||(0,xs.isEmpty)(t))return!1;var n=this.LA(1),s=(0,xs.find)(t,function(o){return i.tokenMatcher(n,o)})!==void 0;return s},r.prototype.canRecoverWithSingleTokenDeletion=function(e){var t=this.tokenMatcher(this.LA(2),e);return t},r.prototype.isInCurrentRuleReSyncSet=function(e){var t=this.getCurrFollowKey(),i=this.getFollowSetFromFollowKey(t);return(0,xs.contains)(i,e)},r.prototype.findReSyncTokenType=function(){for(var e=this.flattenFollowSet(),t=this.LA(1),i=2;;){var n=t.tokenType;if((0,xs.contains)(e,n))return n;t=this.LA(i),i++}},r.prototype.getCurrFollowKey=function(){if(this.RULE_STACK.length===1)return Yi.EOF_FOLLOW_KEY;var e=this.getLastExplicitRuleShortName(),t=this.getLastExplicitRuleOccurrenceIndex(),i=this.getPreviousExplicitRuleShortName();return{ruleName:this.shortRuleNameToFullName(e),idxInCallingRule:t,inRule:this.shortRuleNameToFullName(i)}},r.prototype.buildFullFollowKeyStack=function(){var e=this,t=this.RULE_STACK,i=this.RULE_OCCURRENCE_STACK;return(0,xs.map)(t,function(n,s){return s===0?Yi.EOF_FOLLOW_KEY:{ruleName:e.shortRuleNameToFullName(n),idxInCallingRule:i[s],inRule:e.shortRuleNameToFullName(t[s-1])}})},r.prototype.flattenFollowSet=function(){var e=this,t=(0,xs.map)(this.buildFullFollowKeyStack(),function(i){return e.getFollowSetFromFollowKey(i)});return(0,xs.flatten)(t)},r.prototype.getFollowSetFromFollowKey=function(e){if(e===Yi.EOF_FOLLOW_KEY)return[ey.EOF];var t=e.ruleName+e.idxInCallingRule+LIe.IN+e.inRule;return this.resyncFollows[t]},r.prototype.addToResyncTokens=function(e,t){return this.tokenMatcher(e,ey.EOF)||t.push(e),t},r.prototype.reSyncTo=function(e){for(var t=[],i=this.LA(1);this.tokenMatcher(i,e)===!1;)i=this.SKIP_TOKEN(),this.addToResyncTokens(i,t);return(0,xs.dropRight)(t)},r.prototype.attemptInRepetitionRecovery=function(e,t,i,n,s,o,a){},r.prototype.getCurrentGrammarPath=function(e,t){var i=this.getHumanReadableRuleStack(),n=(0,xs.cloneArr)(this.RULE_OCCURRENCE_STACK),s={ruleStack:i,occurrenceStack:n,lastTok:e,lastTokOccurrence:t};return s},r.prototype.getHumanReadableRuleStack=function(){var e=this;return(0,xs.map)(this.RULE_STACK,function(t){return e.shortRuleNameToFullName(t)})},r}();Yi.Recoverable=OIe;function fG(r,e,t,i,n,s,o){var a=this.getKeyForAutomaticLookahead(i,n),l=this.firstAfterRepMap[a];if(l===void 0){var c=this.getCurrRuleFullName(),u=this.getGAstProductions()[c],g=new s(u,n);l=g.startWalking(),this.firstAfterRepMap[a]=l}var f=l.token,h=l.occurrence,p=l.isEndOfRule;this.RULE_STACK.length===1&&p&&f===void 0&&(f=ey.EOF,h=1),this.shouldInRepetitionRecoveryBeTried(f,h,o)&&this.tryInRepetitionRecovery(r,e,t,f)}Yi.attemptInRepetitionRecovery=fG});var ty=w(Jt=>{"use strict";Object.defineProperty(Jt,"__esModule",{value:!0});Jt.getKeyForAutomaticLookahead=Jt.AT_LEAST_ONE_SEP_IDX=Jt.MANY_SEP_IDX=Jt.AT_LEAST_ONE_IDX=Jt.MANY_IDX=Jt.OPTION_IDX=Jt.OR_IDX=Jt.BITS_FOR_ALT_IDX=Jt.BITS_FOR_RULE_IDX=Jt.BITS_FOR_OCCURRENCE_IDX=Jt.BITS_FOR_METHOD_TYPE=void 0;Jt.BITS_FOR_METHOD_TYPE=4;Jt.BITS_FOR_OCCURRENCE_IDX=8;Jt.BITS_FOR_RULE_IDX=12;Jt.BITS_FOR_ALT_IDX=8;Jt.OR_IDX=1<{"use strict";Object.defineProperty(ry,"__esModule",{value:!0});ry.LooksAhead=void 0;var Ja=Kp(),fo=Yt(),hG=es(),Wa=ty(),Rc=Tp(),UIe=function(){function r(){}return r.prototype.initLooksAhead=function(e){this.dynamicTokensEnabled=(0,fo.has)(e,"dynamicTokensEnabled")?e.dynamicTokensEnabled:hG.DEFAULT_PARSER_CONFIG.dynamicTokensEnabled,this.maxLookahead=(0,fo.has)(e,"maxLookahead")?e.maxLookahead:hG.DEFAULT_PARSER_CONFIG.maxLookahead,this.lookAheadFuncsCache=(0,fo.isES2015MapSupported)()?new Map:[],(0,fo.isES2015MapSupported)()?(this.getLaFuncFromCache=this.getLaFuncFromMap,this.setLaFuncCache=this.setLaFuncCacheUsingMap):(this.getLaFuncFromCache=this.getLaFuncFromObj,this.setLaFuncCache=this.setLaFuncUsingObj)},r.prototype.preComputeLookaheadFunctions=function(e){var t=this;(0,fo.forEach)(e,function(i){t.TRACE_INIT(i.name+" Rule Lookahead",function(){var n=(0,Rc.collectMethods)(i),s=n.alternation,o=n.repetition,a=n.option,l=n.repetitionMandatory,c=n.repetitionMandatoryWithSeparator,u=n.repetitionWithSeparator;(0,fo.forEach)(s,function(g){var f=g.idx===0?"":g.idx;t.TRACE_INIT(""+(0,Rc.getProductionDslName)(g)+f,function(){var h=(0,Ja.buildLookaheadFuncForOr)(g.idx,i,g.maxLookahead||t.maxLookahead,g.hasPredicates,t.dynamicTokensEnabled,t.lookAheadBuilderForAlternatives),p=(0,Wa.getKeyForAutomaticLookahead)(t.fullRuleNameToShort[i.name],Wa.OR_IDX,g.idx);t.setLaFuncCache(p,h)})}),(0,fo.forEach)(o,function(g){t.computeLookaheadFunc(i,g.idx,Wa.MANY_IDX,Ja.PROD_TYPE.REPETITION,g.maxLookahead,(0,Rc.getProductionDslName)(g))}),(0,fo.forEach)(a,function(g){t.computeLookaheadFunc(i,g.idx,Wa.OPTION_IDX,Ja.PROD_TYPE.OPTION,g.maxLookahead,(0,Rc.getProductionDslName)(g))}),(0,fo.forEach)(l,function(g){t.computeLookaheadFunc(i,g.idx,Wa.AT_LEAST_ONE_IDX,Ja.PROD_TYPE.REPETITION_MANDATORY,g.maxLookahead,(0,Rc.getProductionDslName)(g))}),(0,fo.forEach)(c,function(g){t.computeLookaheadFunc(i,g.idx,Wa.AT_LEAST_ONE_SEP_IDX,Ja.PROD_TYPE.REPETITION_MANDATORY_WITH_SEPARATOR,g.maxLookahead,(0,Rc.getProductionDslName)(g))}),(0,fo.forEach)(u,function(g){t.computeLookaheadFunc(i,g.idx,Wa.MANY_SEP_IDX,Ja.PROD_TYPE.REPETITION_WITH_SEPARATOR,g.maxLookahead,(0,Rc.getProductionDslName)(g))})})})},r.prototype.computeLookaheadFunc=function(e,t,i,n,s,o){var a=this;this.TRACE_INIT(""+o+(t===0?"":t),function(){var l=(0,Ja.buildLookaheadFuncForOptionalProd)(t,e,s||a.maxLookahead,a.dynamicTokensEnabled,n,a.lookAheadBuilderForOptional),c=(0,Wa.getKeyForAutomaticLookahead)(a.fullRuleNameToShort[e.name],i,t);a.setLaFuncCache(c,l)})},r.prototype.lookAheadBuilderForOptional=function(e,t,i){return(0,Ja.buildSingleAlternativeLookaheadFunction)(e,t,i)},r.prototype.lookAheadBuilderForAlternatives=function(e,t,i,n){return(0,Ja.buildAlternativesLookAheadFunc)(e,t,i,n)},r.prototype.getKeyForAutomaticLookahead=function(e,t){var i=this.getLastExplicitRuleShortName();return(0,Wa.getKeyForAutomaticLookahead)(i,e,t)},r.prototype.getLaFuncFromCache=function(e){},r.prototype.getLaFuncFromMap=function(e){return this.lookAheadFuncsCache.get(e)},r.prototype.getLaFuncFromObj=function(e){return this.lookAheadFuncsCache[e]},r.prototype.setLaFuncCache=function(e,t){},r.prototype.setLaFuncCacheUsingMap=function(e,t){this.lookAheadFuncsCache.set(e,t)},r.prototype.setLaFuncUsingObj=function(e,t){this.lookAheadFuncsCache[e]=t},r}();ry.LooksAhead=UIe});var dG=w(qo=>{"use strict";Object.defineProperty(qo,"__esModule",{value:!0});qo.addNoneTerminalToCst=qo.addTerminalToCst=qo.setNodeLocationFull=qo.setNodeLocationOnlyOffset=void 0;function KIe(r,e){isNaN(r.startOffset)===!0?(r.startOffset=e.startOffset,r.endOffset=e.endOffset):r.endOffset{"use strict";Object.defineProperty(tl,"__esModule",{value:!0});tl.defineNameProp=tl.functionName=tl.classNameFromInstance=void 0;var YIe=Yt();function qIe(r){return CG(r.constructor)}tl.classNameFromInstance=qIe;var mG="name";function CG(r){var e=r.name;return e||"anonymous"}tl.functionName=CG;function JIe(r,e){var t=Object.getOwnPropertyDescriptor(r,mG);return(0,YIe.isUndefined)(t)||t.configurable?(Object.defineProperty(r,mG,{enumerable:!1,configurable:!0,writable:!1,value:e}),!0):!1}tl.defineNameProp=JIe});var BG=w(Di=>{"use strict";Object.defineProperty(Di,"__esModule",{value:!0});Di.validateRedundantMethods=Di.validateMissingCstMethods=Di.validateVisitor=Di.CstVisitorDefinitionError=Di.createBaseVisitorConstructorWithDefaults=Di.createBaseSemanticVisitorConstructor=Di.defaultVisit=void 0;var Ps=Yt(),Gp=XS();function EG(r,e){for(var t=(0,Ps.keys)(r),i=t.length,n=0;n: + `+(""+s.join(` + +`).replace(/\n/g,` + `)))}}};return t.prototype=i,t.prototype.constructor=t,t._RULE_NAMES=e,t}Di.createBaseSemanticVisitorConstructor=WIe;function zIe(r,e,t){var i=function(){};(0,Gp.defineNameProp)(i,r+"BaseSemanticsWithDefaults");var n=Object.create(t.prototype);return(0,Ps.forEach)(e,function(s){n[s]=EG}),i.prototype=n,i.prototype.constructor=i,i}Di.createBaseVisitorConstructorWithDefaults=zIe;var ZS;(function(r){r[r.REDUNDANT_METHOD=0]="REDUNDANT_METHOD",r[r.MISSING_METHOD=1]="MISSING_METHOD"})(ZS=Di.CstVisitorDefinitionError||(Di.CstVisitorDefinitionError={}));function IG(r,e){var t=yG(r,e),i=wG(r,e);return t.concat(i)}Di.validateVisitor=IG;function yG(r,e){var t=(0,Ps.map)(e,function(i){if(!(0,Ps.isFunction)(r[i]))return{msg:"Missing visitor method: <"+i+"> on "+(0,Gp.functionName)(r.constructor)+" CST Visitor.",type:ZS.MISSING_METHOD,methodName:i}});return(0,Ps.compact)(t)}Di.validateMissingCstMethods=yG;var _Ie=["constructor","visit","validateVisitor"];function wG(r,e){var t=[];for(var i in r)(0,Ps.isFunction)(r[i])&&!(0,Ps.contains)(_Ie,i)&&!(0,Ps.contains)(e,i)&&t.push({msg:"Redundant visitor method: <"+i+"> on "+(0,Gp.functionName)(r.constructor)+` CST Visitor +There is no Grammar Rule corresponding to this method's name. +`,type:ZS.REDUNDANT_METHOD,methodName:i});return t}Di.validateRedundantMethods=wG});var QG=w(iy=>{"use strict";Object.defineProperty(iy,"__esModule",{value:!0});iy.TreeBuilder=void 0;var Qg=dG(),ni=Yt(),bG=BG(),VIe=es(),XIe=function(){function r(){}return r.prototype.initTreeBuilder=function(e){if(this.CST_STACK=[],this.outputCst=e.outputCst,this.nodeLocationTracking=(0,ni.has)(e,"nodeLocationTracking")?e.nodeLocationTracking:VIe.DEFAULT_PARSER_CONFIG.nodeLocationTracking,!this.outputCst)this.cstInvocationStateUpdate=ni.NOOP,this.cstFinallyStateUpdate=ni.NOOP,this.cstPostTerminal=ni.NOOP,this.cstPostNonTerminal=ni.NOOP,this.cstPostRule=ni.NOOP;else if(/full/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=Qg.setNodeLocationFull,this.setNodeLocationFromNode=Qg.setNodeLocationFull,this.cstPostRule=ni.NOOP,this.setInitialNodeLocation=this.setInitialNodeLocationFullRecovery):(this.setNodeLocationFromToken=ni.NOOP,this.setNodeLocationFromNode=ni.NOOP,this.cstPostRule=this.cstPostRuleFull,this.setInitialNodeLocation=this.setInitialNodeLocationFullRegular);else if(/onlyOffset/i.test(this.nodeLocationTracking))this.recoveryEnabled?(this.setNodeLocationFromToken=Qg.setNodeLocationOnlyOffset,this.setNodeLocationFromNode=Qg.setNodeLocationOnlyOffset,this.cstPostRule=ni.NOOP,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRecovery):(this.setNodeLocationFromToken=ni.NOOP,this.setNodeLocationFromNode=ni.NOOP,this.cstPostRule=this.cstPostRuleOnlyOffset,this.setInitialNodeLocation=this.setInitialNodeLocationOnlyOffsetRegular);else if(/none/i.test(this.nodeLocationTracking))this.setNodeLocationFromToken=ni.NOOP,this.setNodeLocationFromNode=ni.NOOP,this.cstPostRule=ni.NOOP,this.setInitialNodeLocation=ni.NOOP;else throw Error('Invalid config option: "'+e.nodeLocationTracking+'"')},r.prototype.setInitialNodeLocationOnlyOffsetRecovery=function(e){e.location={startOffset:NaN,endOffset:NaN}},r.prototype.setInitialNodeLocationOnlyOffsetRegular=function(e){e.location={startOffset:this.LA(1).startOffset,endOffset:NaN}},r.prototype.setInitialNodeLocationFullRecovery=function(e){e.location={startOffset:NaN,startLine:NaN,startColumn:NaN,endOffset:NaN,endLine:NaN,endColumn:NaN}},r.prototype.setInitialNodeLocationFullRegular=function(e){var t=this.LA(1);e.location={startOffset:t.startOffset,startLine:t.startLine,startColumn:t.startColumn,endOffset:NaN,endLine:NaN,endColumn:NaN}},r.prototype.cstInvocationStateUpdate=function(e,t){var i={name:e,children:{}};this.setInitialNodeLocation(i),this.CST_STACK.push(i)},r.prototype.cstFinallyStateUpdate=function(){this.CST_STACK.pop()},r.prototype.cstPostRuleFull=function(e){var t=this.LA(0),i=e.location;i.startOffset<=t.startOffset?(i.endOffset=t.endOffset,i.endLine=t.endLine,i.endColumn=t.endColumn):(i.startOffset=NaN,i.startLine=NaN,i.startColumn=NaN)},r.prototype.cstPostRuleOnlyOffset=function(e){var t=this.LA(0),i=e.location;i.startOffset<=t.startOffset?i.endOffset=t.endOffset:i.startOffset=NaN},r.prototype.cstPostTerminal=function(e,t){var i=this.CST_STACK[this.CST_STACK.length-1];(0,Qg.addTerminalToCst)(i,t,e),this.setNodeLocationFromToken(i.location,t)},r.prototype.cstPostNonTerminal=function(e,t){var i=this.CST_STACK[this.CST_STACK.length-1];(0,Qg.addNoneTerminalToCst)(i,t,e),this.setNodeLocationFromNode(i.location,e.location)},r.prototype.getBaseCstVisitorConstructor=function(){if((0,ni.isUndefined)(this.baseCstVisitorConstructor)){var e=(0,bG.createBaseSemanticVisitorConstructor)(this.className,(0,ni.keys)(this.gastProductionsCache));return this.baseCstVisitorConstructor=e,e}return this.baseCstVisitorConstructor},r.prototype.getBaseCstVisitorConstructorWithDefaults=function(){if((0,ni.isUndefined)(this.baseCstVisitorWithDefaultsConstructor)){var e=(0,bG.createBaseVisitorConstructorWithDefaults)(this.className,(0,ni.keys)(this.gastProductionsCache),this.getBaseCstVisitorConstructor());return this.baseCstVisitorWithDefaultsConstructor=e,e}return this.baseCstVisitorWithDefaultsConstructor},r.prototype.getLastExplicitRuleShortName=function(){var e=this.RULE_STACK;return e[e.length-1]},r.prototype.getPreviousExplicitRuleShortName=function(){var e=this.RULE_STACK;return e[e.length-2]},r.prototype.getLastExplicitRuleOccurrenceIndex=function(){var e=this.RULE_OCCURRENCE_STACK;return e[e.length-1]},r}();iy.TreeBuilder=XIe});var vG=w(ny=>{"use strict";Object.defineProperty(ny,"__esModule",{value:!0});ny.LexerAdapter=void 0;var SG=es(),ZIe=function(){function r(){}return r.prototype.initLexerAdapter=function(){this.tokVector=[],this.tokVectorLength=0,this.currIdx=-1},Object.defineProperty(r.prototype,"input",{get:function(){return this.tokVector},set:function(e){if(this.selfAnalysisDone!==!0)throw Error("Missing invocation at the end of the Parser's constructor.");this.reset(),this.tokVector=e,this.tokVectorLength=e.length},enumerable:!1,configurable:!0}),r.prototype.SKIP_TOKEN=function(){return this.currIdx<=this.tokVector.length-2?(this.consumeToken(),this.LA(1)):SG.END_OF_FILE},r.prototype.LA=function(e){var t=this.currIdx+e;return t<0||this.tokVectorLength<=t?SG.END_OF_FILE:this.tokVector[t]},r.prototype.consumeToken=function(){this.currIdx++},r.prototype.exportLexerState=function(){return this.currIdx},r.prototype.importLexerState=function(e){this.currIdx=e},r.prototype.resetLexerState=function(){this.currIdx=-1},r.prototype.moveToTerminatedState=function(){this.currIdx=this.tokVector.length-1},r.prototype.getLexerPosition=function(){return this.exportLexerState()},r}();ny.LexerAdapter=ZIe});var xG=w(sy=>{"use strict";Object.defineProperty(sy,"__esModule",{value:!0});sy.RecognizerApi=void 0;var kG=Yt(),$Ie=bg(),$S=es(),eye=Op(),tye=WS(),rye=bn(),iye=function(){function r(){}return r.prototype.ACTION=function(e){return e.call(this)},r.prototype.consume=function(e,t,i){return this.consumeInternal(t,e,i)},r.prototype.subrule=function(e,t,i){return this.subruleInternal(t,e,i)},r.prototype.option=function(e,t){return this.optionInternal(t,e)},r.prototype.or=function(e,t){return this.orInternal(t,e)},r.prototype.many=function(e,t){return this.manyInternal(e,t)},r.prototype.atLeastOne=function(e,t){return this.atLeastOneInternal(e,t)},r.prototype.CONSUME=function(e,t){return this.consumeInternal(e,0,t)},r.prototype.CONSUME1=function(e,t){return this.consumeInternal(e,1,t)},r.prototype.CONSUME2=function(e,t){return this.consumeInternal(e,2,t)},r.prototype.CONSUME3=function(e,t){return this.consumeInternal(e,3,t)},r.prototype.CONSUME4=function(e,t){return this.consumeInternal(e,4,t)},r.prototype.CONSUME5=function(e,t){return this.consumeInternal(e,5,t)},r.prototype.CONSUME6=function(e,t){return this.consumeInternal(e,6,t)},r.prototype.CONSUME7=function(e,t){return this.consumeInternal(e,7,t)},r.prototype.CONSUME8=function(e,t){return this.consumeInternal(e,8,t)},r.prototype.CONSUME9=function(e,t){return this.consumeInternal(e,9,t)},r.prototype.SUBRULE=function(e,t){return this.subruleInternal(e,0,t)},r.prototype.SUBRULE1=function(e,t){return this.subruleInternal(e,1,t)},r.prototype.SUBRULE2=function(e,t){return this.subruleInternal(e,2,t)},r.prototype.SUBRULE3=function(e,t){return this.subruleInternal(e,3,t)},r.prototype.SUBRULE4=function(e,t){return this.subruleInternal(e,4,t)},r.prototype.SUBRULE5=function(e,t){return this.subruleInternal(e,5,t)},r.prototype.SUBRULE6=function(e,t){return this.subruleInternal(e,6,t)},r.prototype.SUBRULE7=function(e,t){return this.subruleInternal(e,7,t)},r.prototype.SUBRULE8=function(e,t){return this.subruleInternal(e,8,t)},r.prototype.SUBRULE9=function(e,t){return this.subruleInternal(e,9,t)},r.prototype.OPTION=function(e){return this.optionInternal(e,0)},r.prototype.OPTION1=function(e){return this.optionInternal(e,1)},r.prototype.OPTION2=function(e){return this.optionInternal(e,2)},r.prototype.OPTION3=function(e){return this.optionInternal(e,3)},r.prototype.OPTION4=function(e){return this.optionInternal(e,4)},r.prototype.OPTION5=function(e){return this.optionInternal(e,5)},r.prototype.OPTION6=function(e){return this.optionInternal(e,6)},r.prototype.OPTION7=function(e){return this.optionInternal(e,7)},r.prototype.OPTION8=function(e){return this.optionInternal(e,8)},r.prototype.OPTION9=function(e){return this.optionInternal(e,9)},r.prototype.OR=function(e){return this.orInternal(e,0)},r.prototype.OR1=function(e){return this.orInternal(e,1)},r.prototype.OR2=function(e){return this.orInternal(e,2)},r.prototype.OR3=function(e){return this.orInternal(e,3)},r.prototype.OR4=function(e){return this.orInternal(e,4)},r.prototype.OR5=function(e){return this.orInternal(e,5)},r.prototype.OR6=function(e){return this.orInternal(e,6)},r.prototype.OR7=function(e){return this.orInternal(e,7)},r.prototype.OR8=function(e){return this.orInternal(e,8)},r.prototype.OR9=function(e){return this.orInternal(e,9)},r.prototype.MANY=function(e){this.manyInternal(0,e)},r.prototype.MANY1=function(e){this.manyInternal(1,e)},r.prototype.MANY2=function(e){this.manyInternal(2,e)},r.prototype.MANY3=function(e){this.manyInternal(3,e)},r.prototype.MANY4=function(e){this.manyInternal(4,e)},r.prototype.MANY5=function(e){this.manyInternal(5,e)},r.prototype.MANY6=function(e){this.manyInternal(6,e)},r.prototype.MANY7=function(e){this.manyInternal(7,e)},r.prototype.MANY8=function(e){this.manyInternal(8,e)},r.prototype.MANY9=function(e){this.manyInternal(9,e)},r.prototype.MANY_SEP=function(e){this.manySepFirstInternal(0,e)},r.prototype.MANY_SEP1=function(e){this.manySepFirstInternal(1,e)},r.prototype.MANY_SEP2=function(e){this.manySepFirstInternal(2,e)},r.prototype.MANY_SEP3=function(e){this.manySepFirstInternal(3,e)},r.prototype.MANY_SEP4=function(e){this.manySepFirstInternal(4,e)},r.prototype.MANY_SEP5=function(e){this.manySepFirstInternal(5,e)},r.prototype.MANY_SEP6=function(e){this.manySepFirstInternal(6,e)},r.prototype.MANY_SEP7=function(e){this.manySepFirstInternal(7,e)},r.prototype.MANY_SEP8=function(e){this.manySepFirstInternal(8,e)},r.prototype.MANY_SEP9=function(e){this.manySepFirstInternal(9,e)},r.prototype.AT_LEAST_ONE=function(e){this.atLeastOneInternal(0,e)},r.prototype.AT_LEAST_ONE1=function(e){return this.atLeastOneInternal(1,e)},r.prototype.AT_LEAST_ONE2=function(e){this.atLeastOneInternal(2,e)},r.prototype.AT_LEAST_ONE3=function(e){this.atLeastOneInternal(3,e)},r.prototype.AT_LEAST_ONE4=function(e){this.atLeastOneInternal(4,e)},r.prototype.AT_LEAST_ONE5=function(e){this.atLeastOneInternal(5,e)},r.prototype.AT_LEAST_ONE6=function(e){this.atLeastOneInternal(6,e)},r.prototype.AT_LEAST_ONE7=function(e){this.atLeastOneInternal(7,e)},r.prototype.AT_LEAST_ONE8=function(e){this.atLeastOneInternal(8,e)},r.prototype.AT_LEAST_ONE9=function(e){this.atLeastOneInternal(9,e)},r.prototype.AT_LEAST_ONE_SEP=function(e){this.atLeastOneSepFirstInternal(0,e)},r.prototype.AT_LEAST_ONE_SEP1=function(e){this.atLeastOneSepFirstInternal(1,e)},r.prototype.AT_LEAST_ONE_SEP2=function(e){this.atLeastOneSepFirstInternal(2,e)},r.prototype.AT_LEAST_ONE_SEP3=function(e){this.atLeastOneSepFirstInternal(3,e)},r.prototype.AT_LEAST_ONE_SEP4=function(e){this.atLeastOneSepFirstInternal(4,e)},r.prototype.AT_LEAST_ONE_SEP5=function(e){this.atLeastOneSepFirstInternal(5,e)},r.prototype.AT_LEAST_ONE_SEP6=function(e){this.atLeastOneSepFirstInternal(6,e)},r.prototype.AT_LEAST_ONE_SEP7=function(e){this.atLeastOneSepFirstInternal(7,e)},r.prototype.AT_LEAST_ONE_SEP8=function(e){this.atLeastOneSepFirstInternal(8,e)},r.prototype.AT_LEAST_ONE_SEP9=function(e){this.atLeastOneSepFirstInternal(9,e)},r.prototype.RULE=function(e,t,i){if(i===void 0&&(i=$S.DEFAULT_RULE_CONFIG),(0,kG.contains)(this.definedRulesNames,e)){var n=eye.defaultGrammarValidatorErrorProvider.buildDuplicateRuleNameError({topLevelRule:e,grammarName:this.className}),s={message:n,type:$S.ParserDefinitionErrorType.DUPLICATE_RULE_NAME,ruleName:e};this.definitionErrors.push(s)}this.definedRulesNames.push(e);var o=this.defineRule(e,t,i);return this[e]=o,o},r.prototype.OVERRIDE_RULE=function(e,t,i){i===void 0&&(i=$S.DEFAULT_RULE_CONFIG);var n=[];n=n.concat((0,tye.validateRuleIsOverridden)(e,this.definedRulesNames,this.className)),this.definitionErrors=this.definitionErrors.concat(n);var s=this.defineRule(e,t,i);return this[e]=s,s},r.prototype.BACKTRACK=function(e,t){return function(){this.isBackTrackingStack.push(1);var i=this.saveRecogState();try{return e.apply(this,t),!0}catch(n){if((0,$Ie.isRecognitionException)(n))return!1;throw n}finally{this.reloadRecogState(i),this.isBackTrackingStack.pop()}}},r.prototype.getGAstProductions=function(){return this.gastProductionsCache},r.prototype.getSerializedGastProductions=function(){return(0,rye.serializeGrammar)((0,kG.values)(this.gastProductionsCache))},r}();sy.RecognizerApi=iye});var FG=w(oy=>{"use strict";Object.defineProperty(oy,"__esModule",{value:!0});oy.RecognizerEngine=void 0;var Fr=Yt(),ts=ty(),ay=bg(),PG=Kp(),Sg=Up(),DG=es(),nye=VS(),RG=ZA(),Yp=Eg(),sye=XS(),oye=function(){function r(){}return r.prototype.initRecognizerEngine=function(e,t){if(this.className=(0,sye.classNameFromInstance)(this),this.shortRuleNameToFull={},this.fullRuleNameToShort={},this.ruleShortNameIdx=256,this.tokenMatcher=Yp.tokenStructuredMatcherNoCategories,this.definedRulesNames=[],this.tokensMap={},this.isBackTrackingStack=[],this.RULE_STACK=[],this.RULE_OCCURRENCE_STACK=[],this.gastProductionsCache={},(0,Fr.has)(t,"serializedGrammar"))throw Error(`The Parser's configuration can no longer contain a property. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_6-0-0 + For Further details.`);if((0,Fr.isArray)(e)){if((0,Fr.isEmpty)(e))throw Error(`A Token Vocabulary cannot be empty. + Note that the first argument for the parser constructor + is no longer a Token vector (since v4.0).`);if(typeof e[0].startOffset=="number")throw Error(`The Parser constructor no longer accepts a token vector as the first argument. + See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_4-0-0 + For Further details.`)}if((0,Fr.isArray)(e))this.tokensMap=(0,Fr.reduce)(e,function(o,a){return o[a.name]=a,o},{});else if((0,Fr.has)(e,"modes")&&(0,Fr.every)((0,Fr.flatten)((0,Fr.values)(e.modes)),Yp.isTokenType)){var i=(0,Fr.flatten)((0,Fr.values)(e.modes)),n=(0,Fr.uniq)(i);this.tokensMap=(0,Fr.reduce)(n,function(o,a){return o[a.name]=a,o},{})}else if((0,Fr.isObject)(e))this.tokensMap=(0,Fr.cloneObj)(e);else throw new Error(" argument must be An Array of Token constructors, A dictionary of Token constructors or an IMultiModeLexerDefinition");this.tokensMap.EOF=RG.EOF;var s=(0,Fr.every)((0,Fr.values)(e),function(o){return(0,Fr.isEmpty)(o.categoryMatches)});this.tokenMatcher=s?Yp.tokenStructuredMatcherNoCategories:Yp.tokenStructuredMatcher,(0,Yp.augmentTokenTypes)((0,Fr.values)(this.tokensMap))},r.prototype.defineRule=function(e,t,i){if(this.selfAnalysisDone)throw Error("Grammar rule <"+e+`> may not be defined after the 'performSelfAnalysis' method has been called' +Make sure that all grammar rule definitions are done before 'performSelfAnalysis' is called.`);var n=(0,Fr.has)(i,"resyncEnabled")?i.resyncEnabled:DG.DEFAULT_RULE_CONFIG.resyncEnabled,s=(0,Fr.has)(i,"recoveryValueFunc")?i.recoveryValueFunc:DG.DEFAULT_RULE_CONFIG.recoveryValueFunc,o=this.ruleShortNameIdx<t},r.prototype.orInternal=function(e,t){var i=this.getKeyForAutomaticLookahead(ts.OR_IDX,t),n=(0,Fr.isArray)(e)?e:e.DEF,s=this.getLaFuncFromCache(i),o=s.call(this,n);if(o!==void 0){var a=n[o];return a.ALT.call(this)}this.raiseNoAltException(t,e.ERR_MSG)},r.prototype.ruleFinallyStateUpdate=function(){if(this.RULE_STACK.pop(),this.RULE_OCCURRENCE_STACK.pop(),this.cstFinallyStateUpdate(),this.RULE_STACK.length===0&&this.isAtEndOfInput()===!1){var e=this.LA(1),t=this.errorMessageProvider.buildNotAllInputParsedMessage({firstRedundant:e,ruleName:this.getCurrRuleFullName()});this.SAVE_ERROR(new ay.NotAllInputParsedException(t,e))}},r.prototype.subruleInternal=function(e,t,i){var n;try{var s=i!==void 0?i.ARGS:void 0;return n=e.call(this,t,s),this.cstPostNonTerminal(n,i!==void 0&&i.LABEL!==void 0?i.LABEL:e.ruleName),n}catch(o){this.subruleInternalError(o,i,e.ruleName)}},r.prototype.subruleInternalError=function(e,t,i){throw(0,ay.isRecognitionException)(e)&&e.partialCstResult!==void 0&&(this.cstPostNonTerminal(e.partialCstResult,t!==void 0&&t.LABEL!==void 0?t.LABEL:i),delete e.partialCstResult),e},r.prototype.consumeInternal=function(e,t,i){var n;try{var s=this.LA(1);this.tokenMatcher(s,e)===!0?(this.consumeToken(),n=s):this.consumeInternalError(e,s,i)}catch(o){n=this.consumeInternalRecovery(e,t,o)}return this.cstPostTerminal(i!==void 0&&i.LABEL!==void 0?i.LABEL:e.name,n),n},r.prototype.consumeInternalError=function(e,t,i){var n,s=this.LA(0);throw i!==void 0&&i.ERR_MSG?n=i.ERR_MSG:n=this.errorMessageProvider.buildMismatchTokenMessage({expected:e,actual:t,previous:s,ruleName:this.getCurrRuleFullName()}),this.SAVE_ERROR(new ay.MismatchedTokenException(n,t,s))},r.prototype.consumeInternalRecovery=function(e,t,i){if(this.recoveryEnabled&&i.name==="MismatchedTokenException"&&!this.isBackTracking()){var n=this.getFollowsForInRuleRecovery(e,t);try{return this.tryInRuleRecovery(e,n)}catch(s){throw s.name===nye.IN_RULE_RECOVERY_EXCEPTION?i:s}}else throw i},r.prototype.saveRecogState=function(){var e=this.errors,t=(0,Fr.cloneArr)(this.RULE_STACK);return{errors:e,lexerState:this.exportLexerState(),RULE_STACK:t,CST_STACK:this.CST_STACK}},r.prototype.reloadRecogState=function(e){this.errors=e.errors,this.importLexerState(e.lexerState),this.RULE_STACK=e.RULE_STACK},r.prototype.ruleInvocationStateUpdate=function(e,t,i){this.RULE_OCCURRENCE_STACK.push(i),this.RULE_STACK.push(e),this.cstInvocationStateUpdate(t,e)},r.prototype.isBackTracking=function(){return this.isBackTrackingStack.length!==0},r.prototype.getCurrRuleFullName=function(){var e=this.getLastExplicitRuleShortName();return this.shortRuleNameToFull[e]},r.prototype.shortRuleNameToFullName=function(e){return this.shortRuleNameToFull[e]},r.prototype.isAtEndOfInput=function(){return this.tokenMatcher(this.LA(1),RG.EOF)},r.prototype.reset=function(){this.resetLexerState(),this.isBackTrackingStack=[],this.errors=[],this.RULE_STACK=[],this.CST_STACK=[],this.RULE_OCCURRENCE_STACK=[]},r}();oy.RecognizerEngine=oye});var LG=w(Ay=>{"use strict";Object.defineProperty(Ay,"__esModule",{value:!0});Ay.ErrorHandler=void 0;var ev=bg(),tv=Yt(),NG=Kp(),aye=es(),Aye=function(){function r(){}return r.prototype.initErrorHandler=function(e){this._errors=[],this.errorMessageProvider=(0,tv.has)(e,"errorMessageProvider")?e.errorMessageProvider:aye.DEFAULT_PARSER_CONFIG.errorMessageProvider},r.prototype.SAVE_ERROR=function(e){if((0,ev.isRecognitionException)(e))return e.context={ruleStack:this.getHumanReadableRuleStack(),ruleOccurrenceStack:(0,tv.cloneArr)(this.RULE_OCCURRENCE_STACK)},this._errors.push(e),e;throw Error("Trying to save an Error which is not a RecognitionException")},Object.defineProperty(r.prototype,"errors",{get:function(){return(0,tv.cloneArr)(this._errors)},set:function(e){this._errors=e},enumerable:!1,configurable:!0}),r.prototype.raiseEarlyExitException=function(e,t,i){for(var n=this.getCurrRuleFullName(),s=this.getGAstProductions()[n],o=(0,NG.getLookaheadPathsForOptionalProd)(e,s,t,this.maxLookahead),a=o[0],l=[],c=1;c<=this.maxLookahead;c++)l.push(this.LA(c));var u=this.errorMessageProvider.buildEarlyExitMessage({expectedIterationPaths:a,actual:l,previous:this.LA(0),customUserDescription:i,ruleName:n});throw this.SAVE_ERROR(new ev.EarlyExitException(u,this.LA(1),this.LA(0)))},r.prototype.raiseNoAltException=function(e,t){for(var i=this.getCurrRuleFullName(),n=this.getGAstProductions()[i],s=(0,NG.getLookaheadPathsForOr)(e,n,this.maxLookahead),o=[],a=1;a<=this.maxLookahead;a++)o.push(this.LA(a));var l=this.LA(0),c=this.errorMessageProvider.buildNoViableAltMessage({expectedPathsPerAlt:s,actual:o,previous:l,customUserDescription:t,ruleName:this.getCurrRuleFullName()});throw this.SAVE_ERROR(new ev.NoViableAltException(c,this.LA(1),l))},r}();Ay.ErrorHandler=Aye});var MG=w(ly=>{"use strict";Object.defineProperty(ly,"__esModule",{value:!0});ly.ContentAssist=void 0;var TG=Up(),OG=Yt(),lye=function(){function r(){}return r.prototype.initContentAssist=function(){},r.prototype.computeContentAssist=function(e,t){var i=this.gastProductionsCache[e];if((0,OG.isUndefined)(i))throw Error("Rule ->"+e+"<- does not exist in this grammar.");return(0,TG.nextPossibleTokensAfter)([i],t,this.tokenMatcher,this.maxLookahead)},r.prototype.getNextPossibleTokenTypes=function(e){var t=(0,OG.first)(e.ruleStack),i=this.getGAstProductions(),n=i[t],s=new TG.NextAfterTokenWalker(n,e).startWalking();return s},r}();ly.ContentAssist=lye});var JG=w(cy=>{"use strict";Object.defineProperty(cy,"__esModule",{value:!0});cy.GastRecorder=void 0;var vn=Yt(),Jo=bn(),cye=Rp(),UG=Eg(),KG=ZA(),uye=es(),gye=ty(),uy={description:"This Object indicates the Parser is during Recording Phase"};Object.freeze(uy);var HG=!0,jG=Math.pow(2,gye.BITS_FOR_OCCURRENCE_IDX)-1,GG=(0,KG.createToken)({name:"RECORDING_PHASE_TOKEN",pattern:cye.Lexer.NA});(0,UG.augmentTokenTypes)([GG]);var YG=(0,KG.createTokenInstance)(GG,`This IToken indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,-1,-1,-1,-1,-1,-1);Object.freeze(YG);var fye={name:`This CSTNode indicates the Parser is in Recording Phase + See: https://chevrotain.io/docs/guide/internals.html#grammar-recording for details`,children:{}},pye=function(){function r(){}return r.prototype.initGastRecorder=function(e){this.recordingProdStack=[],this.RECORDING_PHASE=!1},r.prototype.enableRecording=function(){var e=this;this.RECORDING_PHASE=!0,this.TRACE_INIT("Enable Recording",function(){for(var t=function(n){var s=n>0?n:"";e["CONSUME"+s]=function(o,a){return this.consumeInternalRecord(o,n,a)},e["SUBRULE"+s]=function(o,a){return this.subruleInternalRecord(o,n,a)},e["OPTION"+s]=function(o){return this.optionInternalRecord(o,n)},e["OR"+s]=function(o){return this.orInternalRecord(o,n)},e["MANY"+s]=function(o){this.manyInternalRecord(n,o)},e["MANY_SEP"+s]=function(o){this.manySepFirstInternalRecord(n,o)},e["AT_LEAST_ONE"+s]=function(o){this.atLeastOneInternalRecord(n,o)},e["AT_LEAST_ONE_SEP"+s]=function(o){this.atLeastOneSepFirstInternalRecord(n,o)}},i=0;i<10;i++)t(i);e.consume=function(n,s,o){return this.consumeInternalRecord(s,n,o)},e.subrule=function(n,s,o){return this.subruleInternalRecord(s,n,o)},e.option=function(n,s){return this.optionInternalRecord(s,n)},e.or=function(n,s){return this.orInternalRecord(s,n)},e.many=function(n,s){this.manyInternalRecord(n,s)},e.atLeastOne=function(n,s){this.atLeastOneInternalRecord(n,s)},e.ACTION=e.ACTION_RECORD,e.BACKTRACK=e.BACKTRACK_RECORD,e.LA=e.LA_RECORD})},r.prototype.disableRecording=function(){var e=this;this.RECORDING_PHASE=!1,this.TRACE_INIT("Deleting Recording methods",function(){for(var t=0;t<10;t++){var i=t>0?t:"";delete e["CONSUME"+i],delete e["SUBRULE"+i],delete e["OPTION"+i],delete e["OR"+i],delete e["MANY"+i],delete e["MANY_SEP"+i],delete e["AT_LEAST_ONE"+i],delete e["AT_LEAST_ONE_SEP"+i]}delete e.consume,delete e.subrule,delete e.option,delete e.or,delete e.many,delete e.atLeastOne,delete e.ACTION,delete e.BACKTRACK,delete e.LA})},r.prototype.ACTION_RECORD=function(e){},r.prototype.BACKTRACK_RECORD=function(e,t){return function(){return!0}},r.prototype.LA_RECORD=function(e){return uye.END_OF_FILE},r.prototype.topLevelRuleRecord=function(e,t){try{var i=new Jo.Rule({definition:[],name:e});return i.name=e,this.recordingProdStack.push(i),t.call(this),this.recordingProdStack.pop(),i}catch(n){if(n.KNOWN_RECORDER_ERROR!==!0)try{n.message=n.message+` + This error was thrown during the "grammar recording phase" For more info see: + https://chevrotain.io/docs/guide/internals.html#grammar-recording`}catch(s){throw n}throw n}},r.prototype.optionInternalRecord=function(e,t){return qp.call(this,Jo.Option,e,t)},r.prototype.atLeastOneInternalRecord=function(e,t){qp.call(this,Jo.RepetitionMandatory,t,e)},r.prototype.atLeastOneSepFirstInternalRecord=function(e,t){qp.call(this,Jo.RepetitionMandatoryWithSeparator,t,e,HG)},r.prototype.manyInternalRecord=function(e,t){qp.call(this,Jo.Repetition,t,e)},r.prototype.manySepFirstInternalRecord=function(e,t){qp.call(this,Jo.RepetitionWithSeparator,t,e,HG)},r.prototype.orInternalRecord=function(e,t){return hye.call(this,e,t)},r.prototype.subruleInternalRecord=function(e,t,i){if(gy(t),!e||(0,vn.has)(e,"ruleName")===!1){var n=new Error(" argument is invalid"+(" expecting a Parser method reference but got: <"+JSON.stringify(e)+">")+(` + inside top level rule: <`+this.recordingProdStack[0].name+">"));throw n.KNOWN_RECORDER_ERROR=!0,n}var s=(0,vn.peek)(this.recordingProdStack),o=e.ruleName,a=new Jo.NonTerminal({idx:t,nonTerminalName:o,label:i==null?void 0:i.LABEL,referencedRule:void 0});return s.definition.push(a),this.outputCst?fye:uy},r.prototype.consumeInternalRecord=function(e,t,i){if(gy(t),!(0,UG.hasShortKeyProperty)(e)){var n=new Error(" argument is invalid"+(" expecting a TokenType reference but got: <"+JSON.stringify(e)+">")+(` + inside top level rule: <`+this.recordingProdStack[0].name+">"));throw n.KNOWN_RECORDER_ERROR=!0,n}var s=(0,vn.peek)(this.recordingProdStack),o=new Jo.Terminal({idx:t,terminalType:e,label:i==null?void 0:i.LABEL});return s.definition.push(o),YG},r}();cy.GastRecorder=pye;function qp(r,e,t,i){i===void 0&&(i=!1),gy(t);var n=(0,vn.peek)(this.recordingProdStack),s=(0,vn.isFunction)(e)?e:e.DEF,o=new r({definition:[],idx:t});return i&&(o.separator=e.SEP),(0,vn.has)(e,"MAX_LOOKAHEAD")&&(o.maxLookahead=e.MAX_LOOKAHEAD),this.recordingProdStack.push(o),s.call(this),n.definition.push(o),this.recordingProdStack.pop(),uy}function hye(r,e){var t=this;gy(e);var i=(0,vn.peek)(this.recordingProdStack),n=(0,vn.isArray)(r)===!1,s=n===!1?r:r.DEF,o=new Jo.Alternation({definition:[],idx:e,ignoreAmbiguities:n&&r.IGNORE_AMBIGUITIES===!0});(0,vn.has)(r,"MAX_LOOKAHEAD")&&(o.maxLookahead=r.MAX_LOOKAHEAD);var a=(0,vn.some)(s,function(l){return(0,vn.isFunction)(l.GATE)});return o.hasPredicates=a,i.definition.push(o),(0,vn.forEach)(s,function(l){var c=new Jo.Alternative({definition:[]});o.definition.push(c),(0,vn.has)(l,"IGNORE_AMBIGUITIES")?c.ignoreAmbiguities=l.IGNORE_AMBIGUITIES:(0,vn.has)(l,"GATE")&&(c.ignoreAmbiguities=!0),t.recordingProdStack.push(c),l.ALT.call(t),t.recordingProdStack.pop()}),uy}function qG(r){return r===0?"":""+r}function gy(r){if(r<0||r>jG){var e=new Error("Invalid DSL Method idx value: <"+r+`> + `+("Idx value must be a none negative value smaller than "+(jG+1)));throw e.KNOWN_RECORDER_ERROR=!0,e}}});var zG=w(fy=>{"use strict";Object.defineProperty(fy,"__esModule",{value:!0});fy.PerformanceTracer=void 0;var WG=Yt(),dye=es(),Cye=function(){function r(){}return r.prototype.initPerformanceTracer=function(e){if((0,WG.has)(e,"traceInitPerf")){var t=e.traceInitPerf,i=typeof t=="number";this.traceInitMaxIdent=i?t:Infinity,this.traceInitPerf=i?t>0:t}else this.traceInitMaxIdent=0,this.traceInitPerf=dye.DEFAULT_PARSER_CONFIG.traceInitPerf;this.traceInitIndent=-1},r.prototype.TRACE_INIT=function(e,t){if(this.traceInitPerf===!0){this.traceInitIndent++;var i=new Array(this.traceInitIndent+1).join(" ");this.traceInitIndent <"+e+">");var n=(0,WG.timer)(t),s=n.time,o=n.value,a=s>10?console.warn:console.log;return this.traceInitIndent time: "+s+"ms"),this.traceInitIndent--,o}else return t()},r}();fy.PerformanceTracer=Cye});var _G=w(hy=>{"use strict";Object.defineProperty(hy,"__esModule",{value:!0});hy.applyMixins=void 0;function mye(r,e){e.forEach(function(t){var i=t.prototype;Object.getOwnPropertyNames(i).forEach(function(n){if(n!=="constructor"){var s=Object.getOwnPropertyDescriptor(i,n);s&&(s.get||s.set)?Object.defineProperty(r.prototype,n,s):r.prototype[n]=t.prototype[n]}})})}hy.applyMixins=mye});var es=w(Er=>{"use strict";var VG=Er&&Er.__extends||function(){var r=function(e,t){return r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(i,n){i.__proto__=n}||function(i,n){for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(i[s]=n[s])},r(e,t)};return function(e,t){if(typeof t!="function"&&t!==null)throw new TypeError("Class extends value "+String(t)+" is not a constructor or null");r(e,t);function i(){this.constructor=e}e.prototype=t===null?Object.create(t):(i.prototype=t.prototype,new i)}}();Object.defineProperty(Er,"__esModule",{value:!0});Er.EmbeddedActionsParser=Er.CstParser=Er.Parser=Er.EMPTY_ALT=Er.ParserDefinitionErrorType=Er.DEFAULT_RULE_CONFIG=Er.DEFAULT_PARSER_CONFIG=Er.END_OF_FILE=void 0;var an=Yt(),Eye=Lj(),XG=ZA(),ZG=Op(),$G=aG(),Iye=VS(),yye=pG(),wye=QG(),Bye=vG(),bye=xG(),Qye=FG(),Sye=LG(),vye=MG(),kye=JG(),xye=zG(),Pye=_G();Er.END_OF_FILE=(0,XG.createTokenInstance)(XG.EOF,"",NaN,NaN,NaN,NaN,NaN,NaN);Object.freeze(Er.END_OF_FILE);Er.DEFAULT_PARSER_CONFIG=Object.freeze({recoveryEnabled:!1,maxLookahead:3,dynamicTokensEnabled:!1,outputCst:!0,errorMessageProvider:ZG.defaultParserErrorProvider,nodeLocationTracking:"none",traceInitPerf:!1,skipValidations:!1});Er.DEFAULT_RULE_CONFIG=Object.freeze({recoveryValueFunc:function(){},resyncEnabled:!0});var Dye;(function(r){r[r.INVALID_RULE_NAME=0]="INVALID_RULE_NAME",r[r.DUPLICATE_RULE_NAME=1]="DUPLICATE_RULE_NAME",r[r.INVALID_RULE_OVERRIDE=2]="INVALID_RULE_OVERRIDE",r[r.DUPLICATE_PRODUCTIONS=3]="DUPLICATE_PRODUCTIONS",r[r.UNRESOLVED_SUBRULE_REF=4]="UNRESOLVED_SUBRULE_REF",r[r.LEFT_RECURSION=5]="LEFT_RECURSION",r[r.NONE_LAST_EMPTY_ALT=6]="NONE_LAST_EMPTY_ALT",r[r.AMBIGUOUS_ALTS=7]="AMBIGUOUS_ALTS",r[r.CONFLICT_TOKENS_RULES_NAMESPACE=8]="CONFLICT_TOKENS_RULES_NAMESPACE",r[r.INVALID_TOKEN_NAME=9]="INVALID_TOKEN_NAME",r[r.NO_NON_EMPTY_LOOKAHEAD=10]="NO_NON_EMPTY_LOOKAHEAD",r[r.AMBIGUOUS_PREFIX_ALTS=11]="AMBIGUOUS_PREFIX_ALTS",r[r.TOO_MANY_ALTS=12]="TOO_MANY_ALTS"})(Dye=Er.ParserDefinitionErrorType||(Er.ParserDefinitionErrorType={}));function Rye(r){return r===void 0&&(r=void 0),function(){return r}}Er.EMPTY_ALT=Rye;var py=function(){function r(e,t){this.definitionErrors=[],this.selfAnalysisDone=!1;var i=this;if(i.initErrorHandler(t),i.initLexerAdapter(),i.initLooksAhead(t),i.initRecognizerEngine(e,t),i.initRecoverable(t),i.initTreeBuilder(t),i.initContentAssist(),i.initGastRecorder(t),i.initPerformanceTracer(t),(0,an.has)(t,"ignoredIssues"))throw new Error(`The IParserConfig property has been deprecated. + Please use the flag on the relevant DSL method instead. + See: https://chevrotain.io/docs/guide/resolving_grammar_errors.html#IGNORING_AMBIGUITIES + For further details.`);this.skipValidations=(0,an.has)(t,"skipValidations")?t.skipValidations:Er.DEFAULT_PARSER_CONFIG.skipValidations}return r.performSelfAnalysis=function(e){throw Error("The **static** `performSelfAnalysis` method has been deprecated. \nUse the **instance** method with the same name instead.")},r.prototype.performSelfAnalysis=function(){var e=this;this.TRACE_INIT("performSelfAnalysis",function(){var t;e.selfAnalysisDone=!0;var i=e.className;e.TRACE_INIT("toFastProps",function(){(0,an.toFastProperties)(e)}),e.TRACE_INIT("Grammar Recording",function(){try{e.enableRecording(),(0,an.forEach)(e.definedRulesNames,function(s){var o=e[s],a=o.originalGrammarAction,l=void 0;e.TRACE_INIT(s+" Rule",function(){l=e.topLevelRuleRecord(s,a)}),e.gastProductionsCache[s]=l})}finally{e.disableRecording()}});var n=[];if(e.TRACE_INIT("Grammar Resolving",function(){n=(0,$G.resolveGrammar)({rules:(0,an.values)(e.gastProductionsCache)}),e.definitionErrors=e.definitionErrors.concat(n)}),e.TRACE_INIT("Grammar Validations",function(){if((0,an.isEmpty)(n)&&e.skipValidations===!1){var s=(0,$G.validateGrammar)({rules:(0,an.values)(e.gastProductionsCache),maxLookahead:e.maxLookahead,tokenTypes:(0,an.values)(e.tokensMap),errMsgProvider:ZG.defaultGrammarValidatorErrorProvider,grammarName:i});e.definitionErrors=e.definitionErrors.concat(s)}}),(0,an.isEmpty)(e.definitionErrors)&&(e.recoveryEnabled&&e.TRACE_INIT("computeAllProdsFollows",function(){var s=(0,Eye.computeAllProdsFollows)((0,an.values)(e.gastProductionsCache));e.resyncFollows=s}),e.TRACE_INIT("ComputeLookaheadFunctions",function(){e.preComputeLookaheadFunctions((0,an.values)(e.gastProductionsCache))})),!r.DEFER_DEFINITION_ERRORS_HANDLING&&!(0,an.isEmpty)(e.definitionErrors))throw t=(0,an.map)(e.definitionErrors,function(s){return s.message}),new Error(`Parser Definition Errors detected: + `+t.join(` +------------------------------- +`))})},r.DEFER_DEFINITION_ERRORS_HANDLING=!1,r}();Er.Parser=py;(0,Pye.applyMixins)(py,[Iye.Recoverable,yye.LooksAhead,wye.TreeBuilder,Bye.LexerAdapter,Qye.RecognizerEngine,bye.RecognizerApi,Sye.ErrorHandler,vye.ContentAssist,kye.GastRecorder,xye.PerformanceTracer]);var Fye=function(r){VG(e,r);function e(t,i){i===void 0&&(i=Er.DEFAULT_PARSER_CONFIG);var n=this,s=(0,an.cloneObj)(i);return s.outputCst=!0,n=r.call(this,t,s)||this,n}return e}(py);Er.CstParser=Fye;var Nye=function(r){VG(e,r);function e(t,i){i===void 0&&(i=Er.DEFAULT_PARSER_CONFIG);var n=this,s=(0,an.cloneObj)(i);return s.outputCst=!1,n=r.call(this,t,s)||this,n}return e}(py);Er.EmbeddedActionsParser=Nye});var tY=w(dy=>{"use strict";Object.defineProperty(dy,"__esModule",{value:!0});dy.createSyntaxDiagramsCode=void 0;var eY=yS();function Lye(r,e){var t=e===void 0?{}:e,i=t.resourceBase,n=i===void 0?"https://unpkg.com/chevrotain@"+eY.VERSION+"/diagrams/":i,s=t.css,o=s===void 0?"https://unpkg.com/chevrotain@"+eY.VERSION+"/diagrams/diagrams.css":s,a=` + + + + + +`,l=` + +`,c=` + + + + +`,u=` +

+`,g=` + +`,f=` + +`;return a+l+c+u+g+f}dy.createSyntaxDiagramsCode=Lye});var nY=w(Ve=>{"use strict";Object.defineProperty(Ve,"__esModule",{value:!0});Ve.Parser=Ve.createSyntaxDiagramsCode=Ve.clearCache=Ve.GAstVisitor=Ve.serializeProduction=Ve.serializeGrammar=Ve.Terminal=Ve.Rule=Ve.RepetitionWithSeparator=Ve.RepetitionMandatoryWithSeparator=Ve.RepetitionMandatory=Ve.Repetition=Ve.Option=Ve.NonTerminal=Ve.Alternative=Ve.Alternation=Ve.defaultLexerErrorProvider=Ve.NoViableAltException=Ve.NotAllInputParsedException=Ve.MismatchedTokenException=Ve.isRecognitionException=Ve.EarlyExitException=Ve.defaultParserErrorProvider=Ve.tokenName=Ve.tokenMatcher=Ve.tokenLabel=Ve.EOF=Ve.createTokenInstance=Ve.createToken=Ve.LexerDefinitionErrorType=Ve.Lexer=Ve.EMPTY_ALT=Ve.ParserDefinitionErrorType=Ve.EmbeddedActionsParser=Ve.CstParser=Ve.VERSION=void 0;var Tye=yS();Object.defineProperty(Ve,"VERSION",{enumerable:!0,get:function(){return Tye.VERSION}});var Cy=es();Object.defineProperty(Ve,"CstParser",{enumerable:!0,get:function(){return Cy.CstParser}});Object.defineProperty(Ve,"EmbeddedActionsParser",{enumerable:!0,get:function(){return Cy.EmbeddedActionsParser}});Object.defineProperty(Ve,"ParserDefinitionErrorType",{enumerable:!0,get:function(){return Cy.ParserDefinitionErrorType}});Object.defineProperty(Ve,"EMPTY_ALT",{enumerable:!0,get:function(){return Cy.EMPTY_ALT}});var rY=Rp();Object.defineProperty(Ve,"Lexer",{enumerable:!0,get:function(){return rY.Lexer}});Object.defineProperty(Ve,"LexerDefinitionErrorType",{enumerable:!0,get:function(){return rY.LexerDefinitionErrorType}});var vg=ZA();Object.defineProperty(Ve,"createToken",{enumerable:!0,get:function(){return vg.createToken}});Object.defineProperty(Ve,"createTokenInstance",{enumerable:!0,get:function(){return vg.createTokenInstance}});Object.defineProperty(Ve,"EOF",{enumerable:!0,get:function(){return vg.EOF}});Object.defineProperty(Ve,"tokenLabel",{enumerable:!0,get:function(){return vg.tokenLabel}});Object.defineProperty(Ve,"tokenMatcher",{enumerable:!0,get:function(){return vg.tokenMatcher}});Object.defineProperty(Ve,"tokenName",{enumerable:!0,get:function(){return vg.tokenName}});var Oye=Op();Object.defineProperty(Ve,"defaultParserErrorProvider",{enumerable:!0,get:function(){return Oye.defaultParserErrorProvider}});var Jp=bg();Object.defineProperty(Ve,"EarlyExitException",{enumerable:!0,get:function(){return Jp.EarlyExitException}});Object.defineProperty(Ve,"isRecognitionException",{enumerable:!0,get:function(){return Jp.isRecognitionException}});Object.defineProperty(Ve,"MismatchedTokenException",{enumerable:!0,get:function(){return Jp.MismatchedTokenException}});Object.defineProperty(Ve,"NotAllInputParsedException",{enumerable:!0,get:function(){return Jp.NotAllInputParsedException}});Object.defineProperty(Ve,"NoViableAltException",{enumerable:!0,get:function(){return Jp.NoViableAltException}});var Mye=DS();Object.defineProperty(Ve,"defaultLexerErrorProvider",{enumerable:!0,get:function(){return Mye.defaultLexerErrorProvider}});var Wo=bn();Object.defineProperty(Ve,"Alternation",{enumerable:!0,get:function(){return Wo.Alternation}});Object.defineProperty(Ve,"Alternative",{enumerable:!0,get:function(){return Wo.Alternative}});Object.defineProperty(Ve,"NonTerminal",{enumerable:!0,get:function(){return Wo.NonTerminal}});Object.defineProperty(Ve,"Option",{enumerable:!0,get:function(){return Wo.Option}});Object.defineProperty(Ve,"Repetition",{enumerable:!0,get:function(){return Wo.Repetition}});Object.defineProperty(Ve,"RepetitionMandatory",{enumerable:!0,get:function(){return Wo.RepetitionMandatory}});Object.defineProperty(Ve,"RepetitionMandatoryWithSeparator",{enumerable:!0,get:function(){return Wo.RepetitionMandatoryWithSeparator}});Object.defineProperty(Ve,"RepetitionWithSeparator",{enumerable:!0,get:function(){return Wo.RepetitionWithSeparator}});Object.defineProperty(Ve,"Rule",{enumerable:!0,get:function(){return Wo.Rule}});Object.defineProperty(Ve,"Terminal",{enumerable:!0,get:function(){return Wo.Terminal}});var iY=bn();Object.defineProperty(Ve,"serializeGrammar",{enumerable:!0,get:function(){return iY.serializeGrammar}});Object.defineProperty(Ve,"serializeProduction",{enumerable:!0,get:function(){return iY.serializeProduction}});var Uye=Ig();Object.defineProperty(Ve,"GAstVisitor",{enumerable:!0,get:function(){return Uye.GAstVisitor}});function Kye(){console.warn(`The clearCache function was 'soft' removed from the Chevrotain API. + It performs no action other than printing this message. + Please avoid using it as it will be completely removed in the future`)}Ve.clearCache=Kye;var Hye=tY();Object.defineProperty(Ve,"createSyntaxDiagramsCode",{enumerable:!0,get:function(){return Hye.createSyntaxDiagramsCode}});var jye=function(){function r(){throw new Error(`The Parser class has been deprecated, use CstParser or EmbeddedActionsParser instead. +See: https://chevrotain.io/docs/changes/BREAKING_CHANGES.html#_7-0-0`)}return r}();Ve.Parser=jye});var aY=w((eet,sY)=>{var my=nY(),za=my.createToken,oY=my.tokenMatcher,rv=my.Lexer,Gye=my.EmbeddedActionsParser;sY.exports=r=>{let e=za({name:"LogicalOperator",pattern:rv.NA}),t=za({name:"Or",pattern:/\|/,categories:e}),i=za({name:"Xor",pattern:/\^/,categories:e}),n=za({name:"And",pattern:/&/,categories:e}),s=za({name:"Not",pattern:/!/}),o=za({name:"LParen",pattern:/\(/}),a=za({name:"RParen",pattern:/\)/}),l=za({name:"Query",pattern:r}),u=[za({name:"WhiteSpace",pattern:/\s+/,group:rv.SKIPPED}),t,i,n,o,a,s,e,l],g=new rv(u);class f extends Gye{constructor(p){super(u);this.RULE("expression",()=>this.SUBRULE(this.logicalExpression)),this.RULE("logicalExpression",()=>{let y=this.SUBRULE(this.atomicExpression);return this.MANY(()=>{let b=y,v=this.CONSUME(e),k=this.SUBRULE2(this.atomicExpression);oY(v,t)?y=T=>b(T)||k(T):oY(v,i)?y=T=>!!(b(T)^k(T)):y=T=>b(T)&&k(T)}),y}),this.RULE("atomicExpression",()=>this.OR([{ALT:()=>this.SUBRULE(this.parenthesisExpression)},{ALT:()=>{let{image:m}=this.CONSUME(l);return y=>y(m)}},{ALT:()=>{this.CONSUME(s);let m=this.SUBRULE(this.atomicExpression);return y=>!m(y)}}])),this.RULE("parenthesisExpression",()=>{let m;return this.CONSUME(o),m=this.SUBRULE(this.expression),this.CONSUME(a),m}),this.performSelfAnalysis()}}return{TinylogicLexer:g,TinylogicParser:f}}});var AY=w(Ey=>{var Yye=aY();Ey.makeParser=(r=/[a-z]+/)=>{let{TinylogicLexer:e,TinylogicParser:t}=Yye(r),i=new t;return(n,s)=>{let o=e.tokenize(n);return i.input=o.tokens,i.expression()(s)}};Ey.parse=Ey.makeParser()});var cY=w((ret,lY)=>{"use strict";lY.exports={aliceblue:[240,248,255],antiquewhite:[250,235,215],aqua:[0,255,255],aquamarine:[127,255,212],azure:[240,255,255],beige:[245,245,220],bisque:[255,228,196],black:[0,0,0],blanchedalmond:[255,235,205],blue:[0,0,255],blueviolet:[138,43,226],brown:[165,42,42],burlywood:[222,184,135],cadetblue:[95,158,160],chartreuse:[127,255,0],chocolate:[210,105,30],coral:[255,127,80],cornflowerblue:[100,149,237],cornsilk:[255,248,220],crimson:[220,20,60],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgoldenrod:[184,134,11],darkgray:[169,169,169],darkgreen:[0,100,0],darkgrey:[169,169,169],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkseagreen:[143,188,143],darkslateblue:[72,61,139],darkslategray:[47,79,79],darkslategrey:[47,79,79],darkturquoise:[0,206,209],darkviolet:[148,0,211],deeppink:[255,20,147],deepskyblue:[0,191,255],dimgray:[105,105,105],dimgrey:[105,105,105],dodgerblue:[30,144,255],firebrick:[178,34,34],floralwhite:[255,250,240],forestgreen:[34,139,34],fuchsia:[255,0,255],gainsboro:[220,220,220],ghostwhite:[248,248,255],gold:[255,215,0],goldenrod:[218,165,32],gray:[128,128,128],green:[0,128,0],greenyellow:[173,255,47],grey:[128,128,128],honeydew:[240,255,240],hotpink:[255,105,180],indianred:[205,92,92],indigo:[75,0,130],ivory:[255,255,240],khaki:[240,230,140],lavender:[230,230,250],lavenderblush:[255,240,245],lawngreen:[124,252,0],lemonchiffon:[255,250,205],lightblue:[173,216,230],lightcoral:[240,128,128],lightcyan:[224,255,255],lightgoldenrodyellow:[250,250,210],lightgray:[211,211,211],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightsalmon:[255,160,122],lightseagreen:[32,178,170],lightskyblue:[135,206,250],lightslategray:[119,136,153],lightslategrey:[119,136,153],lightsteelblue:[176,196,222],lightyellow:[255,255,224],lime:[0,255,0],limegreen:[50,205,50],linen:[250,240,230],magenta:[255,0,255],maroon:[128,0,0],mediumaquamarine:[102,205,170],mediumblue:[0,0,205],mediumorchid:[186,85,211],mediumpurple:[147,112,219],mediumseagreen:[60,179,113],mediumslateblue:[123,104,238],mediumspringgreen:[0,250,154],mediumturquoise:[72,209,204],mediumvioletred:[199,21,133],midnightblue:[25,25,112],mintcream:[245,255,250],mistyrose:[255,228,225],moccasin:[255,228,181],navajowhite:[255,222,173],navy:[0,0,128],oldlace:[253,245,230],olive:[128,128,0],olivedrab:[107,142,35],orange:[255,165,0],orangered:[255,69,0],orchid:[218,112,214],palegoldenrod:[238,232,170],palegreen:[152,251,152],paleturquoise:[175,238,238],palevioletred:[219,112,147],papayawhip:[255,239,213],peachpuff:[255,218,185],peru:[205,133,63],pink:[255,192,203],plum:[221,160,221],powderblue:[176,224,230],purple:[128,0,128],rebeccapurple:[102,51,153],red:[255,0,0],rosybrown:[188,143,143],royalblue:[65,105,225],saddlebrown:[139,69,19],salmon:[250,128,114],sandybrown:[244,164,96],seagreen:[46,139,87],seashell:[255,245,238],sienna:[160,82,45],silver:[192,192,192],skyblue:[135,206,235],slateblue:[106,90,205],slategray:[112,128,144],slategrey:[112,128,144],snow:[255,250,250],springgreen:[0,255,127],steelblue:[70,130,180],tan:[210,180,140],teal:[0,128,128],thistle:[216,191,216],tomato:[255,99,71],turquoise:[64,224,208],violet:[238,130,238],wheat:[245,222,179],white:[255,255,255],whitesmoke:[245,245,245],yellow:[255,255,0],yellowgreen:[154,205,50]}});var iv=w((iet,uY)=>{var Wp=cY(),gY={};for(let r of Object.keys(Wp))gY[Wp[r]]=r;var at={rgb:{channels:3,labels:"rgb"},hsl:{channels:3,labels:"hsl"},hsv:{channels:3,labels:"hsv"},hwb:{channels:3,labels:"hwb"},cmyk:{channels:4,labels:"cmyk"},xyz:{channels:3,labels:"xyz"},lab:{channels:3,labels:"lab"},lch:{channels:3,labels:"lch"},hex:{channels:1,labels:["hex"]},keyword:{channels:1,labels:["keyword"]},ansi16:{channels:1,labels:["ansi16"]},ansi256:{channels:1,labels:["ansi256"]},hcg:{channels:3,labels:["h","c","g"]},apple:{channels:3,labels:["r16","g16","b16"]},gray:{channels:1,labels:["gray"]}};uY.exports=at;for(let r of Object.keys(at)){if(!("channels"in at[r]))throw new Error("missing channels property: "+r);if(!("labels"in at[r]))throw new Error("missing channel labels property: "+r);if(at[r].labels.length!==at[r].channels)throw new Error("channel and label counts mismatch: "+r);let{channels:e,labels:t}=at[r];delete at[r].channels,delete at[r].labels,Object.defineProperty(at[r],"channels",{value:e}),Object.defineProperty(at[r],"labels",{value:t})}at.rgb.hsl=function(r){let e=r[0]/255,t=r[1]/255,i=r[2]/255,n=Math.min(e,t,i),s=Math.max(e,t,i),o=s-n,a,l;s===n?a=0:e===s?a=(t-i)/o:t===s?a=2+(i-e)/o:i===s&&(a=4+(e-t)/o),a=Math.min(a*60,360),a<0&&(a+=360);let c=(n+s)/2;return s===n?l=0:c<=.5?l=o/(s+n):l=o/(2-s-n),[a,l*100,c*100]};at.rgb.hsv=function(r){let e,t,i,n,s,o=r[0]/255,a=r[1]/255,l=r[2]/255,c=Math.max(o,a,l),u=c-Math.min(o,a,l),g=function(f){return(c-f)/6/u+1/2};return u===0?(n=0,s=0):(s=u/c,e=g(o),t=g(a),i=g(l),o===c?n=i-t:a===c?n=1/3+e-i:l===c&&(n=2/3+t-e),n<0?n+=1:n>1&&(n-=1)),[n*360,s*100,c*100]};at.rgb.hwb=function(r){let e=r[0],t=r[1],i=r[2],n=at.rgb.hsl(r)[0],s=1/255*Math.min(e,Math.min(t,i));return i=1-1/255*Math.max(e,Math.max(t,i)),[n,s*100,i*100]};at.rgb.cmyk=function(r){let e=r[0]/255,t=r[1]/255,i=r[2]/255,n=Math.min(1-e,1-t,1-i),s=(1-e-n)/(1-n)||0,o=(1-t-n)/(1-n)||0,a=(1-i-n)/(1-n)||0;return[s*100,o*100,a*100,n*100]};function qye(r,e){return(r[0]-e[0])**2+(r[1]-e[1])**2+(r[2]-e[2])**2}at.rgb.keyword=function(r){let e=gY[r];if(e)return e;let t=Infinity,i;for(let n of Object.keys(Wp)){let s=Wp[n],o=qye(r,s);o.04045?((e+.055)/1.055)**2.4:e/12.92,t=t>.04045?((t+.055)/1.055)**2.4:t/12.92,i=i>.04045?((i+.055)/1.055)**2.4:i/12.92;let n=e*.4124+t*.3576+i*.1805,s=e*.2126+t*.7152+i*.0722,o=e*.0193+t*.1192+i*.9505;return[n*100,s*100,o*100]};at.rgb.lab=function(r){let e=at.rgb.xyz(r),t=e[0],i=e[1],n=e[2];t/=95.047,i/=100,n/=108.883,t=t>.008856?t**(1/3):7.787*t+16/116,i=i>.008856?i**(1/3):7.787*i+16/116,n=n>.008856?n**(1/3):7.787*n+16/116;let s=116*i-16,o=500*(t-i),a=200*(i-n);return[s,o,a]};at.hsl.rgb=function(r){let e=r[0]/360,t=r[1]/100,i=r[2]/100,n,s,o;if(t===0)return o=i*255,[o,o,o];i<.5?n=i*(1+t):n=i+t-i*t;let a=2*i-n,l=[0,0,0];for(let c=0;c<3;c++)s=e+1/3*-(c-1),s<0&&s++,s>1&&s--,6*s<1?o=a+(n-a)*6*s:2*s<1?o=n:3*s<2?o=a+(n-a)*(2/3-s)*6:o=a,l[c]=o*255;return l};at.hsl.hsv=function(r){let e=r[0],t=r[1]/100,i=r[2]/100,n=t,s=Math.max(i,.01);i*=2,t*=i<=1?i:2-i,n*=s<=1?s:2-s;let o=(i+t)/2,a=i===0?2*n/(s+n):2*t/(i+t);return[e,a*100,o*100]};at.hsv.rgb=function(r){let e=r[0]/60,t=r[1]/100,i=r[2]/100,n=Math.floor(e)%6,s=e-Math.floor(e),o=255*i*(1-t),a=255*i*(1-t*s),l=255*i*(1-t*(1-s));switch(i*=255,n){case 0:return[i,l,o];case 1:return[a,i,o];case 2:return[o,i,l];case 3:return[o,a,i];case 4:return[l,o,i];case 5:return[i,o,a]}};at.hsv.hsl=function(r){let e=r[0],t=r[1]/100,i=r[2]/100,n=Math.max(i,.01),s,o;o=(2-t)*i;let a=(2-t)*n;return s=t*n,s/=a<=1?a:2-a,s=s||0,o/=2,[e,s*100,o*100]};at.hwb.rgb=function(r){let e=r[0]/360,t=r[1]/100,i=r[2]/100,n=t+i,s;n>1&&(t/=n,i/=n);let o=Math.floor(6*e),a=1-i;s=6*e-o,(o&1)!=0&&(s=1-s);let l=t+s*(a-t),c,u,g;switch(o){default:case 6:case 0:c=a,u=l,g=t;break;case 1:c=l,u=a,g=t;break;case 2:c=t,u=a,g=l;break;case 3:c=t,u=l,g=a;break;case 4:c=l,u=t,g=a;break;case 5:c=a,u=t,g=l;break}return[c*255,u*255,g*255]};at.cmyk.rgb=function(r){let e=r[0]/100,t=r[1]/100,i=r[2]/100,n=r[3]/100,s=1-Math.min(1,e*(1-n)+n),o=1-Math.min(1,t*(1-n)+n),a=1-Math.min(1,i*(1-n)+n);return[s*255,o*255,a*255]};at.xyz.rgb=function(r){let e=r[0]/100,t=r[1]/100,i=r[2]/100,n,s,o;return n=e*3.2406+t*-1.5372+i*-.4986,s=e*-.9689+t*1.8758+i*.0415,o=e*.0557+t*-.204+i*1.057,n=n>.0031308?1.055*n**(1/2.4)-.055:n*12.92,s=s>.0031308?1.055*s**(1/2.4)-.055:s*12.92,o=o>.0031308?1.055*o**(1/2.4)-.055:o*12.92,n=Math.min(Math.max(0,n),1),s=Math.min(Math.max(0,s),1),o=Math.min(Math.max(0,o),1),[n*255,s*255,o*255]};at.xyz.lab=function(r){let e=r[0],t=r[1],i=r[2];e/=95.047,t/=100,i/=108.883,e=e>.008856?e**(1/3):7.787*e+16/116,t=t>.008856?t**(1/3):7.787*t+16/116,i=i>.008856?i**(1/3):7.787*i+16/116;let n=116*t-16,s=500*(e-t),o=200*(t-i);return[n,s,o]};at.lab.xyz=function(r){let e=r[0],t=r[1],i=r[2],n,s,o;s=(e+16)/116,n=t/500+s,o=s-i/200;let a=s**3,l=n**3,c=o**3;return s=a>.008856?a:(s-16/116)/7.787,n=l>.008856?l:(n-16/116)/7.787,o=c>.008856?c:(o-16/116)/7.787,n*=95.047,s*=100,o*=108.883,[n,s,o]};at.lab.lch=function(r){let e=r[0],t=r[1],i=r[2],n;n=Math.atan2(i,t)*360/2/Math.PI,n<0&&(n+=360);let o=Math.sqrt(t*t+i*i);return[e,o,n]};at.lch.lab=function(r){let e=r[0],t=r[1],n=r[2]/360*2*Math.PI,s=t*Math.cos(n),o=t*Math.sin(n);return[e,s,o]};at.rgb.ansi16=function(r,e=null){let[t,i,n]=r,s=e===null?at.rgb.hsv(r)[2]:e;if(s=Math.round(s/50),s===0)return 30;let o=30+(Math.round(n/255)<<2|Math.round(i/255)<<1|Math.round(t/255));return s===2&&(o+=60),o};at.hsv.ansi16=function(r){return at.rgb.ansi16(at.hsv.rgb(r),r[2])};at.rgb.ansi256=function(r){let e=r[0],t=r[1],i=r[2];return e===t&&t===i?e<8?16:e>248?231:Math.round((e-8)/247*24)+232:16+36*Math.round(e/255*5)+6*Math.round(t/255*5)+Math.round(i/255*5)};at.ansi16.rgb=function(r){let e=r%10;if(e===0||e===7)return r>50&&(e+=3.5),e=e/10.5*255,[e,e,e];let t=(~~(r>50)+1)*.5,i=(e&1)*t*255,n=(e>>1&1)*t*255,s=(e>>2&1)*t*255;return[i,n,s]};at.ansi256.rgb=function(r){if(r>=232){let s=(r-232)*10+8;return[s,s,s]}r-=16;let e,t=Math.floor(r/36)/5*255,i=Math.floor((e=r%36)/6)/5*255,n=e%6/5*255;return[t,i,n]};at.rgb.hex=function(r){let t=(((Math.round(r[0])&255)<<16)+((Math.round(r[1])&255)<<8)+(Math.round(r[2])&255)).toString(16).toUpperCase();return"000000".substring(t.length)+t};at.hex.rgb=function(r){let e=r.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i);if(!e)return[0,0,0];let t=e[0];e[0].length===3&&(t=t.split("").map(a=>a+a).join(""));let i=parseInt(t,16),n=i>>16&255,s=i>>8&255,o=i&255;return[n,s,o]};at.rgb.hcg=function(r){let e=r[0]/255,t=r[1]/255,i=r[2]/255,n=Math.max(Math.max(e,t),i),s=Math.min(Math.min(e,t),i),o=n-s,a,l;return o<1?a=s/(1-o):a=0,o<=0?l=0:n===e?l=(t-i)/o%6:n===t?l=2+(i-e)/o:l=4+(e-t)/o,l/=6,l%=1,[l*360,o*100,a*100]};at.hsl.hcg=function(r){let e=r[1]/100,t=r[2]/100,i=t<.5?2*e*t:2*e*(1-t),n=0;return i<1&&(n=(t-.5*i)/(1-i)),[r[0],i*100,n*100]};at.hsv.hcg=function(r){let e=r[1]/100,t=r[2]/100,i=e*t,n=0;return i<1&&(n=(t-i)/(1-i)),[r[0],i*100,n*100]};at.hcg.rgb=function(r){let e=r[0]/360,t=r[1]/100,i=r[2]/100;if(t===0)return[i*255,i*255,i*255];let n=[0,0,0],s=e%1*6,o=s%1,a=1-o,l=0;switch(Math.floor(s)){case 0:n[0]=1,n[1]=o,n[2]=0;break;case 1:n[0]=a,n[1]=1,n[2]=0;break;case 2:n[0]=0,n[1]=1,n[2]=o;break;case 3:n[0]=0,n[1]=a,n[2]=1;break;case 4:n[0]=o,n[1]=0,n[2]=1;break;default:n[0]=1,n[1]=0,n[2]=a}return l=(1-t)*i,[(t*n[0]+l)*255,(t*n[1]+l)*255,(t*n[2]+l)*255]};at.hcg.hsv=function(r){let e=r[1]/100,t=r[2]/100,i=e+t*(1-e),n=0;return i>0&&(n=e/i),[r[0],n*100,i*100]};at.hcg.hsl=function(r){let e=r[1]/100,i=r[2]/100*(1-e)+.5*e,n=0;return i>0&&i<.5?n=e/(2*i):i>=.5&&i<1&&(n=e/(2*(1-i))),[r[0],n*100,i*100]};at.hcg.hwb=function(r){let e=r[1]/100,t=r[2]/100,i=e+t*(1-e);return[r[0],(i-e)*100,(1-i)*100]};at.hwb.hcg=function(r){let e=r[1]/100,t=r[2]/100,i=1-t,n=i-e,s=0;return n<1&&(s=(i-n)/(1-n)),[r[0],n*100,s*100]};at.apple.rgb=function(r){return[r[0]/65535*255,r[1]/65535*255,r[2]/65535*255]};at.rgb.apple=function(r){return[r[0]/255*65535,r[1]/255*65535,r[2]/255*65535]};at.gray.rgb=function(r){return[r[0]/100*255,r[0]/100*255,r[0]/100*255]};at.gray.hsl=function(r){return[0,0,r[0]]};at.gray.hsv=at.gray.hsl;at.gray.hwb=function(r){return[0,100,r[0]]};at.gray.cmyk=function(r){return[0,0,0,r[0]]};at.gray.lab=function(r){return[r[0],0,0]};at.gray.hex=function(r){let e=Math.round(r[0]/100*255)&255,i=((e<<16)+(e<<8)+e).toString(16).toUpperCase();return"000000".substring(i.length)+i};at.rgb.gray=function(r){return[(r[0]+r[1]+r[2])/3/255*100]}});var hY=w((net,fY)=>{var Iy=iv();function Jye(){let r={},e=Object.keys(Iy);for(let t=e.length,i=0;i{var nv=iv(),Vye=hY(),kg={},Xye=Object.keys(nv);function Zye(r){let e=function(...t){let i=t[0];return i==null?i:(i.length>1&&(t=i),r(t))};return"conversion"in r&&(e.conversion=r.conversion),e}function $ye(r){let e=function(...t){let i=t[0];if(i==null)return i;i.length>1&&(t=i);let n=r(t);if(typeof n=="object")for(let s=n.length,o=0;o{kg[r]={},Object.defineProperty(kg[r],"channels",{value:nv[r].channels}),Object.defineProperty(kg[r],"labels",{value:nv[r].labels});let e=Vye(r);Object.keys(e).forEach(i=>{let n=e[i];kg[r][i]=$ye(n),kg[r][i].raw=Zye(n)})});pY.exports=kg});var wY=w((oet,CY)=>{"use strict";var mY=(r,e)=>(...t)=>`[${r(...t)+e}m`,EY=(r,e)=>(...t)=>{let i=r(...t);return`[${38+e};5;${i}m`},IY=(r,e)=>(...t)=>{let i=r(...t);return`[${38+e};2;${i[0]};${i[1]};${i[2]}m`},yy=r=>r,yY=(r,e,t)=>[r,e,t],xg=(r,e,t)=>{Object.defineProperty(r,e,{get:()=>{let i=t();return Object.defineProperty(r,e,{value:i,enumerable:!0,configurable:!0}),i},enumerable:!0,configurable:!0})},sv,Pg=(r,e,t,i)=>{sv===void 0&&(sv=dY());let n=i?10:0,s={};for(let[o,a]of Object.entries(sv)){let l=o==="ansi16"?"ansi":o;o===e?s[l]=r(t,n):typeof a=="object"&&(s[l]=r(a[e],n))}return s};function ewe(){let r=new Map,e={modifier:{reset:[0,0],bold:[1,22],dim:[2,22],italic:[3,23],underline:[4,24],inverse:[7,27],hidden:[8,28],strikethrough:[9,29]},color:{black:[30,39],red:[31,39],green:[32,39],yellow:[33,39],blue:[34,39],magenta:[35,39],cyan:[36,39],white:[37,39],blackBright:[90,39],redBright:[91,39],greenBright:[92,39],yellowBright:[93,39],blueBright:[94,39],magentaBright:[95,39],cyanBright:[96,39],whiteBright:[97,39]},bgColor:{bgBlack:[40,49],bgRed:[41,49],bgGreen:[42,49],bgYellow:[43,49],bgBlue:[44,49],bgMagenta:[45,49],bgCyan:[46,49],bgWhite:[47,49],bgBlackBright:[100,49],bgRedBright:[101,49],bgGreenBright:[102,49],bgYellowBright:[103,49],bgBlueBright:[104,49],bgMagentaBright:[105,49],bgCyanBright:[106,49],bgWhiteBright:[107,49]}};e.color.gray=e.color.blackBright,e.bgColor.bgGray=e.bgColor.bgBlackBright,e.color.grey=e.color.blackBright,e.bgColor.bgGrey=e.bgColor.bgBlackBright;for(let[t,i]of Object.entries(e)){for(let[n,s]of Object.entries(i))e[n]={open:`[${s[0]}m`,close:`[${s[1]}m`},i[n]=e[n],r.set(s[0],s[1]);Object.defineProperty(e,t,{value:i,enumerable:!1})}return Object.defineProperty(e,"codes",{value:r,enumerable:!1}),e.color.close="",e.bgColor.close="",xg(e.color,"ansi",()=>Pg(mY,"ansi16",yy,!1)),xg(e.color,"ansi256",()=>Pg(EY,"ansi256",yy,!1)),xg(e.color,"ansi16m",()=>Pg(IY,"rgb",yY,!1)),xg(e.bgColor,"ansi",()=>Pg(mY,"ansi16",yy,!0)),xg(e.bgColor,"ansi256",()=>Pg(EY,"ansi256",yy,!0)),xg(e.bgColor,"ansi16m",()=>Pg(IY,"rgb",yY,!0)),e}Object.defineProperty(CY,"exports",{enumerable:!0,get:ewe})});var bY=w((aet,BY)=>{"use strict";BY.exports=(r,e=process.argv)=>{let t=r.startsWith("-")?"":r.length===1?"-":"--",i=e.indexOf(t+r),n=e.indexOf("--");return i!==-1&&(n===-1||i{"use strict";var twe=require("os"),SY=require("tty"),Ds=bY(),{env:gi}=process,rl;Ds("no-color")||Ds("no-colors")||Ds("color=false")||Ds("color=never")?rl=0:(Ds("color")||Ds("colors")||Ds("color=true")||Ds("color=always"))&&(rl=1);"FORCE_COLOR"in gi&&(gi.FORCE_COLOR==="true"?rl=1:gi.FORCE_COLOR==="false"?rl=0:rl=gi.FORCE_COLOR.length===0?1:Math.min(parseInt(gi.FORCE_COLOR,10),3));function ov(r){return r===0?!1:{level:r,hasBasic:!0,has256:r>=2,has16m:r>=3}}function av(r,e){if(rl===0)return 0;if(Ds("color=16m")||Ds("color=full")||Ds("color=truecolor"))return 3;if(Ds("color=256"))return 2;if(r&&!e&&rl===void 0)return 0;let t=rl||0;if(gi.TERM==="dumb")return t;if(process.platform==="win32"){let i=twe.release().split(".");return Number(i[0])>=10&&Number(i[2])>=10586?Number(i[2])>=14931?3:2:1}if("CI"in gi)return["TRAVIS","CIRCLECI","APPVEYOR","GITLAB_CI"].some(i=>i in gi)||gi.CI_NAME==="codeship"?1:t;if("TEAMCITY_VERSION"in gi)return/^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(gi.TEAMCITY_VERSION)?1:0;if("GITHUB_ACTIONS"in gi)return 1;if(gi.COLORTERM==="truecolor")return 3;if("TERM_PROGRAM"in gi){let i=parseInt((gi.TERM_PROGRAM_VERSION||"").split(".")[0],10);switch(gi.TERM_PROGRAM){case"iTerm.app":return i>=3?3:2;case"Apple_Terminal":return 2}}return/-256(color)?$/i.test(gi.TERM)?2:/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(gi.TERM)||"COLORTERM"in gi?1:t}function rwe(r){let e=av(r,r&&r.isTTY);return ov(e)}QY.exports={supportsColor:rwe,stdout:ov(av(!0,SY.isatty(1))),stderr:ov(av(!0,SY.isatty(2)))}});var xY=w((cet,kY)=>{"use strict";var iwe=(r,e,t)=>{let i=r.indexOf(e);if(i===-1)return r;let n=e.length,s=0,o="";do o+=r.substr(s,i-s)+e+t,s=i+n,i=r.indexOf(e,s);while(i!==-1);return o+=r.substr(s),o},nwe=(r,e,t,i)=>{let n=0,s="";do{let o=r[i-1]==="\r";s+=r.substr(n,(o?i-1:i)-n)+e+(o?`\r +`:` +`)+t,n=i+1,i=r.indexOf(` +`,n)}while(i!==-1);return s+=r.substr(n),s};kY.exports={stringReplaceAll:iwe,stringEncaseCRLFWithFirstIndex:nwe}});var NY=w((uet,PY)=>{"use strict";var swe=/(?:\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.))|(?:\{(~)?(\w+(?:\([^)]*\))?(?:\.\w+(?:\([^)]*\))?)*)(?:[ \t]|(?=\r?\n)))|(\})|((?:.|[\r\n\f])+?)/gi,DY=/(?:^|\.)(\w+)(?:\(([^)]*)\))?/g,owe=/^(['"])((?:\\.|(?!\1)[^\\])*)\1$/,awe=/\\(u(?:[a-f\d]{4}|\{[a-f\d]{1,6}\})|x[a-f\d]{2}|.)|([^\\])/gi,Awe=new Map([["n",` +`],["r","\r"],["t"," "],["b","\b"],["f","\f"],["v","\v"],["0","\0"],["\\","\\"],["e",""],["a","\x07"]]);function RY(r){let e=r[0]==="u",t=r[1]==="{";return e&&!t&&r.length===5||r[0]==="x"&&r.length===3?String.fromCharCode(parseInt(r.slice(1),16)):e&&t?String.fromCodePoint(parseInt(r.slice(2,-1),16)):Awe.get(r)||r}function lwe(r,e){let t=[],i=e.trim().split(/\s*,\s*/g),n;for(let s of i){let o=Number(s);if(!Number.isNaN(o))t.push(o);else if(n=s.match(owe))t.push(n[2].replace(awe,(a,l,c)=>l?RY(l):c));else throw new Error(`Invalid Chalk template style argument: ${s} (in style '${r}')`)}return t}function cwe(r){DY.lastIndex=0;let e=[],t;for(;(t=DY.exec(r))!==null;){let i=t[1];if(t[2]){let n=lwe(i,t[2]);e.push([i].concat(n))}else e.push([i])}return e}function FY(r,e){let t={};for(let n of e)for(let s of n.styles)t[s[0]]=n.inverse?null:s.slice(1);let i=r;for(let[n,s]of Object.entries(t))if(!!Array.isArray(s)){if(!(n in i))throw new Error(`Unknown Chalk style: ${n}`);i=s.length>0?i[n](...s):i[n]}return i}PY.exports=(r,e)=>{let t=[],i=[],n=[];if(e.replace(swe,(s,o,a,l,c,u)=>{if(o)n.push(RY(o));else if(l){let g=n.join("");n=[],i.push(t.length===0?g:FY(r,t)(g)),t.push({inverse:a,styles:cwe(l)})}else if(c){if(t.length===0)throw new Error("Found extraneous } in Chalk template literal");i.push(FY(r,t)(n.join(""))),n=[],t.pop()}else n.push(u)}),i.push(n.join("")),t.length>0){let s=`Chalk template literal is missing ${t.length} closing bracket${t.length===1?"":"s"} (\`}\`)`;throw new Error(s)}return i.join("")}});var gv=w((get,LY)=>{"use strict";var zp=wY(),{stdout:Av,stderr:lv}=vY(),{stringReplaceAll:uwe,stringEncaseCRLFWithFirstIndex:gwe}=xY(),TY=["ansi","ansi","ansi256","ansi16m"],Dg=Object.create(null),fwe=(r,e={})=>{if(e.level>3||e.level<0)throw new Error("The `level` option should be an integer from 0 to 3");let t=Av?Av.level:0;r.level=e.level===void 0?t:e.level},OY=class{constructor(e){return MY(e)}},MY=r=>{let e={};return fwe(e,r),e.template=(...t)=>hwe(e.template,...t),Object.setPrototypeOf(e,wy.prototype),Object.setPrototypeOf(e.template,e),e.template.constructor=()=>{throw new Error("`chalk.constructor()` is deprecated. Use `new chalk.Instance()` instead.")},e.template.Instance=OY,e.template};function wy(r){return MY(r)}for(let[r,e]of Object.entries(zp))Dg[r]={get(){let t=By(this,cv(e.open,e.close,this._styler),this._isEmpty);return Object.defineProperty(this,r,{value:t}),t}};Dg.visible={get(){let r=By(this,this._styler,!0);return Object.defineProperty(this,"visible",{value:r}),r}};var UY=["rgb","hex","keyword","hsl","hsv","hwb","ansi","ansi256"];for(let r of UY)Dg[r]={get(){let{level:e}=this;return function(...t){let i=cv(zp.color[TY[e]][r](...t),zp.color.close,this._styler);return By(this,i,this._isEmpty)}}};for(let r of UY){let e="bg"+r[0].toUpperCase()+r.slice(1);Dg[e]={get(){let{level:t}=this;return function(...i){let n=cv(zp.bgColor[TY[t]][r](...i),zp.bgColor.close,this._styler);return By(this,n,this._isEmpty)}}}}var pwe=Object.defineProperties(()=>{},te(N({},Dg),{level:{enumerable:!0,get(){return this._generator.level},set(r){this._generator.level=r}}})),cv=(r,e,t)=>{let i,n;return t===void 0?(i=r,n=e):(i=t.openAll+r,n=e+t.closeAll),{open:r,close:e,openAll:i,closeAll:n,parent:t}},By=(r,e,t)=>{let i=(...n)=>dwe(i,n.length===1?""+n[0]:n.join(" "));return i.__proto__=pwe,i._generator=r,i._styler=e,i._isEmpty=t,i},dwe=(r,e)=>{if(r.level<=0||!e)return r._isEmpty?"":e;let t=r._styler;if(t===void 0)return e;let{openAll:i,closeAll:n}=t;if(e.indexOf("")!==-1)for(;t!==void 0;)e=uwe(e,t.close,t.open),t=t.parent;let s=e.indexOf(` +`);return s!==-1&&(e=gwe(e,n,i,s)),i+e+n},uv,hwe=(r,...e)=>{let[t]=e;if(!Array.isArray(t))return e.join(" ");let i=e.slice(1),n=[t.raw[0]];for(let s=1;s{"use strict";Rs.isInteger=r=>typeof r=="number"?Number.isInteger(r):typeof r=="string"&&r.trim()!==""?Number.isInteger(Number(r)):!1;Rs.find=(r,e)=>r.nodes.find(t=>t.type===e);Rs.exceedsLimit=(r,e,t=1,i)=>i===!1||!Rs.isInteger(r)||!Rs.isInteger(e)?!1:(Number(e)-Number(r))/Number(t)>=i;Rs.escapeNode=(r,e=0,t)=>{let i=r.nodes[e];!i||(t&&i.type===t||i.type==="open"||i.type==="close")&&i.escaped!==!0&&(i.value="\\"+i.value,i.escaped=!0)};Rs.encloseBrace=r=>r.type!=="brace"?!1:r.commas>>0+r.ranges>>0==0?(r.invalid=!0,!0):!1;Rs.isInvalidBrace=r=>r.type!=="brace"?!1:r.invalid===!0||r.dollar?!0:r.commas>>0+r.ranges>>0==0||r.open!==!0||r.close!==!0?(r.invalid=!0,!0):!1;Rs.isOpenOrClose=r=>r.type==="open"||r.type==="close"?!0:r.open===!0||r.close===!0;Rs.reduce=r=>r.reduce((e,t)=>(t.type==="text"&&e.push(t.value),t.type==="range"&&(t.type="text"),e),[]);Rs.flatten=(...r)=>{let e=[],t=i=>{for(let n=0;n{"use strict";var HY=by();KY.exports=(r,e={})=>{let t=(i,n={})=>{let s=e.escapeInvalid&&HY.isInvalidBrace(n),o=i.invalid===!0&&e.escapeInvalid===!0,a="";if(i.value)return(s||o)&&HY.isOpenOrClose(i)?"\\"+i.value:i.value;if(i.value)return i.value;if(i.nodes)for(let l of i.nodes)a+=t(l);return a};return t(r)}});var GY=w((pet,jY)=>{"use strict";jY.exports=function(r){return typeof r=="number"?r-r==0:typeof r=="string"&&r.trim()!==""?Number.isFinite?Number.isFinite(+r):isFinite(+r):!1}});var ZY=w((det,YY)=>{"use strict";var qY=GY(),Fc=(r,e,t)=>{if(qY(r)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(e===void 0||r===e)return String(r);if(qY(e)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let i=N({relaxZeros:!0},t);typeof i.strictZeros=="boolean"&&(i.relaxZeros=i.strictZeros===!1);let n=String(i.relaxZeros),s=String(i.shorthand),o=String(i.capture),a=String(i.wrap),l=r+":"+e+"="+n+s+o+a;if(Fc.cache.hasOwnProperty(l))return Fc.cache[l].result;let c=Math.min(r,e),u=Math.max(r,e);if(Math.abs(c-u)===1){let m=r+"|"+e;return i.capture?`(${m})`:i.wrap===!1?m:`(?:${m})`}let g=WY(r)||WY(e),f={min:r,max:e,a:c,b:u},h=[],p=[];if(g&&(f.isPadded=g,f.maxLen=String(f.max).length),c<0){let m=u<0?Math.abs(u):1;p=JY(m,Math.abs(c),f,i),c=f.a=0}return u>=0&&(h=JY(c,u,f,i)),f.negatives=p,f.positives=h,f.result=Cwe(p,h,i),i.capture===!0?f.result=`(${f.result})`:i.wrap!==!1&&h.length+p.length>1&&(f.result=`(?:${f.result})`),Fc.cache[l]=f,f.result};function Cwe(r,e,t){let i=fv(r,e,"-",!1,t)||[],n=fv(e,r,"",!1,t)||[],s=fv(r,e,"-?",!0,t)||[];return i.concat(s).concat(n).join("|")}function Ewe(r,e){let t=1,i=1,n=zY(r,t),s=new Set([e]);for(;r<=n&&n<=e;)s.add(n),t+=1,n=zY(r,t);for(n=_Y(e+1,i)-1;r1&&a.count.pop(),a.count.push(u.count[0]),a.string=a.pattern+VY(a.count),o=c+1;continue}t.isPadded&&(g=Bwe(c,t,i)),u.string=g+u.pattern+VY(u.count),s.push(u),o=c+1,a=u}return s}function fv(r,e,t,i,n){let s=[];for(let o of r){let{string:a}=o;!i&&!XY(e,"string",a)&&s.push(t+a),i&&XY(e,"string",a)&&s.push(t+a)}return s}function Iwe(r,e){let t=[];for(let i=0;ie?1:e>r?-1:0}function XY(r,e,t){return r.some(i=>i[e]===t)}function zY(r,e){return Number(String(r).slice(0,-e)+"9".repeat(e))}function _Y(r,e){return r-r%Math.pow(10,e)}function VY(r){let[e=0,t=""]=r;return t||e>1?`{${e+(t?","+t:"")}}`:""}function ywe(r,e,t){return`[${r}${e-r==1?"":"-"}${e}]`}function WY(r){return/^-?(0+)\d/.test(r)}function Bwe(r,e,t){if(!e.isPadded)return r;let i=Math.abs(e.maxLen-String(r).length),n=t.relaxZeros!==!1;switch(i){case 0:return"";case 1:return n?"0?":"0";case 2:return n?"0{0,2}":"00";default:return n?`0{0,${i}}`:`0{${i}}`}}Fc.cache={};Fc.clearCache=()=>Fc.cache={};YY.exports=Fc});var dv=w((Cet,$Y)=>{"use strict";var bwe=require("util"),eq=ZY(),tq=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),Qwe=r=>e=>r===!0?Number(e):String(e),hv=r=>typeof r=="number"||typeof r=="string"&&r!=="",Vp=r=>Number.isInteger(+r),pv=r=>{let e=`${r}`,t=-1;if(e[0]==="-"&&(e=e.slice(1)),e==="0")return!1;for(;e[++t]==="0";);return t>0},Swe=(r,e,t)=>typeof r=="string"||typeof e=="string"?!0:t.stringify===!0,vwe=(r,e,t)=>{if(e>0){let i=r[0]==="-"?"-":"";i&&(r=r.slice(1)),r=i+r.padStart(i?e-1:e,"0")}return t===!1?String(r):r},rq=(r,e)=>{let t=r[0]==="-"?"-":"";for(t&&(r=r.slice(1),e--);r.length{r.negatives.sort((o,a)=>oa?1:0),r.positives.sort((o,a)=>oa?1:0);let t=e.capture?"":"?:",i="",n="",s;return r.positives.length&&(i=r.positives.join("|")),r.negatives.length&&(n=`-(${t}${r.negatives.join("|")})`),i&&n?s=`${i}|${n}`:s=i||n,e.wrap?`(${t}${s})`:s},iq=(r,e,t,i)=>{if(t)return eq(r,e,N({wrap:!1},i));let n=String.fromCharCode(r);if(r===e)return n;let s=String.fromCharCode(e);return`[${n}-${s}]`},nq=(r,e,t)=>{if(Array.isArray(r)){let i=t.wrap===!0,n=t.capture?"":"?:";return i?`(${n}${r.join("|")})`:r.join("|")}return eq(r,e,t)},sq=(...r)=>new RangeError("Invalid range arguments: "+bwe.inspect(...r)),oq=(r,e,t)=>{if(t.strictRanges===!0)throw sq([r,e]);return[]},xwe=(r,e)=>{if(e.strictRanges===!0)throw new TypeError(`Expected step "${r}" to be a number`);return[]},Pwe=(r,e,t=1,i={})=>{let n=Number(r),s=Number(e);if(!Number.isInteger(n)||!Number.isInteger(s)){if(i.strictRanges===!0)throw sq([r,e]);return[]}n===0&&(n=0),s===0&&(s=0);let o=n>s,a=String(r),l=String(e),c=String(t);t=Math.max(Math.abs(t),1);let u=pv(a)||pv(l)||pv(c),g=u?Math.max(a.length,l.length,c.length):0,f=u===!1&&Swe(r,e,i)===!1,h=i.transform||Qwe(f);if(i.toRegex&&t===1)return iq(rq(r,g),rq(e,g),!0,i);let p={negatives:[],positives:[]},m=v=>p[v<0?"negatives":"positives"].push(Math.abs(v)),y=[],b=0;for(;o?n>=s:n<=s;)i.toRegex===!0&&t>1?m(n):y.push(vwe(h(n,b),g,f)),n=o?n-t:n+t,b++;return i.toRegex===!0?t>1?kwe(p,i):nq(y,null,N({wrap:!1},i)):y},Dwe=(r,e,t=1,i={})=>{if(!Vp(r)&&r.length>1||!Vp(e)&&e.length>1)return oq(r,e,i);let n=i.transform||(f=>String.fromCharCode(f)),s=`${r}`.charCodeAt(0),o=`${e}`.charCodeAt(0),a=s>o,l=Math.min(s,o),c=Math.max(s,o);if(i.toRegex&&t===1)return iq(l,c,!1,i);let u=[],g=0;for(;a?s>=o:s<=o;)u.push(n(s,g)),s=a?s-t:s+t,g++;return i.toRegex===!0?nq(u,null,{wrap:!1,options:i}):u},Sy=(r,e,t,i={})=>{if(e==null&&hv(r))return[r];if(!hv(r)||!hv(e))return oq(r,e,i);if(typeof t=="function")return Sy(r,e,1,{transform:t});if(tq(t))return Sy(r,e,0,t);let n=N({},i);return n.capture===!0&&(n.wrap=!0),t=t||n.step||1,Vp(t)?Vp(r)&&Vp(e)?Pwe(r,e,t,n):Dwe(r,e,Math.max(Math.abs(t),1),n):t!=null&&!tq(t)?xwe(t,n):Sy(r,e,1,t)};$Y.exports=Sy});var lq=w((met,aq)=>{"use strict";var Rwe=dv(),Aq=by(),Fwe=(r,e={})=>{let t=(i,n={})=>{let s=Aq.isInvalidBrace(n),o=i.invalid===!0&&e.escapeInvalid===!0,a=s===!0||o===!0,l=e.escapeInvalid===!0?"\\":"",c="";if(i.isOpen===!0||i.isClose===!0)return l+i.value;if(i.type==="open")return a?l+i.value:"(";if(i.type==="close")return a?l+i.value:")";if(i.type==="comma")return i.prev.type==="comma"?"":a?i.value:"|";if(i.value)return i.value;if(i.nodes&&i.ranges>0){let u=Aq.reduce(i.nodes),g=Rwe(...u,te(N({},e),{wrap:!1,toRegex:!0}));if(g.length!==0)return u.length>1&&g.length>1?`(${g})`:g}if(i.nodes)for(let u of i.nodes)c+=t(u,i);return c};return t(r)};aq.exports=Fwe});var gq=w((Eet,cq)=>{"use strict";var Nwe=dv(),uq=Qy(),Rg=by(),Nc=(r="",e="",t=!1)=>{let i=[];if(r=[].concat(r),e=[].concat(e),!e.length)return r;if(!r.length)return t?Rg.flatten(e).map(n=>`{${n}}`):e;for(let n of r)if(Array.isArray(n))for(let s of n)i.push(Nc(s,e,t));else for(let s of e)t===!0&&typeof s=="string"&&(s=`{${s}}`),i.push(Array.isArray(s)?Nc(n,s,t):n+s);return Rg.flatten(i)},Lwe=(r,e={})=>{let t=e.rangeLimit===void 0?1e3:e.rangeLimit,i=(n,s={})=>{n.queue=[];let o=s,a=s.queue;for(;o.type!=="brace"&&o.type!=="root"&&o.parent;)o=o.parent,a=o.queue;if(n.invalid||n.dollar){a.push(Nc(a.pop(),uq(n,e)));return}if(n.type==="brace"&&n.invalid!==!0&&n.nodes.length===2){a.push(Nc(a.pop(),["{}"]));return}if(n.nodes&&n.ranges>0){let g=Rg.reduce(n.nodes);if(Rg.exceedsLimit(...g,e.step,t))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let f=Nwe(...g,e);f.length===0&&(f=uq(n,e)),a.push(Nc(a.pop(),f)),n.nodes=[];return}let l=Rg.encloseBrace(n),c=n.queue,u=n;for(;u.type!=="brace"&&u.type!=="root"&&u.parent;)u=u.parent,c=u.queue;for(let g=0;g{"use strict";fq.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` +`,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Eq=w((yet,pq)=>{"use strict";var Twe=Qy(),{MAX_LENGTH:dq,CHAR_BACKSLASH:Cv,CHAR_BACKTICK:Owe,CHAR_COMMA:Mwe,CHAR_DOT:Uwe,CHAR_LEFT_PARENTHESES:Kwe,CHAR_RIGHT_PARENTHESES:Hwe,CHAR_LEFT_CURLY_BRACE:jwe,CHAR_RIGHT_CURLY_BRACE:Gwe,CHAR_LEFT_SQUARE_BRACKET:Cq,CHAR_RIGHT_SQUARE_BRACKET:mq,CHAR_DOUBLE_QUOTE:Ywe,CHAR_SINGLE_QUOTE:qwe,CHAR_NO_BREAK_SPACE:Jwe,CHAR_ZERO_WIDTH_NOBREAK_SPACE:Wwe}=hq(),zwe=(r,e={})=>{if(typeof r!="string")throw new TypeError("Expected a string");let t=e||{},i=typeof t.maxLength=="number"?Math.min(dq,t.maxLength):dq;if(r.length>i)throw new SyntaxError(`Input length (${r.length}), exceeds max characters (${i})`);let n={type:"root",input:r,nodes:[]},s=[n],o=n,a=n,l=0,c=r.length,u=0,g=0,f,h={},p=()=>r[u++],m=y=>{if(y.type==="text"&&a.type==="dot"&&(a.type="text"),a&&a.type==="text"&&y.type==="text"){a.value+=y.value;return}return o.nodes.push(y),y.parent=o,y.prev=a,a=y,y};for(m({type:"bos"});u0){if(o.ranges>0){o.ranges=0;let y=o.nodes.shift();o.nodes=[y,{type:"text",value:Twe(o)}]}m({type:"comma",value:f}),o.commas++;continue}if(f===Uwe&&g>0&&o.commas===0){let y=o.nodes;if(g===0||y.length===0){m({type:"text",value:f});continue}if(a.type==="dot"){if(o.range=[],a.value+=f,a.type="range",o.nodes.length!==3&&o.nodes.length!==5){o.invalid=!0,o.ranges=0,a.type="text";continue}o.ranges++,o.args=[];continue}if(a.type==="range"){y.pop();let b=y[y.length-1];b.value+=a.value+f,a=b,o.ranges--;continue}m({type:"dot",value:f});continue}m({type:"text",value:f})}do if(o=s.pop(),o.type!=="root"){o.nodes.forEach(v=>{v.nodes||(v.type==="open"&&(v.isOpen=!0),v.type==="close"&&(v.isClose=!0),v.nodes||(v.type="text"),v.invalid=!0)});let y=s[s.length-1],b=y.nodes.indexOf(o);y.nodes.splice(b,1,...o.nodes)}while(s.length>0);return m({type:"eos"}),n};pq.exports=zwe});var wq=w((wet,Iq)=>{"use strict";var yq=Qy(),_we=lq(),Vwe=gq(),Xwe=Eq(),rs=(r,e={})=>{let t=[];if(Array.isArray(r))for(let i of r){let n=rs.create(i,e);Array.isArray(n)?t.push(...n):t.push(n)}else t=[].concat(rs.create(r,e));return e&&e.expand===!0&&e.nodupes===!0&&(t=[...new Set(t)]),t};rs.parse=(r,e={})=>Xwe(r,e);rs.stringify=(r,e={})=>typeof r=="string"?yq(rs.parse(r,e),e):yq(r,e);rs.compile=(r,e={})=>(typeof r=="string"&&(r=rs.parse(r,e)),_we(r,e));rs.expand=(r,e={})=>{typeof r=="string"&&(r=rs.parse(r,e));let t=Vwe(r,e);return e.noempty===!0&&(t=t.filter(Boolean)),e.nodupes===!0&&(t=[...new Set(t)]),t};rs.create=(r,e={})=>r===""||r.length<3?[r]:e.expand!==!0?rs.compile(r,e):rs.expand(r,e);Iq.exports=rs});var Xp=w((Bet,Bq)=>{"use strict";var Zwe=require("path"),zo="\\\\/",bq=`[^${zo}]`,_a="\\.",$we="\\+",eBe="\\?",vy="\\/",tBe="(?=.)",Qq="[^/]",mv=`(?:${vy}|$)`,Sq=`(?:^|${vy})`,Ev=`${_a}{1,2}${mv}`,rBe=`(?!${_a})`,iBe=`(?!${Sq}${Ev})`,nBe=`(?!${_a}{0,1}${mv})`,sBe=`(?!${Ev})`,oBe=`[^.${vy}]`,aBe=`${Qq}*?`,vq={DOT_LITERAL:_a,PLUS_LITERAL:$we,QMARK_LITERAL:eBe,SLASH_LITERAL:vy,ONE_CHAR:tBe,QMARK:Qq,END_ANCHOR:mv,DOTS_SLASH:Ev,NO_DOT:rBe,NO_DOTS:iBe,NO_DOT_SLASH:nBe,NO_DOTS_SLASH:sBe,QMARK_NO_DOT:oBe,STAR:aBe,START_ANCHOR:Sq},ABe=te(N({},vq),{SLASH_LITERAL:`[${zo}]`,QMARK:bq,STAR:`${bq}*?`,DOTS_SLASH:`${_a}{1,2}(?:[${zo}]|$)`,NO_DOT:`(?!${_a})`,NO_DOTS:`(?!(?:^|[${zo}])${_a}{1,2}(?:[${zo}]|$))`,NO_DOT_SLASH:`(?!${_a}{0,1}(?:[${zo}]|$))`,NO_DOTS_SLASH:`(?!${_a}{1,2}(?:[${zo}]|$))`,QMARK_NO_DOT:`[^.${zo}]`,START_ANCHOR:`(?:^|[${zo}])`,END_ANCHOR:`(?:[${zo}]|$)`}),lBe={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};Bq.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:lBe,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:Zwe.sep,extglobChars(r){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${r.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(r){return r===!0?ABe:vq}}});var Zp=w(kn=>{"use strict";var cBe=require("path"),uBe=process.platform==="win32",{REGEX_BACKSLASH:gBe,REGEX_REMOVE_BACKSLASH:fBe,REGEX_SPECIAL_CHARS:hBe,REGEX_SPECIAL_CHARS_GLOBAL:pBe}=Xp();kn.isObject=r=>r!==null&&typeof r=="object"&&!Array.isArray(r);kn.hasRegexChars=r=>hBe.test(r);kn.isRegexChar=r=>r.length===1&&kn.hasRegexChars(r);kn.escapeRegex=r=>r.replace(pBe,"\\$1");kn.toPosixSlashes=r=>r.replace(gBe,"/");kn.removeBackslashes=r=>r.replace(fBe,e=>e==="\\"?"":e);kn.supportsLookbehinds=()=>{let r=process.version.slice(1).split(".").map(Number);return r.length===3&&r[0]>=9||r[0]===8&&r[1]>=10};kn.isWindows=r=>r&&typeof r.windows=="boolean"?r.windows:uBe===!0||cBe.sep==="\\";kn.escapeLast=(r,e,t)=>{let i=r.lastIndexOf(e,t);return i===-1?r:r[i-1]==="\\"?kn.escapeLast(r,e,i-1):`${r.slice(0,i)}\\${r.slice(i)}`};kn.removePrefix=(r,e={})=>{let t=r;return t.startsWith("./")&&(t=t.slice(2),e.prefix="./"),t};kn.wrapOutput=(r,e={},t={})=>{let i=t.contains?"":"^",n=t.contains?"":"$",s=`${i}(?:${r})${n}`;return e.negated===!0&&(s=`(?:^(?!${s}).*$)`),s}});var Lq=w((Qet,kq)=>{"use strict";var xq=Zp(),{CHAR_ASTERISK:Iv,CHAR_AT:dBe,CHAR_BACKWARD_SLASH:$p,CHAR_COMMA:CBe,CHAR_DOT:yv,CHAR_EXCLAMATION_MARK:wv,CHAR_FORWARD_SLASH:Pq,CHAR_LEFT_CURLY_BRACE:Bv,CHAR_LEFT_PARENTHESES:bv,CHAR_LEFT_SQUARE_BRACKET:mBe,CHAR_PLUS:EBe,CHAR_QUESTION_MARK:Dq,CHAR_RIGHT_CURLY_BRACE:IBe,CHAR_RIGHT_PARENTHESES:Rq,CHAR_RIGHT_SQUARE_BRACKET:yBe}=Xp(),Fq=r=>r===Pq||r===$p,Nq=r=>{r.isPrefix!==!0&&(r.depth=r.isGlobstar?Infinity:1)},wBe=(r,e)=>{let t=e||{},i=r.length-1,n=t.parts===!0||t.scanToEnd===!0,s=[],o=[],a=[],l=r,c=-1,u=0,g=0,f=!1,h=!1,p=!1,m=!1,y=!1,b=!1,v=!1,k=!1,T=!1,Y=!1,q=0,$,z,ne={value:"",depth:0,isGlob:!1},ee=()=>c>=i,A=()=>l.charCodeAt(c+1),oe=()=>($=z,l.charCodeAt(++c));for(;c0&&(Z=l.slice(0,u),l=l.slice(u),g-=u),ce&&p===!0&&g>0?(ce=l.slice(0,g),O=l.slice(g)):p===!0?(ce="",O=l):ce=l,ce&&ce!==""&&ce!=="/"&&ce!==l&&Fq(ce.charCodeAt(ce.length-1))&&(ce=ce.slice(0,-1)),t.unescape===!0&&(O&&(O=xq.removeBackslashes(O)),ce&&v===!0&&(ce=xq.removeBackslashes(ce)));let L={prefix:Z,input:r,start:u,base:ce,glob:O,isBrace:f,isBracket:h,isGlob:p,isExtglob:m,isGlobstar:y,negated:k,negatedExtglob:T};if(t.tokens===!0&&(L.maxDepth=0,Fq(z)||o.push(ne),L.tokens=o),t.parts===!0||t.tokens===!0){let de;for(let Be=0;Be{"use strict";var ky=Xp(),is=Zp(),{MAX_LENGTH:xy,POSIX_REGEX_SOURCE:BBe,REGEX_NON_SPECIAL_CHARS:bBe,REGEX_SPECIAL_CHARS_BACKREF:QBe,REPLACEMENTS:Oq}=ky,SBe=(r,e)=>{if(typeof e.expandRange=="function")return e.expandRange(...r,e);r.sort();let t=`[${r.join("-")}]`;try{new RegExp(t)}catch(i){return r.map(n=>is.escapeRegex(n)).join("..")}return t},Fg=(r,e)=>`Missing ${r}: "${e}" - use "\\\\${e}" to match literal characters`,Mq=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");r=Oq[r]||r;let t=N({},e),i=typeof t.maxLength=="number"?Math.min(xy,t.maxLength):xy,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);let s={type:"bos",value:"",output:t.prepend||""},o=[s],a=t.capture?"":"?:",l=is.isWindows(e),c=ky.globChars(l),u=ky.extglobChars(c),{DOT_LITERAL:g,PLUS_LITERAL:f,SLASH_LITERAL:h,ONE_CHAR:p,DOTS_SLASH:m,NO_DOT:y,NO_DOT_SLASH:b,NO_DOTS_SLASH:v,QMARK:k,QMARK_NO_DOT:T,STAR:Y,START_ANCHOR:q}=c,$=V=>`(${a}(?:(?!${q}${V.dot?m:g}).)*?)`,z=t.dot?"":y,ne=t.dot?k:T,ee=t.bash===!0?$(t):Y;t.capture&&(ee=`(${ee})`),typeof t.noext=="boolean"&&(t.noextglob=t.noext);let A={input:r,index:-1,start:0,dot:t.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:o};r=is.removePrefix(r,A),n=r.length;let oe=[],ce=[],Z=[],O=s,L,de=()=>A.index===n-1,Be=A.peek=(V=1)=>r[A.index+V],Ge=A.advance=()=>r[++A.index]||"",re=()=>r.slice(A.index+1),se=(V="",Qe=0)=>{A.consumed+=V,A.index+=Qe},be=V=>{A.output+=V.output!=null?V.output:V.value,se(V.value)},he=()=>{let V=1;for(;Be()==="!"&&(Be(2)!=="("||Be(3)==="?");)Ge(),A.start++,V++;return V%2==0?!1:(A.negated=!0,A.start++,!0)},Fe=V=>{A[V]++,Z.push(V)},Ue=V=>{A[V]--,Z.pop()},xe=V=>{if(O.type==="globstar"){let Qe=A.braces>0&&(V.type==="comma"||V.type==="brace"),le=V.extglob===!0||oe.length&&(V.type==="pipe"||V.type==="paren");V.type!=="slash"&&V.type!=="paren"&&!Qe&&!le&&(A.output=A.output.slice(0,-O.output.length),O.type="star",O.value="*",O.output=ee,A.output+=O.output)}if(oe.length&&V.type!=="paren"&&(oe[oe.length-1].inner+=V.value),(V.value||V.output)&&be(V),O&&O.type==="text"&&V.type==="text"){O.value+=V.value,O.output=(O.output||"")+V.value;return}V.prev=O,o.push(V),O=V},ve=(V,Qe)=>{let le=te(N({},u[Qe]),{conditions:1,inner:""});le.prev=O,le.parens=A.parens,le.output=A.output;let fe=(t.capture?"(":"")+le.open;Fe("parens"),xe({type:V,value:Qe,output:A.output?"":p}),xe({type:"paren",extglob:!0,value:Ge(),output:fe}),oe.push(le)},pe=V=>{let Qe=V.close+(t.capture?")":""),le;if(V.type==="negate"){let fe=ee;V.inner&&V.inner.length>1&&V.inner.includes("/")&&(fe=$(t)),(fe!==ee||de()||/^\)+$/.test(re()))&&(Qe=V.close=`)$))${fe}`),V.inner.includes("*")&&(le=re())&&/^\.[^\\/.]+$/.test(le)&&(Qe=V.close=`)${le})${fe})`),V.prev.type==="bos"&&(A.negatedExtglob=!0)}xe({type:"paren",extglob:!0,value:L,output:Qe}),Ue("parens")};if(t.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(r)){let V=!1,Qe=r.replace(QBe,(le,fe,gt,Ht,Mt,Ei)=>Ht==="\\"?(V=!0,le):Ht==="?"?fe?fe+Ht+(Mt?k.repeat(Mt.length):""):Ei===0?ne+(Mt?k.repeat(Mt.length):""):k.repeat(gt.length):Ht==="."?g.repeat(gt.length):Ht==="*"?fe?fe+Ht+(Mt?ee:""):ee:fe?le:`\\${le}`);return V===!0&&(t.unescape===!0?Qe=Qe.replace(/\\/g,""):Qe=Qe.replace(/\\+/g,le=>le.length%2==0?"\\\\":le?"\\":"")),Qe===r&&t.contains===!0?(A.output=r,A):(A.output=is.wrapOutput(Qe,A,e),A)}for(;!de();){if(L=Ge(),L==="\0")continue;if(L==="\\"){let le=Be();if(le==="/"&&t.bash!==!0||le==="."||le===";")continue;if(!le){L+="\\",xe({type:"text",value:L});continue}let fe=/^\\+/.exec(re()),gt=0;if(fe&&fe[0].length>2&&(gt=fe[0].length,A.index+=gt,gt%2!=0&&(L+="\\")),t.unescape===!0?L=Ge():L+=Ge(),A.brackets===0){xe({type:"text",value:L});continue}}if(A.brackets>0&&(L!=="]"||O.value==="["||O.value==="[^")){if(t.posix!==!1&&L===":"){let le=O.value.slice(1);if(le.includes("[")&&(O.posix=!0,le.includes(":"))){let fe=O.value.lastIndexOf("["),gt=O.value.slice(0,fe),Ht=O.value.slice(fe+2),Mt=BBe[Ht];if(Mt){O.value=gt+Mt,A.backtrack=!0,Ge(),!s.output&&o.indexOf(O)===1&&(s.output=p);continue}}}(L==="["&&Be()!==":"||L==="-"&&Be()==="]")&&(L=`\\${L}`),L==="]"&&(O.value==="["||O.value==="[^")&&(L=`\\${L}`),t.posix===!0&&L==="!"&&O.value==="["&&(L="^"),O.value+=L,be({value:L});continue}if(A.quotes===1&&L!=='"'){L=is.escapeRegex(L),O.value+=L,be({value:L});continue}if(L==='"'){A.quotes=A.quotes===1?0:1,t.keepQuotes===!0&&xe({type:"text",value:L});continue}if(L==="("){Fe("parens"),xe({type:"paren",value:L});continue}if(L===")"){if(A.parens===0&&t.strictBrackets===!0)throw new SyntaxError(Fg("opening","("));let le=oe[oe.length-1];if(le&&A.parens===le.parens+1){pe(oe.pop());continue}xe({type:"paren",value:L,output:A.parens?")":"\\)"}),Ue("parens");continue}if(L==="["){if(t.nobracket===!0||!re().includes("]")){if(t.nobracket!==!0&&t.strictBrackets===!0)throw new SyntaxError(Fg("closing","]"));L=`\\${L}`}else Fe("brackets");xe({type:"bracket",value:L});continue}if(L==="]"){if(t.nobracket===!0||O&&O.type==="bracket"&&O.value.length===1){xe({type:"text",value:L,output:`\\${L}`});continue}if(A.brackets===0){if(t.strictBrackets===!0)throw new SyntaxError(Fg("opening","["));xe({type:"text",value:L,output:`\\${L}`});continue}Ue("brackets");let le=O.value.slice(1);if(O.posix!==!0&&le[0]==="^"&&!le.includes("/")&&(L=`/${L}`),O.value+=L,be({value:L}),t.literalBrackets===!1||is.hasRegexChars(le))continue;let fe=is.escapeRegex(O.value);if(A.output=A.output.slice(0,-O.value.length),t.literalBrackets===!0){A.output+=fe,O.value=fe;continue}O.value=`(${a}${fe}|${O.value})`,A.output+=O.value;continue}if(L==="{"&&t.nobrace!==!0){Fe("braces");let le={type:"brace",value:L,output:"(",outputIndex:A.output.length,tokensIndex:A.tokens.length};ce.push(le),xe(le);continue}if(L==="}"){let le=ce[ce.length-1];if(t.nobrace===!0||!le){xe({type:"text",value:L,output:L});continue}let fe=")";if(le.dots===!0){let gt=o.slice(),Ht=[];for(let Mt=gt.length-1;Mt>=0&&(o.pop(),gt[Mt].type!=="brace");Mt--)gt[Mt].type!=="dots"&&Ht.unshift(gt[Mt].value);fe=SBe(Ht,t),A.backtrack=!0}if(le.comma!==!0&&le.dots!==!0){let gt=A.output.slice(0,le.outputIndex),Ht=A.tokens.slice(le.tokensIndex);le.value=le.output="\\{",L=fe="\\}",A.output=gt;for(let Mt of Ht)A.output+=Mt.output||Mt.value}xe({type:"brace",value:L,output:fe}),Ue("braces"),ce.pop();continue}if(L==="|"){oe.length>0&&oe[oe.length-1].conditions++,xe({type:"text",value:L});continue}if(L===","){let le=L,fe=ce[ce.length-1];fe&&Z[Z.length-1]==="braces"&&(fe.comma=!0,le="|"),xe({type:"comma",value:L,output:le});continue}if(L==="/"){if(O.type==="dot"&&A.index===A.start+1){A.start=A.index+1,A.consumed="",A.output="",o.pop(),O=s;continue}xe({type:"slash",value:L,output:h});continue}if(L==="."){if(A.braces>0&&O.type==="dot"){O.value==="."&&(O.output=g);let le=ce[ce.length-1];O.type="dots",O.output+=L,O.value+=L,le.dots=!0;continue}if(A.braces+A.parens===0&&O.type!=="bos"&&O.type!=="slash"){xe({type:"text",value:L,output:g});continue}xe({type:"dot",value:L,output:g});continue}if(L==="?"){if(!(O&&O.value==="(")&&t.noextglob!==!0&&Be()==="("&&Be(2)!=="?"){ve("qmark",L);continue}if(O&&O.type==="paren"){let fe=Be(),gt=L;if(fe==="<"&&!is.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(O.value==="("&&!/[!=<:]/.test(fe)||fe==="<"&&!/<([!=]|\w+>)/.test(re()))&&(gt=`\\${L}`),xe({type:"text",value:L,output:gt});continue}if(t.dot!==!0&&(O.type==="slash"||O.type==="bos")){xe({type:"qmark",value:L,output:T});continue}xe({type:"qmark",value:L,output:k});continue}if(L==="!"){if(t.noextglob!==!0&&Be()==="("&&(Be(2)!=="?"||!/[!=<:]/.test(Be(3)))){ve("negate",L);continue}if(t.nonegate!==!0&&A.index===0){he();continue}}if(L==="+"){if(t.noextglob!==!0&&Be()==="("&&Be(2)!=="?"){ve("plus",L);continue}if(O&&O.value==="("||t.regex===!1){xe({type:"plus",value:L,output:f});continue}if(O&&(O.type==="bracket"||O.type==="paren"||O.type==="brace")||A.parens>0){xe({type:"plus",value:L});continue}xe({type:"plus",value:f});continue}if(L==="@"){if(t.noextglob!==!0&&Be()==="("&&Be(2)!=="?"){xe({type:"at",extglob:!0,value:L,output:""});continue}xe({type:"text",value:L});continue}if(L!=="*"){(L==="$"||L==="^")&&(L=`\\${L}`);let le=bBe.exec(re());le&&(L+=le[0],A.index+=le[0].length),xe({type:"text",value:L});continue}if(O&&(O.type==="globstar"||O.star===!0)){O.type="star",O.star=!0,O.value+=L,O.output=ee,A.backtrack=!0,A.globstar=!0,se(L);continue}let V=re();if(t.noextglob!==!0&&/^\([^?]/.test(V)){ve("star",L);continue}if(O.type==="star"){if(t.noglobstar===!0){se(L);continue}let le=O.prev,fe=le.prev,gt=le.type==="slash"||le.type==="bos",Ht=fe&&(fe.type==="star"||fe.type==="globstar");if(t.bash===!0&&(!gt||V[0]&&V[0]!=="/")){xe({type:"star",value:L,output:""});continue}let Mt=A.braces>0&&(le.type==="comma"||le.type==="brace"),Ei=oe.length&&(le.type==="pipe"||le.type==="paren");if(!gt&&le.type!=="paren"&&!Mt&&!Ei){xe({type:"star",value:L,output:""});continue}for(;V.slice(0,3)==="/**";){let jt=r[A.index+4];if(jt&&jt!=="/")break;V=V.slice(3),se("/**",3)}if(le.type==="bos"&&de()){O.type="globstar",O.value+=L,O.output=$(t),A.output=O.output,A.globstar=!0,se(L);continue}if(le.type==="slash"&&le.prev.type!=="bos"&&!Ht&&de()){A.output=A.output.slice(0,-(le.output+O.output).length),le.output=`(?:${le.output}`,O.type="globstar",O.output=$(t)+(t.strictSlashes?")":"|$)"),O.value+=L,A.globstar=!0,A.output+=le.output+O.output,se(L);continue}if(le.type==="slash"&&le.prev.type!=="bos"&&V[0]==="/"){let jt=V[1]!==void 0?"|$":"";A.output=A.output.slice(0,-(le.output+O.output).length),le.output=`(?:${le.output}`,O.type="globstar",O.output=`${$(t)}${h}|${h}${jt})`,O.value+=L,A.output+=le.output+O.output,A.globstar=!0,se(L+Ge()),xe({type:"slash",value:"/",output:""});continue}if(le.type==="bos"&&V[0]==="/"){O.type="globstar",O.value+=L,O.output=`(?:^|${h}|${$(t)}${h})`,A.output=O.output,A.globstar=!0,se(L+Ge()),xe({type:"slash",value:"/",output:""});continue}A.output=A.output.slice(0,-O.output.length),O.type="globstar",O.output=$(t),O.value+=L,A.output+=O.output,A.globstar=!0,se(L);continue}let Qe={type:"star",value:L,output:ee};if(t.bash===!0){Qe.output=".*?",(O.type==="bos"||O.type==="slash")&&(Qe.output=z+Qe.output),xe(Qe);continue}if(O&&(O.type==="bracket"||O.type==="paren")&&t.regex===!0){Qe.output=L,xe(Qe);continue}(A.index===A.start||O.type==="slash"||O.type==="dot")&&(O.type==="dot"?(A.output+=b,O.output+=b):t.dot===!0?(A.output+=v,O.output+=v):(A.output+=z,O.output+=z),Be()!=="*"&&(A.output+=p,O.output+=p)),xe(Qe)}for(;A.brackets>0;){if(t.strictBrackets===!0)throw new SyntaxError(Fg("closing","]"));A.output=is.escapeLast(A.output,"["),Ue("brackets")}for(;A.parens>0;){if(t.strictBrackets===!0)throw new SyntaxError(Fg("closing",")"));A.output=is.escapeLast(A.output,"("),Ue("parens")}for(;A.braces>0;){if(t.strictBrackets===!0)throw new SyntaxError(Fg("closing","}"));A.output=is.escapeLast(A.output,"{"),Ue("braces")}if(t.strictSlashes!==!0&&(O.type==="star"||O.type==="bracket")&&xe({type:"maybe_slash",value:"",output:`${h}?`}),A.backtrack===!0){A.output="";for(let V of A.tokens)A.output+=V.output!=null?V.output:V.value,V.suffix&&(A.output+=V.suffix)}return A};Mq.fastpaths=(r,e)=>{let t=N({},e),i=typeof t.maxLength=="number"?Math.min(xy,t.maxLength):xy,n=r.length;if(n>i)throw new SyntaxError(`Input length: ${n}, exceeds maximum allowed length: ${i}`);r=Oq[r]||r;let s=is.isWindows(e),{DOT_LITERAL:o,SLASH_LITERAL:a,ONE_CHAR:l,DOTS_SLASH:c,NO_DOT:u,NO_DOTS:g,NO_DOTS_SLASH:f,STAR:h,START_ANCHOR:p}=ky.globChars(s),m=t.dot?g:u,y=t.dot?f:u,b=t.capture?"":"?:",v={negated:!1,prefix:""},k=t.bash===!0?".*?":h;t.capture&&(k=`(${k})`);let T=z=>z.noglobstar===!0?k:`(${b}(?:(?!${p}${z.dot?c:o}).)*?)`,Y=z=>{switch(z){case"*":return`${m}${l}${k}`;case".*":return`${o}${l}${k}`;case"*.*":return`${m}${k}${o}${l}${k}`;case"*/*":return`${m}${k}${a}${l}${y}${k}`;case"**":return m+T(t);case"**/*":return`(?:${m}${T(t)}${a})?${y}${l}${k}`;case"**/*.*":return`(?:${m}${T(t)}${a})?${y}${k}${o}${l}${k}`;case"**/.*":return`(?:${m}${T(t)}${a})?${o}${l}${k}`;default:{let ne=/^(.*?)\.(\w+)$/.exec(z);if(!ne)return;let ee=Y(ne[1]);return ee?ee+o+ne[2]:void 0}}},q=is.removePrefix(r,v),$=Y(q);return $&&t.strictSlashes!==!0&&($+=`${a}?`),$};Tq.exports=Mq});var Hq=w((ket,Kq)=>{"use strict";var vBe=require("path"),kBe=Lq(),Qv=Uq(),Sv=Zp(),xBe=Xp(),PBe=r=>r&&typeof r=="object"&&!Array.isArray(r),_r=(r,e,t=!1)=>{if(Array.isArray(r)){let u=r.map(f=>_r(f,e,t));return f=>{for(let h of u){let p=h(f);if(p)return p}return!1}}let i=PBe(r)&&r.tokens&&r.input;if(r===""||typeof r!="string"&&!i)throw new TypeError("Expected pattern to be a non-empty string");let n=e||{},s=Sv.isWindows(e),o=i?_r.compileRe(r,e):_r.makeRe(r,e,!1,!0),a=o.state;delete o.state;let l=()=>!1;if(n.ignore){let u=te(N({},e),{ignore:null,onMatch:null,onResult:null});l=_r(n.ignore,u,t)}let c=(u,g=!1)=>{let{isMatch:f,match:h,output:p}=_r.test(u,o,e,{glob:r,posix:s}),m={glob:r,state:a,regex:o,posix:s,input:u,output:p,match:h,isMatch:f};return typeof n.onResult=="function"&&n.onResult(m),f===!1?(m.isMatch=!1,g?m:!1):l(u)?(typeof n.onIgnore=="function"&&n.onIgnore(m),m.isMatch=!1,g?m:!1):(typeof n.onMatch=="function"&&n.onMatch(m),g?m:!0)};return t&&(c.state=a),c};_r.test=(r,e,t,{glob:i,posix:n}={})=>{if(typeof r!="string")throw new TypeError("Expected input to be a string");if(r==="")return{isMatch:!1,output:""};let s=t||{},o=s.format||(n?Sv.toPosixSlashes:null),a=r===i,l=a&&o?o(r):r;return a===!1&&(l=o?o(r):r,a=l===i),(a===!1||s.capture===!0)&&(s.matchBase===!0||s.basename===!0?a=_r.matchBase(r,e,t,n):a=e.exec(l)),{isMatch:Boolean(a),match:a,output:l}};_r.matchBase=(r,e,t,i=Sv.isWindows(t))=>(e instanceof RegExp?e:_r.makeRe(e,t)).test(vBe.basename(r));_r.isMatch=(r,e,t)=>_r(e,t)(r);_r.parse=(r,e)=>Array.isArray(r)?r.map(t=>_r.parse(t,e)):Qv(r,te(N({},e),{fastpaths:!1}));_r.scan=(r,e)=>kBe(r,e);_r.compileRe=(r,e,t=!1,i=!1)=>{if(t===!0)return r.output;let n=e||{},s=n.contains?"":"^",o=n.contains?"":"$",a=`${s}(?:${r.output})${o}`;r&&r.negated===!0&&(a=`^(?!${a}).*$`);let l=_r.toRegex(a,e);return i===!0&&(l.state=r),l};_r.makeRe=(r,e={},t=!1,i=!1)=>{if(!r||typeof r!="string")throw new TypeError("Expected a non-empty string");let n={negated:!1,fastpaths:!0};return e.fastpaths!==!1&&(r[0]==="."||r[0]==="*")&&(n.output=Qv.fastpaths(r,e)),n.output||(n=Qv(r,e)),_r.compileRe(n,e,t,i)};_r.toRegex=(r,e)=>{try{let t=e||{};return new RegExp(r,t.flags||(t.nocase?"i":""))}catch(t){if(e&&e.debug===!0)throw t;return/$^/}};_r.constants=xBe;Kq.exports=_r});var vv=w((xet,jq)=>{"use strict";jq.exports=Hq()});var ns=w((Pet,Gq)=>{"use strict";var Yq=require("util"),qq=wq(),_o=vv(),kv=Zp(),Jq=r=>r===""||r==="./",Pr=(r,e,t)=>{e=[].concat(e),r=[].concat(r);let i=new Set,n=new Set,s=new Set,o=0,a=u=>{s.add(u.output),t&&t.onResult&&t.onResult(u)};for(let u=0;u!i.has(u));if(t&&c.length===0){if(t.failglob===!0)throw new Error(`No matches found for "${e.join(", ")}"`);if(t.nonull===!0||t.nullglob===!0)return t.unescape?e.map(u=>u.replace(/\\/g,"")):e}return c};Pr.match=Pr;Pr.matcher=(r,e)=>_o(r,e);Pr.isMatch=(r,e,t)=>_o(e,t)(r);Pr.any=Pr.isMatch;Pr.not=(r,e,t={})=>{e=[].concat(e).map(String);let i=new Set,n=[],s=a=>{t.onResult&&t.onResult(a),n.push(a.output)},o=Pr(r,e,te(N({},t),{onResult:s}));for(let a of n)o.includes(a)||i.add(a);return[...i]};Pr.contains=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${Yq.inspect(r)}"`);if(Array.isArray(e))return e.some(i=>Pr.contains(r,i,t));if(typeof e=="string"){if(Jq(r)||Jq(e))return!1;if(r.includes(e)||r.startsWith("./")&&r.slice(2).includes(e))return!0}return Pr.isMatch(r,e,te(N({},t),{contains:!0}))};Pr.matchKeys=(r,e,t)=>{if(!kv.isObject(r))throw new TypeError("Expected the first argument to be an object");let i=Pr(Object.keys(r),e,t),n={};for(let s of i)n[s]=r[s];return n};Pr.some=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=_o(String(n),t);if(i.some(o=>s(o)))return!0}return!1};Pr.every=(r,e,t)=>{let i=[].concat(r);for(let n of[].concat(e)){let s=_o(String(n),t);if(!i.every(o=>s(o)))return!1}return!0};Pr.all=(r,e,t)=>{if(typeof r!="string")throw new TypeError(`Expected a string: "${Yq.inspect(r)}"`);return[].concat(e).every(i=>_o(i,t)(r))};Pr.capture=(r,e,t)=>{let i=kv.isWindows(t),s=_o.makeRe(String(r),te(N({},t),{capture:!0})).exec(i?kv.toPosixSlashes(e):e);if(s)return s.slice(1).map(o=>o===void 0?"":o)};Pr.makeRe=(...r)=>_o.makeRe(...r);Pr.scan=(...r)=>_o.scan(...r);Pr.parse=(r,e)=>{let t=[];for(let i of[].concat(r||[]))for(let n of qq(String(i),e))t.push(_o.parse(n,e));return t};Pr.braces=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return e&&e.nobrace===!0||!/\{.*\}/.test(r)?[r]:qq(r,e)};Pr.braceExpand=(r,e)=>{if(typeof r!="string")throw new TypeError("Expected a string");return Pr.braces(r,te(N({},e),{expand:!0}))};Gq.exports=Pr});var zq=w((Det,Wq)=>{"use strict";Wq.exports=({onlyFirst:r=!1}={})=>{let e=["[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)","(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))"].join("|");return new RegExp(e,r?void 0:"g")}});var Vq=w((Ret,_q)=>{"use strict";var DBe=zq();_q.exports=r=>typeof r=="string"?r.replace(DBe(),""):r});var gJ=w((Vet,uJ)=>{"use strict";uJ.exports=(...r)=>[...new Set([].concat(...r))]});var Gv=w((Xet,fJ)=>{"use strict";var GBe=require("stream"),hJ=GBe.PassThrough,YBe=Array.prototype.slice;fJ.exports=qBe;function qBe(){let r=[],e=!1,t=YBe.call(arguments),i=t[t.length-1];i&&!Array.isArray(i)&&i.pipe==null?t.pop():i={};let n=i.end!==!1;i.objectMode==null&&(i.objectMode=!0),i.highWaterMark==null&&(i.highWaterMark=64*1024);let s=hJ(i);function o(){for(let c=0,u=arguments.length;c0||(e=!1,a())}function f(h){function p(){h.removeListener("merge2UnpipeEnd",p),h.removeListener("end",p),g()}if(h._readableState.endEmitted)return g();h.on("merge2UnpipeEnd",p),h.on("end",p),h.pipe(s,{end:!1}),h.resume()}for(let h=0;h{"use strict";Object.defineProperty(Ny,"__esModule",{value:!0});function JBe(r){return r.reduce((e,t)=>[].concat(e,t),[])}Ny.flatten=JBe;function WBe(r,e){let t=[[]],i=0;for(let n of r)e(n)?(i++,t[i]=[]):t[i].push(n);return t}Ny.splitWhen=WBe});var CJ=w(Yv=>{"use strict";Object.defineProperty(Yv,"__esModule",{value:!0});function zBe(r){return r.code==="ENOENT"}Yv.isEnoentCodeError=zBe});var EJ=w(qv=>{"use strict";Object.defineProperty(qv,"__esModule",{value:!0});var mJ=class{constructor(e,t){this.name=e,this.isBlockDevice=t.isBlockDevice.bind(t),this.isCharacterDevice=t.isCharacterDevice.bind(t),this.isDirectory=t.isDirectory.bind(t),this.isFIFO=t.isFIFO.bind(t),this.isFile=t.isFile.bind(t),this.isSocket=t.isSocket.bind(t),this.isSymbolicLink=t.isSymbolicLink.bind(t)}};function _Be(r,e){return new mJ(r,e)}qv.createDirentFromStats=_Be});var IJ=w(Kg=>{"use strict";Object.defineProperty(Kg,"__esModule",{value:!0});var VBe=require("path"),XBe=2,ZBe=/(\\?)([()*?[\]{|}]|^!|[!+@](?=\())/g;function $Be(r){return r.replace(/\\/g,"/")}Kg.unixify=$Be;function e0e(r,e){return VBe.resolve(r,e)}Kg.makeAbsolute=e0e;function t0e(r){return r.replace(ZBe,"\\$2")}Kg.escape=t0e;function r0e(r){if(r.charAt(0)==="."){let e=r.charAt(1);if(e==="/"||e==="\\")return r.slice(XBe)}return r}Kg.removeLeadingDotSegment=r0e});var wJ=w((rtt,yJ)=>{yJ.exports=function(e){if(typeof e!="string"||e==="")return!1;for(var t;t=/(\\).|([@?!+*]\(.*\))/g.exec(e);){if(t[2])return!0;e=e.slice(t.index+t[0].length)}return!1}});var QJ=w((itt,BJ)=>{var i0e=wJ(),bJ={"{":"}","(":")","[":"]"},n0e=function(r){if(r[0]==="!")return!0;for(var e=0,t=-2,i=-2,n=-2,s=-2,o=-2;ee&&(o===-1||o>i||(o=r.indexOf("\\",e),o===-1||o>i)))||n!==-1&&r[e]==="{"&&r[e+1]!=="}"&&(n=r.indexOf("}",e),n>e&&(o=r.indexOf("\\",e),o===-1||o>n))||s!==-1&&r[e]==="("&&r[e+1]==="?"&&/[:!=]/.test(r[e+2])&&r[e+3]!==")"&&(s=r.indexOf(")",e),s>e&&(o=r.indexOf("\\",e),o===-1||o>s))||t!==-1&&r[e]==="("&&r[e+1]!=="|"&&(tt&&(o=r.indexOf("\\",t),o===-1||o>s))))return!0;if(r[e]==="\\"){var a=r[e+1];e+=2;var l=bJ[a];if(l){var c=r.indexOf(l,e);c!==-1&&(e=c+1)}if(r[e]==="!")return!0}else e++}return!1},s0e=function(r){if(r[0]==="!")return!0;for(var e=0;e{"use strict";var o0e=QJ(),a0e=require("path").posix.dirname,A0e=require("os").platform()==="win32",Jv="/",l0e=/\\/g,c0e=/[\{\[].*[\}\]]$/,u0e=/(^|[^\\])([\{\[]|\([^\)]+$)/,g0e=/\\([\!\*\?\|\[\]\(\)\{\}])/g;SJ.exports=function(e,t){var i=Object.assign({flipBackslashes:!0},t);i.flipBackslashes&&A0e&&e.indexOf(Jv)<0&&(e=e.replace(l0e,Jv)),c0e.test(e)&&(e+=Jv),e+="a";do e=a0e(e);while(o0e(e)||u0e.test(e));return e.replace(g0e,"$1")}});var TJ=w(si=>{"use strict";Object.defineProperty(si,"__esModule",{value:!0});var f0e=require("path"),h0e=vJ(),kJ=ns(),p0e=vv(),xJ="**",d0e="\\",C0e=/[*?]|^!/,m0e=/\[.*]/,E0e=/(?:^|[^!*+?@])\(.*\|.*\)/,I0e=/[!*+?@]\(.*\)/,y0e=/{.*(?:,|\.\.).*}/;function DJ(r,e={}){return!PJ(r,e)}si.isStaticPattern=DJ;function PJ(r,e={}){return!!(e.caseSensitiveMatch===!1||r.includes(d0e)||C0e.test(r)||m0e.test(r)||E0e.test(r)||e.extglob!==!1&&I0e.test(r)||e.braceExpansion!==!1&&y0e.test(r))}si.isDynamicPattern=PJ;function w0e(r){return Ly(r)?r.slice(1):r}si.convertToPositivePattern=w0e;function B0e(r){return"!"+r}si.convertToNegativePattern=B0e;function Ly(r){return r.startsWith("!")&&r[1]!=="("}si.isNegativePattern=Ly;function RJ(r){return!Ly(r)}si.isPositivePattern=RJ;function b0e(r){return r.filter(Ly)}si.getNegativePatterns=b0e;function Q0e(r){return r.filter(RJ)}si.getPositivePatterns=Q0e;function S0e(r){return h0e(r,{flipBackslashes:!1})}si.getBaseDirectory=S0e;function v0e(r){return r.includes(xJ)}si.hasGlobStar=v0e;function FJ(r){return r.endsWith("/"+xJ)}si.endsWithSlashGlobStar=FJ;function k0e(r){let e=f0e.basename(r);return FJ(r)||DJ(e)}si.isAffectDepthOfReadingPattern=k0e;function x0e(r){return r.reduce((e,t)=>e.concat(NJ(t)),[])}si.expandPatternsWithBraceExpansion=x0e;function NJ(r){return kJ.braces(r,{expand:!0,nodupes:!0})}si.expandBraceExpansion=NJ;function P0e(r,e){let t=p0e.scan(r,Object.assign(Object.assign({},e),{parts:!0}));return t.parts.length===0?[r]:t.parts}si.getPatternParts=P0e;function LJ(r,e){return kJ.makeRe(r,e)}si.makeRe=LJ;function D0e(r,e){return r.map(t=>LJ(t,e))}si.convertPatternsToRe=D0e;function R0e(r,e){return e.some(t=>t.test(r))}si.matchAny=R0e});var MJ=w(Wv=>{"use strict";Object.defineProperty(Wv,"__esModule",{value:!0});var F0e=Gv();function N0e(r){let e=F0e(r);return r.forEach(t=>{t.once("error",i=>e.emit("error",i))}),e.once("close",()=>OJ(r)),e.once("end",()=>OJ(r)),e}Wv.merge=N0e;function OJ(r){r.forEach(e=>e.emit("close"))}});var UJ=w(Ty=>{"use strict";Object.defineProperty(Ty,"__esModule",{value:!0});function L0e(r){return typeof r=="string"}Ty.isString=L0e;function T0e(r){return r===""}Ty.isEmpty=T0e});var Za=w(Xa=>{"use strict";Object.defineProperty(Xa,"__esModule",{value:!0});var O0e=dJ();Xa.array=O0e;var M0e=CJ();Xa.errno=M0e;var U0e=EJ();Xa.fs=U0e;var K0e=IJ();Xa.path=K0e;var H0e=TJ();Xa.pattern=H0e;var j0e=MJ();Xa.stream=j0e;var G0e=UJ();Xa.string=G0e});var YJ=w($a=>{"use strict";Object.defineProperty($a,"__esModule",{value:!0});var Uc=Za();function Y0e(r,e){let t=KJ(r),i=HJ(r,e.ignore),n=t.filter(l=>Uc.pattern.isStaticPattern(l,e)),s=t.filter(l=>Uc.pattern.isDynamicPattern(l,e)),o=zv(n,i,!1),a=zv(s,i,!0);return o.concat(a)}$a.generate=Y0e;function zv(r,e,t){let i=jJ(r);return"."in i?[_v(".",r,e,t)]:GJ(i,e,t)}$a.convertPatternsToTasks=zv;function KJ(r){return Uc.pattern.getPositivePatterns(r)}$a.getPositivePatterns=KJ;function HJ(r,e){return Uc.pattern.getNegativePatterns(r).concat(e).map(Uc.pattern.convertToPositivePattern)}$a.getNegativePatternsAsPositive=HJ;function jJ(r){let e={};return r.reduce((t,i)=>{let n=Uc.pattern.getBaseDirectory(i);return n in t?t[n].push(i):t[n]=[i],t},e)}$a.groupPatternsByBaseDirectory=jJ;function GJ(r,e,t){return Object.keys(r).map(i=>_v(i,r[i],e,t))}$a.convertPatternGroupsToTasks=GJ;function _v(r,e,t,i){return{dynamic:i,positive:e,negative:t,base:r,patterns:[].concat(e,t.map(Uc.pattern.convertToNegativePattern))}}$a.convertPatternGroupToTask=_v});var JJ=w(Oy=>{"use strict";Object.defineProperty(Oy,"__esModule",{value:!0});Oy.read=void 0;function q0e(r,e,t){e.fs.lstat(r,(i,n)=>{if(i!==null){qJ(t,i);return}if(!n.isSymbolicLink()||!e.followSymbolicLink){Vv(t,n);return}e.fs.stat(r,(s,o)=>{if(s!==null){if(e.throwErrorOnBrokenSymbolicLink){qJ(t,s);return}Vv(t,n);return}e.markSymbolicLink&&(o.isSymbolicLink=()=>!0),Vv(t,o)})})}Oy.read=q0e;function qJ(r,e){r(e)}function Vv(r,e){r(null,e)}});var WJ=w(My=>{"use strict";Object.defineProperty(My,"__esModule",{value:!0});My.read=void 0;function J0e(r,e){let t=e.fs.lstatSync(r);if(!t.isSymbolicLink()||!e.followSymbolicLink)return t;try{let i=e.fs.statSync(r);return e.markSymbolicLink&&(i.isSymbolicLink=()=>!0),i}catch(i){if(!e.throwErrorOnBrokenSymbolicLink)return t;throw i}}My.read=J0e});var zJ=w(il=>{"use strict";Object.defineProperty(il,"__esModule",{value:!0});il.createFileSystemAdapter=il.FILE_SYSTEM_ADAPTER=void 0;var Uy=require("fs");il.FILE_SYSTEM_ADAPTER={lstat:Uy.lstat,stat:Uy.stat,lstatSync:Uy.lstatSync,statSync:Uy.statSync};function W0e(r){return r===void 0?il.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},il.FILE_SYSTEM_ADAPTER),r)}il.createFileSystemAdapter=W0e});var VJ=w(Xv=>{"use strict";Object.defineProperty(Xv,"__esModule",{value:!0});var z0e=zJ(),_J=class{constructor(e={}){this._options=e,this.followSymbolicLink=this._getValue(this._options.followSymbolicLink,!0),this.fs=z0e.createFileSystemAdapter(this._options.fs),this.markSymbolicLink=this._getValue(this._options.markSymbolicLink,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0)}_getValue(e,t){return e!=null?e:t}};Xv.default=_J});var Kc=w(nl=>{"use strict";Object.defineProperty(nl,"__esModule",{value:!0});nl.statSync=nl.stat=nl.Settings=void 0;var XJ=JJ(),_0e=WJ(),Zv=VJ();nl.Settings=Zv.default;function V0e(r,e,t){if(typeof e=="function"){XJ.read(r,$v(),e);return}XJ.read(r,$v(e),t)}nl.stat=V0e;function X0e(r,e){let t=$v(e);return _0e.read(r,t)}nl.statSync=X0e;function $v(r={}){return r instanceof Zv.default?r:new Zv.default(r)}});var $J=w((ptt,ZJ)=>{ZJ.exports=Z0e;function Z0e(r,e){var t,i,n,s=!0;Array.isArray(r)?(t=[],i=r.length):(n=Object.keys(r),t={},i=n.length);function o(l){function c(){e&&e(l,t),e=null}s?process.nextTick(c):c()}function a(l,c,u){t[l]=u,(--i==0||c)&&o(c)}i?n?n.forEach(function(l){r[l](function(c,u){a(l,c,u)})}):r.forEach(function(l,c){l(function(u,g){a(c,u,g)})}):o(null),s=!1}});var ek=w(Ky=>{"use strict";Object.defineProperty(Ky,"__esModule",{value:!0});Ky.IS_SUPPORT_READDIR_WITH_FILE_TYPES=void 0;var Hy=process.versions.node.split(".");if(Hy[0]===void 0||Hy[1]===void 0)throw new Error(`Unexpected behavior. The 'process.versions.node' variable has invalid value: ${process.versions.node}`);var eW=Number.parseInt(Hy[0],10),$0e=Number.parseInt(Hy[1],10),tW=10,ebe=10,tbe=eW>tW,rbe=eW===tW&&$0e>=ebe;Ky.IS_SUPPORT_READDIR_WITH_FILE_TYPES=tbe||rbe});var iW=w(jy=>{"use strict";Object.defineProperty(jy,"__esModule",{value:!0});jy.createDirentFromStats=void 0;var rW=class{constructor(e,t){this.name=e,this.isBlockDevice=t.isBlockDevice.bind(t),this.isCharacterDevice=t.isCharacterDevice.bind(t),this.isDirectory=t.isDirectory.bind(t),this.isFIFO=t.isFIFO.bind(t),this.isFile=t.isFile.bind(t),this.isSocket=t.isSocket.bind(t),this.isSymbolicLink=t.isSymbolicLink.bind(t)}};function ibe(r,e){return new rW(r,e)}jy.createDirentFromStats=ibe});var tk=w(Gy=>{"use strict";Object.defineProperty(Gy,"__esModule",{value:!0});Gy.fs=void 0;var nbe=iW();Gy.fs=nbe});var rk=w(Yy=>{"use strict";Object.defineProperty(Yy,"__esModule",{value:!0});Yy.joinPathSegments=void 0;function sbe(r,e,t){return r.endsWith(t)?r+e:r+t+e}Yy.joinPathSegments=sbe});var lW=w(sl=>{"use strict";Object.defineProperty(sl,"__esModule",{value:!0});sl.readdir=sl.readdirWithFileTypes=sl.read=void 0;var obe=Kc(),nW=$J(),abe=ek(),sW=tk(),oW=rk();function Abe(r,e,t){if(!e.stats&&abe.IS_SUPPORT_READDIR_WITH_FILE_TYPES){aW(r,e,t);return}AW(r,e,t)}sl.read=Abe;function aW(r,e,t){e.fs.readdir(r,{withFileTypes:!0},(i,n)=>{if(i!==null){qy(t,i);return}let s=n.map(a=>({dirent:a,name:a.name,path:oW.joinPathSegments(r,a.name,e.pathSegmentSeparator)}));if(!e.followSymbolicLinks){ik(t,s);return}let o=s.map(a=>lbe(a,e));nW(o,(a,l)=>{if(a!==null){qy(t,a);return}ik(t,l)})})}sl.readdirWithFileTypes=aW;function lbe(r,e){return t=>{if(!r.dirent.isSymbolicLink()){t(null,r);return}e.fs.stat(r.path,(i,n)=>{if(i!==null){if(e.throwErrorOnBrokenSymbolicLink){t(i);return}t(null,r);return}r.dirent=sW.fs.createDirentFromStats(r.name,n),t(null,r)})}}function AW(r,e,t){e.fs.readdir(r,(i,n)=>{if(i!==null){qy(t,i);return}let s=n.map(o=>{let a=oW.joinPathSegments(r,o,e.pathSegmentSeparator);return l=>{obe.stat(a,e.fsStatSettings,(c,u)=>{if(c!==null){l(c);return}let g={name:o,path:a,dirent:sW.fs.createDirentFromStats(o,u)};e.stats&&(g.stats=u),l(null,g)})}});nW(s,(o,a)=>{if(o!==null){qy(t,o);return}ik(t,a)})})}sl.readdir=AW;function qy(r,e){r(e)}function ik(r,e){r(null,e)}});var hW=w(ol=>{"use strict";Object.defineProperty(ol,"__esModule",{value:!0});ol.readdir=ol.readdirWithFileTypes=ol.read=void 0;var cbe=Kc(),ube=ek(),cW=tk(),uW=rk();function gbe(r,e){return!e.stats&&ube.IS_SUPPORT_READDIR_WITH_FILE_TYPES?gW(r,e):fW(r,e)}ol.read=gbe;function gW(r,e){return e.fs.readdirSync(r,{withFileTypes:!0}).map(i=>{let n={dirent:i,name:i.name,path:uW.joinPathSegments(r,i.name,e.pathSegmentSeparator)};if(n.dirent.isSymbolicLink()&&e.followSymbolicLinks)try{let s=e.fs.statSync(n.path);n.dirent=cW.fs.createDirentFromStats(n.name,s)}catch(s){if(e.throwErrorOnBrokenSymbolicLink)throw s}return n})}ol.readdirWithFileTypes=gW;function fW(r,e){return e.fs.readdirSync(r).map(i=>{let n=uW.joinPathSegments(r,i,e.pathSegmentSeparator),s=cbe.statSync(n,e.fsStatSettings),o={name:i,path:n,dirent:cW.fs.createDirentFromStats(i,s)};return e.stats&&(o.stats=s),o})}ol.readdir=fW});var pW=w(al=>{"use strict";Object.defineProperty(al,"__esModule",{value:!0});al.createFileSystemAdapter=al.FILE_SYSTEM_ADAPTER=void 0;var Hg=require("fs");al.FILE_SYSTEM_ADAPTER={lstat:Hg.lstat,stat:Hg.stat,lstatSync:Hg.lstatSync,statSync:Hg.statSync,readdir:Hg.readdir,readdirSync:Hg.readdirSync};function fbe(r){return r===void 0?al.FILE_SYSTEM_ADAPTER:Object.assign(Object.assign({},al.FILE_SYSTEM_ADAPTER),r)}al.createFileSystemAdapter=fbe});var CW=w(nk=>{"use strict";Object.defineProperty(nk,"__esModule",{value:!0});var hbe=require("path"),pbe=Kc(),dbe=pW(),dW=class{constructor(e={}){this._options=e,this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!1),this.fs=dbe.createFileSystemAdapter(this._options.fs),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,hbe.sep),this.stats=this._getValue(this._options.stats,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!0),this.fsStatSettings=new pbe.Settings({followSymbolicLink:this.followSymbolicLinks,fs:this.fs,throwErrorOnBrokenSymbolicLink:this.throwErrorOnBrokenSymbolicLink})}_getValue(e,t){return e!=null?e:t}};nk.default=dW});var Jy=w(Al=>{"use strict";Object.defineProperty(Al,"__esModule",{value:!0});Al.Settings=Al.scandirSync=Al.scandir=void 0;var mW=lW(),Cbe=hW(),sk=CW();Al.Settings=sk.default;function mbe(r,e,t){if(typeof e=="function"){mW.read(r,ok(),e);return}mW.read(r,ok(e),t)}Al.scandir=mbe;function Ebe(r,e){let t=ok(e);return Cbe.read(r,t)}Al.scandirSync=Ebe;function ok(r={}){return r instanceof sk.default?r:new sk.default(r)}});var IW=w((Qtt,EW)=>{"use strict";function Ibe(r){var e=new r,t=e;function i(){var s=e;return s.next?e=s.next:(e=new r,t=e),s.next=null,s}function n(s){t.next=s,t=s}return{get:i,release:n}}EW.exports=Ibe});var wW=w((Stt,ak)=>{"use strict";var ybe=IW();function yW(r,e,t){if(typeof r=="function"&&(t=e,e=r,r=null),t<1)throw new Error("fastqueue concurrency must be greater than 1");var i=ybe(wbe),n=null,s=null,o=0,a=null,l={push:m,drain:Ls,saturated:Ls,pause:u,paused:!1,concurrency:t,running:c,resume:h,idle:p,length:g,getQueue:f,unshift:y,empty:Ls,kill:v,killAndDrain:k,error:T};return l;function c(){return o}function u(){l.paused=!0}function g(){for(var Y=n,q=0;Y;)Y=Y.next,q++;return q}function f(){for(var Y=n,q=[];Y;)q.push(Y.value),Y=Y.next;return q}function h(){if(!!l.paused){l.paused=!1;for(var Y=0;Y{"use strict";Object.defineProperty(Zo,"__esModule",{value:!0});Zo.joinPathSegments=Zo.replacePathSegmentSeparator=Zo.isAppliedFilter=Zo.isFatalError=void 0;function bbe(r,e){return r.errorFilter===null?!0:!r.errorFilter(e)}Zo.isFatalError=bbe;function Qbe(r,e){return r===null||r(e)}Zo.isAppliedFilter=Qbe;function Sbe(r,e){return r.split(/[/\\]/).join(e)}Zo.replacePathSegmentSeparator=Sbe;function vbe(r,e,t){return r===""?e:r.endsWith(t)?r+e:r+t+e}Zo.joinPathSegments=vbe});var lk=w(Ak=>{"use strict";Object.defineProperty(Ak,"__esModule",{value:!0});var kbe=Wy(),BW=class{constructor(e,t){this._root=e,this._settings=t,this._root=kbe.replacePathSegmentSeparator(e,t.pathSegmentSeparator)}};Ak.default=BW});var uk=w(ck=>{"use strict";Object.defineProperty(ck,"__esModule",{value:!0});var xbe=require("events"),Pbe=Jy(),Dbe=wW(),zy=Wy(),Rbe=lk(),bW=class extends Rbe.default{constructor(e,t){super(e,t);this._settings=t,this._scandir=Pbe.scandir,this._emitter=new xbe.EventEmitter,this._queue=Dbe(this._worker.bind(this),this._settings.concurrency),this._isFatalError=!1,this._isDestroyed=!1,this._queue.drain=()=>{this._isFatalError||this._emitter.emit("end")}}read(){return this._isFatalError=!1,this._isDestroyed=!1,setImmediate(()=>{this._pushToQueue(this._root,this._settings.basePath)}),this._emitter}get isDestroyed(){return this._isDestroyed}destroy(){if(this._isDestroyed)throw new Error("The reader is already destroyed");this._isDestroyed=!0,this._queue.killAndDrain()}onEntry(e){this._emitter.on("entry",e)}onError(e){this._emitter.once("error",e)}onEnd(e){this._emitter.once("end",e)}_pushToQueue(e,t){let i={directory:e,base:t};this._queue.push(i,n=>{n!==null&&this._handleError(n)})}_worker(e,t){this._scandir(e.directory,this._settings.fsScandirSettings,(i,n)=>{if(i!==null){t(i,void 0);return}for(let s of n)this._handleEntry(s,e.base);t(null,void 0)})}_handleError(e){this._isDestroyed||!zy.isFatalError(this._settings,e)||(this._isFatalError=!0,this._isDestroyed=!0,this._emitter.emit("error",e))}_handleEntry(e,t){if(this._isDestroyed||this._isFatalError)return;let i=e.path;t!==void 0&&(e.path=zy.joinPathSegments(t,e.name,this._settings.pathSegmentSeparator)),zy.isAppliedFilter(this._settings.entryFilter,e)&&this._emitEntry(e),e.dirent.isDirectory()&&zy.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,e.path)}_emitEntry(e){this._emitter.emit("entry",e)}};ck.default=bW});var SW=w(gk=>{"use strict";Object.defineProperty(gk,"__esModule",{value:!0});var Fbe=uk(),QW=class{constructor(e,t){this._root=e,this._settings=t,this._reader=new Fbe.default(this._root,this._settings),this._storage=new Set}read(e){this._reader.onError(t=>{Nbe(e,t)}),this._reader.onEntry(t=>{this._storage.add(t)}),this._reader.onEnd(()=>{Lbe(e,[...this._storage])}),this._reader.read()}};gk.default=QW;function Nbe(r,e){r(e)}function Lbe(r,e){r(null,e)}});var kW=w(fk=>{"use strict";Object.defineProperty(fk,"__esModule",{value:!0});var Tbe=require("stream"),Obe=uk(),vW=class{constructor(e,t){this._root=e,this._settings=t,this._reader=new Obe.default(this._root,this._settings),this._stream=new Tbe.Readable({objectMode:!0,read:()=>{},destroy:()=>{this._reader.isDestroyed||this._reader.destroy()}})}read(){return this._reader.onError(e=>{this._stream.emit("error",e)}),this._reader.onEntry(e=>{this._stream.push(e)}),this._reader.onEnd(()=>{this._stream.push(null)}),this._reader.read(),this._stream}};fk.default=vW});var PW=w(hk=>{"use strict";Object.defineProperty(hk,"__esModule",{value:!0});var Mbe=Jy(),_y=Wy(),Ube=lk(),xW=class extends Ube.default{constructor(){super(...arguments);this._scandir=Mbe.scandirSync,this._storage=new Set,this._queue=new Set}read(){return this._pushToQueue(this._root,this._settings.basePath),this._handleQueue(),[...this._storage]}_pushToQueue(e,t){this._queue.add({directory:e,base:t})}_handleQueue(){for(let e of this._queue.values())this._handleDirectory(e.directory,e.base)}_handleDirectory(e,t){try{let i=this._scandir(e,this._settings.fsScandirSettings);for(let n of i)this._handleEntry(n,t)}catch(i){this._handleError(i)}}_handleError(e){if(!!_y.isFatalError(this._settings,e))throw e}_handleEntry(e,t){let i=e.path;t!==void 0&&(e.path=_y.joinPathSegments(t,e.name,this._settings.pathSegmentSeparator)),_y.isAppliedFilter(this._settings.entryFilter,e)&&this._pushToStorage(e),e.dirent.isDirectory()&&_y.isAppliedFilter(this._settings.deepFilter,e)&&this._pushToQueue(i,e.path)}_pushToStorage(e){this._storage.add(e)}};hk.default=xW});var RW=w(pk=>{"use strict";Object.defineProperty(pk,"__esModule",{value:!0});var Kbe=PW(),DW=class{constructor(e,t){this._root=e,this._settings=t,this._reader=new Kbe.default(this._root,this._settings)}read(){return this._reader.read()}};pk.default=DW});var NW=w(dk=>{"use strict";Object.defineProperty(dk,"__esModule",{value:!0});var Hbe=require("path"),jbe=Jy(),FW=class{constructor(e={}){this._options=e,this.basePath=this._getValue(this._options.basePath,void 0),this.concurrency=this._getValue(this._options.concurrency,Number.POSITIVE_INFINITY),this.deepFilter=this._getValue(this._options.deepFilter,null),this.entryFilter=this._getValue(this._options.entryFilter,null),this.errorFilter=this._getValue(this._options.errorFilter,null),this.pathSegmentSeparator=this._getValue(this._options.pathSegmentSeparator,Hbe.sep),this.fsScandirSettings=new jbe.Settings({followSymbolicLinks:this._options.followSymbolicLinks,fs:this._options.fs,pathSegmentSeparator:this._options.pathSegmentSeparator,stats:this._options.stats,throwErrorOnBrokenSymbolicLink:this._options.throwErrorOnBrokenSymbolicLink})}_getValue(e,t){return e!=null?e:t}};dk.default=FW});var mk=w($o=>{"use strict";Object.defineProperty($o,"__esModule",{value:!0});$o.Settings=$o.walkStream=$o.walkSync=$o.walk=void 0;var LW=SW(),Gbe=kW(),Ybe=RW(),Ck=NW();$o.Settings=Ck.default;function qbe(r,e,t){if(typeof e=="function"){new LW.default(r,Vy()).read(e);return}new LW.default(r,Vy(e)).read(t)}$o.walk=qbe;function Jbe(r,e){let t=Vy(e);return new Ybe.default(r,t).read()}$o.walkSync=Jbe;function Wbe(r,e){let t=Vy(e);return new Gbe.default(r,t).read()}$o.walkStream=Wbe;function Vy(r={}){return r instanceof Ck.default?r:new Ck.default(r)}});var Ik=w(Ek=>{"use strict";Object.defineProperty(Ek,"__esModule",{value:!0});var zbe=require("path"),_be=Kc(),TW=Za(),OW=class{constructor(e){this._settings=e,this._fsStatSettings=new _be.Settings({followSymbolicLink:this._settings.followSymbolicLinks,fs:this._settings.fs,throwErrorOnBrokenSymbolicLink:this._settings.followSymbolicLinks})}_getFullEntryPath(e){return zbe.resolve(this._settings.cwd,e)}_makeEntry(e,t){let i={name:t,path:t,dirent:TW.fs.createDirentFromStats(t,e)};return this._settings.stats&&(i.stats=e),i}_isFatalError(e){return!TW.errno.isEnoentCodeError(e)&&!this._settings.suppressErrors}};Ek.default=OW});var wk=w(yk=>{"use strict";Object.defineProperty(yk,"__esModule",{value:!0});var Vbe=require("stream"),Xbe=Kc(),Zbe=mk(),$be=Ik(),MW=class extends $be.default{constructor(){super(...arguments);this._walkStream=Zbe.walkStream,this._stat=Xbe.stat}dynamic(e,t){return this._walkStream(e,t)}static(e,t){let i=e.map(this._getFullEntryPath,this),n=new Vbe.PassThrough({objectMode:!0});n._write=(s,o,a)=>this._getEntry(i[s],e[s],t).then(l=>{l!==null&&t.entryFilter(l)&&n.push(l),s===i.length-1&&n.end(),a()}).catch(a);for(let s=0;sthis._makeEntry(n,t)).catch(n=>{if(i.errorFilter(n))return null;throw n})}_getStat(e){return new Promise((t,i)=>{this._stat(e,this._fsStatSettings,(n,s)=>n===null?t(s):i(n))})}};yk.default=MW});var KW=w(Bk=>{"use strict";Object.defineProperty(Bk,"__esModule",{value:!0});var jg=Za(),UW=class{constructor(e,t,i){this._patterns=e,this._settings=t,this._micromatchOptions=i,this._storage=[],this._fillStorage()}_fillStorage(){let e=jg.pattern.expandPatternsWithBraceExpansion(this._patterns);for(let t of e){let i=this._getPatternSegments(t),n=this._splitSegmentsIntoSections(i);this._storage.push({complete:n.length<=1,pattern:t,segments:i,sections:n})}}_getPatternSegments(e){return jg.pattern.getPatternParts(e,this._micromatchOptions).map(i=>jg.pattern.isDynamicPattern(i,this._settings)?{dynamic:!0,pattern:i,patternRe:jg.pattern.makeRe(i,this._micromatchOptions)}:{dynamic:!1,pattern:i})}_splitSegmentsIntoSections(e){return jg.array.splitWhen(e,t=>t.dynamic&&jg.pattern.hasGlobStar(t.pattern))}};Bk.default=UW});var jW=w(bk=>{"use strict";Object.defineProperty(bk,"__esModule",{value:!0});var eQe=KW(),HW=class extends eQe.default{match(e){let t=e.split("/"),i=t.length,n=this._storage.filter(s=>!s.complete||s.segments.length>i);for(let s of n){let o=s.sections[0];if(!s.complete&&i>o.length||t.every((l,c)=>{let u=s.segments[c];return!!(u.dynamic&&u.patternRe.test(l)||!u.dynamic&&u.pattern===l)}))return!0}return!1}};bk.default=HW});var YW=w(Qk=>{"use strict";Object.defineProperty(Qk,"__esModule",{value:!0});var Xy=Za(),tQe=jW(),GW=class{constructor(e,t){this._settings=e,this._micromatchOptions=t}getFilter(e,t,i){let n=this._getMatcher(t),s=this._getNegativePatternsRe(i);return o=>this._filter(e,o,n,s)}_getMatcher(e){return new tQe.default(e,this._settings,this._micromatchOptions)}_getNegativePatternsRe(e){let t=e.filter(Xy.pattern.isAffectDepthOfReadingPattern);return Xy.pattern.convertPatternsToRe(t,this._micromatchOptions)}_filter(e,t,i,n){let s=this._getEntryLevel(e,t.path);if(this._isSkippedByDeep(s)||this._isSkippedSymbolicLink(t))return!1;let o=Xy.path.removeLeadingDotSegment(t.path);return this._isSkippedByPositivePatterns(o,i)?!1:this._isSkippedByNegativePatterns(o,n)}_isSkippedByDeep(e){return e>=this._settings.deep}_isSkippedSymbolicLink(e){return!this._settings.followSymbolicLinks&&e.dirent.isSymbolicLink()}_getEntryLevel(e,t){let i=e.split("/").length;return t.split("/").length-(e===""?0:i)}_isSkippedByPositivePatterns(e,t){return!this._settings.baseNameMatch&&!t.match(e)}_isSkippedByNegativePatterns(e,t){return!Xy.pattern.matchAny(e,t)}};Qk.default=GW});var JW=w(Sk=>{"use strict";Object.defineProperty(Sk,"__esModule",{value:!0});var od=Za(),qW=class{constructor(e,t){this._settings=e,this._micromatchOptions=t,this.index=new Map}getFilter(e,t){let i=od.pattern.convertPatternsToRe(e,this._micromatchOptions),n=od.pattern.convertPatternsToRe(t,this._micromatchOptions);return s=>this._filter(s,i,n)}_filter(e,t,i){if(this._settings.unique){if(this._isDuplicateEntry(e))return!1;this._createIndexRecord(e)}if(this._onlyFileFilter(e)||this._onlyDirectoryFilter(e)||this._isSkippedByAbsoluteNegativePatterns(e,i))return!1;let n=this._settings.baseNameMatch?e.name:e.path;return this._isMatchToPatterns(n,t)&&!this._isMatchToPatterns(e.path,i)}_isDuplicateEntry(e){return this.index.has(e.path)}_createIndexRecord(e){this.index.set(e.path,void 0)}_onlyFileFilter(e){return this._settings.onlyFiles&&!e.dirent.isFile()}_onlyDirectoryFilter(e){return this._settings.onlyDirectories&&!e.dirent.isDirectory()}_isSkippedByAbsoluteNegativePatterns(e,t){if(!this._settings.absolute)return!1;let i=od.path.makeAbsolute(this._settings.cwd,e.path);return this._isMatchToPatterns(i,t)}_isMatchToPatterns(e,t){let i=od.path.removeLeadingDotSegment(e);return od.pattern.matchAny(i,t)}};Sk.default=qW});var zW=w(vk=>{"use strict";Object.defineProperty(vk,"__esModule",{value:!0});var rQe=Za(),WW=class{constructor(e){this._settings=e}getFilter(){return e=>this._isNonFatalError(e)}_isNonFatalError(e){return rQe.errno.isEnoentCodeError(e)||this._settings.suppressErrors}};vk.default=WW});var XW=w(kk=>{"use strict";Object.defineProperty(kk,"__esModule",{value:!0});var _W=Za(),VW=class{constructor(e){this._settings=e}getTransformer(){return e=>this._transform(e)}_transform(e){let t=e.path;return this._settings.absolute&&(t=_W.path.makeAbsolute(this._settings.cwd,t),t=_W.path.unixify(t)),this._settings.markDirectories&&e.dirent.isDirectory()&&(t+="/"),this._settings.objectMode?Object.assign(Object.assign({},e),{path:t}):t}};kk.default=VW});var Zy=w(xk=>{"use strict";Object.defineProperty(xk,"__esModule",{value:!0});var iQe=require("path"),nQe=YW(),sQe=JW(),oQe=zW(),aQe=XW(),ZW=class{constructor(e){this._settings=e,this.errorFilter=new oQe.default(this._settings),this.entryFilter=new sQe.default(this._settings,this._getMicromatchOptions()),this.deepFilter=new nQe.default(this._settings,this._getMicromatchOptions()),this.entryTransformer=new aQe.default(this._settings)}_getRootDirectory(e){return iQe.resolve(this._settings.cwd,e.base)}_getReaderOptions(e){let t=e.base==="."?"":e.base;return{basePath:t,pathSegmentSeparator:"/",concurrency:this._settings.concurrency,deepFilter:this.deepFilter.getFilter(t,e.positive,e.negative),entryFilter:this.entryFilter.getFilter(e.positive,e.negative),errorFilter:this.errorFilter.getFilter(),followSymbolicLinks:this._settings.followSymbolicLinks,fs:this._settings.fs,stats:this._settings.stats,throwErrorOnBrokenSymbolicLink:this._settings.throwErrorOnBrokenSymbolicLink,transform:this.entryTransformer.getTransformer()}}_getMicromatchOptions(){return{dot:this._settings.dot,matchBase:this._settings.baseNameMatch,nobrace:!this._settings.braceExpansion,nocase:!this._settings.caseSensitiveMatch,noext:!this._settings.extglob,noglobstar:!this._settings.globstar,posix:!0,strictSlashes:!1}}};xk.default=ZW});var e3=w(Pk=>{"use strict";Object.defineProperty(Pk,"__esModule",{value:!0});var AQe=wk(),lQe=Zy(),$W=class extends lQe.default{constructor(){super(...arguments);this._reader=new AQe.default(this._settings)}read(e){let t=this._getRootDirectory(e),i=this._getReaderOptions(e),n=[];return new Promise((s,o)=>{let a=this.api(t,e,i);a.once("error",o),a.on("data",l=>n.push(i.transform(l))),a.once("end",()=>s(n))})}api(e,t,i){return t.dynamic?this._reader.dynamic(e,i):this._reader.static(t.patterns,i)}};Pk.default=$W});var r3=w(Dk=>{"use strict";Object.defineProperty(Dk,"__esModule",{value:!0});var cQe=require("stream"),uQe=wk(),gQe=Zy(),t3=class extends gQe.default{constructor(){super(...arguments);this._reader=new uQe.default(this._settings)}read(e){let t=this._getRootDirectory(e),i=this._getReaderOptions(e),n=this.api(t,e,i),s=new cQe.Readable({objectMode:!0,read:()=>{}});return n.once("error",o=>s.emit("error",o)).on("data",o=>s.emit("data",i.transform(o))).once("end",()=>s.emit("end")),s.once("close",()=>n.destroy()),s}api(e,t,i){return t.dynamic?this._reader.dynamic(e,i):this._reader.static(t.patterns,i)}};Dk.default=t3});var n3=w(Rk=>{"use strict";Object.defineProperty(Rk,"__esModule",{value:!0});var fQe=Kc(),hQe=mk(),pQe=Ik(),i3=class extends pQe.default{constructor(){super(...arguments);this._walkSync=hQe.walkSync,this._statSync=fQe.statSync}dynamic(e,t){return this._walkSync(e,t)}static(e,t){let i=[];for(let n of e){let s=this._getFullEntryPath(n),o=this._getEntry(s,n,t);o===null||!t.entryFilter(o)||i.push(o)}return i}_getEntry(e,t,i){try{let n=this._getStat(e);return this._makeEntry(n,t)}catch(n){if(i.errorFilter(n))return null;throw n}}_getStat(e){return this._statSync(e,this._fsStatSettings)}};Rk.default=i3});var o3=w(Fk=>{"use strict";Object.defineProperty(Fk,"__esModule",{value:!0});var dQe=n3(),CQe=Zy(),s3=class extends CQe.default{constructor(){super(...arguments);this._reader=new dQe.default(this._settings)}read(e){let t=this._getRootDirectory(e),i=this._getReaderOptions(e);return this.api(t,e,i).map(i.transform)}api(e,t,i){return t.dynamic?this._reader.dynamic(e,i):this._reader.static(t.patterns,i)}};Fk.default=s3});var A3=w(ad=>{"use strict";Object.defineProperty(ad,"__esModule",{value:!0});var Gg=require("fs"),mQe=require("os"),EQe=mQe.cpus().length;ad.DEFAULT_FILE_SYSTEM_ADAPTER={lstat:Gg.lstat,lstatSync:Gg.lstatSync,stat:Gg.stat,statSync:Gg.statSync,readdir:Gg.readdir,readdirSync:Gg.readdirSync};var a3=class{constructor(e={}){this._options=e,this.absolute=this._getValue(this._options.absolute,!1),this.baseNameMatch=this._getValue(this._options.baseNameMatch,!1),this.braceExpansion=this._getValue(this._options.braceExpansion,!0),this.caseSensitiveMatch=this._getValue(this._options.caseSensitiveMatch,!0),this.concurrency=this._getValue(this._options.concurrency,EQe),this.cwd=this._getValue(this._options.cwd,process.cwd()),this.deep=this._getValue(this._options.deep,Infinity),this.dot=this._getValue(this._options.dot,!1),this.extglob=this._getValue(this._options.extglob,!0),this.followSymbolicLinks=this._getValue(this._options.followSymbolicLinks,!0),this.fs=this._getFileSystemMethods(this._options.fs),this.globstar=this._getValue(this._options.globstar,!0),this.ignore=this._getValue(this._options.ignore,[]),this.markDirectories=this._getValue(this._options.markDirectories,!1),this.objectMode=this._getValue(this._options.objectMode,!1),this.onlyDirectories=this._getValue(this._options.onlyDirectories,!1),this.onlyFiles=this._getValue(this._options.onlyFiles,!0),this.stats=this._getValue(this._options.stats,!1),this.suppressErrors=this._getValue(this._options.suppressErrors,!1),this.throwErrorOnBrokenSymbolicLink=this._getValue(this._options.throwErrorOnBrokenSymbolicLink,!1),this.unique=this._getValue(this._options.unique,!0),this.onlyDirectories&&(this.onlyFiles=!1),this.stats&&(this.objectMode=!0)}_getValue(e,t){return e===void 0?t:e}_getFileSystemMethods(e={}){return Object.assign(Object.assign({},ad.DEFAULT_FILE_SYSTEM_ADAPTER),e)}};ad.default=a3});var $y=w((Vtt,l3)=>{"use strict";var c3=YJ(),IQe=e3(),yQe=r3(),wQe=o3(),Nk=A3(),Hc=Za();async function Tk(r,e){Yg(r);let t=Lk(r,IQe.default,e),i=await Promise.all(t);return Hc.array.flatten(i)}(function(r){function e(o,a){Yg(o);let l=Lk(o,wQe.default,a);return Hc.array.flatten(l)}r.sync=e;function t(o,a){Yg(o);let l=Lk(o,yQe.default,a);return Hc.stream.merge(l)}r.stream=t;function i(o,a){Yg(o);let l=[].concat(o),c=new Nk.default(a);return c3.generate(l,c)}r.generateTasks=i;function n(o,a){Yg(o);let l=new Nk.default(a);return Hc.pattern.isDynamicPattern(o,l)}r.isDynamicPattern=n;function s(o){return Yg(o),Hc.path.escape(o)}r.escapePath=s})(Tk||(Tk={}));function Lk(r,e,t){let i=[].concat(r),n=new Nk.default(t),s=c3.generate(i,n),o=new e(n);return s.map(o.read,o)}function Yg(r){if(![].concat(r).every(i=>Hc.string.isString(i)&&!Hc.string.isEmpty(i)))throw new TypeError("Patterns must be a string (non empty) or an array of strings")}l3.exports=Tk});var g3=w(jc=>{"use strict";var{promisify:BQe}=require("util"),u3=require("fs");async function Ok(r,e,t){if(typeof t!="string")throw new TypeError(`Expected a string, got ${typeof t}`);try{return(await BQe(u3[r])(t))[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}function Mk(r,e,t){if(typeof t!="string")throw new TypeError(`Expected a string, got ${typeof t}`);try{return u3[r](t)[e]()}catch(i){if(i.code==="ENOENT")return!1;throw i}}jc.isFile=Ok.bind(null,"stat","isFile");jc.isDirectory=Ok.bind(null,"stat","isDirectory");jc.isSymlink=Ok.bind(null,"lstat","isSymbolicLink");jc.isFileSync=Mk.bind(null,"statSync","isFile");jc.isDirectorySync=Mk.bind(null,"statSync","isDirectory");jc.isSymlinkSync=Mk.bind(null,"lstatSync","isSymbolicLink")});var C3=w((Ztt,Uk)=>{"use strict";var Gc=require("path"),f3=g3(),h3=r=>r.length>1?`{${r.join(",")}}`:r[0],p3=(r,e)=>{let t=r[0]==="!"?r.slice(1):r;return Gc.isAbsolute(t)?t:Gc.join(e,t)},bQe=(r,e)=>Gc.extname(r)?`**/${r}`:`**/${r}.${h3(e)}`,d3=(r,e)=>{if(e.files&&!Array.isArray(e.files))throw new TypeError(`Expected \`files\` to be of type \`Array\` but received type \`${typeof e.files}\``);if(e.extensions&&!Array.isArray(e.extensions))throw new TypeError(`Expected \`extensions\` to be of type \`Array\` but received type \`${typeof e.extensions}\``);return e.files&&e.extensions?e.files.map(t=>Gc.posix.join(r,bQe(t,e.extensions))):e.files?e.files.map(t=>Gc.posix.join(r,`**/${t}`)):e.extensions?[Gc.posix.join(r,`**/*.${h3(e.extensions)}`)]:[Gc.posix.join(r,"**")]};Uk.exports=async(r,e)=>{if(e=N({cwd:process.cwd()},e),typeof e.cwd!="string")throw new TypeError(`Expected \`cwd\` to be of type \`string\` but received type \`${typeof e.cwd}\``);let t=await Promise.all([].concat(r).map(async i=>await f3.isDirectory(p3(i,e.cwd))?d3(i,e):i));return[].concat.apply([],t)};Uk.exports.sync=(r,e)=>{if(e=N({cwd:process.cwd()},e),typeof e.cwd!="string")throw new TypeError(`Expected \`cwd\` to be of type \`string\` but received type \`${typeof e.cwd}\``);let t=[].concat(r).map(i=>f3.isDirectorySync(p3(i,e.cwd))?d3(i,e):i);return[].concat.apply([],t)}});var v3=w(($tt,m3)=>{function E3(r){return Array.isArray(r)?r:[r]}var I3="",y3=" ",Kk="\\",QQe=/^\s+$/,SQe=/^\\!/,vQe=/^\\#/,kQe=/\r?\n/g,xQe=/^\.*\/|^\.+$/,Hk="/",w3=typeof Symbol!="undefined"?Symbol.for("node-ignore"):"node-ignore",PQe=(r,e,t)=>Object.defineProperty(r,e,{value:t}),DQe=/([0-z])-([0-z])/g,RQe=r=>r.replace(DQe,(e,t,i)=>t.charCodeAt(0)<=i.charCodeAt(0)?e:I3),FQe=r=>{let{length:e}=r;return r.slice(0,e-e%2)},NQe=[[/\\?\s+$/,r=>r.indexOf("\\")===0?y3:I3],[/\\\s/g,()=>y3],[/[\\$.|*+(){^]/g,r=>`\\${r}`],[/(?!\\)\?/g,()=>"[^/]"],[/^\//,()=>"^"],[/\//g,()=>"\\/"],[/^\^*\\\*\\\*\\\//,()=>"^(?:.*\\/)?"],[/^(?=[^^])/,function(){return/\/(?!$)/.test(this)?"^":"(?:^|\\/)"}],[/\\\/\\\*\\\*(?=\\\/|$)/g,(r,e,t)=>e+6`${e}[^\\/]*`],[/\\\\\\(?=[$.|*+(){^])/g,()=>Kk],[/\\\\/g,()=>Kk],[/(\\)?\[([^\]/]*?)(\\*)($|\])/g,(r,e,t,i,n)=>e===Kk?`\\[${t}${FQe(i)}${n}`:n==="]"&&i.length%2==0?`[${RQe(t)}${i}]`:"[]"],[/(?:[^*])$/,r=>/\/$/.test(r)?`${r}$`:`${r}(?=$|\\/$)`],[/(\^|\\\/)?\\\*$/,(r,e)=>`${e?`${e}[^/]+`:"[^/]*"}(?=$|\\/$)`]],B3=Object.create(null),LQe=(r,e)=>{let t=B3[r];return t||(t=NQe.reduce((i,n)=>i.replace(n[0],n[1].bind(r)),r),B3[r]=t),e?new RegExp(t,"i"):new RegExp(t)},jk=r=>typeof r=="string",TQe=r=>r&&jk(r)&&!QQe.test(r)&&r.indexOf("#")!==0,OQe=r=>r.split(kQe),b3=class{constructor(e,t,i,n){this.origin=e,this.pattern=t,this.negative=i,this.regex=n}},MQe=(r,e)=>{let t=r,i=!1;r.indexOf("!")===0&&(i=!0,r=r.substr(1)),r=r.replace(SQe,"!").replace(vQe,"#");let n=LQe(r,e);return new b3(t,r,i,n)},UQe=(r,e)=>{throw new e(r)},eA=(r,e,t)=>jk(r)?r?eA.isNotRelative(r)?t(`path should be a \`path.relative()\`d string, but got "${e}"`,RangeError):!0:t("path must not be empty",TypeError):t(`path must be a string, but got \`${e}\``,TypeError),Q3=r=>xQe.test(r);eA.isNotRelative=Q3;eA.convert=r=>r;var S3=class{constructor({ignorecase:e=!0}={}){PQe(this,w3,!0),this._rules=[],this._ignorecase=e,this._initCache()}_initCache(){this._ignoreCache=Object.create(null),this._testCache=Object.create(null)}_addPattern(e){if(e&&e[w3]){this._rules=this._rules.concat(e._rules),this._added=!0;return}if(TQe(e)){let t=MQe(e,this._ignorecase);this._added=!0,this._rules.push(t)}}add(e){return this._added=!1,E3(jk(e)?OQe(e):e).forEach(this._addPattern,this),this._added&&this._initCache(),this}addPattern(e){return this.add(e)}_testOne(e,t){let i=!1,n=!1;return this._rules.forEach(s=>{let{negative:o}=s;if(n===o&&i!==n||o&&!i&&!n&&!t)return;s.regex.test(e)&&(i=!o,n=o)}),{ignored:i,unignored:n}}_test(e,t,i,n){let s=e&&eA.convert(e);return eA(s,e,UQe),this._t(s,t,i,n)}_t(e,t,i,n){if(e in t)return t[e];if(n||(n=e.split(Hk)),n.pop(),!n.length)return t[e]=this._testOne(e,i);let s=this._t(n.join(Hk)+Hk,t,i,n);return t[e]=s.ignored?s:this._testOne(e,i)}ignores(e){return this._test(e,this._ignoreCache,!1).ignored}createFilter(){return e=>!this.ignores(e)}filter(e){return E3(e).filter(this.createFilter())}test(e){return this._test(e,this._testCache,!0)}},ew=r=>new S3(r),KQe=()=>!1,HQe=r=>eA(r&&eA.convert(r),r,KQe);ew.isPathValid=HQe;ew.default=ew;m3.exports=ew;if(typeof process!="undefined"&&(process.env&&process.env.IGNORE_TEST_WIN32||process.platform==="win32")){let r=t=>/^\\\\\?\\/.test(t)||/["<>|\u0000-\u001F]+/u.test(t)?t:t.replace(/\\/g,"/");eA.convert=r;let e=/^[a-z]:\//i;eA.isNotRelative=t=>e.test(t)||Q3(t)}});var x3=w((ert,k3)=>{"use strict";k3.exports=r=>{let e=/^\\\\\?\\/.test(r),t=/[^\u0000-\u0080]+/.test(r);return e||t?r:r.replace(/\\/g,"/")}});var T3=w((trt,Gk)=>{"use strict";var{promisify:jQe}=require("util"),P3=require("fs"),tA=require("path"),D3=$y(),GQe=v3(),Ad=x3(),R3=["**/node_modules/**","**/flow-typed/**","**/coverage/**","**/.git"],YQe=jQe(P3.readFile),qQe=r=>e=>e.startsWith("!")?"!"+tA.posix.join(r,e.slice(1)):tA.posix.join(r,e),JQe=(r,e)=>{let t=Ad(tA.relative(e.cwd,tA.dirname(e.fileName)));return r.split(/\r?\n/).filter(Boolean).filter(i=>!i.startsWith("#")).map(qQe(t))},F3=r=>{let e=GQe();for(let t of r)e.add(JQe(t.content,{cwd:t.cwd,fileName:t.filePath}));return e},WQe=(r,e)=>{if(r=Ad(r),tA.isAbsolute(e)){if(Ad(e).startsWith(r))return e;throw new Error(`Path ${e} is not in cwd ${r}`)}return tA.join(r,e)},N3=(r,e)=>t=>r.ignores(Ad(tA.relative(e,WQe(e,t.path||t)))),zQe=async(r,e)=>{let t=tA.join(e,r),i=await YQe(t,"utf8");return{cwd:e,filePath:t,content:i}},_Qe=(r,e)=>{let t=tA.join(e,r),i=P3.readFileSync(t,"utf8");return{cwd:e,filePath:t,content:i}},L3=({ignore:r=[],cwd:e=Ad(process.cwd())}={})=>({ignore:r,cwd:e});Gk.exports=async r=>{r=L3(r);let e=await D3("**/.gitignore",{ignore:R3.concat(r.ignore),cwd:r.cwd}),t=await Promise.all(e.map(n=>zQe(n,r.cwd))),i=F3(t);return N3(i,r.cwd)};Gk.exports.sync=r=>{r=L3(r);let t=D3.sync("**/.gitignore",{ignore:R3.concat(r.ignore),cwd:r.cwd}).map(n=>_Qe(n,r.cwd)),i=F3(t);return N3(i,r.cwd)}});var K3=w((rrt,O3)=>{"use strict";var{Transform:VQe}=require("stream"),Yk=class extends VQe{constructor(){super({objectMode:!0})}},M3=class extends Yk{constructor(e){super();this._filter=e}_transform(e,t,i){this._filter(e)&&this.push(e),i()}},U3=class extends Yk{constructor(){super();this._pushed=new Set}_transform(e,t,i){this._pushed.has(e)||(this.push(e),this._pushed.add(e)),i()}};O3.exports={FilterStream:M3,UniqueStream:U3}});var zk=w((irt,Yc)=>{"use strict";var H3=require("fs"),tw=gJ(),XQe=Gv(),rw=$y(),iw=C3(),qk=T3(),{FilterStream:ZQe,UniqueStream:$Qe}=K3(),j3=()=>!1,G3=r=>r[0]==="!",eSe=r=>{if(!r.every(e=>typeof e=="string"))throw new TypeError("Patterns must be a string or an array of strings")},tSe=(r={})=>{if(!r.cwd)return;let e;try{e=H3.statSync(r.cwd)}catch{return}if(!e.isDirectory())throw new Error("The `cwd` option must be a path to a directory")},rSe=r=>r.stats instanceof H3.Stats?r.path:r,nw=(r,e)=>{r=tw([].concat(r)),eSe(r),tSe(e);let t=[];e=N({ignore:[],expandDirectories:!0},e);for(let[i,n]of r.entries()){if(G3(n))continue;let s=r.slice(i).filter(a=>G3(a)).map(a=>a.slice(1)),o=te(N({},e),{ignore:e.ignore.concat(s)});t.push({pattern:n,options:o})}return t},iSe=(r,e)=>{let t={};return r.options.cwd&&(t.cwd=r.options.cwd),Array.isArray(r.options.expandDirectories)?t=te(N({},t),{files:r.options.expandDirectories}):typeof r.options.expandDirectories=="object"&&(t=N(N({},t),r.options.expandDirectories)),e(r.pattern,t)},Jk=(r,e)=>r.options.expandDirectories?iSe(r,e):[r.pattern],Y3=r=>r&&r.gitignore?qk.sync({cwd:r.cwd,ignore:r.ignore}):j3,Wk=r=>e=>{let{options:t}=r;return t.ignore&&Array.isArray(t.ignore)&&t.expandDirectories&&(t.ignore=iw.sync(t.ignore)),{pattern:e,options:t}};Yc.exports=async(r,e)=>{let t=nw(r,e),i=async()=>e&&e.gitignore?qk({cwd:e.cwd,ignore:e.ignore}):j3,n=async()=>{let l=await Promise.all(t.map(async c=>{let u=await Jk(c,iw);return Promise.all(u.map(Wk(c)))}));return tw(...l)},[s,o]=await Promise.all([i(),n()]),a=await Promise.all(o.map(l=>rw(l.pattern,l.options)));return tw(...a).filter(l=>!s(rSe(l)))};Yc.exports.sync=(r,e)=>{let t=nw(r,e),i=[];for(let o of t){let a=Jk(o,iw.sync).map(Wk(o));i.push(...a)}let n=Y3(e),s=[];for(let o of i)s=tw(s,rw.sync(o.pattern,o.options));return s.filter(o=>!n(o))};Yc.exports.stream=(r,e)=>{let t=nw(r,e),i=[];for(let a of t){let l=Jk(a,iw.sync).map(Wk(a));i.push(...l)}let n=Y3(e),s=new ZQe(a=>!n(a)),o=new $Qe;return XQe(i.map(a=>rw.stream(a.pattern,a.options))).pipe(s).pipe(o)};Yc.exports.generateGlobTasks=nw;Yc.exports.hasMagic=(r,e)=>[].concat(r).some(t=>rw.isDynamicPattern(t,e));Yc.exports.gitignore=qk});var Fn=w((Prt,s4)=>{function dSe(r){var e=typeof r;return r!=null&&(e=="object"||e=="function")}s4.exports=dSe});var ix=w((Drt,o4)=>{var CSe=typeof global=="object"&&global&&global.Object===Object&&global;o4.exports=CSe});var Ts=w((Rrt,a4)=>{var mSe=ix(),ESe=typeof self=="object"&&self&&self.Object===Object&&self,ISe=mSe||ESe||Function("return this")();a4.exports=ISe});var l4=w((Frt,A4)=>{var ySe=Ts(),wSe=function(){return ySe.Date.now()};A4.exports=wSe});var u4=w((Nrt,c4)=>{var BSe=/\s/;function bSe(r){for(var e=r.length;e--&&BSe.test(r.charAt(e)););return e}c4.exports=bSe});var f4=w((Lrt,g4)=>{var QSe=u4(),SSe=/^\s+/;function vSe(r){return r&&r.slice(0,QSe(r)+1).replace(SSe,"")}g4.exports=vSe});var Wc=w((Trt,h4)=>{var kSe=Ts(),xSe=kSe.Symbol;h4.exports=xSe});var m4=w((Ort,p4)=>{var d4=Wc(),C4=Object.prototype,PSe=C4.hasOwnProperty,DSe=C4.toString,Id=d4?d4.toStringTag:void 0;function RSe(r){var e=PSe.call(r,Id),t=r[Id];try{r[Id]=void 0;var i=!0}catch(s){}var n=DSe.call(r);return i&&(e?r[Id]=t:delete r[Id]),n}p4.exports=RSe});var I4=w((Mrt,E4)=>{var FSe=Object.prototype,NSe=FSe.toString;function LSe(r){return NSe.call(r)}E4.exports=LSe});var zc=w((Urt,y4)=>{var w4=Wc(),TSe=m4(),OSe=I4(),MSe="[object Null]",USe="[object Undefined]",B4=w4?w4.toStringTag:void 0;function KSe(r){return r==null?r===void 0?USe:MSe:B4&&B4 in Object(r)?TSe(r):OSe(r)}y4.exports=KSe});var ra=w((Krt,b4)=>{function HSe(r){return r!=null&&typeof r=="object"}b4.exports=HSe});var yd=w((Hrt,Q4)=>{var jSe=zc(),GSe=ra(),YSe="[object Symbol]";function qSe(r){return typeof r=="symbol"||GSe(r)&&jSe(r)==YSe}Q4.exports=qSe});var x4=w((jrt,S4)=>{var JSe=f4(),v4=Fn(),WSe=yd(),k4=0/0,zSe=/^[-+]0x[0-9a-f]+$/i,_Se=/^0b[01]+$/i,VSe=/^0o[0-7]+$/i,XSe=parseInt;function ZSe(r){if(typeof r=="number")return r;if(WSe(r))return k4;if(v4(r)){var e=typeof r.valueOf=="function"?r.valueOf():r;r=v4(e)?e+"":e}if(typeof r!="string")return r===0?r:+r;r=JSe(r);var t=_Se.test(r);return t||VSe.test(r)?XSe(r.slice(2),t?2:8):zSe.test(r)?k4:+r}S4.exports=ZSe});var R4=w((Grt,P4)=>{var $Se=Fn(),nx=l4(),D4=x4(),eve="Expected a function",tve=Math.max,rve=Math.min;function ive(r,e,t){var i,n,s,o,a,l,c=0,u=!1,g=!1,f=!0;if(typeof r!="function")throw new TypeError(eve);e=D4(e)||0,$Se(t)&&(u=!!t.leading,g="maxWait"in t,s=g?tve(D4(t.maxWait)||0,e):s,f="trailing"in t?!!t.trailing:f);function h(q){var $=i,z=n;return i=n=void 0,c=q,o=r.apply(z,$),o}function p(q){return c=q,a=setTimeout(b,e),u?h(q):o}function m(q){var $=q-l,z=q-c,ne=e-$;return g?rve(ne,s-z):ne}function y(q){var $=q-l,z=q-c;return l===void 0||$>=e||$<0||g&&z>=s}function b(){var q=nx();if(y(q))return v(q);a=setTimeout(b,m(q))}function v(q){return a=void 0,f&&i?h(q):(i=n=void 0,o)}function k(){a!==void 0&&clearTimeout(a),c=0,i=l=n=a=void 0}function T(){return a===void 0?o:v(nx())}function Y(){var q=nx(),$=y(q);if(i=arguments,n=this,l=q,$){if(a===void 0)return p(l);if(g)return clearTimeout(a),a=setTimeout(b,e),h(l)}return a===void 0&&(a=setTimeout(b,e)),o}return Y.cancel=k,Y.flush=T,Y}P4.exports=ive});var N4=w((Yrt,F4)=>{var nve=R4(),sve=Fn(),ove="Expected a function";function ave(r,e,t){var i=!0,n=!0;if(typeof r!="function")throw new TypeError(ove);return sve(t)&&(i="leading"in t?!!t.leading:i,n="trailing"in t?!!t.trailing:n),nve(r,e,{leading:i,maxWait:e,trailing:n})}F4.exports=ave});var nA=w((iA,bw)=>{"use strict";Object.defineProperty(iA,"__esModule",{value:!0});var j4=["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array","BigInt64Array","BigUint64Array"];function Ive(r){return j4.includes(r)}var yve=["Function","Generator","AsyncGenerator","GeneratorFunction","AsyncGeneratorFunction","AsyncFunction","Observable","Array","Buffer","Object","RegExp","Date","Error","Map","Set","WeakMap","WeakSet","ArrayBuffer","SharedArrayBuffer","DataView","Promise","URL","FormData","URLSearchParams","HTMLElement",...j4];function wve(r){return yve.includes(r)}var Bve=["null","undefined","string","number","bigint","boolean","symbol"];function bve(r){return Bve.includes(r)}function Zg(r){return e=>typeof e===r}var{toString:G4}=Object.prototype,kd=r=>{let e=G4.call(r).slice(8,-1);if(/HTML\w+Element/.test(e)&&W.domElement(r))return"HTMLElement";if(wve(e))return e},hr=r=>e=>kd(e)===r;function W(r){if(r===null)return"null";switch(typeof r){case"undefined":return"undefined";case"string":return"string";case"number":return"number";case"boolean":return"boolean";case"function":return"Function";case"bigint":return"bigint";case"symbol":return"symbol";default:}if(W.observable(r))return"Observable";if(W.array(r))return"Array";if(W.buffer(r))return"Buffer";let e=kd(r);if(e)return e;if(r instanceof String||r instanceof Boolean||r instanceof Number)throw new TypeError("Please don't use object wrappers for primitive types");return"Object"}W.undefined=Zg("undefined");W.string=Zg("string");var Qve=Zg("number");W.number=r=>Qve(r)&&!W.nan(r);W.bigint=Zg("bigint");W.function_=Zg("function");W.null_=r=>r===null;W.class_=r=>W.function_(r)&&r.toString().startsWith("class ");W.boolean=r=>r===!0||r===!1;W.symbol=Zg("symbol");W.numericString=r=>W.string(r)&&!W.emptyStringOrWhitespace(r)&&!Number.isNaN(Number(r));W.array=(r,e)=>Array.isArray(r)?W.function_(e)?r.every(e):!0:!1;W.buffer=r=>{var e,t,i,n;return(n=(i=(t=(e=r)===null||e===void 0?void 0:e.constructor)===null||t===void 0?void 0:t.isBuffer)===null||i===void 0?void 0:i.call(t,r))!==null&&n!==void 0?n:!1};W.nullOrUndefined=r=>W.null_(r)||W.undefined(r);W.object=r=>!W.null_(r)&&(typeof r=="object"||W.function_(r));W.iterable=r=>{var e;return W.function_((e=r)===null||e===void 0?void 0:e[Symbol.iterator])};W.asyncIterable=r=>{var e;return W.function_((e=r)===null||e===void 0?void 0:e[Symbol.asyncIterator])};W.generator=r=>W.iterable(r)&&W.function_(r.next)&&W.function_(r.throw);W.asyncGenerator=r=>W.asyncIterable(r)&&W.function_(r.next)&&W.function_(r.throw);W.nativePromise=r=>hr("Promise")(r);var Sve=r=>{var e,t;return W.function_((e=r)===null||e===void 0?void 0:e.then)&&W.function_((t=r)===null||t===void 0?void 0:t.catch)};W.promise=r=>W.nativePromise(r)||Sve(r);W.generatorFunction=hr("GeneratorFunction");W.asyncGeneratorFunction=r=>kd(r)==="AsyncGeneratorFunction";W.asyncFunction=r=>kd(r)==="AsyncFunction";W.boundFunction=r=>W.function_(r)&&!r.hasOwnProperty("prototype");W.regExp=hr("RegExp");W.date=hr("Date");W.error=hr("Error");W.map=r=>hr("Map")(r);W.set=r=>hr("Set")(r);W.weakMap=r=>hr("WeakMap")(r);W.weakSet=r=>hr("WeakSet")(r);W.int8Array=hr("Int8Array");W.uint8Array=hr("Uint8Array");W.uint8ClampedArray=hr("Uint8ClampedArray");W.int16Array=hr("Int16Array");W.uint16Array=hr("Uint16Array");W.int32Array=hr("Int32Array");W.uint32Array=hr("Uint32Array");W.float32Array=hr("Float32Array");W.float64Array=hr("Float64Array");W.bigInt64Array=hr("BigInt64Array");W.bigUint64Array=hr("BigUint64Array");W.arrayBuffer=hr("ArrayBuffer");W.sharedArrayBuffer=hr("SharedArrayBuffer");W.dataView=hr("DataView");W.directInstanceOf=(r,e)=>Object.getPrototypeOf(r)===e.prototype;W.urlInstance=r=>hr("URL")(r);W.urlString=r=>{if(!W.string(r))return!1;try{return new URL(r),!0}catch(e){return!1}};W.truthy=r=>Boolean(r);W.falsy=r=>!r;W.nan=r=>Number.isNaN(r);W.primitive=r=>W.null_(r)||bve(typeof r);W.integer=r=>Number.isInteger(r);W.safeInteger=r=>Number.isSafeInteger(r);W.plainObject=r=>{if(G4.call(r)!=="[object Object]")return!1;let e=Object.getPrototypeOf(r);return e===null||e===Object.getPrototypeOf({})};W.typedArray=r=>Ive(kd(r));var vve=r=>W.safeInteger(r)&&r>=0;W.arrayLike=r=>!W.nullOrUndefined(r)&&!W.function_(r)&&vve(r.length);W.inRange=(r,e)=>{if(W.number(e))return r>=Math.min(0,e)&&r<=Math.max(e,0);if(W.array(e)&&e.length===2)return r>=Math.min(...e)&&r<=Math.max(...e);throw new TypeError(`Invalid range: ${JSON.stringify(e)}`)};var kve=1,xve=["innerHTML","ownerDocument","style","attributes","nodeValue"];W.domElement=r=>W.object(r)&&r.nodeType===kve&&W.string(r.nodeName)&&!W.plainObject(r)&&xve.every(e=>e in r);W.observable=r=>{var e,t,i,n;return r?r===((t=(e=r)[Symbol.observable])===null||t===void 0?void 0:t.call(e))||r===((n=(i=r)["@@observable"])===null||n===void 0?void 0:n.call(i)):!1};W.nodeStream=r=>W.object(r)&&W.function_(r.pipe)&&!W.observable(r);W.infinite=r=>r===Infinity||r===-Infinity;var Y4=r=>e=>W.integer(e)&&Math.abs(e%2)===r;W.evenInteger=Y4(0);W.oddInteger=Y4(1);W.emptyArray=r=>W.array(r)&&r.length===0;W.nonEmptyArray=r=>W.array(r)&&r.length>0;W.emptyString=r=>W.string(r)&&r.length===0;W.nonEmptyString=r=>W.string(r)&&r.length>0;var Pve=r=>W.string(r)&&!/\S/.test(r);W.emptyStringOrWhitespace=r=>W.emptyString(r)||Pve(r);W.emptyObject=r=>W.object(r)&&!W.map(r)&&!W.set(r)&&Object.keys(r).length===0;W.nonEmptyObject=r=>W.object(r)&&!W.map(r)&&!W.set(r)&&Object.keys(r).length>0;W.emptySet=r=>W.set(r)&&r.size===0;W.nonEmptySet=r=>W.set(r)&&r.size>0;W.emptyMap=r=>W.map(r)&&r.size===0;W.nonEmptyMap=r=>W.map(r)&&r.size>0;W.propertyKey=r=>W.any([W.string,W.number,W.symbol],r);W.formData=r=>hr("FormData")(r);W.urlSearchParams=r=>hr("URLSearchParams")(r);var q4=(r,e,t)=>{if(!W.function_(e))throw new TypeError(`Invalid predicate: ${JSON.stringify(e)}`);if(t.length===0)throw new TypeError("Invalid number of values");return r.call(t,e)};W.any=(r,...e)=>(W.array(r)?r:[r]).some(i=>q4(Array.prototype.some,i,e));W.all=(r,...e)=>q4(Array.prototype.every,r,e);var We=(r,e,t,i={})=>{if(!r){let{multipleValues:n}=i,s=n?`received values of types ${[...new Set(t.map(o=>`\`${W(o)}\``))].join(", ")}`:`received value of type \`${W(t)}\``;throw new TypeError(`Expected value which is \`${e}\`, ${s}.`)}};iA.assert={undefined:r=>We(W.undefined(r),"undefined",r),string:r=>We(W.string(r),"string",r),number:r=>We(W.number(r),"number",r),bigint:r=>We(W.bigint(r),"bigint",r),function_:r=>We(W.function_(r),"Function",r),null_:r=>We(W.null_(r),"null",r),class_:r=>We(W.class_(r),"Class",r),boolean:r=>We(W.boolean(r),"boolean",r),symbol:r=>We(W.symbol(r),"symbol",r),numericString:r=>We(W.numericString(r),"string with a number",r),array:(r,e)=>{We(W.array(r),"Array",r),e&&r.forEach(e)},buffer:r=>We(W.buffer(r),"Buffer",r),nullOrUndefined:r=>We(W.nullOrUndefined(r),"null or undefined",r),object:r=>We(W.object(r),"Object",r),iterable:r=>We(W.iterable(r),"Iterable",r),asyncIterable:r=>We(W.asyncIterable(r),"AsyncIterable",r),generator:r=>We(W.generator(r),"Generator",r),asyncGenerator:r=>We(W.asyncGenerator(r),"AsyncGenerator",r),nativePromise:r=>We(W.nativePromise(r),"native Promise",r),promise:r=>We(W.promise(r),"Promise",r),generatorFunction:r=>We(W.generatorFunction(r),"GeneratorFunction",r),asyncGeneratorFunction:r=>We(W.asyncGeneratorFunction(r),"AsyncGeneratorFunction",r),asyncFunction:r=>We(W.asyncFunction(r),"AsyncFunction",r),boundFunction:r=>We(W.boundFunction(r),"Function",r),regExp:r=>We(W.regExp(r),"RegExp",r),date:r=>We(W.date(r),"Date",r),error:r=>We(W.error(r),"Error",r),map:r=>We(W.map(r),"Map",r),set:r=>We(W.set(r),"Set",r),weakMap:r=>We(W.weakMap(r),"WeakMap",r),weakSet:r=>We(W.weakSet(r),"WeakSet",r),int8Array:r=>We(W.int8Array(r),"Int8Array",r),uint8Array:r=>We(W.uint8Array(r),"Uint8Array",r),uint8ClampedArray:r=>We(W.uint8ClampedArray(r),"Uint8ClampedArray",r),int16Array:r=>We(W.int16Array(r),"Int16Array",r),uint16Array:r=>We(W.uint16Array(r),"Uint16Array",r),int32Array:r=>We(W.int32Array(r),"Int32Array",r),uint32Array:r=>We(W.uint32Array(r),"Uint32Array",r),float32Array:r=>We(W.float32Array(r),"Float32Array",r),float64Array:r=>We(W.float64Array(r),"Float64Array",r),bigInt64Array:r=>We(W.bigInt64Array(r),"BigInt64Array",r),bigUint64Array:r=>We(W.bigUint64Array(r),"BigUint64Array",r),arrayBuffer:r=>We(W.arrayBuffer(r),"ArrayBuffer",r),sharedArrayBuffer:r=>We(W.sharedArrayBuffer(r),"SharedArrayBuffer",r),dataView:r=>We(W.dataView(r),"DataView",r),urlInstance:r=>We(W.urlInstance(r),"URL",r),urlString:r=>We(W.urlString(r),"string with a URL",r),truthy:r=>We(W.truthy(r),"truthy",r),falsy:r=>We(W.falsy(r),"falsy",r),nan:r=>We(W.nan(r),"NaN",r),primitive:r=>We(W.primitive(r),"primitive",r),integer:r=>We(W.integer(r),"integer",r),safeInteger:r=>We(W.safeInteger(r),"integer",r),plainObject:r=>We(W.plainObject(r),"plain object",r),typedArray:r=>We(W.typedArray(r),"TypedArray",r),arrayLike:r=>We(W.arrayLike(r),"array-like",r),domElement:r=>We(W.domElement(r),"HTMLElement",r),observable:r=>We(W.observable(r),"Observable",r),nodeStream:r=>We(W.nodeStream(r),"Node.js Stream",r),infinite:r=>We(W.infinite(r),"infinite number",r),emptyArray:r=>We(W.emptyArray(r),"empty array",r),nonEmptyArray:r=>We(W.nonEmptyArray(r),"non-empty array",r),emptyString:r=>We(W.emptyString(r),"empty string",r),nonEmptyString:r=>We(W.nonEmptyString(r),"non-empty string",r),emptyStringOrWhitespace:r=>We(W.emptyStringOrWhitespace(r),"empty string or whitespace",r),emptyObject:r=>We(W.emptyObject(r),"empty object",r),nonEmptyObject:r=>We(W.nonEmptyObject(r),"non-empty object",r),emptySet:r=>We(W.emptySet(r),"empty set",r),nonEmptySet:r=>We(W.nonEmptySet(r),"non-empty set",r),emptyMap:r=>We(W.emptyMap(r),"empty map",r),nonEmptyMap:r=>We(W.nonEmptyMap(r),"non-empty map",r),propertyKey:r=>We(W.propertyKey(r),"PropertyKey",r),formData:r=>We(W.formData(r),"FormData",r),urlSearchParams:r=>We(W.urlSearchParams(r),"URLSearchParams",r),evenInteger:r=>We(W.evenInteger(r),"even integer",r),oddInteger:r=>We(W.oddInteger(r),"odd integer",r),directInstanceOf:(r,e)=>We(W.directInstanceOf(r,e),"T",r),inRange:(r,e)=>We(W.inRange(r,e),"in range",r),any:(r,...e)=>We(W.any(r,...e),"predicate returns truthy for any value",e,{multipleValues:!0}),all:(r,...e)=>We(W.all(r,...e),"predicate returns truthy for all values",e,{multipleValues:!0})};Object.defineProperties(W,{class:{value:W.class_},function:{value:W.function_},null:{value:W.null_}});Object.defineProperties(iA.assert,{class:{value:iA.assert.class_},function:{value:iA.assert.function_},null:{value:iA.assert.null_}});iA.default=W;bw.exports=W;bw.exports.default=W;bw.exports.assert=iA.assert});var J4=w((_it,bx)=>{"use strict";var Qx=class extends Error{constructor(e){super(e||"Promise was canceled");this.name="CancelError"}get isCanceled(){return!0}},xd=class{static fn(e){return(...t)=>new xd((i,n,s)=>{t.push(s),e(...t).then(i,n)})}constructor(e){this._cancelHandlers=[],this._isPending=!0,this._isCanceled=!1,this._rejectOnCancel=!0,this._promise=new Promise((t,i)=>{this._reject=i;let n=a=>{this._isPending=!1,t(a)},s=a=>{this._isPending=!1,i(a)},o=a=>{if(!this._isPending)throw new Error("The `onCancel` handler was attached after the promise settled.");this._cancelHandlers.push(a)};return Object.defineProperties(o,{shouldReject:{get:()=>this._rejectOnCancel,set:a=>{this._rejectOnCancel=a}}}),e(n,s,o)})}then(e,t){return this._promise.then(e,t)}catch(e){return this._promise.catch(e)}finally(e){return this._promise.finally(e)}cancel(e){if(!(!this._isPending||this._isCanceled)){if(this._cancelHandlers.length>0)try{for(let t of this._cancelHandlers)t()}catch(t){this._reject(t)}this._isCanceled=!0,this._rejectOnCancel&&this._reject(new Qx(e))}}get isCanceled(){return this._isCanceled}};Object.setPrototypeOf(xd.prototype,Promise.prototype);bx.exports=xd;bx.exports.CancelError=Qx});var W4=w((Sx,vx)=>{"use strict";Object.defineProperty(Sx,"__esModule",{value:!0});var Dve=require("tls"),kx=(r,e)=>{let t;typeof e=="function"?t={connect:e}:t=e;let i=typeof t.connect=="function",n=typeof t.secureConnect=="function",s=typeof t.close=="function",o=()=>{i&&t.connect(),r instanceof Dve.TLSSocket&&n&&(r.authorized?t.secureConnect():r.authorizationError||r.once("secureConnect",t.secureConnect)),s&&r.once("close",t.close)};r.writable&&!r.connecting?o():r.connecting?r.once("connect",o):r.destroyed&&s&&t.close(r._hadError)};Sx.default=kx;vx.exports=kx;vx.exports.default=kx});var z4=w((xx,Px)=>{"use strict";Object.defineProperty(xx,"__esModule",{value:!0});var Rve=W4(),Fve=Number(process.versions.node.split(".")[0]),Dx=r=>{let e={start:Date.now(),socket:void 0,lookup:void 0,connect:void 0,secureConnect:void 0,upload:void 0,response:void 0,end:void 0,error:void 0,abort:void 0,phases:{wait:void 0,dns:void 0,tcp:void 0,tls:void 0,request:void 0,firstByte:void 0,download:void 0,total:void 0}};r.timings=e;let t=o=>{let a=o.emit.bind(o);o.emit=(l,...c)=>(l==="error"&&(e.error=Date.now(),e.phases.total=e.error-e.start,o.emit=a),a(l,...c))};t(r),r.prependOnceListener("abort",()=>{e.abort=Date.now(),(!e.response||Fve>=13)&&(e.phases.total=Date.now()-e.start)});let i=o=>{e.socket=Date.now(),e.phases.wait=e.socket-e.start;let a=()=>{e.lookup=Date.now(),e.phases.dns=e.lookup-e.socket};o.prependOnceListener("lookup",a),Rve.default(o,{connect:()=>{e.connect=Date.now(),e.lookup===void 0&&(o.removeListener("lookup",a),e.lookup=e.connect,e.phases.dns=e.lookup-e.socket),e.phases.tcp=e.connect-e.lookup},secureConnect:()=>{e.secureConnect=Date.now(),e.phases.tls=e.secureConnect-e.connect}})};r.socket?i(r.socket):r.prependOnceListener("socket",i);let n=()=>{var o;e.upload=Date.now(),e.phases.request=e.upload-(o=e.secureConnect,o!=null?o:e.connect)};return(()=>typeof r.writableFinished=="boolean"?r.writableFinished:r.finished&&r.outputSize===0&&(!r.socket||r.socket.writableLength===0))()?n():r.prependOnceListener("finish",n),r.prependOnceListener("response",o=>{e.response=Date.now(),e.phases.firstByte=e.response-e.upload,o.timings=e,t(o),o.prependOnceListener("end",()=>{e.end=Date.now(),e.phases.download=e.end-e.response,e.phases.total=e.end-e.start})}),e};xx.default=Dx;Px.exports=Dx;Px.exports.default=Dx});var tz=w((Vit,Rx)=>{"use strict";var{V4MAPPED:Nve,ADDRCONFIG:Lve,ALL:_4,promises:{Resolver:V4},lookup:Tve}=require("dns"),{promisify:Fx}=require("util"),Ove=require("os"),$g=Symbol("cacheableLookupCreateConnection"),Nx=Symbol("cacheableLookupInstance"),X4=Symbol("expires"),Mve=typeof _4=="number",Z4=r=>{if(!(r&&typeof r.createConnection=="function"))throw new Error("Expected an Agent instance as the first argument")},Uve=r=>{for(let e of r)e.family!==6&&(e.address=`::ffff:${e.address}`,e.family=6)},$4=()=>{let r=!1,e=!1;for(let t of Object.values(Ove.networkInterfaces()))for(let i of t)if(!i.internal&&(i.family==="IPv6"?e=!0:r=!0,r&&e))return{has4:r,has6:e};return{has4:r,has6:e}},Kve=r=>Symbol.iterator in r,ez={ttl:!0},Hve={all:!0},Lx=class{constructor({cache:e=new Map,maxTtl:t=Infinity,fallbackDuration:i=3600,errorTtl:n=.15,resolver:s=new V4,lookup:o=Tve}={}){if(this.maxTtl=t,this.errorTtl=n,this._cache=e,this._resolver=s,this._dnsLookup=Fx(o),this._resolver instanceof V4?(this._resolve4=this._resolver.resolve4.bind(this._resolver),this._resolve6=this._resolver.resolve6.bind(this._resolver)):(this._resolve4=Fx(this._resolver.resolve4.bind(this._resolver)),this._resolve6=Fx(this._resolver.resolve6.bind(this._resolver))),this._iface=$4(),this._pending={},this._nextRemovalTime=!1,this._hostnamesToFallback=new Set,i<1)this._fallback=!1;else{this._fallback=!0;let a=setInterval(()=>{this._hostnamesToFallback.clear()},i*1e3);a.unref&&a.unref()}this.lookup=this.lookup.bind(this),this.lookupAsync=this.lookupAsync.bind(this)}set servers(e){this.clear(),this._resolver.setServers(e)}get servers(){return this._resolver.getServers()}lookup(e,t,i){if(typeof t=="function"?(i=t,t={}):typeof t=="number"&&(t={family:t}),!i)throw new Error("Callback must be a function.");this.lookupAsync(e,t).then(n=>{t.all?i(null,n):i(null,n.address,n.family,n.expires,n.ttl)},i)}async lookupAsync(e,t={}){typeof t=="number"&&(t={family:t});let i=await this.query(e);if(t.family===6){let n=i.filter(s=>s.family===6);t.hints&Nve&&(Mve&&t.hints&_4||n.length===0)?Uve(i):i=n}else t.family===4&&(i=i.filter(n=>n.family===4));if(t.hints&Lve){let{_iface:n}=this;i=i.filter(s=>s.family===6?n.has6:n.has4)}if(i.length===0){let n=new Error(`cacheableLookup ENOTFOUND ${e}`);throw n.code="ENOTFOUND",n.hostname=e,n}return t.all?i:i[0]}async query(e){let t=await this._cache.get(e);if(!t){let i=this._pending[e];if(i)t=await i;else{let n=this.queryAndCache(e);this._pending[e]=n,t=await n}}return t=t.map(i=>N({},i)),t}async _resolve(e){let t=async c=>{try{return await c}catch(u){if(u.code==="ENODATA"||u.code==="ENOTFOUND")return[];throw u}},[i,n]=await Promise.all([this._resolve4(e,ez),this._resolve6(e,ez)].map(c=>t(c))),s=0,o=0,a=0,l=Date.now();for(let c of i)c.family=4,c.expires=l+c.ttl*1e3,s=Math.max(s,c.ttl);for(let c of n)c.family=6,c.expires=l+c.ttl*1e3,o=Math.max(o,c.ttl);return i.length>0?n.length>0?a=Math.min(s,o):a=s:a=o,{entries:[...i,...n],cacheTtl:a}}async _lookup(e){try{return{entries:await this._dnsLookup(e,{all:!0}),cacheTtl:0}}catch(t){return{entries:[],cacheTtl:0}}}async _set(e,t,i){if(this.maxTtl>0&&i>0){i=Math.min(i,this.maxTtl)*1e3,t[X4]=Date.now()+i;try{await this._cache.set(e,t,i)}catch(n){this.lookupAsync=async()=>{let s=new Error("Cache Error. Please recreate the CacheableLookup instance.");throw s.cause=n,s}}Kve(this._cache)&&this._tick(i)}}async queryAndCache(e){if(this._hostnamesToFallback.has(e))return this._dnsLookup(e,Hve);try{let t=await this._resolve(e);t.entries.length===0&&this._fallback&&(t=await this._lookup(e),t.entries.length!==0&&this._hostnamesToFallback.add(e));let i=t.entries.length===0?this.errorTtl:t.cacheTtl;return await this._set(e,t.entries,i),delete this._pending[e],t.entries}catch(t){throw delete this._pending[e],t}}_tick(e){let t=this._nextRemovalTime;(!t||e{this._nextRemovalTime=!1;let i=Infinity,n=Date.now();for(let[s,o]of this._cache){let a=o[X4];n>=a?this._cache.delete(s):a("lookup"in t||(t.lookup=this.lookup),e[$g](t,i))}uninstall(e){if(Z4(e),e[$g]){if(e[Nx]!==this)throw new Error("The agent is not owned by this CacheableLookup instance");e.createConnection=e[$g],delete e[$g],delete e[Nx]}}updateInterfaceInfo(){let{_iface:e}=this;this._iface=$4(),(e.has4&&!this._iface.has4||e.has6&&!this._iface.has6)&&this._cache.clear()}clear(e){if(e){this._cache.delete(e);return}this._cache.clear()}};Rx.exports=Lx;Rx.exports.default=Lx});var nz=w((Xit,Tx)=>{"use strict";var jve=typeof URL=="undefined"?require("url").URL:URL,Gve="text/plain",Yve="us-ascii",rz=(r,e)=>e.some(t=>t instanceof RegExp?t.test(r):t===r),qve=(r,{stripHash:e})=>{let t=r.match(/^data:([^,]*?),([^#]*?)(?:#(.*))?$/);if(!t)throw new Error(`Invalid URL: ${r}`);let i=t[1].split(";"),n=t[2],s=e?"":t[3],o=!1;i[i.length-1]==="base64"&&(i.pop(),o=!0);let a=(i.shift()||"").toLowerCase(),c=[...i.map(u=>{let[g,f=""]=u.split("=").map(h=>h.trim());return g==="charset"&&(f=f.toLowerCase(),f===Yve)?"":`${g}${f?`=${f}`:""}`}).filter(Boolean)];return o&&c.push("base64"),(c.length!==0||a&&a!==Gve)&&c.unshift(a),`data:${c.join(";")},${o?n.trim():n}${s?`#${s}`:""}`},iz=(r,e)=>{if(e=N({defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0},e),Reflect.has(e,"normalizeHttps"))throw new Error("options.normalizeHttps is renamed to options.forceHttp");if(Reflect.has(e,"normalizeHttp"))throw new Error("options.normalizeHttp is renamed to options.forceHttps");if(Reflect.has(e,"stripFragment"))throw new Error("options.stripFragment is renamed to options.stripHash");if(r=r.trim(),/^data:/i.test(r))return qve(r,e);let t=r.startsWith("//");!t&&/^\.*\//.test(r)||(r=r.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,e.defaultProtocol));let n=new jve(r);if(e.forceHttp&&e.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(e.forceHttp&&n.protocol==="https:"&&(n.protocol="http:"),e.forceHttps&&n.protocol==="http:"&&(n.protocol="https:"),e.stripAuthentication&&(n.username="",n.password=""),e.stripHash&&(n.hash=""),n.pathname&&(n.pathname=n.pathname.replace(/((?!:).|^)\/{2,}/g,(s,o)=>/^(?!\/)/g.test(o)?`${o}/`:"/")),n.pathname&&(n.pathname=decodeURI(n.pathname)),e.removeDirectoryIndex===!0&&(e.removeDirectoryIndex=[/^index\.[a-z]+$/]),Array.isArray(e.removeDirectoryIndex)&&e.removeDirectoryIndex.length>0){let s=n.pathname.split("/"),o=s[s.length-1];rz(o,e.removeDirectoryIndex)&&(s=s.slice(0,s.length-1),n.pathname=s.slice(1).join("/")+"/")}if(n.hostname&&(n.hostname=n.hostname.replace(/\.$/,""),e.stripWWW&&/^www\.([a-z\-\d]{2,63})\.([a-z.]{2,5})$/.test(n.hostname)&&(n.hostname=n.hostname.replace(/^www\./,""))),Array.isArray(e.removeQueryParameters))for(let s of[...n.searchParams.keys()])rz(s,e.removeQueryParameters)&&n.searchParams.delete(s);return e.sortQueryParameters&&n.searchParams.sort(),e.removeTrailingSlash&&(n.pathname=n.pathname.replace(/\/$/,"")),r=n.toString(),(e.removeTrailingSlash||n.pathname==="/")&&n.hash===""&&(r=r.replace(/\/$/,"")),t&&!e.normalizeProtocol&&(r=r.replace(/^http:\/\//,"//")),e.stripProtocol&&(r=r.replace(/^(?:https?:)?\/\//,"")),r};Tx.exports=iz;Tx.exports.default=iz});var az=w((Zit,sz)=>{sz.exports=oz;function oz(r,e){if(r&&e)return oz(r)(e);if(typeof r!="function")throw new TypeError("need wrapper function");return Object.keys(r).forEach(function(i){t[i]=r[i]}),t;function t(){for(var i=new Array(arguments.length),n=0;n{var Az=az();Ox.exports=Az(Qw);Ox.exports.strict=Az(lz);Qw.proto=Qw(function(){Object.defineProperty(Function.prototype,"once",{value:function(){return Qw(this)},configurable:!0}),Object.defineProperty(Function.prototype,"onceStrict",{value:function(){return lz(this)},configurable:!0})});function Qw(r){var e=function(){return e.called?e.value:(e.called=!0,e.value=r.apply(this,arguments))};return e.called=!1,e}function lz(r){var e=function(){if(e.called)throw new Error(e.onceError);return e.called=!0,e.value=r.apply(this,arguments)},t=r.name||"Function wrapped with `once`";return e.onceError=t+" shouldn't be called more than once",e.called=!1,e}});var Ux=w((ent,cz)=>{var Jve=Mx(),Wve=function(){},zve=function(r){return r.setHeader&&typeof r.abort=="function"},_ve=function(r){return r.stdio&&Array.isArray(r.stdio)&&r.stdio.length===3},uz=function(r,e,t){if(typeof e=="function")return uz(r,null,e);e||(e={}),t=Jve(t||Wve);var i=r._writableState,n=r._readableState,s=e.readable||e.readable!==!1&&r.readable,o=e.writable||e.writable!==!1&&r.writable,a=function(){r.writable||l()},l=function(){o=!1,s||t.call(r)},c=function(){s=!1,o||t.call(r)},u=function(p){t.call(r,p?new Error("exited with error code: "+p):null)},g=function(p){t.call(r,p)},f=function(){if(s&&!(n&&n.ended))return t.call(r,new Error("premature close"));if(o&&!(i&&i.ended))return t.call(r,new Error("premature close"))},h=function(){r.req.on("finish",l)};return zve(r)?(r.on("complete",l),r.on("abort",f),r.req?h():r.on("request",h)):o&&!i&&(r.on("end",a),r.on("close",a)),_ve(r)&&r.on("exit",u),r.on("end",c),r.on("finish",l),e.error!==!1&&r.on("error",g),r.on("close",f),function(){r.removeListener("complete",l),r.removeListener("abort",f),r.removeListener("request",h),r.req&&r.req.removeListener("finish",l),r.removeListener("end",a),r.removeListener("close",a),r.removeListener("finish",l),r.removeListener("exit",u),r.removeListener("end",c),r.removeListener("error",g),r.removeListener("close",f)}};cz.exports=uz});var hz=w((tnt,gz)=>{var Vve=Mx(),Xve=Ux(),Kx=require("fs"),Pd=function(){},Zve=/^v?\.0/.test(process.version),Sw=function(r){return typeof r=="function"},$ve=function(r){return!Zve||!Kx?!1:(r instanceof(Kx.ReadStream||Pd)||r instanceof(Kx.WriteStream||Pd))&&Sw(r.close)},eke=function(r){return r.setHeader&&Sw(r.abort)},tke=function(r,e,t,i){i=Vve(i);var n=!1;r.on("close",function(){n=!0}),Xve(r,{readable:e,writable:t},function(o){if(o)return i(o);n=!0,i()});var s=!1;return function(o){if(!n&&!s){if(s=!0,$ve(r))return r.close(Pd);if(eke(r))return r.abort();if(Sw(r.destroy))return r.destroy();i(o||new Error("stream was destroyed"))}}},fz=function(r){r()},rke=function(r,e){return r.pipe(e)},ike=function(){var r=Array.prototype.slice.call(arguments),e=Sw(r[r.length-1]||Pd)&&r.pop()||Pd;if(Array.isArray(r[0])&&(r=r[0]),r.length<2)throw new Error("pump requires two streams per minimum");var t,i=r.map(function(n,s){var o=s0;return tke(n,o,a,function(l){t||(t=l),l&&i.forEach(fz),!o&&(i.forEach(fz),e(t))})});return r.reduce(rke)};gz.exports=ike});var dz=w((rnt,pz)=>{"use strict";var{PassThrough:nke}=require("stream");pz.exports=r=>{r=N({},r);let{array:e}=r,{encoding:t}=r,i=t==="buffer",n=!1;e?n=!(t||i):t=t||"utf8",i&&(t=null);let s=new nke({objectMode:n});t&&s.setEncoding(t);let o=0,a=[];return s.on("data",l=>{a.push(l),n?o=a.length:o+=l.length}),s.getBufferedValue=()=>e?a:i?Buffer.concat(a,o):a.join(""),s.getBufferedLength=()=>o,s}});var Cz=w((int,ef)=>{"use strict";var ske=hz(),oke=dz(),Hx=class extends Error{constructor(){super("maxBuffer exceeded");this.name="MaxBufferError"}};async function vw(r,e){if(!r)return Promise.reject(new Error("Expected a stream"));e=N({maxBuffer:Infinity},e);let{maxBuffer:t}=e,i;return await new Promise((n,s)=>{let o=a=>{a&&(a.bufferedData=i.getBufferedValue()),s(a)};i=ske(r,oke(e),a=>{if(a){o(a);return}n()}),i.on("data",()=>{i.getBufferedLength()>t&&o(new Hx)})}),i.getBufferedValue()}ef.exports=vw;ef.exports.default=vw;ef.exports.buffer=(r,e)=>vw(r,te(N({},e),{encoding:"buffer"}));ef.exports.array=(r,e)=>vw(r,te(N({},e),{array:!0}));ef.exports.MaxBufferError=Hx});var Ez=w((snt,mz)=>{"use strict";var ake=[200,203,204,206,300,301,404,405,410,414,501],Ake=[200,203,204,300,301,302,303,307,308,404,405,410,414,501],lke={date:!0,connection:!0,"keep-alive":!0,"proxy-authenticate":!0,"proxy-authorization":!0,te:!0,trailer:!0,"transfer-encoding":!0,upgrade:!0},cke={"content-length":!0,"content-encoding":!0,"transfer-encoding":!0,"content-range":!0};function jx(r){let e={};if(!r)return e;let t=r.trim().split(/\s*,\s*/);for(let i of t){let[n,s]=i.split(/\s*=\s*/,2);e[n]=s===void 0?!0:s.replace(/^"|"$/g,"")}return e}function uke(r){let e=[];for(let t in r){let i=r[t];e.push(i===!0?t:t+"="+i)}if(!!e.length)return e.join(", ")}mz.exports=class{constructor(e,t,{shared:i,cacheHeuristic:n,immutableMinTimeToLive:s,ignoreCargoCult:o,trustServerDate:a,_fromObject:l}={}){if(l){this._fromObject(l);return}if(!t||!t.headers)throw Error("Response headers missing");this._assertRequestHasHeaders(e),this._responseTime=this.now(),this._isShared=i!==!1,this._trustServerDate=a!==void 0?a:!0,this._cacheHeuristic=n!==void 0?n:.1,this._immutableMinTtl=s!==void 0?s:24*3600*1e3,this._status="status"in t?t.status:200,this._resHeaders=t.headers,this._rescc=jx(t.headers["cache-control"]),this._method="method"in e?e.method:"GET",this._url=e.url,this._host=e.headers.host,this._noAuthorization=!e.headers.authorization,this._reqHeaders=t.headers.vary?e.headers:null,this._reqcc=jx(e.headers["cache-control"]),o&&"pre-check"in this._rescc&&"post-check"in this._rescc&&(delete this._rescc["pre-check"],delete this._rescc["post-check"],delete this._rescc["no-cache"],delete this._rescc["no-store"],delete this._rescc["must-revalidate"],this._resHeaders=Object.assign({},this._resHeaders,{"cache-control":uke(this._rescc)}),delete this._resHeaders.expires,delete this._resHeaders.pragma),!t.headers["cache-control"]&&/no-cache/.test(t.headers.pragma)&&(this._rescc["no-cache"]=!0)}now(){return Date.now()}storable(){return!!(!this._reqcc["no-store"]&&(this._method==="GET"||this._method==="HEAD"||this._method==="POST"&&this._hasExplicitExpiration())&&Ake.indexOf(this._status)!==-1&&!this._rescc["no-store"]&&(!this._isShared||!this._rescc.private)&&(!this._isShared||this._noAuthorization||this._allowsStoringAuthenticated())&&(this._resHeaders.expires||this._rescc.public||this._rescc["max-age"]||this._rescc["s-maxage"]||ake.indexOf(this._status)!==-1))}_hasExplicitExpiration(){return this._isShared&&this._rescc["s-maxage"]||this._rescc["max-age"]||this._resHeaders.expires}_assertRequestHasHeaders(e){if(!e||!e.headers)throw Error("Request headers missing")}satisfiesWithoutRevalidation(e){this._assertRequestHasHeaders(e);let t=jx(e.headers["cache-control"]);return t["no-cache"]||/no-cache/.test(e.headers.pragma)||t["max-age"]&&this.age()>t["max-age"]||t["min-fresh"]&&this.timeToLive()<1e3*t["min-fresh"]||this.stale()&&!(t["max-stale"]&&!this._rescc["must-revalidate"]&&(t["max-stale"]===!0||t["max-stale"]>this.age()-this.maxAge()))?!1:this._requestMatches(e,!1)}_requestMatches(e,t){return(!this._url||this._url===e.url)&&this._host===e.headers.host&&(!e.method||this._method===e.method||t&&e.method==="HEAD")&&this._varyMatches(e)}_allowsStoringAuthenticated(){return this._rescc["must-revalidate"]||this._rescc.public||this._rescc["s-maxage"]}_varyMatches(e){if(!this._resHeaders.vary)return!0;if(this._resHeaders.vary==="*")return!1;let t=this._resHeaders.vary.trim().toLowerCase().split(/\s*,\s*/);for(let i of t)if(e.headers[i]!==this._reqHeaders[i])return!1;return!0}_copyWithoutHopByHopHeaders(e){let t={};for(let i in e)lke[i]||(t[i]=e[i]);if(e.connection){let i=e.connection.trim().split(/\s*,\s*/);for(let n of i)delete t[n]}if(t.warning){let i=t.warning.split(/,/).filter(n=>!/^\s*1[0-9][0-9]/.test(n));i.length?t.warning=i.join(",").trim():delete t.warning}return t}responseHeaders(){let e=this._copyWithoutHopByHopHeaders(this._resHeaders),t=this.age();return t>3600*24&&!this._hasExplicitExpiration()&&this.maxAge()>3600*24&&(e.warning=(e.warning?`${e.warning}, `:"")+'113 - "rfc7234 5.5.4"'),e.age=`${Math.round(t)}`,e.date=new Date(this.now()).toUTCString(),e}date(){return this._trustServerDate?this._serverDate():this._responseTime}_serverDate(){let e=Date.parse(this._resHeaders.date);if(isFinite(e)){let t=8*3600*1e3;if(Math.abs(this._responseTime-e)e&&(e=i)}let t=(this.now()-this._responseTime)/1e3;return e+t}_ageValue(){let e=parseInt(this._resHeaders.age);return isFinite(e)?e:0}maxAge(){if(!this.storable()||this._rescc["no-cache"]||this._isShared&&this._resHeaders["set-cookie"]&&!this._rescc.public&&!this._rescc.immutable||this._resHeaders.vary==="*")return 0;if(this._isShared){if(this._rescc["proxy-revalidate"])return 0;if(this._rescc["s-maxage"])return parseInt(this._rescc["s-maxage"],10)}if(this._rescc["max-age"])return parseInt(this._rescc["max-age"],10);let e=this._rescc.immutable?this._immutableMinTtl:0,t=this._serverDate();if(this._resHeaders.expires){let i=Date.parse(this._resHeaders.expires);return Number.isNaN(i)||ii)return Math.max(e,(t-i)/1e3*this._cacheHeuristic)}return e}timeToLive(){return Math.max(0,this.maxAge()-this.age())*1e3}stale(){return this.maxAge()<=this.age()}static fromObject(e){return new this(void 0,void 0,{_fromObject:e})}_fromObject(e){if(this._responseTime)throw Error("Reinitialized");if(!e||e.v!==1)throw Error("Invalid serialization");this._responseTime=e.t,this._isShared=e.sh,this._cacheHeuristic=e.ch,this._immutableMinTtl=e.imm!==void 0?e.imm:24*3600*1e3,this._status=e.st,this._resHeaders=e.resh,this._rescc=e.rescc,this._method=e.m,this._url=e.u,this._host=e.h,this._noAuthorization=e.a,this._reqHeaders=e.reqh,this._reqcc=e.reqcc}toObject(){return{v:1,t:this._responseTime,sh:this._isShared,ch:this._cacheHeuristic,imm:this._immutableMinTtl,st:this._status,resh:this._resHeaders,rescc:this._rescc,m:this._method,u:this._url,h:this._host,a:this._noAuthorization,reqh:this._reqHeaders,reqcc:this._reqcc}}revalidationHeaders(e){this._assertRequestHasHeaders(e);let t=this._copyWithoutHopByHopHeaders(e.headers);if(delete t["if-range"],!this._requestMatches(e,!0)||!this.storable())return delete t["if-none-match"],delete t["if-modified-since"],t;if(this._resHeaders.etag&&(t["if-none-match"]=t["if-none-match"]?`${t["if-none-match"]}, ${this._resHeaders.etag}`:this._resHeaders.etag),t["accept-ranges"]||t["if-match"]||t["if-unmodified-since"]||this._method&&this._method!="GET"){if(delete t["if-modified-since"],t["if-none-match"]){let n=t["if-none-match"].split(/,/).filter(s=>!/^\s*W\//.test(s));n.length?t["if-none-match"]=n.join(",").trim():delete t["if-none-match"]}}else this._resHeaders["last-modified"]&&!t["if-modified-since"]&&(t["if-modified-since"]=this._resHeaders["last-modified"]);return t}revalidatedPolicy(e,t){if(this._assertRequestHasHeaders(e),!t||!t.headers)throw Error("Response headers missing");let i=!1;if(t.status!==void 0&&t.status!=304?i=!1:t.headers.etag&&!/^\s*W\//.test(t.headers.etag)?i=this._resHeaders.etag&&this._resHeaders.etag.replace(/^\s*W\//,"")===t.headers.etag:this._resHeaders.etag&&t.headers.etag?i=this._resHeaders.etag.replace(/^\s*W\//,"")===t.headers.etag.replace(/^\s*W\//,""):this._resHeaders["last-modified"]?i=this._resHeaders["last-modified"]===t.headers["last-modified"]:!this._resHeaders.etag&&!this._resHeaders["last-modified"]&&!t.headers.etag&&!t.headers["last-modified"]&&(i=!0),!i)return{policy:new this.constructor(e,t),modified:t.status!=304,matches:!1};let n={};for(let o in this._resHeaders)n[o]=o in t.headers&&!cke[o]?t.headers[o]:this._resHeaders[o];let s=Object.assign({},t,{status:this._status,method:this._method,headers:n});return{policy:new this.constructor(e,s,{shared:this._isShared,cacheHeuristic:this._cacheHeuristic,immutableMinTimeToLive:this._immutableMinTtl,trustServerDate:this._trustServerDate}),modified:!1,matches:!0}}}});var kw=w((ont,Iz)=>{"use strict";Iz.exports=r=>{let e={};for(let[t,i]of Object.entries(r))e[t.toLowerCase()]=i;return e}});var Bz=w((ant,yz)=>{"use strict";var gke=require("stream").Readable,fke=kw(),wz=class extends gke{constructor(e,t,i,n){if(typeof e!="number")throw new TypeError("Argument `statusCode` should be a number");if(typeof t!="object")throw new TypeError("Argument `headers` should be an object");if(!(i instanceof Buffer))throw new TypeError("Argument `body` should be a buffer");if(typeof n!="string")throw new TypeError("Argument `url` should be a string");super();this.statusCode=e,this.headers=fke(t),this.body=i,this.url=n}_read(){this.push(this.body),this.push(null)}};yz.exports=wz});var Qz=w((Ant,bz)=>{"use strict";var hke=["destroy","setTimeout","socket","headers","trailers","rawHeaders","statusCode","httpVersion","httpVersionMinor","httpVersionMajor","rawTrailers","statusMessage"];bz.exports=(r,e)=>{let t=new Set(Object.keys(r).concat(hke));for(let i of t)i in e||(e[i]=typeof r[i]=="function"?r[i].bind(r):r[i])}});var vz=w((lnt,Sz)=>{"use strict";var pke=require("stream").PassThrough,dke=Qz(),Cke=r=>{if(!(r&&r.pipe))throw new TypeError("Parameter `response` must be a response stream.");let e=new pke;return dke(r,e),r.pipe(e)};Sz.exports=Cke});var kz=w(Gx=>{Gx.stringify=function r(e){if(typeof e=="undefined")return e;if(e&&Buffer.isBuffer(e))return JSON.stringify(":base64:"+e.toString("base64"));if(e&&e.toJSON&&(e=e.toJSON()),e&&typeof e=="object"){var t="",i=Array.isArray(e);t=i?"[":"{";var n=!0;for(var s in e){var o=typeof e[s]=="function"||!i&&typeof e[s]=="undefined";Object.hasOwnProperty.call(e,s)&&!o&&(n||(t+=","),n=!1,i?e[s]==null?t+="null":t+=r(e[s]):e[s]!==void 0&&(t+=r(s)+":"+r(e[s])))}return t+=i?"]":"}",t}else return typeof e=="string"?JSON.stringify(/^:/.test(e)?":"+e:e):typeof e=="undefined"?"null":JSON.stringify(e)};Gx.parse=function(r){return JSON.parse(r,function(e,t){return typeof t=="string"?/^:base64:/.test(t)?Buffer.from(t.substring(8),"base64"):/^:/.test(t)?t.substring(1):t:t})}});var Rz=w((unt,xz)=>{"use strict";var mke=require("events"),Pz=kz(),Eke=r=>{let e={redis:"@keyv/redis",mongodb:"@keyv/mongo",mongo:"@keyv/mongo",sqlite:"@keyv/sqlite",postgresql:"@keyv/postgres",postgres:"@keyv/postgres",mysql:"@keyv/mysql"};if(r.adapter||r.uri){let t=r.adapter||/^[^:]*/.exec(r.uri)[0];return new(require(e[t]))(r)}return new Map},Dz=class extends mke{constructor(e,t){super();if(this.opts=Object.assign({namespace:"keyv",serialize:Pz.stringify,deserialize:Pz.parse},typeof e=="string"?{uri:e}:e,t),!this.opts.store){let i=Object.assign({},this.opts);this.opts.store=Eke(i)}typeof this.opts.store.on=="function"&&this.opts.store.on("error",i=>this.emit("error",i)),this.opts.store.namespace=this.opts.namespace}_getKeyPrefix(e){return`${this.opts.namespace}:${e}`}get(e,t){e=this._getKeyPrefix(e);let{store:i}=this.opts;return Promise.resolve().then(()=>i.get(e)).then(n=>typeof n=="string"?this.opts.deserialize(n):n).then(n=>{if(n!==void 0){if(typeof n.expires=="number"&&Date.now()>n.expires){this.delete(e);return}return t&&t.raw?n:n.value}})}set(e,t,i){e=this._getKeyPrefix(e),typeof i=="undefined"&&(i=this.opts.ttl),i===0&&(i=void 0);let{store:n}=this.opts;return Promise.resolve().then(()=>{let s=typeof i=="number"?Date.now()+i:null;return t={value:t,expires:s},this.opts.serialize(t)}).then(s=>n.set(e,s,i)).then(()=>!0)}delete(e){e=this._getKeyPrefix(e);let{store:t}=this.opts;return Promise.resolve().then(()=>t.delete(e))}clear(){let{store:e}=this.opts;return Promise.resolve().then(()=>e.clear())}};xz.exports=Dz});var Lz=w((gnt,Fz)=>{"use strict";var Ike=require("events"),xw=require("url"),yke=nz(),wke=Cz(),Yx=Ez(),Nz=Bz(),Bke=kw(),bke=vz(),Qke=Rz(),na=class{constructor(e,t){if(typeof e!="function")throw new TypeError("Parameter `request` must be a function");return this.cache=new Qke({uri:typeof t=="string"&&t,store:typeof t!="string"&&t,namespace:"cacheable-request"}),this.createCacheableRequest(e)}createCacheableRequest(e){return(t,i)=>{let n;if(typeof t=="string")n=qx(xw.parse(t)),t={};else if(t instanceof xw.URL)n=qx(xw.parse(t.toString())),t={};else{let[g,...f]=(t.path||"").split("?"),h=f.length>0?`?${f.join("?")}`:"";n=qx(te(N({},t),{pathname:g,search:h}))}t=N(N({headers:{},method:"GET",cache:!0,strictTtl:!1,automaticFailover:!1},t),Ske(n)),t.headers=Bke(t.headers);let s=new Ike,o=yke(xw.format(n),{stripWWW:!1,removeTrailingSlash:!1,stripAuthentication:!1}),a=`${t.method}:${o}`,l=!1,c=!1,u=g=>{c=!0;let f=!1,h,p=new Promise(y=>{h=()=>{f||(f=!0,y())}}),m=y=>{if(l&&!g.forceRefresh){y.status=y.statusCode;let v=Yx.fromObject(l.cachePolicy).revalidatedPolicy(g,y);if(!v.modified){let k=v.policy.responseHeaders();y=new Nz(l.statusCode,k,l.body,l.url),y.cachePolicy=v.policy,y.fromCache=!0}}y.fromCache||(y.cachePolicy=new Yx(g,y,g),y.fromCache=!1);let b;g.cache&&y.cachePolicy.storable()?(b=bke(y),(async()=>{try{let v=wke.buffer(y);if(await Promise.race([p,new Promise(q=>y.once("end",q))]),f)return;let k=await v,T={cachePolicy:y.cachePolicy.toObject(),url:y.url,statusCode:y.fromCache?l.statusCode:y.statusCode,body:k},Y=g.strictTtl?y.cachePolicy.timeToLive():void 0;g.maxTtl&&(Y=Y?Math.min(Y,g.maxTtl):g.maxTtl),await this.cache.set(a,T,Y)}catch(v){s.emit("error",new na.CacheError(v))}})()):g.cache&&l&&(async()=>{try{await this.cache.delete(a)}catch(v){s.emit("error",new na.CacheError(v))}})(),s.emit("response",b||y),typeof i=="function"&&i(b||y)};try{let y=e(g,m);y.once("error",h),y.once("abort",h),s.emit("request",y)}catch(y){s.emit("error",new na.RequestError(y))}};return(async()=>{let g=async h=>{await Promise.resolve();let p=h.cache?await this.cache.get(a):void 0;if(typeof p=="undefined")return u(h);let m=Yx.fromObject(p.cachePolicy);if(m.satisfiesWithoutRevalidation(h)&&!h.forceRefresh){let y=m.responseHeaders(),b=new Nz(p.statusCode,y,p.body,p.url);b.cachePolicy=m,b.fromCache=!0,s.emit("response",b),typeof i=="function"&&i(b)}else l=p,h.headers=m.revalidationHeaders(h),u(h)},f=h=>s.emit("error",new na.CacheError(h));this.cache.once("error",f),s.on("response",()=>this.cache.removeListener("error",f));try{await g(t)}catch(h){t.automaticFailover&&!c&&u(t),s.emit("error",new na.CacheError(h))}})(),s}}};function Ske(r){let e=N({},r);return e.path=`${r.pathname||"/"}${r.search||""}`,delete e.pathname,delete e.search,e}function qx(r){return{protocol:r.protocol,auth:r.auth,hostname:r.hostname||r.host||"localhost",port:r.port,pathname:r.pathname,search:r.search}}na.RequestError=class extends Error{constructor(r){super(r.message);this.name="RequestError",Object.assign(this,r)}};na.CacheError=class extends Error{constructor(r){super(r.message);this.name="CacheError",Object.assign(this,r)}};Fz.exports=na});var Oz=w((fnt,Tz)=>{"use strict";var vke=["aborted","complete","headers","httpVersion","httpVersionMinor","httpVersionMajor","method","rawHeaders","rawTrailers","setTimeout","socket","statusCode","statusMessage","trailers","url"];Tz.exports=(r,e)=>{if(e._readableState.autoDestroy)throw new Error("The second stream must have the `autoDestroy` option set to `false`");let t=new Set(Object.keys(r).concat(vke)),i={};for(let n of t)n in e||(i[n]={get(){let s=r[n];return typeof s=="function"?s.bind(r):s},set(s){r[n]=s},enumerable:!0,configurable:!1});return Object.defineProperties(e,i),r.once("aborted",()=>{e.destroy(),e.emit("aborted")}),r.once("close",()=>{r.complete&&e.readable?e.once("end",()=>{e.emit("close")}):e.emit("close")}),e}});var Uz=w((hnt,Mz)=>{"use strict";var{Transform:kke,PassThrough:xke}=require("stream"),Jx=require("zlib"),Pke=Oz();Mz.exports=r=>{let e=(r.headers["content-encoding"]||"").toLowerCase();if(!["gzip","deflate","br"].includes(e))return r;let t=e==="br";if(t&&typeof Jx.createBrotliDecompress!="function")return r.destroy(new Error("Brotli is not supported on Node.js < 12")),r;let i=!0,n=new kke({transform(a,l,c){i=!1,c(null,a)},flush(a){a()}}),s=new xke({autoDestroy:!1,destroy(a,l){r.destroy(),l(a)}}),o=t?Jx.createBrotliDecompress():Jx.createUnzip();return o.once("error",a=>{if(i&&!r.readable){s.end();return}s.destroy(a)}),Pke(r,s),r.pipe(n).pipe(o).pipe(s),s}});var Wx=w((pnt,Kz)=>{"use strict";var Hz=class{constructor(e={}){if(!(e.maxSize&&e.maxSize>0))throw new TypeError("`maxSize` must be a number greater than 0");this.maxSize=e.maxSize,this.onEviction=e.onEviction,this.cache=new Map,this.oldCache=new Map,this._size=0}_set(e,t){if(this.cache.set(e,t),this._size++,this._size>=this.maxSize){if(this._size=0,typeof this.onEviction=="function")for(let[i,n]of this.oldCache.entries())this.onEviction(i,n);this.oldCache=this.cache,this.cache=new Map}}get(e){if(this.cache.has(e))return this.cache.get(e);if(this.oldCache.has(e)){let t=this.oldCache.get(e);return this.oldCache.delete(e),this._set(e,t),t}}set(e,t){return this.cache.has(e)?this.cache.set(e,t):this._set(e,t),this}has(e){return this.cache.has(e)||this.oldCache.has(e)}peek(e){if(this.cache.has(e))return this.cache.get(e);if(this.oldCache.has(e))return this.oldCache.get(e)}delete(e){let t=this.cache.delete(e);return t&&this._size--,this.oldCache.delete(e)||t}clear(){this.cache.clear(),this.oldCache.clear(),this._size=0}*keys(){for(let[e]of this)yield e}*values(){for(let[,e]of this)yield e}*[Symbol.iterator](){for(let e of this.cache)yield e;for(let e of this.oldCache){let[t]=e;this.cache.has(t)||(yield e)}}get size(){let e=0;for(let t of this.oldCache.keys())this.cache.has(t)||e++;return Math.min(this._size+e,this.maxSize)}};Kz.exports=Hz});var _x=w((dnt,jz)=>{"use strict";var Dke=require("events"),Rke=require("tls"),Fke=require("http2"),Nke=Wx(),gn=Symbol("currentStreamsCount"),Gz=Symbol("request"),Os=Symbol("cachedOriginSet"),tf=Symbol("gracefullyClosing"),Lke=["maxDeflateDynamicTableSize","maxSessionMemory","maxHeaderListPairs","maxOutstandingPings","maxReservedRemoteStreams","maxSendHeaderBlockLength","paddingStrategy","localAddress","path","rejectUnauthorized","minDHSize","ca","cert","clientCertEngine","ciphers","key","pfx","servername","minVersion","maxVersion","secureProtocol","crl","honorCipherOrder","ecdhCurve","dhparam","secureOptions","sessionIdContext"],Tke=(r,e,t)=>{let i=0,n=r.length;for(;i>>1;t(r[s],e)?i=s+1:n=s}return i},Oke=(r,e)=>r.remoteSettings.maxConcurrentStreams>e.remoteSettings.maxConcurrentStreams,zx=(r,e)=>{for(let t of r)t[Os].lengthe[Os].includes(i))&&t[gn]+e[gn]<=e.remoteSettings.maxConcurrentStreams&&Yz(t)},Mke=(r,e)=>{for(let t of r)e[Os].lengtht[Os].includes(i))&&e[gn]+t[gn]<=t.remoteSettings.maxConcurrentStreams&&Yz(e)},qz=({agent:r,isFree:e})=>{let t={};for(let i in r.sessions){let s=r.sessions[i].filter(o=>{let a=o[sA.kCurrentStreamsCount]{r[tf]=!0,r[gn]===0&&r.close()},sA=class extends Dke{constructor({timeout:e=6e4,maxSessions:t=Infinity,maxFreeSessions:i=10,maxCachedTlsSessions:n=100}={}){super();this.sessions={},this.queue={},this.timeout=e,this.maxSessions=t,this.maxFreeSessions=i,this._freeSessionsCount=0,this._sessionsCount=0,this.settings={enablePush:!1},this.tlsSessionCache=new Nke({maxSize:n})}static normalizeOrigin(e,t){return typeof e=="string"&&(e=new URL(e)),t&&e.hostname!==t&&(e.hostname=t),e.origin}normalizeOptions(e){let t="";if(e)for(let i of Lke)e[i]&&(t+=`:${e[i]}`);return t}_tryToCreateNewSession(e,t){if(!(e in this.queue)||!(t in this.queue[e]))return;let i=this.queue[e][t];this._sessionsCount{Array.isArray(i)?(i=[...i],n()):i=[{resolve:n,reject:s}];let o=this.normalizeOptions(t),a=sA.normalizeOrigin(e,t&&t.servername);if(a===void 0){for(let{reject:u}of i)u(new TypeError("The `origin` argument needs to be a string or an URL object"));return}if(o in this.sessions){let u=this.sessions[o],g=-1,f=-1,h;for(let p of u){let m=p.remoteSettings.maxConcurrentStreams;if(m=m||p[tf]||p.destroyed)continue;h||(g=m),y>f&&(h=p,f=y)}}if(h){if(i.length!==1){for(let{reject:p}of i){let m=new Error(`Expected the length of listeners to be 1, got ${i.length}. +Please report this to https://github.com/szmarczak/http2-wrapper/`);p(m)}return}i[0].resolve(h);return}}if(o in this.queue){if(a in this.queue[o]){this.queue[o][a].listeners.push(...i),this._tryToCreateNewSession(o,a);return}}else this.queue[o]={};let l=()=>{o in this.queue&&this.queue[o][a]===c&&(delete this.queue[o][a],Object.keys(this.queue[o]).length===0&&delete this.queue[o])},c=()=>{let u=`${a}:${o}`,g=!1;try{let f=Fke.connect(e,N({createConnection:this.createConnection,settings:this.settings,session:this.tlsSessionCache.get(u)},t));f[gn]=0,f[tf]=!1;let h=()=>f[gn]{this.tlsSessionCache.set(u,y)}),f.once("error",y=>{for(let{reject:b}of i)b(y);this.tlsSessionCache.delete(u)}),f.setTimeout(this.timeout,()=>{f.destroy()}),f.once("close",()=>{if(g){p&&this._freeSessionsCount--,this._sessionsCount--;let y=this.sessions[o];y.splice(y.indexOf(f),1),y.length===0&&delete this.sessions[o]}else{let y=new Error("Session closed without receiving a SETTINGS frame");y.code="HTTP2WRAPPER_NOSETTINGS";for(let{reject:b}of i)b(y);l()}this._tryToCreateNewSession(o,a)});let m=()=>{if(!(!(o in this.queue)||!h())){for(let y of f[Os])if(y in this.queue[o]){let{listeners:b}=this.queue[o][y];for(;b.length!==0&&h();)b.shift().resolve(f);let v=this.queue[o];if(v[y].listeners.length===0&&(delete v[y],Object.keys(v).length===0)){delete this.queue[o];break}if(!h())break}}};f.on("origin",()=>{f[Os]=f.originSet,!!h()&&(m(),zx(this.sessions[o],f))}),f.once("remoteSettings",()=>{if(f.ref(),f.unref(),this._sessionsCount++,c.destroyed){let y=new Error("Agent has been destroyed");for(let b of i)b.reject(y);f.destroy();return}f[Os]=f.originSet;{let y=this.sessions;if(o in y){let b=y[o];b.splice(Tke(b,f,Oke),0,f)}else y[o]=[f]}this._freeSessionsCount+=1,g=!0,this.emit("session",f),m(),l(),f[gn]===0&&this._freeSessionsCount>this.maxFreeSessions&&f.close(),i.length!==0&&(this.getSession(a,t,i),i.length=0),f.on("remoteSettings",()=>{m(),zx(this.sessions[o],f)})}),f[Gz]=f.request,f.request=(y,b)=>{if(f[tf])throw new Error("The session is gracefully closing. No new streams are allowed.");let v=f[Gz](y,b);return f.ref(),++f[gn],f[gn]===f.remoteSettings.maxConcurrentStreams&&this._freeSessionsCount--,v.once("close",()=>{if(p=h(),--f[gn],!f.destroyed&&!f.closed&&(Mke(this.sessions[o],f),h()&&!f.closed)){p||(this._freeSessionsCount++,p=!0);let k=f[gn]===0;k&&f.unref(),k&&(this._freeSessionsCount>this.maxFreeSessions||f[tf])?f.close():(zx(this.sessions[o],f),m())}}),v}}catch(f){for(let h of i)h.reject(f);l()}};c.listeners=i,c.completed=!1,c.destroyed=!1,this.queue[o][a]=c,this._tryToCreateNewSession(o,a)})}request(e,t,i,n){return new Promise((s,o)=>{this.getSession(e,t,[{reject:o,resolve:a=>{try{s(a.request(i,n))}catch(l){o(l)}}}])})}createConnection(e,t){return sA.connect(e,t)}static connect(e,t){t.ALPNProtocols=["h2"];let i=e.port||443,n=e.hostname||e.host;return typeof t.servername=="undefined"&&(t.servername=n),Rke.connect(i,n,t)}closeFreeSessions(){for(let e of Object.values(this.sessions))for(let t of e)t[gn]===0&&t.close()}destroy(e){for(let t of Object.values(this.sessions))for(let i of t)i.destroy(e);for(let t of Object.values(this.queue))for(let i of Object.values(t))i.destroyed=!0;this.queue={}}get freeSessions(){return qz({agent:this,isFree:!0})}get busySessions(){return qz({agent:this,isFree:!1})}};sA.kCurrentStreamsCount=gn;sA.kGracefullyClosing=tf;jz.exports={Agent:sA,globalAgent:new sA}});var Vx=w((Cnt,Jz)=>{"use strict";var{Readable:Uke}=require("stream"),Wz=class extends Uke{constructor(e,t){super({highWaterMark:t,autoDestroy:!1});this.statusCode=null,this.statusMessage="",this.httpVersion="2.0",this.httpVersionMajor=2,this.httpVersionMinor=0,this.headers={},this.trailers={},this.req=null,this.aborted=!1,this.complete=!1,this.upgrade=null,this.rawHeaders=[],this.rawTrailers=[],this.socket=e,this.connection=e,this._dumped=!1}_destroy(e){this.req._request.destroy(e)}setTimeout(e,t){return this.req.setTimeout(e,t),this}_dump(){this._dumped||(this._dumped=!0,this.removeAllListeners("data"),this.resume())}_read(){this.req&&this.req._request.resume()}};Jz.exports=Wz});var Xx=w((mnt,zz)=>{"use strict";zz.exports=r=>{let e={protocol:r.protocol,hostname:typeof r.hostname=="string"&&r.hostname.startsWith("[")?r.hostname.slice(1,-1):r.hostname,host:r.host,hash:r.hash,search:r.search,pathname:r.pathname,href:r.href,path:`${r.pathname||""}${r.search||""}`};return typeof r.port=="string"&&r.port.length!==0&&(e.port=Number(r.port)),(r.username||r.password)&&(e.auth=`${r.username||""}:${r.password||""}`),e}});var Vz=w((Ent,_z)=>{"use strict";_z.exports=(r,e,t)=>{for(let i of t)r.on(i,(...n)=>e.emit(i,...n))}});var Zz=w((Int,Xz)=>{"use strict";Xz.exports=r=>{switch(r){case":method":case":scheme":case":authority":case":path":return!0;default:return!1}}});var e8=w((wnt,$z)=>{"use strict";var rf=(r,e,t)=>{$z.exports[e]=class extends r{constructor(...n){super(typeof t=="string"?t:t(n));this.name=`${super.name} [${e}]`,this.code=e}}};rf(TypeError,"ERR_INVALID_ARG_TYPE",r=>{let e=r[0].includes(".")?"property":"argument",t=r[1],i=Array.isArray(t);return i&&(t=`${t.slice(0,-1).join(", ")} or ${t.slice(-1)}`),`The "${r[0]}" ${e} must be ${i?"one of":"of"} type ${t}. Received ${typeof r[2]}`});rf(TypeError,"ERR_INVALID_PROTOCOL",r=>`Protocol "${r[0]}" not supported. Expected "${r[1]}"`);rf(Error,"ERR_HTTP_HEADERS_SENT",r=>`Cannot ${r[0]} headers after they are sent to the client`);rf(TypeError,"ERR_INVALID_HTTP_TOKEN",r=>`${r[0]} must be a valid HTTP token [${r[1]}]`);rf(TypeError,"ERR_HTTP_INVALID_HEADER_VALUE",r=>`Invalid value "${r[0]} for header "${r[1]}"`);rf(TypeError,"ERR_INVALID_CHAR",r=>`Invalid character in ${r[0]} [${r[1]}]`)});var tP=w((Bnt,t8)=>{"use strict";var Kke=require("http2"),{Writable:Hke}=require("stream"),{Agent:r8,globalAgent:jke}=_x(),Gke=Vx(),Yke=Xx(),qke=Vz(),Jke=Zz(),{ERR_INVALID_ARG_TYPE:Zx,ERR_INVALID_PROTOCOL:Wke,ERR_HTTP_HEADERS_SENT:i8,ERR_INVALID_HTTP_TOKEN:zke,ERR_HTTP_INVALID_HEADER_VALUE:_ke,ERR_INVALID_CHAR:Vke}=e8(),{HTTP2_HEADER_STATUS:n8,HTTP2_HEADER_METHOD:s8,HTTP2_HEADER_PATH:o8,HTTP2_METHOD_CONNECT:Xke}=Kke.constants,Wi=Symbol("headers"),$x=Symbol("origin"),eP=Symbol("session"),a8=Symbol("options"),Pw=Symbol("flushedHeaders"),Dd=Symbol("jobs"),Zke=/^[\^`\-\w!#$%&*+.|~]+$/,$ke=/[^\t\u0020-\u007E\u0080-\u00FF]/,A8=class extends Hke{constructor(e,t,i){super({autoDestroy:!1});let n=typeof e=="string"||e instanceof URL;if(n&&(e=Yke(e instanceof URL?e:new URL(e))),typeof t=="function"||t===void 0?(i=t,t=n?e:N({},e)):t=N(N({},e),t),t.h2session)this[eP]=t.h2session;else if(t.agent===!1)this.agent=new r8({maxFreeSessions:0});else if(typeof t.agent=="undefined"||t.agent===null)typeof t.createConnection=="function"?(this.agent=new r8({maxFreeSessions:0}),this.agent.createConnection=t.createConnection):this.agent=jke;else if(typeof t.agent.request=="function")this.agent=t.agent;else throw new Zx("options.agent",["Agent-like Object","undefined","false"],t.agent);if(t.protocol&&t.protocol!=="https:")throw new Wke(t.protocol,"https:");let s=t.port||t.defaultPort||this.agent&&this.agent.defaultPort||443,o=t.hostname||t.host||"localhost";delete t.hostname,delete t.host,delete t.port;let{timeout:a}=t;if(t.timeout=void 0,this[Wi]=Object.create(null),this[Dd]=[],this.socket=null,this.connection=null,this.method=t.method||"GET",this.path=t.path,this.res=null,this.aborted=!1,this.reusedSocket=!1,t.headers)for(let[l,c]of Object.entries(t.headers))this.setHeader(l,c);t.auth&&!("authorization"in this[Wi])&&(this[Wi].authorization="Basic "+Buffer.from(t.auth).toString("base64")),t.session=t.tlsSession,t.path=t.socketPath,this[a8]=t,s===443?(this[$x]=`https://${o}`,":authority"in this[Wi]||(this[Wi][":authority"]=o)):(this[$x]=`https://${o}:${s}`,":authority"in this[Wi]||(this[Wi][":authority"]=`${o}:${s}`)),a&&this.setTimeout(a),i&&this.once("response",i),this[Pw]=!1}get method(){return this[Wi][s8]}set method(e){e&&(this[Wi][s8]=e.toUpperCase())}get path(){return this[Wi][o8]}set path(e){e&&(this[Wi][o8]=e)}get _mustNotHaveABody(){return this.method==="GET"||this.method==="HEAD"||this.method==="DELETE"}_write(e,t,i){if(this._mustNotHaveABody){i(new Error("The GET, HEAD and DELETE methods must NOT have a body"));return}this.flushHeaders();let n=()=>this._request.write(e,t,i);this._request?n():this[Dd].push(n)}_final(e){if(this.destroyed)return;this.flushHeaders();let t=()=>{if(this._mustNotHaveABody){e();return}this._request.end(e)};this._request?t():this[Dd].push(t)}abort(){this.res&&this.res.complete||(this.aborted||process.nextTick(()=>this.emit("abort")),this.aborted=!0,this.destroy())}_destroy(e,t){this.res&&this.res._dump(),this._request&&this._request.destroy(),t(e)}async flushHeaders(){if(this[Pw]||this.destroyed)return;this[Pw]=!0;let e=this.method===Xke,t=i=>{if(this._request=i,this.destroyed){i.destroy();return}e||qke(i,this,["timeout","continue","close","error"]);let n=o=>(...a)=>{!this.writable&&!this.destroyed?o(...a):this.once("finish",()=>{o(...a)})};i.once("response",n((o,a,l)=>{let c=new Gke(this.socket,i.readableHighWaterMark);this.res=c,c.req=this,c.statusCode=o[n8],c.headers=o,c.rawHeaders=l,c.once("end",()=>{this.aborted?(c.aborted=!0,c.emit("aborted")):(c.complete=!0,c.socket=null,c.connection=null)}),e?(c.upgrade=!0,this.emit("connect",c,i,Buffer.alloc(0))?this.emit("close"):i.destroy()):(i.on("data",u=>{!c._dumped&&!c.push(u)&&i.pause()}),i.once("end",()=>{c.push(null)}),this.emit("response",c)||c._dump())})),i.once("headers",n(o=>this.emit("information",{statusCode:o[n8]}))),i.once("trailers",n((o,a,l)=>{let{res:c}=this;c.trailers=o,c.rawTrailers=l}));let{socket:s}=i.session;this.socket=s,this.connection=s;for(let o of this[Dd])o();this.emit("socket",this.socket)};if(this[eP])try{t(this[eP].request(this[Wi]))}catch(i){this.emit("error",i)}else{this.reusedSocket=!0;try{t(await this.agent.request(this[$x],this[a8],this[Wi]))}catch(i){this.emit("error",i)}}}getHeader(e){if(typeof e!="string")throw new Zx("name","string",e);return this[Wi][e.toLowerCase()]}get headersSent(){return this[Pw]}removeHeader(e){if(typeof e!="string")throw new Zx("name","string",e);if(this.headersSent)throw new i8("remove");delete this[Wi][e.toLowerCase()]}setHeader(e,t){if(this.headersSent)throw new i8("set");if(typeof e!="string"||!Zke.test(e)&&!Jke(e))throw new zke("Header name",e);if(typeof t=="undefined")throw new _ke(t,e);if($ke.test(t))throw new Vke("header content",e);this[Wi][e.toLowerCase()]=t}setNoDelay(){}setSocketKeepAlive(){}setTimeout(e,t){let i=()=>this._request.setTimeout(e,t);return this._request?i():this[Dd].push(i),this}get maxHeadersCount(){if(!this.destroyed&&this._request)return this._request.session.localSettings.maxHeaderListSize}set maxHeadersCount(e){}};t8.exports=A8});var c8=w((bnt,l8)=>{"use strict";var exe=require("tls");l8.exports=(r={})=>new Promise((e,t)=>{let i=exe.connect(r,()=>{r.resolveSocket?(i.off("error",t),e({alpnProtocol:i.alpnProtocol,socket:i})):(i.destroy(),e({alpnProtocol:i.alpnProtocol}))});i.on("error",t)})});var g8=w((Qnt,u8)=>{"use strict";var txe=require("net");u8.exports=r=>{let e=r.host,t=r.headers&&r.headers.host;return t&&(t.startsWith("[")?t.indexOf("]")===-1?e=t:e=t.slice(1,-1):e=t.split(":",1)[0]),txe.isIP(e)?"":e}});var p8=w((Snt,rP)=>{"use strict";var f8=require("http"),iP=require("https"),rxe=c8(),ixe=Wx(),nxe=tP(),sxe=g8(),oxe=Xx(),Dw=new ixe({maxSize:100}),Rd=new Map,h8=(r,e,t)=>{e._httpMessage={shouldKeepAlive:!0};let i=()=>{r.emit("free",e,t)};e.on("free",i);let n=()=>{r.removeSocket(e,t)};e.on("close",n);let s=()=>{r.removeSocket(e,t),e.off("close",n),e.off("free",i),e.off("agentRemove",s)};e.on("agentRemove",s),r.emit("free",e,t)},axe=async r=>{let e=`${r.host}:${r.port}:${r.ALPNProtocols.sort()}`;if(!Dw.has(e)){if(Rd.has(e))return(await Rd.get(e)).alpnProtocol;let{path:t,agent:i}=r;r.path=r.socketPath;let n=rxe(r);Rd.set(e,n);try{let{socket:s,alpnProtocol:o}=await n;if(Dw.set(e,o),r.path=t,o==="h2")s.destroy();else{let{globalAgent:a}=iP,l=iP.Agent.prototype.createConnection;i?i.createConnection===l?h8(i,s,r):s.destroy():a.createConnection===l?h8(a,s,r):s.destroy()}return Rd.delete(e),o}catch(s){throw Rd.delete(e),s}}return Dw.get(e)};rP.exports=async(r,e,t)=>{if((typeof r=="string"||r instanceof URL)&&(r=oxe(new URL(r))),typeof e=="function"&&(t=e,e=void 0),e=te(N(N({ALPNProtocols:["h2","http/1.1"]},r),e),{resolveSocket:!0}),!Array.isArray(e.ALPNProtocols)||e.ALPNProtocols.length===0)throw new Error("The `ALPNProtocols` option must be an Array with at least one entry");e.protocol=e.protocol||"https:";let i=e.protocol==="https:";e.host=e.hostname||e.host||"localhost",e.session=e.tlsSession,e.servername=e.servername||sxe(e),e.port=e.port||(i?443:80),e._defaultAgent=i?iP.globalAgent:f8.globalAgent;let n=e.agent;if(n){if(n.addRequest)throw new Error("The `options.agent` object can contain only `http`, `https` or `http2` properties");e.agent=n[i?"https":"http"]}return i&&await axe(e)==="h2"?(n&&(e.agent=n.http2),new nxe(e,t)):f8.request(e,t)};rP.exports.protocolCache=Dw});var C8=w((vnt,d8)=>{"use strict";var Axe=require("http2"),lxe=_x(),nP=tP(),cxe=Vx(),uxe=p8(),gxe=(r,e,t)=>new nP(r,e,t),fxe=(r,e,t)=>{let i=new nP(r,e,t);return i.end(),i};d8.exports=te(N(te(N({},Axe),{ClientRequest:nP,IncomingMessage:cxe}),lxe),{request:gxe,get:fxe,auto:uxe})});var oP=w(sP=>{"use strict";Object.defineProperty(sP,"__esModule",{value:!0});var m8=nA();sP.default=r=>m8.default.nodeStream(r)&&m8.default.function_(r.getBoundary)});var w8=w(aP=>{"use strict";Object.defineProperty(aP,"__esModule",{value:!0});var E8=require("fs"),I8=require("util"),y8=nA(),hxe=oP(),pxe=I8.promisify(E8.stat);aP.default=async(r,e)=>{if(e&&"content-length"in e)return Number(e["content-length"]);if(!r)return 0;if(y8.default.string(r))return Buffer.byteLength(r);if(y8.default.buffer(r))return r.length;if(hxe.default(r))return I8.promisify(r.getLength.bind(r))();if(r instanceof E8.ReadStream){let{size:t}=await pxe(r.path);return t===0?void 0:t}}});var lP=w(AP=>{"use strict";Object.defineProperty(AP,"__esModule",{value:!0});function dxe(r,e,t){let i={};for(let n of t)i[n]=(...s)=>{e.emit(n,...s)},r.on(n,i[n]);return()=>{for(let n of t)r.off(n,i[n])}}AP.default=dxe});var B8=w(cP=>{"use strict";Object.defineProperty(cP,"__esModule",{value:!0});cP.default=()=>{let r=[];return{once(e,t,i){e.once(t,i),r.push({origin:e,event:t,fn:i})},unhandleAll(){for(let e of r){let{origin:t,event:i,fn:n}=e;t.removeListener(i,n)}r.length=0}}}});var Q8=w(Fd=>{"use strict";Object.defineProperty(Fd,"__esModule",{value:!0});Fd.TimeoutError=void 0;var Cxe=require("net"),mxe=B8(),b8=Symbol("reentry"),Exe=()=>{},uP=class extends Error{constructor(e,t){super(`Timeout awaiting '${t}' for ${e}ms`);this.event=t,this.name="TimeoutError",this.code="ETIMEDOUT"}};Fd.TimeoutError=uP;Fd.default=(r,e,t)=>{if(b8 in r)return Exe;r[b8]=!0;let i=[],{once:n,unhandleAll:s}=mxe.default(),o=(g,f,h)=>{var p;let m=setTimeout(f,g,g,h);(p=m.unref)===null||p===void 0||p.call(m);let y=()=>{clearTimeout(m)};return i.push(y),y},{host:a,hostname:l}=t,c=(g,f)=>{r.destroy(new uP(g,f))},u=()=>{for(let g of i)g();s()};if(r.once("error",g=>{if(u(),r.listenerCount("error")===0)throw g}),r.once("close",u),n(r,"response",g=>{n(g,"end",u)}),typeof e.request!="undefined"&&o(e.request,c,"request"),typeof e.socket!="undefined"){let g=()=>{c(e.socket,"socket")};r.setTimeout(e.socket,g),i.push(()=>{r.removeListener("timeout",g)})}return n(r,"socket",g=>{var f;let{socketPath:h}=r;if(g.connecting){let p=Boolean(h!=null?h:Cxe.isIP((f=l!=null?l:a)!==null&&f!==void 0?f:"")!==0);if(typeof e.lookup!="undefined"&&!p&&typeof g.address().address=="undefined"){let m=o(e.lookup,c,"lookup");n(g,"lookup",m)}if(typeof e.connect!="undefined"){let m=()=>o(e.connect,c,"connect");p?n(g,"connect",m()):n(g,"lookup",y=>{y===null&&n(g,"connect",m())})}typeof e.secureConnect!="undefined"&&t.protocol==="https:"&&n(g,"connect",()=>{let m=o(e.secureConnect,c,"secureConnect");n(g,"secureConnect",m)})}if(typeof e.send!="undefined"){let p=()=>o(e.send,c,"send");g.connecting?n(g,"connect",()=>{n(r,"upload-complete",p())}):n(r,"upload-complete",p())}}),typeof e.response!="undefined"&&n(r,"upload-complete",()=>{let g=o(e.response,c,"response");n(r,"response",g)}),u}});var v8=w(gP=>{"use strict";Object.defineProperty(gP,"__esModule",{value:!0});var S8=nA();gP.default=r=>{r=r;let e={protocol:r.protocol,hostname:S8.default.string(r.hostname)&&r.hostname.startsWith("[")?r.hostname.slice(1,-1):r.hostname,host:r.host,hash:r.hash,search:r.search,pathname:r.pathname,href:r.href,path:`${r.pathname||""}${r.search||""}`};return S8.default.string(r.port)&&r.port.length>0&&(e.port=Number(r.port)),(r.username||r.password)&&(e.auth=`${r.username||""}:${r.password||""}`),e}});var k8=w(fP=>{"use strict";Object.defineProperty(fP,"__esModule",{value:!0});var Ixe=require("url"),yxe=["protocol","host","hostname","port","pathname","search"];fP.default=(r,e)=>{var t,i;if(e.path){if(e.pathname)throw new TypeError("Parameters `path` and `pathname` are mutually exclusive.");if(e.search)throw new TypeError("Parameters `path` and `search` are mutually exclusive.");if(e.searchParams)throw new TypeError("Parameters `path` and `searchParams` are mutually exclusive.")}if(e.search&&e.searchParams)throw new TypeError("Parameters `search` and `searchParams` are mutually exclusive.");if(!r){if(!e.protocol)throw new TypeError("No URL protocol specified");r=`${e.protocol}//${(i=(t=e.hostname)!==null&&t!==void 0?t:e.host)!==null&&i!==void 0?i:""}`}let n=new Ixe.URL(r);if(e.path){let s=e.path.indexOf("?");s===-1?e.pathname=e.path:(e.pathname=e.path.slice(0,s),e.search=e.path.slice(s+1)),delete e.path}for(let s of yxe)e[s]&&(n[s]=e[s].toString());return n}});var P8=w(hP=>{"use strict";Object.defineProperty(hP,"__esModule",{value:!0});var x8=class{constructor(){this.weakMap=new WeakMap,this.map=new Map}set(e,t){typeof e=="object"?this.weakMap.set(e,t):this.map.set(e,t)}get(e){return typeof e=="object"?this.weakMap.get(e):this.map.get(e)}has(e){return typeof e=="object"?this.weakMap.has(e):this.map.has(e)}};hP.default=x8});var dP=w(pP=>{"use strict";Object.defineProperty(pP,"__esModule",{value:!0});var wxe=async r=>{let e=[],t=0;for await(let i of r)e.push(i),t+=Buffer.byteLength(i);return Buffer.isBuffer(e[0])?Buffer.concat(e,t):Buffer.from(e.join(""))};pP.default=wxe});var R8=w(Xc=>{"use strict";Object.defineProperty(Xc,"__esModule",{value:!0});Xc.dnsLookupIpVersionToFamily=Xc.isDnsLookupIpVersion=void 0;var D8={auto:0,ipv4:4,ipv6:6};Xc.isDnsLookupIpVersion=r=>r in D8;Xc.dnsLookupIpVersionToFamily=r=>{if(Xc.isDnsLookupIpVersion(r))return D8[r];throw new Error("Invalid DNS lookup IP version")}});var CP=w(Rw=>{"use strict";Object.defineProperty(Rw,"__esModule",{value:!0});Rw.isResponseOk=void 0;Rw.isResponseOk=r=>{let{statusCode:e}=r,t=r.request.options.followRedirect?299:399;return e>=200&&e<=t||e===304}});var N8=w(mP=>{"use strict";Object.defineProperty(mP,"__esModule",{value:!0});var F8=new Set;mP.default=r=>{F8.has(r)||(F8.add(r),process.emitWarning(`Got: ${r}`,{type:"DeprecationWarning"}))}});var L8=w(EP=>{"use strict";Object.defineProperty(EP,"__esModule",{value:!0});var Ir=nA(),Bxe=(r,e)=>{if(Ir.default.null_(r.encoding))throw new TypeError("To get a Buffer, set `options.responseType` to `buffer` instead");Ir.assert.any([Ir.default.string,Ir.default.undefined],r.encoding),Ir.assert.any([Ir.default.boolean,Ir.default.undefined],r.resolveBodyOnly),Ir.assert.any([Ir.default.boolean,Ir.default.undefined],r.methodRewriting),Ir.assert.any([Ir.default.boolean,Ir.default.undefined],r.isStream),Ir.assert.any([Ir.default.string,Ir.default.undefined],r.responseType),r.responseType===void 0&&(r.responseType="text");let{retry:t}=r;if(e?r.retry=N({},e.retry):r.retry={calculateDelay:i=>i.computedValue,limit:0,methods:[],statusCodes:[],errorCodes:[],maxRetryAfter:void 0},Ir.default.object(t)?(r.retry=N(N({},r.retry),t),r.retry.methods=[...new Set(r.retry.methods.map(i=>i.toUpperCase()))],r.retry.statusCodes=[...new Set(r.retry.statusCodes)],r.retry.errorCodes=[...new Set(r.retry.errorCodes)]):Ir.default.number(t)&&(r.retry.limit=t),Ir.default.undefined(r.retry.maxRetryAfter)&&(r.retry.maxRetryAfter=Math.min(...[r.timeout.request,r.timeout.connect].filter(Ir.default.number))),Ir.default.object(r.pagination)){e&&(r.pagination=N(N({},e.pagination),r.pagination));let{pagination:i}=r;if(!Ir.default.function_(i.transform))throw new Error("`options.pagination.transform` must be implemented");if(!Ir.default.function_(i.shouldContinue))throw new Error("`options.pagination.shouldContinue` must be implemented");if(!Ir.default.function_(i.filter))throw new TypeError("`options.pagination.filter` must be implemented");if(!Ir.default.function_(i.paginate))throw new Error("`options.pagination.paginate` must be implemented")}return r.responseType==="json"&&r.headers.accept===void 0&&(r.headers.accept="application/json"),r};EP.default=Bxe});var T8=w(Nd=>{"use strict";Object.defineProperty(Nd,"__esModule",{value:!0});Nd.retryAfterStatusCodes=void 0;Nd.retryAfterStatusCodes=new Set([413,429,503]);var bxe=({attemptCount:r,retryOptions:e,error:t,retryAfter:i})=>{if(r>e.limit)return 0;let n=e.methods.includes(t.options.method),s=e.errorCodes.includes(t.code),o=t.response&&e.statusCodes.includes(t.response.statusCode);if(!n||!s&&!o)return 0;if(t.response){if(i)return e.maxRetryAfter===void 0||i>e.maxRetryAfter?0:i;if(t.response.statusCode===413)return 0}let a=Math.random()*100;return 2**(r-1)*1e3+a};Nd.default=bxe});var Td=w(qt=>{"use strict";Object.defineProperty(qt,"__esModule",{value:!0});qt.UnsupportedProtocolError=qt.ReadError=qt.TimeoutError=qt.UploadError=qt.CacheError=qt.HTTPError=qt.MaxRedirectsError=qt.RequestError=qt.setNonEnumerableProperties=qt.knownHookEvents=qt.withoutBody=qt.kIsNormalizedAlready=void 0;var O8=require("util"),M8=require("stream"),Qxe=require("fs"),fl=require("url"),U8=require("http"),IP=require("http"),Sxe=require("https"),vxe=z4(),kxe=tz(),K8=Lz(),xxe=Uz(),Pxe=C8(),Dxe=kw(),Ee=nA(),Rxe=w8(),H8=oP(),Fxe=lP(),j8=Q8(),Nxe=v8(),G8=k8(),Lxe=P8(),Txe=dP(),Y8=R8(),Oxe=CP(),hl=N8(),Mxe=L8(),Uxe=T8(),yP,Fi=Symbol("request"),Fw=Symbol("response"),nf=Symbol("responseSize"),sf=Symbol("downloadedSize"),of=Symbol("bodySize"),af=Symbol("uploadedSize"),Nw=Symbol("serverResponsesPiped"),q8=Symbol("unproxyEvents"),J8=Symbol("isFromCache"),wP=Symbol("cancelTimeouts"),W8=Symbol("startedReading"),Af=Symbol("stopReading"),Lw=Symbol("triggerRead"),pl=Symbol("body"),Ld=Symbol("jobs"),z8=Symbol("originalResponse"),_8=Symbol("retryTimeout");qt.kIsNormalizedAlready=Symbol("isNormalizedAlready");var Kxe=Ee.default.string(process.versions.brotli);qt.withoutBody=new Set(["GET","HEAD"]);qt.knownHookEvents=["init","beforeRequest","beforeRedirect","beforeError","beforeRetry","afterResponse"];function Hxe(r){for(let e in r){let t=r[e];if(!Ee.default.string(t)&&!Ee.default.number(t)&&!Ee.default.boolean(t)&&!Ee.default.null_(t)&&!Ee.default.undefined(t))throw new TypeError(`The \`searchParams\` value '${String(t)}' must be a string, number, boolean or null`)}}function jxe(r){return Ee.default.object(r)&&!("statusCode"in r)}var BP=new Lxe.default,Gxe=async r=>new Promise((e,t)=>{let i=n=>{t(n)};r.pending||e(),r.once("error",i),r.once("ready",()=>{r.off("error",i),e()})}),Yxe=new Set([300,301,302,303,304,307,308]),qxe=["context","body","json","form"];qt.setNonEnumerableProperties=(r,e)=>{let t={};for(let i of r)if(!!i)for(let n of qxe)n in i&&(t[n]={writable:!0,configurable:!0,enumerable:!1,value:i[n]});Object.defineProperties(e,t)};var hi=class extends Error{constructor(e,t,i){var n;super(e);if(Error.captureStackTrace(this,this.constructor),this.name="RequestError",this.code=t.code,i instanceof bP?(Object.defineProperty(this,"request",{enumerable:!1,value:i}),Object.defineProperty(this,"response",{enumerable:!1,value:i[Fw]}),Object.defineProperty(this,"options",{enumerable:!1,value:i.options})):Object.defineProperty(this,"options",{enumerable:!1,value:i}),this.timings=(n=this.request)===null||n===void 0?void 0:n.timings,Ee.default.string(t.stack)&&Ee.default.string(this.stack)){let s=this.stack.indexOf(this.message)+this.message.length,o=this.stack.slice(s).split(` +`).reverse(),a=t.stack.slice(t.stack.indexOf(t.message)+t.message.length).split(` +`).reverse();for(;a.length!==0&&a[0]===o[0];)o.shift();this.stack=`${this.stack.slice(0,s)}${o.reverse().join(` +`)}${a.reverse().join(` +`)}`}}};qt.RequestError=hi;var QP=class extends hi{constructor(e){super(`Redirected ${e.options.maxRedirects} times. Aborting.`,{},e);this.name="MaxRedirectsError"}};qt.MaxRedirectsError=QP;var SP=class extends hi{constructor(e){super(`Response code ${e.statusCode} (${e.statusMessage})`,{},e.request);this.name="HTTPError"}};qt.HTTPError=SP;var vP=class extends hi{constructor(e,t){super(e.message,e,t);this.name="CacheError"}};qt.CacheError=vP;var kP=class extends hi{constructor(e,t){super(e.message,e,t);this.name="UploadError"}};qt.UploadError=kP;var xP=class extends hi{constructor(e,t,i){super(e.message,e,i);this.name="TimeoutError",this.event=e.event,this.timings=t}};qt.TimeoutError=xP;var Tw=class extends hi{constructor(e,t){super(e.message,e,t);this.name="ReadError"}};qt.ReadError=Tw;var PP=class extends hi{constructor(e){super(`Unsupported protocol "${e.url.protocol}"`,{},e);this.name="UnsupportedProtocolError"}};qt.UnsupportedProtocolError=PP;var Jxe=["socket","connect","continue","information","upgrade","timeout"],bP=class extends M8.Duplex{constructor(e,t={},i){super({autoDestroy:!1,highWaterMark:0});this[sf]=0,this[af]=0,this.requestInitialized=!1,this[Nw]=new Set,this.redirects=[],this[Af]=!1,this[Lw]=!1,this[Ld]=[],this.retryCount=0,this._progressCallbacks=[];let n=()=>this._unlockWrite(),s=()=>this._lockWrite();this.on("pipe",c=>{c.prependListener("data",n),c.on("data",s),c.prependListener("end",n),c.on("end",s)}),this.on("unpipe",c=>{c.off("data",n),c.off("data",s),c.off("end",n),c.off("end",s)}),this.on("pipe",c=>{c instanceof IP.IncomingMessage&&(this.options.headers=N(N({},c.headers),this.options.headers))});let{json:o,body:a,form:l}=t;if((o||a||l)&&this._lockWrite(),qt.kIsNormalizedAlready in t)this.options=t;else try{this.options=this.constructor.normalizeArguments(e,t,i)}catch(c){Ee.default.nodeStream(t.body)&&t.body.destroy(),this.destroy(c);return}(async()=>{var c;try{this.options.body instanceof Qxe.ReadStream&&await Gxe(this.options.body);let{url:u}=this.options;if(!u)throw new TypeError("Missing `url` property");if(this.requestUrl=u.toString(),decodeURI(this.requestUrl),await this._finalizeBody(),await this._makeRequest(),this.destroyed){(c=this[Fi])===null||c===void 0||c.destroy();return}for(let g of this[Ld])g();this[Ld].length=0,this.requestInitialized=!0}catch(u){if(u instanceof hi){this._beforeError(u);return}this.destroyed||this.destroy(u)}})()}static normalizeArguments(e,t,i){var n,s,o,a,l;let c=t;if(Ee.default.object(e)&&!Ee.default.urlInstance(e))t=N(N(N({},i),e),t);else{if(e&&t&&t.url!==void 0)throw new TypeError("The `url` option is mutually exclusive with the `input` argument");t=N(N({},i),t),e!==void 0&&(t.url=e),Ee.default.urlInstance(t.url)&&(t.url=new fl.URL(t.url.toString()))}if(t.cache===!1&&(t.cache=void 0),t.dnsCache===!1&&(t.dnsCache=void 0),Ee.assert.any([Ee.default.string,Ee.default.undefined],t.method),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.headers),Ee.assert.any([Ee.default.string,Ee.default.urlInstance,Ee.default.undefined],t.prefixUrl),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.cookieJar),Ee.assert.any([Ee.default.object,Ee.default.string,Ee.default.undefined],t.searchParams),Ee.assert.any([Ee.default.object,Ee.default.string,Ee.default.undefined],t.cache),Ee.assert.any([Ee.default.object,Ee.default.number,Ee.default.undefined],t.timeout),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.context),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.hooks),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.decompress),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.ignoreInvalidCookies),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.followRedirect),Ee.assert.any([Ee.default.number,Ee.default.undefined],t.maxRedirects),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.throwHttpErrors),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.http2),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.allowGetBody),Ee.assert.any([Ee.default.string,Ee.default.undefined],t.localAddress),Ee.assert.any([Y8.isDnsLookupIpVersion,Ee.default.undefined],t.dnsLookupIpVersion),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.https),Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.rejectUnauthorized),t.https&&(Ee.assert.any([Ee.default.boolean,Ee.default.undefined],t.https.rejectUnauthorized),Ee.assert.any([Ee.default.function_,Ee.default.undefined],t.https.checkServerIdentity),Ee.assert.any([Ee.default.string,Ee.default.object,Ee.default.array,Ee.default.undefined],t.https.certificateAuthority),Ee.assert.any([Ee.default.string,Ee.default.object,Ee.default.array,Ee.default.undefined],t.https.key),Ee.assert.any([Ee.default.string,Ee.default.object,Ee.default.array,Ee.default.undefined],t.https.certificate),Ee.assert.any([Ee.default.string,Ee.default.undefined],t.https.passphrase),Ee.assert.any([Ee.default.string,Ee.default.buffer,Ee.default.array,Ee.default.undefined],t.https.pfx)),Ee.assert.any([Ee.default.object,Ee.default.undefined],t.cacheOptions),Ee.default.string(t.method)?t.method=t.method.toUpperCase():t.method="GET",t.headers===(i==null?void 0:i.headers)?t.headers=N({},t.headers):t.headers=Dxe(N(N({},i==null?void 0:i.headers),t.headers)),"slashes"in t)throw new TypeError("The legacy `url.Url` has been deprecated. Use `URL` instead.");if("auth"in t)throw new TypeError("Parameter `auth` is deprecated. Use `username` / `password` instead.");if("searchParams"in t&&t.searchParams&&t.searchParams!==(i==null?void 0:i.searchParams)){let h;if(Ee.default.string(t.searchParams)||t.searchParams instanceof fl.URLSearchParams)h=new fl.URLSearchParams(t.searchParams);else{Hxe(t.searchParams),h=new fl.URLSearchParams;for(let p in t.searchParams){let m=t.searchParams[p];m===null?h.append(p,""):m!==void 0&&h.append(p,m)}}(n=i==null?void 0:i.searchParams)===null||n===void 0||n.forEach((p,m)=>{h.has(m)||h.append(m,p)}),t.searchParams=h}if(t.username=(s=t.username)!==null&&s!==void 0?s:"",t.password=(o=t.password)!==null&&o!==void 0?o:"",Ee.default.undefined(t.prefixUrl)?t.prefixUrl=(a=i==null?void 0:i.prefixUrl)!==null&&a!==void 0?a:"":(t.prefixUrl=t.prefixUrl.toString(),t.prefixUrl!==""&&!t.prefixUrl.endsWith("/")&&(t.prefixUrl+="/")),Ee.default.string(t.url)){if(t.url.startsWith("/"))throw new Error("`input` must not start with a slash when using `prefixUrl`");t.url=G8.default(t.prefixUrl+t.url,t)}else(Ee.default.undefined(t.url)&&t.prefixUrl!==""||t.protocol)&&(t.url=G8.default(t.prefixUrl,t));if(t.url){"port"in t&&delete t.port;let{prefixUrl:h}=t;Object.defineProperty(t,"prefixUrl",{set:m=>{let y=t.url;if(!y.href.startsWith(m))throw new Error(`Cannot change \`prefixUrl\` from ${h} to ${m}: ${y.href}`);t.url=new fl.URL(m+y.href.slice(h.length)),h=m},get:()=>h});let{protocol:p}=t.url;if(p==="unix:"&&(p="http:",t.url=new fl.URL(`http://unix${t.url.pathname}${t.url.search}`)),t.searchParams&&(t.url.search=t.searchParams.toString()),p!=="http:"&&p!=="https:")throw new PP(t);t.username===""?t.username=t.url.username:t.url.username=t.username,t.password===""?t.password=t.url.password:t.url.password=t.password}let{cookieJar:u}=t;if(u){let{setCookie:h,getCookieString:p}=u;Ee.assert.function_(h),Ee.assert.function_(p),h.length===4&&p.length===0&&(h=O8.promisify(h.bind(t.cookieJar)),p=O8.promisify(p.bind(t.cookieJar)),t.cookieJar={setCookie:h,getCookieString:p})}let{cache:g}=t;if(g&&(BP.has(g)||BP.set(g,new K8((h,p)=>{let m=h[Fi](h,p);return Ee.default.promise(m)&&(m.once=(y,b)=>{if(y==="error")m.catch(b);else if(y==="abort")(async()=>{try{(await m).once("abort",b)}catch(v){}})();else throw new Error(`Unknown HTTP2 promise event: ${y}`);return m}),m},g))),t.cacheOptions=N({},t.cacheOptions),t.dnsCache===!0)yP||(yP=new kxe.default),t.dnsCache=yP;else if(!Ee.default.undefined(t.dnsCache)&&!t.dnsCache.lookup)throw new TypeError(`Parameter \`dnsCache\` must be a CacheableLookup instance or a boolean, got ${Ee.default(t.dnsCache)}`);Ee.default.number(t.timeout)?t.timeout={request:t.timeout}:i&&t.timeout!==i.timeout?t.timeout=N(N({},i.timeout),t.timeout):t.timeout=N({},t.timeout),t.context||(t.context={});let f=t.hooks===(i==null?void 0:i.hooks);t.hooks=N({},t.hooks);for(let h of qt.knownHookEvents)if(h in t.hooks)if(Ee.default.array(t.hooks[h]))t.hooks[h]=[...t.hooks[h]];else throw new TypeError(`Parameter \`${h}\` must be an Array, got ${Ee.default(t.hooks[h])}`);else t.hooks[h]=[];if(i&&!f)for(let h of qt.knownHookEvents)i.hooks[h].length>0&&(t.hooks[h]=[...i.hooks[h],...t.hooks[h]]);if("family"in t&&hl.default('"options.family" was never documented, please use "options.dnsLookupIpVersion"'),(i==null?void 0:i.https)&&(t.https=N(N({},i.https),t.https)),"rejectUnauthorized"in t&&hl.default('"options.rejectUnauthorized" is now deprecated, please use "options.https.rejectUnauthorized"'),"checkServerIdentity"in t&&hl.default('"options.checkServerIdentity" was never documented, please use "options.https.checkServerIdentity"'),"ca"in t&&hl.default('"options.ca" was never documented, please use "options.https.certificateAuthority"'),"key"in t&&hl.default('"options.key" was never documented, please use "options.https.key"'),"cert"in t&&hl.default('"options.cert" was never documented, please use "options.https.certificate"'),"passphrase"in t&&hl.default('"options.passphrase" was never documented, please use "options.https.passphrase"'),"pfx"in t&&hl.default('"options.pfx" was never documented, please use "options.https.pfx"'),"followRedirects"in t)throw new TypeError("The `followRedirects` option does not exist. Use `followRedirect` instead.");if(t.agent){for(let h in t.agent)if(h!=="http"&&h!=="https"&&h!=="http2")throw new TypeError(`Expected the \`options.agent\` properties to be \`http\`, \`https\` or \`http2\`, got \`${h}\``)}return t.maxRedirects=(l=t.maxRedirects)!==null&&l!==void 0?l:0,qt.setNonEnumerableProperties([i,c],t),Mxe.default(t,i)}_lockWrite(){let e=()=>{throw new TypeError("The payload has been already provided")};this.write=e,this.end=e}_unlockWrite(){this.write=super.write,this.end=super.end}async _finalizeBody(){let{options:e}=this,{headers:t}=e,i=!Ee.default.undefined(e.form),n=!Ee.default.undefined(e.json),s=!Ee.default.undefined(e.body),o=i||n||s,a=qt.withoutBody.has(e.method)&&!(e.method==="GET"&&e.allowGetBody);if(this._cannotHaveBody=a,o){if(a)throw new TypeError(`The \`${e.method}\` method cannot be used with a body`);if([s,i,n].filter(l=>l).length>1)throw new TypeError("The `body`, `json` and `form` options are mutually exclusive");if(s&&!(e.body instanceof M8.Readable)&&!Ee.default.string(e.body)&&!Ee.default.buffer(e.body)&&!H8.default(e.body))throw new TypeError("The `body` option must be a stream.Readable, string or Buffer");if(i&&!Ee.default.object(e.form))throw new TypeError("The `form` option must be an Object");{let l=!Ee.default.string(t["content-type"]);s?(H8.default(e.body)&&l&&(t["content-type"]=`multipart/form-data; boundary=${e.body.getBoundary()}`),this[pl]=e.body):i?(l&&(t["content-type"]="application/x-www-form-urlencoded"),this[pl]=new fl.URLSearchParams(e.form).toString()):(l&&(t["content-type"]="application/json"),this[pl]=e.stringifyJson(e.json));let c=await Rxe.default(this[pl],e.headers);Ee.default.undefined(t["content-length"])&&Ee.default.undefined(t["transfer-encoding"])&&!a&&!Ee.default.undefined(c)&&(t["content-length"]=String(c))}}else a?this._lockWrite():this._unlockWrite();this[of]=Number(t["content-length"])||void 0}async _onResponseBase(e){let{options:t}=this,{url:i}=t;this[z8]=e,t.decompress&&(e=xxe(e));let n=e.statusCode,s=e;s.statusMessage=s.statusMessage?s.statusMessage:U8.STATUS_CODES[n],s.url=t.url.toString(),s.requestUrl=this.requestUrl,s.redirectUrls=this.redirects,s.request=this,s.isFromCache=e.fromCache||!1,s.ip=this.ip,s.retryCount=this.retryCount,this[J8]=s.isFromCache,this[nf]=Number(e.headers["content-length"])||void 0,this[Fw]=e,e.once("end",()=>{this[nf]=this[sf],this.emit("downloadProgress",this.downloadProgress)}),e.once("error",a=>{e.destroy(),this._beforeError(new Tw(a,this))}),e.once("aborted",()=>{this._beforeError(new Tw({name:"Error",message:"The server aborted pending request",code:"ECONNRESET"},this))}),this.emit("downloadProgress",this.downloadProgress);let o=e.headers["set-cookie"];if(Ee.default.object(t.cookieJar)&&o){let a=o.map(async l=>t.cookieJar.setCookie(l,i.toString()));t.ignoreInvalidCookies&&(a=a.map(async l=>l.catch(()=>{})));try{await Promise.all(a)}catch(l){this._beforeError(l);return}}if(t.followRedirect&&e.headers.location&&Yxe.has(n)){if(e.resume(),this[Fi]&&(this[wP](),delete this[Fi],this[q8]()),(n===303&&t.method!=="GET"&&t.method!=="HEAD"||!t.methodRewriting)&&(t.method="GET","body"in t&&delete t.body,"json"in t&&delete t.json,"form"in t&&delete t.form,this[pl]=void 0,delete t.headers["content-length"]),this.redirects.length>=t.maxRedirects){this._beforeError(new QP(this));return}try{let l=Buffer.from(e.headers.location,"binary").toString(),c=new fl.URL(l,i),u=c.toString();decodeURI(u),c.hostname!==i.hostname||c.port!==i.port?("host"in t.headers&&delete t.headers.host,"cookie"in t.headers&&delete t.headers.cookie,"authorization"in t.headers&&delete t.headers.authorization,(t.username||t.password)&&(t.username="",t.password="")):(c.username=t.username,c.password=t.password),this.redirects.push(u),t.url=c;for(let g of t.hooks.beforeRedirect)await g(t,s);this.emit("redirect",s,t),await this._makeRequest()}catch(l){this._beforeError(l);return}return}if(t.isStream&&t.throwHttpErrors&&!Oxe.isResponseOk(s)){this._beforeError(new SP(s));return}e.on("readable",()=>{this[Lw]&&this._read()}),this.on("resume",()=>{e.resume()}),this.on("pause",()=>{e.pause()}),e.once("end",()=>{this.push(null)}),this.emit("response",e);for(let a of this[Nw])if(!a.headersSent){for(let l in e.headers){let c=t.decompress?l!=="content-encoding":!0,u=e.headers[l];c&&a.setHeader(l,u)}a.statusCode=n}}async _onResponse(e){try{await this._onResponseBase(e)}catch(t){this._beforeError(t)}}_onRequest(e){let{options:t}=this,{timeout:i,url:n}=t;vxe.default(e),this[wP]=j8.default(e,i,n);let s=t.cache?"cacheableResponse":"response";e.once(s,l=>{this._onResponse(l)}),e.once("error",l=>{var c;e.destroy(),(c=e.res)===null||c===void 0||c.removeAllListeners("end"),l=l instanceof j8.TimeoutError?new xP(l,this.timings,this):new hi(l.message,l,this),this._beforeError(l)}),this[q8]=Fxe.default(e,this,Jxe),this[Fi]=e,this.emit("uploadProgress",this.uploadProgress);let o=this[pl],a=this.redirects.length===0?this:e;Ee.default.nodeStream(o)?(o.pipe(a),o.once("error",l=>{this._beforeError(new kP(l,this))})):(this._unlockWrite(),Ee.default.undefined(o)?(this._cannotHaveBody||this._noPipe)&&(a.end(),this._lockWrite()):(this._writeRequest(o,void 0,()=>{}),a.end(),this._lockWrite())),this.emit("request",e)}async _createCacheableRequest(e,t){return new Promise((i,n)=>{Object.assign(t,Nxe.default(e)),delete t.url;let s,o=BP.get(t.cache)(t,async a=>{a._readableState.autoDestroy=!1,s&&(await s).emit("cacheableResponse",a),i(a)});t.url=e,o.once("error",n),o.once("request",async a=>{s=a,i(s)})})}async _makeRequest(){var e,t,i,n,s;let{options:o}=this,{headers:a}=o;for(let b in a)if(Ee.default.undefined(a[b]))delete a[b];else if(Ee.default.null_(a[b]))throw new TypeError(`Use \`undefined\` instead of \`null\` to delete the \`${b}\` header`);if(o.decompress&&Ee.default.undefined(a["accept-encoding"])&&(a["accept-encoding"]=Kxe?"gzip, deflate, br":"gzip, deflate"),o.cookieJar){let b=await o.cookieJar.getCookieString(o.url.toString());Ee.default.nonEmptyString(b)&&(o.headers.cookie=b)}for(let b of o.hooks.beforeRequest){let v=await b(o);if(!Ee.default.undefined(v)){o.request=()=>v;break}}o.body&&this[pl]!==o.body&&(this[pl]=o.body);let{agent:l,request:c,timeout:u,url:g}=o;if(o.dnsCache&&!("lookup"in o)&&(o.lookup=o.dnsCache.lookup),g.hostname==="unix"){let b=/(?.+?):(?.+)/.exec(`${g.pathname}${g.search}`);if(b==null?void 0:b.groups){let{socketPath:v,path:k}=b.groups;Object.assign(o,{socketPath:v,path:k,host:""})}}let f=g.protocol==="https:",h;o.http2?h=Pxe.auto:h=f?Sxe.request:U8.request;let p=(e=o.request)!==null&&e!==void 0?e:h,m=o.cache?this._createCacheableRequest:p;l&&!o.http2&&(o.agent=l[f?"https":"http"]),o[Fi]=p,delete o.request,delete o.timeout;let y=o;if(y.shared=(t=o.cacheOptions)===null||t===void 0?void 0:t.shared,y.cacheHeuristic=(i=o.cacheOptions)===null||i===void 0?void 0:i.cacheHeuristic,y.immutableMinTimeToLive=(n=o.cacheOptions)===null||n===void 0?void 0:n.immutableMinTimeToLive,y.ignoreCargoCult=(s=o.cacheOptions)===null||s===void 0?void 0:s.ignoreCargoCult,o.dnsLookupIpVersion!==void 0)try{y.family=Y8.dnsLookupIpVersionToFamily(o.dnsLookupIpVersion)}catch(b){throw new Error("Invalid `dnsLookupIpVersion` option value")}o.https&&("rejectUnauthorized"in o.https&&(y.rejectUnauthorized=o.https.rejectUnauthorized),o.https.checkServerIdentity&&(y.checkServerIdentity=o.https.checkServerIdentity),o.https.certificateAuthority&&(y.ca=o.https.certificateAuthority),o.https.certificate&&(y.cert=o.https.certificate),o.https.key&&(y.key=o.https.key),o.https.passphrase&&(y.passphrase=o.https.passphrase),o.https.pfx&&(y.pfx=o.https.pfx));try{let b=await m(g,y);Ee.default.undefined(b)&&(b=h(g,y)),o.request=c,o.timeout=u,o.agent=l,o.https&&("rejectUnauthorized"in o.https&&delete y.rejectUnauthorized,o.https.checkServerIdentity&&delete y.checkServerIdentity,o.https.certificateAuthority&&delete y.ca,o.https.certificate&&delete y.cert,o.https.key&&delete y.key,o.https.passphrase&&delete y.passphrase,o.https.pfx&&delete y.pfx),jxe(b)?this._onRequest(b):this.writable?(this.once("finish",()=>{this._onResponse(b)}),this._unlockWrite(),this.end(),this._lockWrite()):this._onResponse(b)}catch(b){throw b instanceof K8.CacheError?new vP(b,this):new hi(b.message,b,this)}}async _error(e){try{for(let t of this.options.hooks.beforeError)e=await t(e)}catch(t){e=new hi(t.message,t,this)}this.destroy(e)}_beforeError(e){if(this[Af])return;let{options:t}=this,i=this.retryCount+1;this[Af]=!0,e instanceof hi||(e=new hi(e.message,e,this));let n=e,{response:s}=n;(async()=>{if(s&&!s.body){s.setEncoding(this._readableState.encoding);try{s.rawBody=await Txe.default(s),s.body=s.rawBody.toString()}catch(o){}}if(this.listenerCount("retry")!==0){let o;try{let a;s&&"retry-after"in s.headers&&(a=Number(s.headers["retry-after"]),Number.isNaN(a)?(a=Date.parse(s.headers["retry-after"])-Date.now(),a<=0&&(a=1)):a*=1e3),o=await t.retry.calculateDelay({attemptCount:i,retryOptions:t.retry,error:n,retryAfter:a,computedValue:Uxe.default({attemptCount:i,retryOptions:t.retry,error:n,retryAfter:a,computedValue:0})})}catch(a){this._error(new hi(a.message,a,this));return}if(o){let a=async()=>{try{for(let l of this.options.hooks.beforeRetry)await l(this.options,n,i)}catch(l){this._error(new hi(l.message,e,this));return}this.destroyed||(this.destroy(),this.emit("retry",i,e))};this[_8]=setTimeout(a,o);return}}this._error(n)})()}_read(){this[Lw]=!0;let e=this[Fw];if(e&&!this[Af]){e.readableLength&&(this[Lw]=!1);let t;for(;(t=e.read())!==null;){this[sf]+=t.length,this[W8]=!0;let i=this.downloadProgress;i.percent<1&&this.emit("downloadProgress",i),this.push(t)}}}_write(e,t,i){let n=()=>{this._writeRequest(e,t,i)};this.requestInitialized?n():this[Ld].push(n)}_writeRequest(e,t,i){this[Fi].destroyed||(this._progressCallbacks.push(()=>{this[af]+=Buffer.byteLength(e,t);let n=this.uploadProgress;n.percent<1&&this.emit("uploadProgress",n)}),this[Fi].write(e,t,n=>{!n&&this._progressCallbacks.length>0&&this._progressCallbacks.shift()(),i(n)}))}_final(e){let t=()=>{for(;this._progressCallbacks.length!==0;)this._progressCallbacks.shift()();if(!(Fi in this)){e();return}if(this[Fi].destroyed){e();return}this[Fi].end(i=>{i||(this[of]=this[af],this.emit("uploadProgress",this.uploadProgress),this[Fi].emit("upload-complete")),e(i)})};this.requestInitialized?t():this[Ld].push(t)}_destroy(e,t){var i;this[Af]=!0,clearTimeout(this[_8]),Fi in this&&(this[wP](),((i=this[Fw])===null||i===void 0?void 0:i.complete)||this[Fi].destroy()),e!==null&&!Ee.default.undefined(e)&&!(e instanceof hi)&&(e=new hi(e.message,e,this)),t(e)}get _isAboutToError(){return this[Af]}get ip(){var e;return(e=this.socket)===null||e===void 0?void 0:e.remoteAddress}get aborted(){var e,t,i;return((t=(e=this[Fi])===null||e===void 0?void 0:e.destroyed)!==null&&t!==void 0?t:this.destroyed)&&!((i=this[z8])===null||i===void 0?void 0:i.complete)}get socket(){var e,t;return(t=(e=this[Fi])===null||e===void 0?void 0:e.socket)!==null&&t!==void 0?t:void 0}get downloadProgress(){let e;return this[nf]?e=this[sf]/this[nf]:this[nf]===this[sf]?e=1:e=0,{percent:e,transferred:this[sf],total:this[nf]}}get uploadProgress(){let e;return this[of]?e=this[af]/this[of]:this[of]===this[af]?e=1:e=0,{percent:e,transferred:this[af],total:this[of]}}get timings(){var e;return(e=this[Fi])===null||e===void 0?void 0:e.timings}get isFromCache(){return this[J8]}pipe(e,t){if(this[W8])throw new Error("Failed to pipe. The response has been emitted already.");return e instanceof IP.ServerResponse&&this[Nw].add(e),super.pipe(e,t)}unpipe(e){return e instanceof IP.ServerResponse&&this[Nw].delete(e),super.unpipe(e),this}};qt.default=bP});var Od=w(Eo=>{"use strict";var Wxe=Eo&&Eo.__createBinding||(Object.create?function(r,e,t,i){i===void 0&&(i=t),Object.defineProperty(r,i,{enumerable:!0,get:function(){return e[t]}})}:function(r,e,t,i){i===void 0&&(i=t),r[i]=e[t]}),zxe=Eo&&Eo.__exportStar||function(r,e){for(var t in r)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&Wxe(e,r,t)};Object.defineProperty(Eo,"__esModule",{value:!0});Eo.CancelError=Eo.ParseError=void 0;var V8=Td(),X8=class extends V8.RequestError{constructor(e,t){let{options:i}=t.request;super(`${e.message} in "${i.url.toString()}"`,e,t.request);this.name="ParseError"}};Eo.ParseError=X8;var Z8=class extends V8.RequestError{constructor(e){super("Promise was canceled",{},e);this.name="CancelError"}get isCanceled(){return!0}};Eo.CancelError=Z8;zxe(Td(),Eo)});var e5=w(DP=>{"use strict";Object.defineProperty(DP,"__esModule",{value:!0});var $8=Od(),_xe=(r,e,t,i)=>{let{rawBody:n}=r;try{if(e==="text")return n.toString(i);if(e==="json")return n.length===0?"":t(n.toString());if(e==="buffer")return n;throw new $8.ParseError({message:`Unknown body type '${e}'`,name:"Error"},r)}catch(s){throw new $8.ParseError(s,r)}};DP.default=_xe});var RP=w(dl=>{"use strict";var Vxe=dl&&dl.__createBinding||(Object.create?function(r,e,t,i){i===void 0&&(i=t),Object.defineProperty(r,i,{enumerable:!0,get:function(){return e[t]}})}:function(r,e,t,i){i===void 0&&(i=t),r[i]=e[t]}),Xxe=dl&&dl.__exportStar||function(r,e){for(var t in r)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&Vxe(e,r,t)};Object.defineProperty(dl,"__esModule",{value:!0});var Zxe=require("events"),$xe=nA(),ePe=J4(),Ow=Od(),t5=e5(),r5=Td(),tPe=lP(),rPe=dP(),i5=CP(),iPe=["request","response","redirect","uploadProgress","downloadProgress"];function n5(r){let e,t,i=new Zxe.EventEmitter,n=new ePe((o,a,l)=>{let c=u=>{let g=new r5.default(void 0,r);g.retryCount=u,g._noPipe=!0,l(()=>g.destroy()),l.shouldReject=!1,l(()=>a(new Ow.CancelError(g))),e=g,g.once("response",async p=>{var m;if(p.retryCount=u,p.request.aborted)return;let y;try{y=await rPe.default(g),p.rawBody=y}catch(T){return}if(g._isAboutToError)return;let b=((m=p.headers["content-encoding"])!==null&&m!==void 0?m:"").toLowerCase(),v=["gzip","deflate","br"].includes(b),{options:k}=g;if(v&&!k.decompress)p.body=y;else try{p.body=t5.default(p,k.responseType,k.parseJson,k.encoding)}catch(T){if(p.body=y.toString(),i5.isResponseOk(p)){g._beforeError(T);return}}try{for(let[T,Y]of k.hooks.afterResponse.entries())p=await Y(p,async q=>{let $=r5.default.normalizeArguments(void 0,te(N({},q),{retry:{calculateDelay:()=>0},throwHttpErrors:!1,resolveBodyOnly:!1}),k);$.hooks.afterResponse=$.hooks.afterResponse.slice(0,T);for(let ne of $.hooks.beforeRetry)await ne($);let z=n5($);return l(()=>{z.catch(()=>{}),z.cancel()}),z})}catch(T){g._beforeError(new Ow.RequestError(T.message,T,g));return}if(!i5.isResponseOk(p)){g._beforeError(new Ow.HTTPError(p));return}t=p,o(g.options.resolveBodyOnly?p.body:p)});let f=p=>{if(n.isCanceled)return;let{options:m}=g;if(p instanceof Ow.HTTPError&&!m.throwHttpErrors){let{response:y}=p;o(g.options.resolveBodyOnly?y.body:y);return}a(p)};g.once("error",f);let h=g.options.body;g.once("retry",(p,m)=>{var y,b;if(h===((y=m.request)===null||y===void 0?void 0:y.options.body)&&$xe.default.nodeStream((b=m.request)===null||b===void 0?void 0:b.options.body)){f(m);return}c(p)}),tPe.default(g,i,iPe)};c(0)});n.on=(o,a)=>(i.on(o,a),n);let s=o=>{let a=(async()=>{await n;let{options:l}=t.request;return t5.default(t,o,l.parseJson,l.encoding)})();return Object.defineProperties(a,Object.getOwnPropertyDescriptors(n)),a};return n.json=()=>{let{headers:o}=e.options;return!e.writableFinished&&o.accept===void 0&&(o.accept="application/json"),s("json")},n.buffer=()=>s("buffer"),n.text=()=>s("text"),n}dl.default=n5;Xxe(Od(),dl)});var s5=w(FP=>{"use strict";Object.defineProperty(FP,"__esModule",{value:!0});var nPe=Od();function sPe(r,...e){let t=(async()=>{if(r instanceof nPe.RequestError)try{for(let n of e)if(n)for(let s of n)r=await s(r)}catch(n){r=n}throw r})(),i=()=>t;return t.json=i,t.text=i,t.buffer=i,t.on=i,t}FP.default=sPe});var A5=w(NP=>{"use strict";Object.defineProperty(NP,"__esModule",{value:!0});var o5=nA();function a5(r){for(let e of Object.values(r))(o5.default.plainObject(e)||o5.default.array(e))&&a5(e);return Object.freeze(r)}NP.default=a5});var c5=w(l5=>{"use strict";Object.defineProperty(l5,"__esModule",{value:!0})});var LP=w(Ms=>{"use strict";var oPe=Ms&&Ms.__createBinding||(Object.create?function(r,e,t,i){i===void 0&&(i=t),Object.defineProperty(r,i,{enumerable:!0,get:function(){return e[t]}})}:function(r,e,t,i){i===void 0&&(i=t),r[i]=e[t]}),aPe=Ms&&Ms.__exportStar||function(r,e){for(var t in r)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&oPe(e,r,t)};Object.defineProperty(Ms,"__esModule",{value:!0});Ms.defaultHandler=void 0;var u5=nA(),Us=RP(),APe=s5(),Mw=Td(),lPe=A5(),cPe={RequestError:Us.RequestError,CacheError:Us.CacheError,ReadError:Us.ReadError,HTTPError:Us.HTTPError,MaxRedirectsError:Us.MaxRedirectsError,TimeoutError:Us.TimeoutError,ParseError:Us.ParseError,CancelError:Us.CancelError,UnsupportedProtocolError:Us.UnsupportedProtocolError,UploadError:Us.UploadError},uPe=async r=>new Promise(e=>{setTimeout(e,r)}),{normalizeArguments:Uw}=Mw.default,g5=(...r)=>{let e;for(let t of r)e=Uw(void 0,t,e);return e},gPe=r=>r.isStream?new Mw.default(void 0,r):Us.default(r),fPe=r=>"defaults"in r&&"options"in r.defaults,hPe=["get","post","put","patch","head","delete"];Ms.defaultHandler=(r,e)=>e(r);var f5=(r,e)=>{if(r)for(let t of r)t(e)},h5=r=>{r._rawHandlers=r.handlers,r.handlers=r.handlers.map(i=>(n,s)=>{let o,a=i(n,l=>(o=s(l),o));if(a!==o&&!n.isStream&&o){let l=a,{then:c,catch:u,finally:g}=l;Object.setPrototypeOf(l,Object.getPrototypeOf(o)),Object.defineProperties(l,Object.getOwnPropertyDescriptors(o)),l.then=c,l.catch=u,l.finally=g}return a});let e=(i,n={},s)=>{var o,a;let l=0,c=u=>r.handlers[l++](u,l===r.handlers.length?gPe:c);if(u5.default.plainObject(i)){let u=N(N({},i),n);Mw.setNonEnumerableProperties([i,n],u),n=u,i=void 0}try{let u;try{f5(r.options.hooks.init,n),f5((o=n.hooks)===null||o===void 0?void 0:o.init,n)}catch(f){u=f}let g=Uw(i,n,s!=null?s:r.options);if(g[Mw.kIsNormalizedAlready]=!0,u)throw new Us.RequestError(u.message,u,g);return c(g)}catch(u){if(n.isStream)throw u;return APe.default(u,r.options.hooks.beforeError,(a=n.hooks)===null||a===void 0?void 0:a.beforeError)}};e.extend=(...i)=>{let n=[r.options],s=[...r._rawHandlers],o;for(let a of i)fPe(a)?(n.push(a.defaults.options),s.push(...a.defaults._rawHandlers),o=a.defaults.mutableDefaults):(n.push(a),"handlers"in a&&s.push(...a.handlers),o=a.mutableDefaults);return s=s.filter(a=>a!==Ms.defaultHandler),s.length===0&&s.push(Ms.defaultHandler),h5({options:g5(...n),handlers:s,mutableDefaults:Boolean(o)})};let t=async function*(i,n){let s=Uw(i,n,r.options);s.resolveBodyOnly=!1;let o=s.pagination;if(!u5.default.object(o))throw new TypeError("`options.pagination` must be implemented");let a=[],{countLimit:l}=o,c=0;for(;c{let s=[];for await(let o of t(i,n))s.push(o);return s},e.paginate.each=t,e.stream=(i,n)=>e(i,te(N({},n),{isStream:!0}));for(let i of hPe)e[i]=(n,s)=>e(n,te(N({},s),{method:i})),e.stream[i]=(n,s)=>e(n,te(N({},s),{method:i,isStream:!0}));return Object.assign(e,cPe),Object.defineProperty(e,"defaults",{value:r.mutableDefaults?r:lPe.default(r),writable:r.mutableDefaults,configurable:r.mutableDefaults,enumerable:!0}),e.mergeOptions=g5,e};Ms.default=h5;aPe(c5(),Ms)});var Hw=w((oA,Kw)=>{"use strict";var pPe=oA&&oA.__createBinding||(Object.create?function(r,e,t,i){i===void 0&&(i=t),Object.defineProperty(r,i,{enumerable:!0,get:function(){return e[t]}})}:function(r,e,t,i){i===void 0&&(i=t),r[i]=e[t]}),p5=oA&&oA.__exportStar||function(r,e){for(var t in r)t!=="default"&&!Object.prototype.hasOwnProperty.call(e,t)&&pPe(e,r,t)};Object.defineProperty(oA,"__esModule",{value:!0});var dPe=require("url"),d5=LP(),CPe={options:{method:"GET",retry:{limit:2,methods:["GET","PUT","HEAD","DELETE","OPTIONS","TRACE"],statusCodes:[408,413,429,500,502,503,504,521,522,524],errorCodes:["ETIMEDOUT","ECONNRESET","EADDRINUSE","ECONNREFUSED","EPIPE","ENOTFOUND","ENETUNREACH","EAI_AGAIN"],maxRetryAfter:void 0,calculateDelay:({computedValue:r})=>r},timeout:{},headers:{"user-agent":"got (https://github.com/sindresorhus/got)"},hooks:{init:[],beforeRequest:[],beforeRedirect:[],beforeRetry:[],beforeError:[],afterResponse:[]},cache:void 0,dnsCache:void 0,decompress:!0,throwHttpErrors:!0,followRedirect:!0,isStream:!1,responseType:"text",resolveBodyOnly:!1,maxRedirects:10,prefixUrl:"",methodRewriting:!0,ignoreInvalidCookies:!1,context:{},http2:!1,allowGetBody:!1,https:void 0,pagination:{transform:r=>r.request.options.responseType==="json"?r.body:JSON.parse(r.body),paginate:r=>{if(!Reflect.has(r.headers,"link"))return!1;let e=r.headers.link.split(","),t;for(let i of e){let n=i.split(";");if(n[1].includes("next")){t=n[0].trimStart().trim(),t=t.slice(1,-1);break}}return t?{url:new dPe.URL(t)}:!1},filter:()=>!0,shouldContinue:()=>!0,countLimit:Infinity,backoff:0,requestLimit:1e4,stackAllItems:!0},parseJson:r=>JSON.parse(r),stringifyJson:r=>JSON.stringify(r),cacheOptions:{}},handlers:[d5.defaultHandler],mutableDefaults:!1},TP=d5.default(CPe);oA.default=TP;Kw.exports=TP;Kw.exports.default=TP;Kw.exports.__esModule=!0;p5(LP(),oA);p5(RP(),oA)});var I5=w(lf=>{"use strict";var Vnt=require("net"),mPe=require("tls"),OP=require("http"),C5=require("https"),EPe=require("events"),Xnt=require("assert"),IPe=require("util");lf.httpOverHttp=yPe;lf.httpsOverHttp=wPe;lf.httpOverHttps=BPe;lf.httpsOverHttps=bPe;function yPe(r){var e=new aA(r);return e.request=OP.request,e}function wPe(r){var e=new aA(r);return e.request=OP.request,e.createSocket=m5,e.defaultPort=443,e}function BPe(r){var e=new aA(r);return e.request=C5.request,e}function bPe(r){var e=new aA(r);return e.request=C5.request,e.createSocket=m5,e.defaultPort=443,e}function aA(r){var e=this;e.options=r||{},e.proxyOptions=e.options.proxy||{},e.maxSockets=e.options.maxSockets||OP.Agent.defaultMaxSockets,e.requests=[],e.sockets=[],e.on("free",function(i,n,s,o){for(var a=E5(n,s,o),l=0,c=e.requests.length;l=this.maxSockets){s.requests.push(o);return}s.createSocket(o,function(a){a.on("free",l),a.on("close",c),a.on("agentRemove",c),e.onSocket(a);function l(){s.emit("free",a,o)}function c(u){s.removeSocket(a),a.removeListener("free",l),a.removeListener("close",c),a.removeListener("agentRemove",c)}})};aA.prototype.createSocket=function(e,t){var i=this,n={};i.sockets.push(n);var s=MP({},i.proxyOptions,{method:"CONNECT",path:e.host+":"+e.port,agent:!1,headers:{host:e.host+":"+e.port}});e.localAddress&&(s.localAddress=e.localAddress),s.proxyAuth&&(s.headers=s.headers||{},s.headers["Proxy-Authorization"]="Basic "+new Buffer(s.proxyAuth).toString("base64")),Cl("making CONNECT request");var o=i.request(s);o.useChunkedEncodingByDefault=!1,o.once("response",a),o.once("upgrade",l),o.once("connect",c),o.once("error",u),o.end();function a(g){g.upgrade=!0}function l(g,f,h){process.nextTick(function(){c(g,f,h)})}function c(g,f,h){if(o.removeAllListeners(),f.removeAllListeners(),g.statusCode!==200){Cl("tunneling socket could not be established, statusCode=%d",g.statusCode),f.destroy();var p=new Error("tunneling socket could not be established, statusCode="+g.statusCode);p.code="ECONNRESET",e.request.emit("error",p),i.removeSocket(n);return}if(h.length>0){Cl("got illegal response body from proxy"),f.destroy();var p=new Error("got illegal response body from proxy");p.code="ECONNRESET",e.request.emit("error",p),i.removeSocket(n);return}return Cl("tunneling connection has established"),i.sockets[i.sockets.indexOf(n)]=f,t(f)}function u(g){o.removeAllListeners(),Cl(`tunneling socket could not be established, cause=%s +`,g.message,g.stack);var f=new Error("tunneling socket could not be established, cause="+g.message);f.code="ECONNRESET",e.request.emit("error",f),i.removeSocket(n)}};aA.prototype.removeSocket=function(e){var t=this.sockets.indexOf(e);if(t!==-1){this.sockets.splice(t,1);var i=this.requests.shift();i&&this.createSocket(i,function(n){i.request.onSocket(n)})}};function m5(r,e){var t=this;aA.prototype.createSocket.call(t,r,function(i){var n=r.request.getHeader("host"),s=MP({},t.options,{socket:i,servername:n?n.replace(/:.*$/,""):r.host}),o=mPe.connect(0,s);t.sockets[t.sockets.indexOf(i)]=o,e(o)})}function E5(r,e,t){return typeof r=="string"?{host:r,port:e,localAddress:t}:r}function MP(r){for(var e=1,t=arguments.length;e{y5.exports=I5()});var R5=w((Yw,GP)=>{var D5=Object.assign({},require("fs")),YP=function(){var r=typeof document!="undefined"&&document.currentScript?document.currentScript.src:void 0;return typeof __filename!="undefined"&&(r=r||__filename),function(e){e=e||{};var t=typeof e!="undefined"?e:{},i,n;t.ready=new Promise(function(d,E){i=d,n=E});var s={},o;for(o in t)t.hasOwnProperty(o)&&(s[o]=t[o]);var a=[],l="./this.program",c=function(d,E){throw E},u=!1,g=!0,f="";function h(d){return t.locateFile?t.locateFile(d,f):f+d}var p,m,y,b;g&&(u?f=require("path").dirname(f)+"/":f=__dirname+"/",p=function(E,I){var D=xa(E);return D?I?D:D.toString():(y||(y=D5),b||(b=require("path")),E=b.normalize(E),y.readFileSync(E,I?null:"utf8"))},m=function(E){var I=p(E,!0);return I.buffer||(I=new Uint8Array(I)),Z(I.buffer),I},process.argv.length>1&&(l=process.argv[1].replace(/\\/g,"/")),a=process.argv.slice(2),c=function(d){process.exit(d)},t.inspect=function(){return"[Emscripten Module object]"});var v=t.print||console.log.bind(console),k=t.printErr||console.warn.bind(console);for(o in s)s.hasOwnProperty(o)&&(t[o]=s[o]);s=null,t.arguments&&(a=t.arguments),t.thisProgram&&(l=t.thisProgram),t.quit&&(c=t.quit);var T=16;function Y(d,E){return E||(E=T),Math.ceil(d/E)*E}var q=0,$=function(d){q=d},z;t.wasmBinary&&(z=t.wasmBinary);var ne=t.noExitRuntime||!0;typeof WebAssembly!="object"&&vr("no native wasm support detected");function ee(d,E,I){switch(E=E||"i8",E.charAt(E.length-1)==="*"&&(E="i32"),E){case"i1":return pe[d>>0];case"i8":return pe[d>>0];case"i16":return Qe[d>>1];case"i32":return fe[d>>2];case"i64":return fe[d>>2];case"float":return Ht[d>>2];case"double":return Mt[d>>3];default:vr("invalid type for getValue: "+E)}return null}var A,oe=!1,ce;function Z(d,E){d||vr("Assertion failed: "+E)}function O(d){var E=t["_"+d];return Z(E,"Cannot call unknown function "+d+", make sure it is exported"),E}function L(d,E,I,D,M){var _={string:function(st){var yt=0;if(st!=null&&st!==0){var ke=(st.length<<2)+1;yt=B(ke),be(st,yt,ke)}return yt},array:function(st){var yt=B(st.length);return Ue(st,yt),yt}};function ie(st){return E==="string"?re(st):E==="boolean"?Boolean(st):st}var we=O(d),me=[],_e=0;if(D)for(var ot=0;ot=D);)++M;if(M-E>16&&d.subarray&&Be)return Be.decode(d.subarray(E,M));for(var _="";E>10,56320|_e&1023)}}return _}function re(d,E){return d?Ge(V,d,E):""}function se(d,E,I,D){if(!(D>0))return 0;for(var M=I,_=I+D-1,ie=0;ie=55296&&we<=57343){var me=d.charCodeAt(++ie);we=65536+((we&1023)<<10)|me&1023}if(we<=127){if(I>=_)break;E[I++]=we}else if(we<=2047){if(I+1>=_)break;E[I++]=192|we>>6,E[I++]=128|we&63}else if(we<=65535){if(I+2>=_)break;E[I++]=224|we>>12,E[I++]=128|we>>6&63,E[I++]=128|we&63}else{if(I+3>=_)break;E[I++]=240|we>>18,E[I++]=128|we>>12&63,E[I++]=128|we>>6&63,E[I++]=128|we&63}}return E[I]=0,I-M}function be(d,E,I){return se(d,V,E,I)}function he(d){for(var E=0,I=0;I=55296&&D<=57343&&(D=65536+((D&1023)<<10)|d.charCodeAt(++I)&1023),D<=127?++E:D<=2047?E+=2:D<=65535?E+=3:E+=4}return E}function Fe(d){var E=he(d)+1,I=Et(E);return I&&se(d,pe,I,E),I}function Ue(d,E){pe.set(d,E)}function xe(d,E){return d%E>0&&(d+=E-d%E),d}var ve,pe,V,Qe,le,fe,gt,Ht,Mt;function Ei(d){ve=d,t.HEAP8=pe=new Int8Array(d),t.HEAP16=Qe=new Int16Array(d),t.HEAP32=fe=new Int32Array(d),t.HEAPU8=V=new Uint8Array(d),t.HEAPU16=le=new Uint16Array(d),t.HEAPU32=gt=new Uint32Array(d),t.HEAPF32=Ht=new Float32Array(d),t.HEAPF64=Mt=new Float64Array(d)}var jt=t.INITIAL_MEMORY||16777216,Qr,Oi=[],$s=[],Hn=[],jn=!1;function Sr(){if(t.preRun)for(typeof t.preRun=="function"&&(t.preRun=[t.preRun]);t.preRun.length;)Qa(t.preRun.shift());Do(Oi)}function Gn(){jn=!0,!t.noFSInit&&!S.init.initialized&&S.init(),ps.init(),Do($s)}function fs(){if(t.postRun)for(typeof t.postRun=="function"&&(t.postRun=[t.postRun]);t.postRun.length;)Lu(t.postRun.shift());Do(Hn)}function Qa(d){Oi.unshift(d)}function RA(d){$s.unshift(d)}function Lu(d){Hn.unshift(d)}var hs=0,FA=null,Sa=null;function Tu(d){return d}function NA(d){hs++,t.monitorRunDependencies&&t.monitorRunDependencies(hs)}function LA(d){if(hs--,t.monitorRunDependencies&&t.monitorRunDependencies(hs),hs==0&&(FA!==null&&(clearInterval(FA),FA=null),Sa)){var E=Sa;Sa=null,E()}}t.preloadedImages={},t.preloadedAudios={};function vr(d){t.onAbort&&t.onAbort(d),d+="",k(d),oe=!0,ce=1,d="abort("+d+"). Build with -s ASSERTIONS=1 for more info.";var E=new WebAssembly.RuntimeError(d);throw n(E),E}var _l="data:application/octet-stream;base64,";function Ou(d){return d.startsWith(_l)}var Po="data:application/octet-stream;base64,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ACgCFC0AAiAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQADIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAAQgACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0ABSAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAGIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAAcgACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0ACCAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAJIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAAogACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0ACyAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAMIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAA0gACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0ADiAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAPIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhRBEGo2AhQgACAAKAIIQQFrIgE2AgggAQ0ACyAAIAAoAhhB8f8DcDYCGCAAIAAoAgxB8f8DcDYCDAwBCwsgACgCEARAA0AgACgCEEEQTwRAIAAgACgCEEEQazYCECAAIAAoAhQtAAAgACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0AASAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQACIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAAMgACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0ABCAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAFIAAoAhhqNgIYIAAgACgCGCAAKAIMajYCDCAAIAAoAhQtAAYgACgCGGo2AhggACAAKAIYIAAoAgxqNgIMIAAgACgCFC0AByAAKAIYajYCGCAAIAAoAhggACgCDGo2AgwgACAAKAIULQAIIAAoAhhqNgIYIAAgACgCGCAAKAIMajYC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NgIYIAEoAgxCADcDICABKAIMQgA3AyggASgCDEEANgIwIAEoAgxBADYCNCABKAIMQQA2AjggASgCDEEANgI8IAEoAgxBADsBQCABKAIMQYCA2I14NgJEIAEoAgxCADcDSCABKAIMQQA7AVAgASgCDEEAOwFSIAEoAgxBADYCVAvSEwEBfyMAQbABayIDJAAgAyAANgKoASADIAE2AqQBIAMgAjYCoAEgA0EANgKQASADIAMoAqQBKAIwQQAQOjYClAEgAyADKAKkASgCOEEAEDo2ApgBAkACQAJAAkAgAygClAFBAkYEQCADKAKYAUEBRg0BCyADKAKUAUEBRgRAIAMoApgBQQJGDQELIAMoApQBQQJHDQEgAygCmAFBAkcNAQsgAygCpAEiACAALwEMQYAQcjsBDAwBCyADKAKkASIAIAAvAQxB/+8DcTsBDCADKAKUAUECRgRAIANB9eABIAMoAqQBKAIwIAMoAqgBQQhqEI4BNgKQASADKAKQAUUEQCADQX82AqwBDAMLCwJAIAMoAqABQYACcQ0AIAMoApgBQQJHDQAgA0H1xgEgAygCpAEoAjggAygCqAFBCGoQjgE2AkggAygCSEUEQCADKAKQARAjIANBfzYCrAEMAwsgAygCSCADKAKQATYCACADIAMoAkg2ApABCwsCQCADKAKkAS8BUkUEQCADKAKkASIAIAAvAQxB/v8DcTsBDAwBCyADKAKkASIAIAAvAQxBAXI7AQwLIAMgAygCpAEgAygCoAEQZUEBcToAhgEgAyADKAKgAUGACnFBgApHBH8gAy0AhgEFQQELQQFxOgCHASADAn9BASADKAKkAS8BUkGBAkYNABpBASADKAKkAS8BUkGCAkYNABogAygCpAEvAVJBgwJGC0EBcToAhQEgAy0AhwFBAXEEQCADIANBIGpCHBApNgIcIAMoAhxFBEAgAygCqAFBCGpBDkEAEBQgAygCkAEQIyADQX82AqwBDAILAkAgAygCoAFBgAJxBEACQCADKAKgAUGACHENACADKAKkASkDIEL/////D1YNACADKAKkASkDKEL/////D1gNAgsgAygCHCADKAKkASkDKBAtIAMoAhwgAygCpAEpAyAQLQwBCwJAAkAgAygCoAFBgAhxDQAgAygCpAEpAyBC/////w9WDQAgAygCpAEpAyhC/////w9WDQAgAygCpAEpA0hC/////w9YDQELIAMoAqQBKQMoQv////8PWgRAIAMoAhwgAygCpAEpAygQLQsgAygCpAEpAyBC/////w9aBEAgAygCHCADKAKkASkDIBAtCyADKAKkASkDSEL/////D1oEQCADKAIcIAMoAqQBKQNIEC0LCwsCfyMAQRBrIgAgAygCHDYCDCAAKAIMLQAAQQFxRQsEQCADKAKoAUEIakEUQQAQFCADKAIcEBYgAygCkAEQIyADQX82AqwBDAILIANBAQJ/IwBBEGsiACADKAIcNgIMAn4gACgCDC0AAEEBcQRAIAAoAgwpAxAMAQtCAAunQf//A3ELIANBIGpBgAYQVTYCjAEgAygCHBAWIAMoAowBIAMoApABNgIAIAMgAygCjAE2ApABCyADLQCFAUEBcQRAIAMgA0EVakIHECk2AhAgAygCEEUEQCADKAKoAUEIakEOQQAQFCADKAKQARAjIANBfzYCrAEMAgsgAygCEEECEB8gAygCEEG9EkECEEEgAygCECADKAKkAS8BUkH/AXEQlgEgAygCECADKAKkASgCEEH//wNxEB8CfyMAQRBrIgAgAygCEDYCDCAAKAIMLQAAQQFxRQsEQCADKAKoAUEIakEUQQAQFCADKAIQEBYgAygCkAEQIyADQX82AqwBDAILIANBgbICQQcgA0EVakGABhBVNgIMIAMoAhAQFiADKAIMIAMoApABNgIAIAMgAygCDDYCkAELIAMgA0HQAGpCLhApIgA2AkwgAEUEQCADKAKoAUEIakEOQQAQFCADKAKQARAjIANBfzYCrAEMAQsgAygCTEHxEkH2EiADKAKgAUGAAnEbQQQQQSADKAKgAUGAAnFFBEAgAygCTCADLQCGAUEBcQR/QS0FIAMoAqQBLwEIC0H//wNxEB8LIAMoAkwgAy0AhgFBAXEEf0EtBSADKAKkAS8BCgtB//8DcRAfIAMoAkwgAygCpAEvAQwQHwJAIAMtAIUBQQFxBEAgAygCTEHjABAfDAELIAMoAkwgAygCpAEoAhBB//8DcRAfCyADKAKkASgCFCADQZ4BaiADQZwBahCNASADKAJMIAMvAZ4BEB8gAygCTCADLwGcARAfAkACQCADLQCFAUEBcUUNACADKAKkASkDKEIUWg0AIAMoAkxBABAhDAELIAMoAkwgAygCpAEoAhgQIQsCQAJAIAMoAqABQYACcUGAAkcNACADKAKkASkDIEL/////D1QEQCADKAKkASkDKEL/////D1QNAQsgAygCTEF/ECEgAygCTEF/ECEMAQsCQCADKAKkASkDIEL/////D1QEQCADKAJMIAMoAqQBKQMgpxAhDAELIAMoAkxBfxAhCwJAIAMoAqQBKQMoQv////8PVARAIAMoAkwgAygCpAEpAyinECEMAQsgAygCTEF/ECELCyADKAJMIAMoAqQBKAIwEFFB//8DcRAfIAMgAygCpAEoAjQgAygCoAEQkgFB//8DcSADKAKQAUGABhCSAUH//wNxajYCiAEgAygCTCADKAKIAUH//wNxEB8gAygCoAFBgAJxRQRAIAMoAkwgAygCpAEoAjgQUUH//wNxEB8gAygCTCADKAKkASgCPEH//wNxEB8gAygCTCADKAKkAS8BQBAfIAMoAkwgAygCpAEoAkQQIQJAIAMoAqQBKQNIQv////8PVARAIAMoAkwgAygCpAEpA0inECEMAQsgAygCTEF/ECELCwJ/IwBBEGsiACADKAJMNgIMIAAoAgwtAABBAXFFCwRAIAMoAqgBQQhqQRRBABAUIAMoAkwQFiADKAKQARAjIANBfzYCrAEMAQsgAygCqAEgA0HQAGoCfiMAQRBrIgAgAygCTDYCDAJ+IAAoAgwtAABBAXEEQCAAKAIMKQMQDAELQgALCxA2QQBIBEAgAygCTBAWIAMoApABECMgA0F/NgKsAQwBCyADKAJMEBYgAygCpAEoAjAEQCADKAKoASADKAKkASgCMBCFAUEASARAIAMoApABECMgA0F/NgKsAQwCCwsgAygCkAEEQCADKAKoASADKAKQAUGABhCRAUEASARAIAMoApABECMgA0F/NgKsAQwCCwsgAygCkAEQIyADKAKkASgCNARAIAMoAqgBIAMoAqQBKAI0IAMoAqABEJEBQQBIBEAgA0F/NgKsAQwCCwsgAygCoAFBgAJxRQRAIAMoAqQBKAI4BEAgAygCqAEgAygCpAEoAjgQhQFBAEgEQCADQX82AqwBDAMLCwsgAyADLQCHAUEBcTYCrAELIAMoAqwBIQAgA0GwAWokACAAC+ACAQF/IwBBIGsiBCQAIAQgADsBGiAEIAE7ARggBCACNgIUIAQgAzYCECAEQRAQGCIANgIMAkAg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AAAgAygCHEHcFmogAygCDEECdGooAgAgAygCHEHYKGpqLQAASg0ADAELIAMoAhxB3BZqIAMoAhRBAnRqIAMoAhxB3BZqIAMoAgxBAnRqKAIANgIAIAMgAygCDDYCFCADIAMoAgxBAXQ2AgwMAQsLIAMoAhxB3BZqIAMoAhRBAnRqIAMoAhA2AgAL1xMBA38jAEEwayICJAAgAiAANgIsIAIgATYCKCACIAIoAigoAgA2AiQgAiACKAIoKAIIKAIANgIgIAIgAigCKCgCCCgCDDYCHCACQX82AhAgAigCLEEANgLQKCACKAIsQb0ENgLUKCACQQA2AhgDQCACKAIYIAIoAhxIBEACQCACKAIkIAIoAhhBAnRqLwEABEAgAiACKAIYIgE2AhAgAigCLEHcFmohAyACKAIsIgQoAtAoQQFqIQAgBCAANgLQKCAAQQJ0IANqIAE2AgAgAigCGCACKAIsQdgoampBADoAAAwBCyACKAIkIAIoAhhBAnRqQQA7AQILIAIgAigCGEEBajYCGAwBCwsDQCACKAIsKALQKEECSARAAkAgAigCEEECSARAIAIgAigCEEEBaiIANgIQDAELQQAhAAsgAigCLEHcFmohAyACKAIsIgQoAtAoQQFqIQEgBCABNgLQKCABQQJ0IANqIAA2AgAgAiAANgIMIAIoAiQgAigCDEECdGpBATsBACACKAIMIAIoAixB2ChqakEAOgAAIAIoAiwiACAAKAKoLUEBazYCqC0gAigCIARAIAIoAiwiACAAKAKsLSACKAIgIAIoAgxBAnRqLwECazYCrC0LDAELCyACKAIoIAIoAhA2AgQgAiACKAIsKALQKEECbTYCGANAIAIoAhhBAU4EQCACKAIsIAIoAiQgAigCGBB5IAIgAigCGEEBazYCGAwBCwsgAiACKAIcNgIMA0AgAiACKAIsKALgFjYCGCACKAIsQdwWaiEBIAIoAiwiAygC0CghACADIABBAWs2AtAoIAIoAiwgAEECdCABaigCADYC4BYgAigCLCACKAIkQQEQeSACIAIoAiwoAuAWNgIUIAIoAhghASACKAIsQdwWaiEDIAIoAiwiBCgC1ChBAWshACAEIAA2AtQoIABBAnQgA2ogATYCACACKAIUIQEgAigCLEHcFmohAyACKAIsIgQoAtQoQQFrIQAgBCAANgLUKCAA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AQsgAigCCEEANgIAIAIoAghCADcDECACKAIIQgA3AwggAigCCEIANwMgIAIoAghCADcDGCACKAIIQQA2AiggAigCCEEAOgAsIAIoAgggAikDECACKAIMEI8BQQFxRQRAIAIoAggQJSACQQA2AhwMAQsgAiACKAIINgIcCyACKAIcIQEgAkEgaiQAIAEL1gIBAX8jAEEgayIDJAAgAyAANgIYIAMgATYCFCADIAI2AhAgAyADQQxqQgQQKTYCCAJAIAMoAghFBEAgA0F/NgIcDAELA0AgAygCFARAIAMoAhQoAgQgAygCEHFBgAZxBEAgAygCCEIAECwaIAMoAgggAygCFC8BCBAfIAMoAgggAygCFC8BChAfAn8jAEEQayIAIAMoAgg2AgwgACgCDC0AAEEBcUULBEAgAygCGEEIakEUQQAQFCADKAIIEBYgA0F/NgIcDAQLIAMoAhggA0EMakIEEDZBAEgEQCADKAIIEBYgA0F/NgIcDAQLIAMoAhQvAQoEQCADKAIYIAMoAhQoAgwgAygCFC8BCq0QNkEASARAIAMoAggQFiADQX82AhwMBQsLCyADIAMoAhQoAgA2AhQMAQsLIAMoAggQFiADQQA2AhwLIAMoAhwhACADQSBqJAAgAAtoAQF/IwBBEGsiAiAANgIMIAIgATYCCCACQQA7AQYDQCACKAIMBEAgAigCDCgCBCACKAIIcUGABnEEQCACIAIoAgwvAQogAi8BBkEEamo7AQYLIAIgAigCDCgCADYCDAwBCwsgAi8BBgvwAQEBfyMAQRBrIgEkACABIAA2AgwgASABKAIMNgIIIAFBADYCBANAIAEoAgwEQAJAAkAgASgCDC8BCEH1xgFGDQAgASgCDC8BCEH14AFGDQAgASgCDC8BCEGBsgJGDQAgASgCDC8BCEEBRw0BCyABIAEoAgwoAgA2AgAgASgCCCABKAIMRgRAIAEgASgCADYCCAsgASgCDEEANgIAIAEoAgwQIyABKAIEBEAgASgCBCABKAIANgIACyABIAEoAgA2AgwMAgsgASABKAIMNgIEIAEgASgCDCgCADYCDAwBCwsgASgCCCEAIAFBEGokACAAC7IEAQF/IwBBQGoiBSQAIAUgADYCOCAFIAE7ATYgBSACNgIwIAUgAzYCLCAFIAQ2AiggBSAFKAI4IAUvATatECkiADYCJAJAIABFBEAgBSgCKEEOQQAQFCAFQQA6AD8MAQsgBUEANgIgIAVBADYCGANAAn8jAEEQayIAIAUoAiQ2AgwgACgCDC0AAEEBcQsEfyAFKAIkEDBCBFoFQQALQQFxBEAgBSAFKAIkEB07ARYgBSAFKAIkEB07ARQgBSAFKAIkIAUvARStEB42AhAgBSgCEEUEQCAFKAIoQRVBABAUIAUoAiQQFiAFKAIYECMgBUEAOgA/DAMLIAUgBS8BFiAFLwEUIAUoAhAgBSgCMBBVIgA2AhwgAEUEQCAFKAIoQQ5BABAUIAUoAiQQFiAFKAIYECMgBUEAOgA/DAMLAkAgBSgCGARAIAUoAiAgBSgCHDYCACAFIAUoAhw2AiAMAQsgBSAFKAIcIgA2AiAgBSAANgIYCwwBCwsgBSgCJBBHQQFxRQRAIAUgBSgCJBAwPgIMIAUgBSgCJCAFKAIMrRAeNgIIAkACQCAFKAIMQQRPDQAgBSgCCEUNACAFKAIIQZEVIAUoAgwQT0UNAQsgBSgCKEEVQQAQFCAFKAIkEBYgBSgCGBAjIAVBADoAPwwCCwsgBSgCJBAWAkAgBSgCLARAIAUoAiwgBSgCGDYCAAwBCyAFKAIYECMLIAVBAToAPwsgBS0AP0EBcSEAIAVBQGskACAAC+8CAQF/IwBBIGsiAiQAIAIgADYCGCACIAE2AhQCQCACKAIYRQRAIAIgAigCFDYCHAwBCyACIAIoAhg2AggDQCACKAIIKAIABEAgAiACKAIIKAIANgIIDAELCwNAIAIoAhQEQCACIAIoAhQoAgA2AhAgAkEANgIEIAIgAigCGDYCDANAAkAgAigCDEUNAAJAIAIoAgwvAQggAigCFC8BCEcNACACKAIMLwEKIAIoAhQvAQpHDQAgAigCDC8BCgRAIAIoAgwoAgwgAigCFCgCDCACKAIMLwEKEE8NAQsgAigCDCIAIAAoAgQgAigCFCgCBEGABnFyNgIEIAJBATYCBAwBCyACIAIoAgwoAgA2AgwMAQsLIAIoAhRBADYCAAJAIAIoAgQEQCACKAIUECMMAQsgAigCCCACKAIUIgA2AgAgAiAANgIICyACIAIoAhA2AhQMAQsLIAIgAigCGDYCHAsgAigCHCEAIAJBIGokACAAC18BAX8jAEEQayICJAAgAiAANgIIIAIgAToAByACIAIoAghCARAeNgIAAkAgAigCAEUEQCACQX82AgwMAQsgAigCACACLQAHOgAAIAJBADYCDAsgAigCDBogAkEQaiQAC1QBAX8jAEEQayIBJAAgASAANgIIIAEgASgCCEIBEB42AgQCQCABKAIERQRAIAFBADoADwwBCyABIAEoAgQtAAA6AA8LIAEtAA8hACABQRBqJAAgAAucBgECfyMAQSBrIgIkACACIAA2AhggAiABNwMQAkAgAikDECACKAIYKQMwWgRAIAIoAhhBCGpBEkEAEBQgAkF/NgIcDAELIAIoAhgoAhhBAnEEQCACKAIYQQhqQRlBABAUIAJBfzYCHAwBCyACIAIoAhggAikDEEEAIAIoAhhBCGoQTSIANgIMIABFBEAgAkF/NgIcDAELIAIoAhgoAlAgAigCDCACKAIYQQhqEFlBAXFFBEAgAkF/NgIcDAELAn8gAigCGCEDIAIpAxAhASMAQTBrIgAkACAAIAM2AiggACABNwMgIABBATYCHAJAIAApAyAgACgCKCkDMFoEQCAAKAIoQQhqQRJBABAUIABBfzYCLAwBCwJAIAAoAhwNACAAKAIoKAJAIAApAyCnQQR0aigCBEUNACAAKAIoKAJAIAApAyCnQQR0aigCBCgCAEECcUUNAAJAIAAoAigoAkAgACkDIKdBBHRqKAIABEAgACAAKAIoIAApAyBBCCAAKAIoQQhqEE0iAzYCDCADRQRAIABBfzYCLAwECyAAIAAoAiggACgCDEEAQQAQWDcDEAJAIAApAxBCAFMNACAAKQMQIAApAyBRDQAgACgCKEEIakEKQQAQFCAAQX82AiwMBAsMAQsgAEEANgIMCyAAIAAoAiggACkDIEEAIAAoAihBCGoQTSIDNgIIIANFBEAgAEF/NgIsDAILIAAoAgwEQCAAKAIoKAJQIAAoAgwgACkDIEEAIAAoAihBCGoQdEEBcUUEQCAAQX82AiwMAwsLIAAoAigoAlAgACgCCCAAKAIoQQhqEFlBAXFFBEAgACgCKCgCUCAAKAIMQQAQWRogAEF/NgIsDAILCyAAKAIoKAJAIAApAyCnQQR0aigCBBA3IAAoAigoAkAgACkDIKdBBHRqQQA2AgQgACgCKCgCQCAAKQMgp0EEdGoQXiAAQQA2AiwLIAAoAiwhAyAAQTBqJAAgAwsEQCACQX82AhwMAQsgAigCGCgCQCACKQMQp0EEdGpBAToADCACQQA2AhwLIAIoAhwhACACQSBqJAAgAAulBAEBfyMAQTBrIgUkACAFIAA2AiggBSABNwMgIAUgAjYCHCAFIAM6ABsgBSAENgIUAkAgBSgCKCAFKQMgQQBBABA/RQRAIAVBfzYCLAwBCyAFKAIoKAIYQQJxBEAgBSgCKEEIakEZQQAQFCAFQX82AiwMAQsgBSAFKAIoKAJAIAUpAyCnQQR0ajYCECAFAn8gBSgCECgCAARAIAUoAhAoAgAvAQhBCHYMAQtBAws6AAsgBQJ/IAUoAhAoAgAEQCAFKAIQKAIAKAJEDAELQYCA2I14CzYCBEEBIQAgBSAFLQAbIAUtAAtGBH8gBSgCFCAFKAIERwVBAQtBAXE2AgwCQCAFKAIMBEAgBSgCECgCBEUEQCAFKAIQKAIAEEAhACAFKAIQIAA2AgQgAEUEQCAFKAIoQQhqQQ5BABAUIAVBfzYCLAwECwsgBSgCECgCBCAFKAIQKAIELwEIQf8BcSAFLQAbQQh0cjsBCCAFKAIQKAIEIAUoAhQ2AkQgBSgCECgCBCIAIAAoAgBBEHI2AgAMAQsgBSgCECgCBARAIAUoAhAoAgQiACAAKAIAQW9xNgIAAkAgBSgCECgCBCgCAEUEQCAFKAIQKAIEEDcgBSgCEEEANgIEDAELIAUoAhAoAgQgBSgCECgCBC8BCEH/AXEgBS0AC0EIdHI7AQggBSgCECgCBCAFKAIENgJECwsLIAVBADYCLAsgBSgCLCEAIAVBMGokACAAC90PAgF/AX4jAEFAaiIEJAAgBCAANgI0IARCfzcDKCAEIAE2AiQgBCACNgIgIAQgAzYCHAJAIAQoAjQoAhhBAnEEQCAEKAI0QQhqQRlBABAUIARCfzcDOAwBCyAEIAQoAjQpAzA3AxAgBCkDKEJ/UQRAIARCfzcDCCAEKAIcQYDAAHEEQCAEIAQoAjQgBCgCJCAEKAIcQQAQWDcDCAsgBCkDCEJ/UQRAIAQoAjQhASMAQUBqIgAkACAAIAE2AjQCQCAAKAI0KQM4IAAoAjQpAzBCAXxYBEAgACAAKAI0KQM4NwMYIAAgACkDGEIBhjcDEAJAIAApAxBCEFQEQCAAQhA3AxAMAQsgACkDEEKACFYEQCAAQoAINwMQCwsgACAAKQMQIAApAxh8NwMYIAAgACkDGKdBBHStNwMIIAApAwggACgCNCkDOKdBBHStVARAIAAoAjRBCGpBDkEAEBQgAEJ/NwM4DAILIAAgACgCNCgCQCAAKQMYp0EEdBBONgIkIAAoAiRFBEAgACgCNEEIakEOQQAQFCAAQn83AzgMAgsgACgCNCAAKAIkNgJAIAAoAjQgACkDGDcDOAsgACgCNCIBKQMwIQUgASAFQgF8NwMwIAAgBTcDKCAAKAI0KAJAIAApAyinQQR0ahC1ASAAIAApAyg3AzgLIAApAzghBSAAQUBrJAAgBCAFNwMIIAVCAFMEQCAEQn83AzgMAwsLIAQgBCkDCDcDKAsCQCAEKAIkRQ0AIAQoAjQhASAEKQMoIQUgBCgCJCECIAQoAhwhAyMAQUBqIgAkACAAIAE2AjggACAFNwMwIAAgAjYCLCAAIAM2AigCQCAAKQMwIAAoAjgpAzBaBEAgACgCOEEIakESQQAQFCAAQX82AjwMAQsgACgCOCgCGEECcQRAIAAoAjhBCGpBGUEAEBQgAEF/NgI8DAELAkACQCAAKAIsRQ0AIAAoAiwsAABFDQAgACAAKAIsIAAoAiwQLkH//wNxIAAoAiggACgCOEEIahBQIgE2AiAgAUUEQCAAQX82AjwMAwsCQCAAKAIoQYAwcQ0AIAAoAiBBABA6QQNHDQAgACgCIEECNgIICwwBCyAAQQA2AiALIAAgACgCOCAAKAIsQQBBABBYIgU3AxACQCAFQgBTDQAgACkDECAAKQMwUQ0AIAAoAiAQJCAAKAI4QQhqQQpBABAUIABBfzYCPAwBCwJAIAApAxBCAFMNACAAKQMQIAApAzBSDQAgACgCIBAkIABBADYCPAwBCyAAIAAoAjgoAkAgACkDMKdBBHRqNgIkAkAgACgCJCgCAARAIAAgACgCJCgCACgCMCAAKAIgEIYBQQBHOgAfDAELIABBADoAHwsCQCAALQAfQQFxDQAgACgCJCgCBA0AIAAoAiQoAgAQQCEBIAAoAiQgATYCBCABRQRAIAAoAjhBCGpBDkEAEBQgACgCIBAkIABBfzYCPAwCCwsgAAJ/IAAtAB9BAXEEQCAAKAIkKAIAKAIwDAELIAAoAiALQQBBACAAKAI4QQhqEEYiATYCCCABRQRAIAAoAiAQJCAAQX82AjwMAQsCQCAAKAIkKAIEBEAgACAAKAIkKAIEKAIwNgIEDAELAkAgACgCJCgCAARAIAAgACgCJCgCACgCMDYCBAwBCyAAQQA2AgQLCwJAIAAoAgQEQCAAIAAoAgRBAEEAIAAoAjhBCGoQRiIBNgIMIAFFBEAgACgCIBAkIABBfzYCPAwDCwwBCyAAQQA2AgwLIAAoAjgoAlAgACgCCCAAKQMwQQAgACgCOEEIahB0QQFxRQRAIAAoAiAQJCAAQX82AjwMAQsgACgCDARAIAAoAjgoAlAgACgCDEEAEFkaCwJAIAAtAB9BAXEEQCAAKAIkKAIEBEAgACgCJCgCBCgCAEECcQRAIAAoAiQoAgQoAjAQJCAAKAIkKAIEIgEgASgCAEF9cTYCAAJAIAAoAiQoAgQoAgBFBEAgACgCJCgCBBA3IAAoAiRBADYCBAwBCyAAKAIkKAIEIAAoAiQoAgAoAjA2AjALCwsgACgCIBAkDAELIAAoAiQoAgQoAgBBAnEEQCAAKAIkKAIEKAIwECQLIAAoAiQoAgQiASABKAIAQQJyNgIAIAAoAiQoAgQgACgCIDYCMAsgAEEANgI8CyAAKAI8IQEgAEFAayQAIAFFDQAgBCgCNCkDMCAEKQMQUgRAIAQoAjQoAkAgBCkDKKdBBHRqEHcgBCgCNCAEKQMQNwMwCyAEQn83AzgMAQsgBCgCNCgCQCAEKQMop0EEdGoQXgJAIAQoAjQoAkAgBCkDKKdBBHRqKAIARQ0AIAQoAjQoAkAgBCkDKKdBBHRqKAIEBEAgBCgCNCgCQCAEKQMop0EEdGooAgQoAgBBAXENAQsgBCgCNCgCQCAEKQMop0EEdGooAgRFBEAgBCgCNCgCQCAEKQMop0EEdGooAgAQQCEAIAQoAjQoAkAgBCkDKKdBBHRqIAA2AgQgAEUEQCAEKAI0QQhqQQ5BABAUIARCfzcDOAwDCwsgBCgCNCgCQCAEKQMop0EEdGooAgRBfjYCECAEKAI0KAJAIAQpAyinQQR0aigCBCIAIAAoAgBBAXI2AgALIAQoAjQoAkAgBCkDKKdBBHRqIAQoAiA2AgggBCAEKQMoNwM4CyAEKQM4IQUgBEFAayQAIAULqgEBAX8jAEEwayICJAAgAiAANgIoIAIgATcDICACQQA2AhwCQAJAIAIoAigoAiRBAUYEQCACKAIcRQ0BIAIoAhxBAUYNASACKAIcQQJGDQELIAIoAihBDGpBEkEAEBQgAkF/NgIsDAELIAIgAikDIDcDCCACIAIoAhw2AhAgAkF/QQAgAigCKCACQQhqQhBBDBAgQgBTGzYCLAsgAigCLCEAIAJBMGokACAAC6UyAwZ/AX4BfCMAQeAAayIEJAAgBCAANgJYIAQgATYCVCAEIAI2AlACQAJAIAQoAlRBAE4EQCAEKAJYDQELIAQoAlBBEkEAEBQgBEEANgJcDAELIAQgBCgCVDYCTCMAQRBrIgAgBCgCWDYCDCAEIAAoAgwpAxg3A0BB4JoBKQMAQn9RBEAgBEF/NgIUIARBAzYCECAEQQc2AgwgBEEGNgIIIARBAjYCBCAEQQE2AgBB4JoBQQAgBBA0NwMAIARBfzYCNCAEQQ82AjAgBEENNgIsIARBDDYCKCAEQQo2AiQgBEEJNgIgQeiaAUEIIARBIGoQNDcDAAtB4JoBKQMAIAQpA0BB4JoBKQMAg1IEQCAEKAJQQRxBABAUIARBADYCXAwBC0HomgEpAwAgBCkDQEHomgEpAwCDUgRAIAQgBCgCTEEQcjYCTAsgBCgCTEEYcUEYRgRAIAQoAlBBGUEAEBQgBEEANgJcDAELIAQoAlghASAEKAJQIQIjAEHQAGsiACQAIAAgATYCSCAAIAI2AkQgAEEIahA7AkAgACgCSCAAQQhqEDkEQCMAQRBrIgEgACgCSDYCDCAAIAEoAgxBDGo2AgQjAEEQayIBIAAoAgQ2AgwCQCABKAIMKAIAQQVHDQAjAEEQayIBIAAoAgQ2AgwgASgCDCgCBEEsRw0AIABBADYCTAwCCyAAKAJEIAAoAgQQRSAAQX82AkwMAQsgAEEBNgJMCyAAKAJMIQEgAEHQAGokACAEIAE2AjwCQAJAAkAgBCgCPEEBag4CAAECCyAEQQA2AlwMAgsgBCgCTEEBcUUEQCAEKAJQQQlBABAUIARBADYCXAwCCyAEIAQoAlggBCgCTCAEKAJQEGk2AlwMAQsgBCgCTEECcQRAIAQoAlBBCkEAEBQgBEEANgJcDAELIAQoAlgQSEEASARAIAQoAlAgBCgCWBAXIARBADYCXAwBCwJAIAQoAkxBCHEEQCAEIAQoAlggBCgCTCAEKAJQEGk2AjgMAQsgBCgCWCEAIAQoAkwhASAEKAJQIQIjAEHwAGsiAyQAIAMgADYCaCADIAE2AmQgAyACNgJgIANBIGoQOwJAIAMoAmggA0EgahA5QQBIBEAgAygCYCADKAJoEBcgA0EANgJsDAELIAMpAyBCBINQBEAgAygCYEEEQYoBEBQgA0EANgJsDAELIAMgAykDODcDGCADIAMoAmggAygCZCADKAJgEGkiADYCXCAARQRAIANBADYCbAwBCwJAIAMpAxhQRQ0AIAMoAmgQngFBAXFFDQAgAyADKAJcNgJsDAELIAMoAlwhACADKQMYIQkjAEHgAGsiAiQAIAIgADYCWCACIAk3A1ACQCACKQNQQhZUBEAgAigCWEEIakETQQAQFCACQQA2AlwMAQsgAgJ+IAIpA1BCqoAEVARAIAIpA1AMAQtCqoAECzcDMCACKAJYKAIAQgAgAikDMH1BAhAnQQBIBEAjAEEQayIAIAIoAlgoAgA2AgwgAiAAKAIMQQxqNgIIAkACfyMAQRBrIgAgAigCCDYCDCAAKAIMKAIAQQRGCwRAIwBBEGsiACACKAIINgIMIAAoAgwoAgRBFkYNAQsgAigCWEEIaiACKAIIEEUgAkEANgJcDAILCyACIAIoAlgoAgAQSSIJNwM4IAlCAFMEQCACKAJYQQhqIAIoAlgoAgAQFyACQQA2AlwMAQsgAiACKAJYKAIAIAIpAzBBACACKAJYQQhqEEIiADYCDCAARQRAIAJBADYCXAwBCyACQn83AyAgAkEANgJMIAIpAzBCqoAEWgRAIAIoAgxCFBAsGgsgAkEQakETQQAQFCACIAIoAgxCABAeNgJEA0ACQCACKAJEIQEgAigCDBAwQhJ9pyEFIwBBIGsiACQAIAAgATYCGCAAIAU2AhQgAEHsEjYCECAAQQQ2AgwCQAJAIAAoAhQgACgCDE8EQCAAKAIMDQELIABBADYCHAwBCyAAIAAoAhhBAWs2AggDQAJAIAAgACgCCEEBaiAAKAIQLQAAIAAoAhggACgCCGsgACgCFCAAKAIMa2oQqwEiATYCCCABRQ0AIAAoAghBAWogACgCEEEBaiAAKAIMQQFrEE8NASAAIAAoAgg2AhwMAgsLIABBADYCHAsgACgCHCEBIABBIGokACACIAE2AkQgAUUNACACKAIMIAIoAkQCfyMAQRBrIgAgAigCDDYCDCAAKAIMKAIEC2usECwaIAIoAlghASACKAIMIQUgAikDOCEJIwBB8ABrIgAkACAAIAE2AmggACAFNgJkIAAgCTcDWCAAIAJBEGo2AlQjAEEQayIBIAAoAmQ2AgwgAAJ+IAEoAgwtAABBAXEEQCABKAIMKQMQDAELQgALNwMwAkAgACgCZBAwQhZUBEAgACgCVEETQQAQFCAAQQA2AmwMAQsgACgCZEIEEB4oAABB0JaVMEcEQCAAKAJUQRNBABAUIABBADYCbAwBCwJAAkAgACkDMEIUVA0AIwBBEGsiASAAKAJkNgIMIAEoAgwoAgQgACkDMKdqQRRrKAAAQdCWmThHDQAgACgCZCAAKQMwQhR9ECwaIAAoAmgoAgAhBSAAKAJkIQYgACkDWCEJIAAoAmgoAhQhByAAKAJUIQgjAEGwAWsiASQAIAEgBTYCqAEgASAGNgKkASABIAk3A5gBIAEgBzYClAEgASAINgKQASMAQRBrIgUgASgCpAE2AgwgAQJ+IAUoAgwtAABBAXEEQCAFKAIMKQMQDAELQgALNwMYIAEoAqQBQgQQHhogASABKAKkARAdQf//A3E2AhAgASABKAKkARAdQf//A3E2AgggASABKAKkARAxNwM4AkAgASkDOEL///////////8AVgRAIAEoApABQQRBFhAUIAFBADYCrAEMAQsgASkDOEI4fCABKQMYIAEpA5gBfFYEQCABKAKQAUEVQQAQFCABQQA2AqwBDAELAkACQCABKQM4IAEpA5gBVA0AIAEpAzhCOHwgASkDmAECfiMAQRBrIgUgASgCpAE2AgwgBSgCDCkDCAt8Vg0AIAEoAqQBIAEpAzggASkDmAF9ECwaIAFBADoAFwwBCyABKAKoASABKQM4QQAQJ0EASARAIAEoApABIAEoAqgBEBcgAUEANgKsAQwCCyABIAEoAqgBQjggAUFAayABKAKQARBCIgU2AqQBIAVFBEAgAUEANgKsAQwCCyABQQE6ABcLIAEoAqQBQgQQHigAAEHQlpkwRwRAIAEoApABQRVBABAUIAEtABdBAXEEQCABKAKkARAWCyABQQA2AqwBDAELIAEgASgCpAEQMTcDMAJAIAEoApQBQQRxRQ0AIAEpAzAgASkDOHxCDHwgASkDmAEgASkDGHxRDQAgASgCkAFBFUEAEBQgAS0AF0EBcQRAIAEoAqQBEBYLIAFBADYCrAEMAQsgASgCpAFCBBAeGiABIAEoAqQBECo2AgwgASABKAKkARAqNgIEIAEoAhBB//8DRgRAIAEgASgCDDYCEAsgASgCCEH//wNGBEAgASABKAIENgIICwJAIAEoApQBQQRxRQ0AIAEoAgggASgCBEYEQCABKAIQIAEoAgxGDQELIAEoApABQRVBABAUIAEtABdBAXEEQCABKAKkARAWCyABQQA2AqwBDAELAkAgASgCEEUEQCABKAIIRQ0BCyABKAKQAUEBQQAQFCABLQAXQQFxBEAgASgCpAEQFgsgAUEANgKsAQwBCyABIAEoAqQBEDE3AyggASABKAKkARAxNwMgIAEpAyggASkDIFIEQCABKAKQAUEBQQAQFCABLQAXQQFxBEAgASgCpAEQFgsgAUEANgKsAQwBCyABIAEoAqQBEDE3AzAgASABKAKkARAxNwOAAQJ/IwBBEGsiBSABKAKkATYCDCAFKAIMLQAAQQFxRQsEQCABKAKQAUEUQQAQFCABLQAXQQFxBEAgASgCpAEQFgsgAUEANgKsAQwBCyABLQAXQQFxBEAgASgCpAEQFgsCQCABKQOAAUL///////////8AWARAIAEpA4ABIAEpA4ABIAEpAzB8WA0BCyABKAKQAUEEQRYQFCABQQA2AqwBDAELIAEpA4ABIAEpAzB8IAEpA5gBIAEpAzh8VgRAIAEoApABQRVBABAUIAFBADYCrAEMAQsCQCABKAKUAUEEcUUNACABKQOAASABKQMwfCABKQOYASABKQM4fFENACABKAKQAUEVQQAQFCABQQA2AqwBDAELIAEpAyggASkDMEIugFYEQCABKAKQAUEVQQAQFCABQQA2AqwBDAELIAEgASkDKCABKAKQARCQASIFNgKMASAFRQRAIAFBADYCrAEMAQsgASgCjAFBAToALCABKAKMASABKQMwNwMYIAEoAowBIAEpA4ABNwMgIAEgASgCjAE2AqwBCyABKAKsASEFIAFBsAFqJAAgACAFNgJQDAELIAAoAmQgACkDMBAsGiAAKAJkIQUgACkDWCEJIAAoAmgoAhQhBiAAKAJUIQcjAEHQAGsiASQAIAEgBTYCSCABIAk3A0AgASAGNgI8IAEgBzYCOAJAIAEoAkgQMEIWVARAIAEoAjhBFUEAEBQgAUEANgJMDAELIwBBEGsiBSABKAJINgIMIAECfiAFKAIMLQAAQQFxBEAgBSgCDCkDEAwBC0IACzcDCCABKAJI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ECADQcgAEBgiADYCDAJAIABFBEAgAygCEEEBQbSbASgCABAUIANBADYCHAwBCyADKAIMIAMoAhA2AgAgAygCDCADLQAbQQFxOgAEIAMoAgwgAygCFDYCCAJAIAMoAgwoAghBAU4EQCADKAIMKAIIQQlMDQELIAMoAgxBCTYCCAsgAygCDEEAOgAMIAMoAgxBADYCMCADKAIMQQA2AjQgAygCDEEANgI4IAMgAygCDDYCHAsgAygCHCEAIANBIGokACAACzgBAX8jAEEQayIBIAA2AgwgASgCDEEANgIAIAEoAgxBADYCBCABKAIMQQA2AgggASgCDEEAOgAMC+MIAQF/IwBBQGoiAiAANgI4IAIgATYCNCACIAIoAjgoAnw2AjAgAiACKAI4KAI4IAIoAjgoAmxqNgIsIAIgAigCOCgCeDYCICACIAIoAjgoApABNgIcIAICfyACKAI4KAJsIAIoAjgoAixBhgJrSwRAIAIoAjgoAmwgAigCOCgCLEGGAmtrDAELQQALNgIYIAIgAigCOCgCQDYCFCACIAIoAjgoAjQ2AhAgAiACKAI4KAI4IAIoAjgoAmxqQYICajYCDCACIAIoAiwgAigCIEEBa2otAAA6AAsgAiACKAIsIAIoAiBqLQAAOgAKIAIoAjgoAnggAigCOCgCjAFPBEAgAiACKAIwQQJ2NgIwCyACKAIcIAIoAjgoAnRLBEAgAiACKAI4KAJ0NgIcCwNAAkAgAiACKAI4KAI4IAIoAjRqNgIoAkAgAigCKCACKAIgai0AACACLQAKRw0AIAIoAiggAigCIEEBa2otAAAgAi0AC0cNACACKAIoLQAAIAIoAiwtAABHDQAgAiACKAIoIgBBAWo2AiggAC0AASACKAIsLQABRwRADAELIAIgAigCLEECajYCLCACIAIoAihBAWo2AigDQCACIAIoAiwiAEEBajYCLCAALQABIQEgAiACKAIoIgBBAWo2AigCf0EAIAAtAAEgAUcNABogAiACKAIsIgBBAWo2AiwgAC0AASEBIAIgAigCKCIAQQFqNgIoQQAgAC0AASABRw0AGiACIAIoAiwiAEEBajYCLCAALQABIQEgAiACKAIoIgBBAWo2AihBACAALQABIAFHDQAaIAIgAigCLCIAQQFqNgIsIAAtAAEhASACIAIoAigiAEEBajYCKEEAIAAtAAEgAUcNABogAiACKAIsIgBBAWo2AiwgAC0AASEBIAIgAigCKCIAQQFqNgIoQQAgAC0AASABRw0AGiACIAIoAiwiAEEBajYCLCAALQABIQEgAiACKAIoIgBBAWo2AihBACAALQABIAFHDQAaIAIgAigCLCIAQQFqNgIsIAAtAAEhASACIAIoAigiAEEBajYCKEEAIAAtAAEgAUcNABogAiACKAIsIgBBAWo2AiwgAC0AASEBIAIgAigCKCIAQQFqNgIoQQAgAC0AASABRw0AGiACKAIsIAIoAgxJC0EBcQ0ACyACQYICIAIoAgwgAigCLGtrNgIkIAIgAigCDEGCAms2AiwgAigCJCACKAIgSgRAIAIoAjggAigCNDYCcCACIAIoAiQ2AiAgAigCJCACKAIcTg0CIAIgAigCLCACKAIgQQFrai0AADoACyACIAIoAiwgAigCIGotAAA6AAoLCyACIAIoAhQgAigCNCACKAIQcUEBdGovAQAiATYCNEEAIQAgASACKAIYSwR/IAIgAigCMEEBayIANgIwIABBAEcFQQALQQFxDQELCwJAIAIoAiAgAigCOCgCdE0EQCACIAIoAiA2AjwMAQsgAiACKAI4KAJ0NgI8CyACKAI8C5IQAQF/IwBBMGsiAiQAIAIgADYCKCACIAE2AiQgAgJ/IAIoAigoAiwgAigCKCgCDEEFa0kEQCACKAIoKAIsDAELIAIoAigoAgxBBWsLNgIgIAJBADYCECACIAIoAigoAgAoAgQ2AgwDQAJAIAJB//8DNgIcIAIgAigCKCgCvC1BKmpBA3U2AhQgAigCKCgCACgCECACKAIUSQ0AIAIgAigCKCgCACgCECACKAIUazYCFCACIAIoAigoAmwgAigCKCgCXGs2AhggAigCHCACKAIYIAIoAigoAgAoAgRqSwRAIAIgAigCGCACKAIoKAIAKAIEajYCHAsgAigCHCACKAIUSwRAIAIgAigCFDYCHAsCQCACKAIcIAIoAiBPDQACQCACKAIcRQRAIAIoAiRBBEcNAQsgAigCJEUNACACKAIcIAIoAhggAigCKCgCACgCBGpGDQELDAELQQAhACACIAIoAiRBBEYEfyACKAIcIAIoAhggAigCKCgCACgCBGpGBUEAC0EBcTYCECACKAIoQQBBACACKAIQEF0gAigCKCgCCCACKAIoKAIUQQRraiACKAIcOgAAIAIoAigoAgggAigCKCgCFEEDa2ogAigCHEEIdjoAACACKAIoKAIIIAIoAigoAhRBAmtqIAIoAhxBf3M6AAAgAigCKCgCCCACKAIoKAIUQQFraiACKAIcQX9zQQh2OgAAIAIoAigoAgAQHCACKAIYBEAgAigCGCACKAIcSwRAIAIgAigCHDYCGAsgAigCKCgCACgCDCACKAIoKAI4IAIoAigoAlxqIAIoAhgQGRogAigCKCgCACIAIAIoAhggACgCDGo2AgwgAigCKCgCACIAIAAoAhAgAigCGGs2AhAgAigCKCgCACIAIAIoAhggACgCFGo2AhQgAigCKCIAIAIoAhggACgCXGo2AlwgAiACKAIcIAIoAhhrNgIcCyACKAIcBEAgAigCKCgCACACKAIoKAIAKAIMIAIoAhwQdhogAigCKCgCACIAIAIoAhwgACgCDGo2AgwgAigCKCgCACIAIAAoAhAgAigCHGs2AhAgAigCKCgCACIAIAIoAhwgACgCFGo2AhQLIAIoAhBFDQELCyACIAIoAgwgAigCKCgCACgCBGs2AgwgAigCDARAAkAgAigCDCACKAIoKAIsTwRAIAIoAihBAjYCsC0gAigCKCgCOCACKAIoKAIAKAIAIAIoAigoAixrIAIoAigoAiwQGRogAigCKCACKAIoKAIsNgJsDAELIAIoAgwgAigCKCgCPCACKAIoKAJsa08EQCACKAIoIgAgACgCbCACKAIoKAIsazYCbCACKAIoKAI4IAIoAigoAjggAigCKCgCLGogAigCKCgCbBAZGiACKAIoKAKwLUECSQRAIAIoAigiACAAKAKwLUEBajYCsC0LCyACKAIoKAI4IAIoAigoAmxqIAIoAigoAgAoAgAgAigCDGsgAigCDBAZGiACKAIoIgAgAigCDCAAKAJsajYCbAsgAigCKCACKAIoKAJsNgJcIAIoAigiAQJ/IAIoAgwgAigCKCgCLCACKAIoKAK0LWtLBEAgAigCKCgCLCACKAIoKAK0LWsMAQsgAigCDAsgASgCtC1qNgK0LQsgAigCKCgCwC0gAigCKCgCbEkEQCACKAIoIAIoAigoAmw2AsAtCwJAIAIoAhAEQCACQQM2AiwMAQsCQCACKAIkRQ0AIAIoAiRBBEYNACACKAIoKAIAKAIEDQAgAigCKCgCbCACKAIoKAJcRw0AIAJBATYCLAwBCyACIAIoAigoAjwgAigCKCgCbGtBAWs2AhQCQCACKAIoKAIAKAIEIAIoAhRNDQAgAigCKCgCXCACKAIoKAIsSA0AIAIoAigiACAAKAJcIAIoAigoAixrNgJcIAIoAigiACAAKAJsIAIoAigoAixrNgJsIAIoAigoAjggAigCKCgCOCACKAIoKAIsaiACKAIoKAJsEBkaIAIoAigoArAtQQJJBEAgAigCKCIAIAAoArAtQQFqNgKwLQsgAiACKAIoKAIsIAIoAhRqNgIUCyACKAIUIAIoAigoAgAoAgRLBEAgAiACKAIoKAIAKAIENgIUCyACKAIUBEAgAigCKCgCACACKAIoKAI4IAIoAigoAmxqIAIoAhQQdhogAigCKCIAIAIoAhQgACgCbGo2AmwLIAIoAigoAsAtIAIoAigoAmxJBEAgAigCKCACKAIoKAJsNgLALQsgAiACKAIoKAK8LUEqakEDdTYCFCACIAIoAigoAgwgAigCFGtB//8DSwR/Qf//AwUgAigCKCgCDCACKAIUaws2AhQgAgJ/IAIoAhQgAigCKCgCLEsEQCACKAIoKAIsDAELIAIoAhQLNgIgIAIgAigCKCgCbCACKAIoKAJcazYCGAJAIAIoAhggAigCIEkEQCACKAIYRQRAIAIoAiRBBEcNAgsgAigCJEUNASACKAIoKAIAKAIEDQEgAigCGCACKAIUSw0BCyACAn8gAigCGCACKAIUSwRAIAIoAhQMAQsgAigCGAs2AhwgAgJ/QQAgAigCJEEERw0AGkEAIAIoAigoAgAoAgQNABogAigCHCACKAIYRgtBAXE2AhAgAigCKCACKAIoKAI4IAIoAigoAlxqIAIoAhwgAigCEBBdIAIoAigiACACKAIcIAAoAlxqNgJcIAIoAigoAgAQHAsgAkECQQAgAigCEBs2AiwLIAIoAiwhACACQTBqJAAgAAuyAgEBfyMAQRBrIgEkACABIAA2AggCQCABKAIIEHgEQCABQX42AgwMAQsgASABKAIIKAIcKAIENgIEIAEoAggoAhwoAggEQCABKAIIKAIoIAEoAggoAhwoAgggASgCCCgCJBEEAAsgASgCCCgCHCgCRARAIAEoAggoAiggASgCCCgCHCgCRCABKAIIKAIkEQQACyABKAIIKAIc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DQAgAiEAIANBAWsiAw0ACwsgBCgCFBogASACNgKgAQwCCyMAQRBrIgAgASgCqAEoAgQ2AgwgAUEAIAAoAgxrQQJ0QajZAGooAgA2AqABDAELIAFBADYCoAELCwJAIAEoAqABRQRAIAEgASgCpAE2AqwBDAELIAEgASgCoAEQLgJ/IAEoAqQBBEAgASgCpAEQLkECagwBC0EAC2pBAWoQGCIANgIcIABFBEAgAUG4EygCADYCrAEMAQsgASgCHCEAAn8gASgCpAEEQCABKAKkAQwBC0H6EgshA0HfEkH6EiABKAKkARshAiABIAEoAqABNgIIIAEgAjYCBCABIAM2AgAgAEG+CiABEG8gASgCqAEgASgCHDYCCCABIAEoAhw2AqwBCyABKAKsASEAIAFBsAFqJAAgAAsIAEEBQTgQfwszAQF/IAAoAhQiAyABIAIgACgCECADayIBIAEgAksbIgEQGRogACAAKAIUIAFqNgIUIAILjwUCBn4BfyABIAEoAgBBD2pBcHEiAUEQajYCACAAAnwgASkDACEDIAEpAwghBiMAQSBrIggkAAJAIAZC////////////AIMiBEKAgICAgIDAgDx9IARCgICAgICAwP/DAH1UBEAgBkIEhiADQjyIhCEEIANC//////////8PgyIDQoGAgICAgICACFoEQCAEQoGAgICAgICAwAB8IQIMAgsgBEKAgICAgICAgEB9IQIgA0KAgICAgICAgAiFQgBSDQEgAiAEQgGDfCECDAELIANQIARCgICAgICAwP//AFQgBEKAgICAgIDA//8AURtFBEAgBkIEhiADQjyIhEL/////////A4NCgICAgICAgPz/AIQhAgwBC0KAgICAgICA+P8AIQIgBEL///////+//8MAVg0AQgAhAiAEQjCIpyIAQZH3AEkNACADIQIgBkL///////8/g0KAgICAgIDAAIQiBSEHAkAgAEGB9wBrIgFBwABxBEAgAiABQUBqrYYhB0IAIQIMAQsgAUUNACAHIAGtIgSGIAJBwAAgAWutiIQhByACIASGIQILIAggAjcDECAIIAc3AxgCQEGB+AAgAGsiAEHAAHEEQCAFIABBQGqtiCEDQgAhBQwBCyAARQ0AIAVBwAAgAGuthiADIACtIgKIhCEDIAUgAoghBQsgCCADNwMAIAggBTcDCCAIKQMI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ADYCtC0gASgCFEEERgRAIAEoAhgCfyABKAIYKAJcQQBOBEAgASgCGCgCOCABKAIYKAJcagwBC0EACyABKAIYKAJsIAEoAhgoAlxrQQEQKCABKAIYIAEoAhgoAmw2AlwgASgCGCgCABAcIAEoAhgoAgAoAhBFBEAgAUECNgIcDAILIAFBAzYCHAwBCyABKAIYKAKgLQRAIAEoAhgCfyABKAIYKAJcQQBOBEAgASgCGCgCOCABKAIYKAJcagwBC0EACyABKAIYKAJsIAEoAhgoAlxrQQAQKCABKAIYIAEoAhgoAmw2AlwgASgCGCgCABAcIAEoAhgoAgAoAhBFBEAgAUEANgIcDAILCyABQQE2AhwLIAEoAhwhAiABQSBqJAAgAgwBCwJ/IAAoAiwoAogBQQNGBEAgACgCLCECIAAoAjQhAyMAQTBrIgEkACABIAI2AiggASADNgIkAkADQAJAIAEoAigoAnRBggJNBEAgASgCKBBcAkAgASgCKCgCdEGCAksNACABKAIkDQAgAUEANgIsDAQLIAEoAigoAnRFDQELIAEoAihBADYCYAJAIAEoAigoAnRBA0kNACABKAIoKAJsRQ0AIAEgASgCKCgCOCABKAIoKAJsakEBazYCGCABIAEoAhgtAAA2AhwgASgCHCECIAEgASgCGCIDQQFqNgIYAkAgAy0AASACRw0AIAEoAhwhAiABIAEoAhgiA0EBajYCGCADLQABIAJHDQAgASgCHCECIAEgASgCGCIDQQFqNgIYIAMtAAEgAkcNACABIAEoAigoAjggASgCKCgCbGpBggJqNgIUA0AgASgCHCECIAEgASgCGCIDQQFqNgIYAn9BACADLQABIAJHDQAaIAEoAhwhAiABIAEoAhgiA0EBajYCGEEAIAMtAAEgAkcNABogASgCHCECIAEgASgCGCIDQQFqNgIYQQAgAy0AASACRw0AGiABKAIcIQIgASABKAIYIgNBAWo2AhhBACADLQABIAJHDQAaIAEoAhwhAiABIAEoAhgiA0EBajYCGEEAIAMtAAEgAkcNABogASgCHCECIAEgASgCGCIDQQFqNgIYQQAgAy0AASACRw0AGiABKAIcIQIgASABKAIYIgNBAWo2AhhBACADLQABIAJHDQAaIAEoAhwhAiABIAEoAhgiA0EBajYCGEEAIAMtAAEgAkcNABog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AS8BEiECIAEgASgCSCIDQQFqNgJIIAMgAjoAAAwCCyABKAIMQRBxBEAgASABLwESNgIIIAEgASgCDEEPcTYCDCABKAIMBEAgASgCKCABKAIMSQRAIAEgASgCUCICQQFqNgJQIAEgASgCLCACLQAAIAEoAih0ajYCLCABIAEoAihBCGo2AigLIAEgASgCCCABKAIsQQEgASgCDHRBAWtxajYCCCABIAEoAiwgASgCDHY2AiwgASABKAIoIAEoAgxrNgIoCyABKAIoQQ9JBEAgASABKAJQIgJBAWo2AlAgASABKAIsIAItAAAgASgCKHRqNgIsIAEgASgCKEEIajYCKCABIAEoAlAiAkEBajYCUCABIAEoAiwgAi0AACABKAIodGo2AiwgASABKAIoQQhqNgIoCyABIAEoAiAgASgCLCABKAIYcUECdGooAQA2ARACQANAIAEgAS0AETYCDCABIAEoAiwgASgCDHY2AiwgASABKAIoIAEoAgxrNgIoIAEgAS0AEDYCDCABKAIMQRBxBEAgASABLwESNgIEIAEgASgCDEEPcTYCDCABKAIoIAEoAgxJBEAgASABKAJQIgJBAWo2AlAgASABKAIsIAItAAAgASgCKHRqNgIsIAEgASgCKEEIajYCKCABKAIoIAEoAgxJBEAgASABKAJQIgJBAWo2AlAgASABKAIsIAItAAAgASgCKHRqNgIsIAEgASgCKEEIajYCKAsLIAEgASgCBCABKAIsQQEgASgCDHRBAWtxajYCBCABIAEoAiwgASgCDHY2AiwgASABKAIoIAEoAgxrNgIoIAEgASgCSCABKAJEazYCDAJAIAEoAgQgASgCDEsEQCABIAEoAgQgASgCDGs2AgwgASgCDCABKAI4SwRAIAEoAlQoAsQ3BEAgASgCXEHdDDYCGCABKAJUQdH+ADYCBAwKCwsgASABKAIwNgIAAkAgASgCNEUEQCABIAEoAgAgASgCPCABKAIMa2o2AgAgASgCDCABKAIISQRAIAEgASgCCCABKAIMazYCCANAIAEgASgCACICQQFqNgIAIAItAAAhAiABIAEoAkgiA0EBajYCSCADIAI6AAAgASABKAIMQQFrIgI2AgwgAg0ACyABIAEoAkggASgCBGs2AgALDAELAkAgASgCNCABKAIMSQRAIAEgASgCACABKAI8IAEoAjRqIAEoAgxrajYCACABIAEoAgwgASgCNGs2AgwgASgCDCABKAIISQRAIAEgASgCCCABKAIMazYCCANAIAEgASgCACICQQFqNgIAIAItAAAhAiABIAEoAkgiA0EBajYCSCADIAI6AAAgASABKAIMQQFrIgI2AgwgAg0ACyABIAEoAjA2AgAgASgCNCABKAIISQRAIAEgASgCNDYCDCABIAEoAgggASgCDGs2AggDQCABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAAIAEgASgCDEEBayICNgIMIAINAAsgASABKAJIIAEoAgRrNgIACwsMAQsgASABKAIAIAEoAjQgASgCDGtqNgIAIAEoAgwgASgCCEkEQCABIAEoAgggASgCDGs2AggDQCABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAAIAEgASgCDEEBayICNgIMIAINAAsgASABKAJIIAEoAgRrNgIACwsLA0AgASgCCEECSwRAIAEgASgCACICQQFqNgIAIAItAAAhAiABIAEoAkgiA0EBajYCSCADIAI6AAAgASABKAIAIgJBAWo2AgAgAi0AACECIAEgASgCSCIDQQFqNgJIIAMgAjoAACABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAAIAEgASgCCEEDazYCCAwBCwsMAQsgASABKAJIIAEoAgRrNgIAA0AgASABKAIAIgJBAWo2AgAgAi0AACECIAEgASgCSCIDQQFqNgJIIAMgAjoAACABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAAIAEgASgCACICQQFqNgIAIAItAAAhAiABIAEoAkgiA0EBajYCSCADIAI6AAAgASABKAIIQQNrNgIIIAEoAghBAksNAAsLIAEoAggEQCABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAAIAEoAghBAUsEQCABIAEoAgAiAkEBajYCACACLQAAIQIgASABKAJIIgNBAWo2AkggAyACOgAACwsMAgsgASgCDEHAAHFFBEAgASABKAIgIAEvARIgASgCLEEBIAEoAgx0QQFrcWpBAnRqKAEANgEQDAELCyAB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AG5hbgAvZGV2L3VyYW5kb20AaW52YWxpZCBjb2RlIC0tIG1pc3NpbmcgZW5kLW9mLWJsb2NrAGluY29ycmVjdCBoZWFkZXIgY2hlY2sAaW5jb3JyZWN0IGxlbmd0aCBjaGVjawBpbmNvcnJlY3QgZGF0YSBjaGVjawBpbnZhbGlkIGRpc3RhbmNlIHRvbyBmYXIgYmFjawBoZWFkZXIgY3JjIG1pc21hdGNoAGluZgBpbnZhbGlkIHdpbmRvdyBzaXplAFJlYWQtb25seSBhcmNoaXZlAE5vdCBhIHppcCBhcmNoaXZlAFJlc291cmNlIHN0aWxsIGluIHVzZQBNYWxsb2MgZmFpbHVyZQBpbnZhbGlkIGJsb2NrIHR5cGUARmFpbHVyZSB0byBjcmVhdGUgdGVtcG9yYXJ5IGZpbGUAQ2FuJ3Qgb3BlbiBmaWxlAE5vIHN1Y2ggZmlsZQBQcmVtYXR1cmUgZW5kIG9mIGZpbGUAQ2FuJ3QgcmVtb3ZlIGZpbGUAaW52YWxpZCBsaXRlcmFsL2xlbmd0aCBjb2RlAGludmFsaWQgZGlzdGFuY2UgY29kZQB1bmtub3duIGNvbXByZXNzaW9uIG1ldGhvZABzdHJlYW0gZW5kAENvbXByZXNzZWQgZGF0YSBpbnZhbGlkAE11bHRpLWRpc2sgemlwIGFyY2hpdmVzIG5vdCBzdXBwb3J0ZWQAT3BlcmF0aW9uIG5vdCBzdXBwb3J0ZWQARW5jcnlwdGlvbiBtZXRob2Qgbm90IHN1cHBvcnRlZABDb21wcmVzc2lvbiBtZXRob2Qgbm90IHN1cHBvcnRlZABFbnRyeSBoYXMgYmVlbiBkZWxldGVkAENvbnRhaW5pbmcgemlwIGFyY2hpdmUgd2FzIGNsb3NlZABDbG9zaW5nIHppcCBhcmNoaXZlIGZhaWxlZABSZW5hbWluZyB0ZW1wb3JhcnkgZmlsZSBmYWlsZWQARW50cnkgaGFzIGJlZW4gY2hhbmdlZABObyBwYXNzd29yZCBwcm92aWRlZABXcm9uZyBwYXNzd29yZCBwcm92aWRlZABVbmtub3duIGVycm9yICVkAHJiAHIrYgByd2EAJXMuWFhYWFhYAE5BTgBJTkYAQUUAMS4yLjExAC9wcm9jL3NlbGYvZmQvAC4AKG51bGwpADogAFBLBgcA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0gAIADIACACyAAgAcgAIAPIACAAKAAgAigAIAEoACADKAAgAKgAIAKoACABqAAgA6gAIABoACACaAAgAWgAIANoACAA6AAgAugAIAHoACAD6AAgABgAIAIYACABGAAgAxgAIACYACACmAAgAZgAIAOYACAAWAAgAlgAIAFYACADWAAgANgAIALYACAB2AAgA9gAIAA4ACACOAAgATgAIAM4ACAAuAAgArgAIAG4ACADuAAgAHgAIAJ4ACABeAAgA3gAIAD4ACAC+AAgAfgAIAP4ACAABAAgAgQAIAEEACADBAAgAIQAIAKEACABhAAgA4QAIABEACACRAAgAUQAIANEACAAxAAgAsQAIAHEACADxAAgACQAIAIkACABJAAgAyQAIACkACACpAAgAaQAIAOkACAAZAAgAmQAIAFkACADZAAgAOQAIALkACAB5AAgA+QAIAAUACACFAAgARQAIAMUACAAlAAgApQAIAGUACADlAAgAFQAIAJUACABVAAgA1QAIADUACAC1AAgAdQAIAPUACAANAAgAjQAIAE0ACADNAAgALQAIAK0ACABtAAgA7QAIAB0ACACdAAgAXQAIAN0ACAA9AAgAvQAIAH0ACAD9AAgAEwAJABMBCQCTAAkAkwEJAFMACQBTAQkA0wAJANMBCQAzAAkAMwEJALMACQCzAQkAcwAJAHMBCQDzAAkA8wEJAAsACQALAQkAiwAJAIsBCQBLAAkASwEJAMsACQDLAQkAKwAJACsBCQCrAAkAqwEJAGsACQBrAQkA6wAJAOsBCQAbAAkAGwEJAJsACQCbAQkAWwAJAFsBCQDbAAkA2wEJADsACQA7AQkAuwAJALsBCQB7AAkAewEJAPsACQD7AQkABwAJAAcBCQCHAAkAhwEJAEcACQBHAQkAxwAJAMcBCQAnAAkAJwEJAKcACQCnAQkAZwAJAGcBCQDnAAkA5wEJABcACQAXAQkAlwAJAJcBCQBXAAkAVwEJANcACQDXAQkANwAJADcBCQC3AAkAtwEJAHcACQB3AQkA9wAJAPcBCQAPAAkADwEJAI8ACQCPAQkATwAJAE8BCQDPAAkAzwEJAC8ACQAvAQkArwAJAK8BCQBvAAkAbwEJAO8ACQDvAQkAHwAJAB8BCQCfAAkAnwEJAF8ACQBfAQkA3wAJAN8BCQA/AAkAPwEJAL8ACQC/AQkAfwAJAH8BCQD/AAkA/wEJAAAABwBAAAcAIAAHAGAABwAQAAcAUAAHADAABwBwAAcACAAHAEgABwAoAAcAaAAHABgABwBYAAcAOAAHAHgABwAEAAcARAAHACQABwBkAAcAFAAHAFQABwA0AAcAdAAHAAMACACDAAgAQwAIAMMACAAjAAgAowAIAGMACADjAAgAAAAFABAABQAIAAUAGAAFAAQABQAUAAUADAAFABwABQACAAUAEgAFAAoABQAaAAUABgAFABYABQAOAAUAHgAFAAEABQARAAUACQAFABkABQAFAAUAFQAFAA0ABQAdAAUAAwAFABMABQALAAUAGwAFAAcABQAXAAUAQbDqAAtNAQAAAAEAAAABAAAAAQAAAAIAAAACAAAAAgAAAAIAAAADAAAAAwAAAAMAAAADAAAABAAAAAQAAAAEAAAABAAAAAUAAAAFAAAABQAAAAUAQaDrAAtlAQAAAAEAAAACAAAAAgAAAAMAAAADAAAABAAAAAQAAAAFAAAABQAAAAYAAAAGAAAABwAAAAcAAAAIAAAACAAAAAkAAAAJAAAACgAAAAoAAAALAAAACwAAAAwAAAAMAAAADQAAAA0AQdDsAAsjAgAAAAMAAAAHAAAAAAAAABAREgAIBwkGCgULBAwDDQIOAQ8AQYTtAAtpAQAAAAIAAAADAAAABAAAAAUAAAAGAAAABwAAAAgAAAAKAAAADAAAAA4AAAAQAAAAFAAAABgAAAAcAAAAIAAAACgAAAAwAAAAOAAAAEAAAABQAAAAYAAAAHAAAACAAAAAoAAAAMAAAADgAEGE7gALegEAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAQAAAAGAAAACAAAAAwAAAAAABAACAAQAAAAIAAAADAAAABAAAAAYAAAAIAAAADAAAABAAAAAYAAAAIAAAADAAAABAAAAAYAAAMS4yLjExAEGI7wALbQcAAAAEAAQACAAEAAgAAAAEAAUAEAAIAAgAAAAEAAYAIAAgAAgAAAAEAAQAEAAQAAkAAAAIABAAIAAgAAkAAAAIABAAgACAAAkAAAAIACAAgAAAAQkAAAAgAIAAAgEABAkAAAAgAAIBAgEAEAkAQYDwAAulAgMABAAFAAYABwAIAAkACgALAA0ADwARABMAFwAbAB8AIwArADMAOwBDAFMAYwBzAIMAowDDAOMAAgEAAAAAAAAQABAAEAAQABAAEAAQABAAEQARABEAEQASABIAEgASABMAEwATABMAFAAUABQAFAAVABUAFQAVABAATQDKAAAAAQACAAMABAAFAAcACQANABEAGQAhADEAQQBhAIEAwQABAYEBAQIBAwEEAQYBCAEMARABGAEgATABQAFgAAAAABAAEAAQABAAEQARABIAEgATABMAFAAUABUAFQAWABYAFwAXABgAGAAZABkAGgAaABsAGwAcABwAHQAdAEAAQAAQABEAEgAAAAgABwAJAAYACgAFAAsABAAMAAMADQACAA4AAQAPAEGw8gALwRFgBwAAAAhQAAAIEAAUCHMAEgcfAAAIcAAACDAAAAnAABAHCgAACGAAAAggAAAJoAAACAAAAAiAAAAIQAAACeAAEAcGAAAIWAAACBgAAAmQABMHOwAACHgAAAg4AAAJ0AARBxEAAAhoAAAIKAAACbAAAAgIAAAIiAAACEgAAAnwABAHBAAACFQAAAgUABUI4wATBysAAAh0AAAINAAACcgAEQcNAAAIZAAACCQAAAmoAAAIBAAACIQAAAhEAAAJ6AAQBwgAAAhcAAAIHAAACZgAFAdTAAAIfAAACDwAAAnYABIHFwAACGwAAAgsAAAJuAAACAwAAAiMAAAITAAACfgAEAcDAAAIUgAACBIAFQijABMHIwAACHIAAAgyAAAJxAARBwsAAAhiAAAIIgAACaQAAAgCAAAIggAACEIAAAnkABAHBwAACFoAAAgaAAAJlAAUB0MAAAh6AAAIOgAACdQAEgcTAAAIagAACCoAAAm0AAAICgAACIoAAAhKAAAJ9AAQBwUAAAhWAAAIFgBACAAAEwczAAAIdgAACDYAAAnMABEHDwAACGYAAAgmAAAJrAAACAYAAAiGAAAIRgAACewAEAcJAAAIXgAACB4AAAmcABQHYwAACH4AAAg+AAAJ3AASBxsAAAhuAAAILgAACbwAAAgOAAAIjgAACE4AAAn8AGAHAAAACFEAAAgRABUIgwASBx8AAAhxAAAIMQAACcIAEAcKAAAIYQAACCEAAAmiAAAIAQAACIEAAAhBAAAJ4gAQBwYAAAhZAAAIGQAACZIAEwc7AAAIeQAACDkAAAnSABEHEQAACGkAAAgpAAAJsgAACAkAAAiJAAAISQAACfIAEAcEAAAIVQAACBUAEAgCARMHKwAACHUAAAg1AAAJygARBw0AAAhlAAAIJQAACaoAAAgFAAAIhQAACEUAAAnqABAHCAAACF0AAAgdAAAJmgAUB1MAAAh9AAAIPQAACdoAEgcXAAAIbQAACC0AAAm6AAAIDQAACI0AAAhNAAAJ+gAQBwMAAAhTAAAIEwAVCMMAEwcjAAAIcwAACDMAAAnGABEHCwAACGMAAAgjAAAJpgAACAMAAAiDAAAIQwAACeYAEAcHAAAIWwAACBsAAAmWABQHQwAACHsAAAg7AAAJ1gASBxMAAAhrAAAIKwAACbYAAAgLAAAIiwAACEsAAAn2ABAHBQAACFcAAAgXAEAIAAATBzMAAAh3AAAINwAACc4AEQcPAAAIZwAACCcAAAmuAAAIBwAACIcAAAhHAAAJ7gAQBwkAAAhfAAAIHwAACZ4AFAdjAAAIfwAACD8AAAneABIHGwAACG8AAAgvAAAJvgAACA8AAAiPAAAITwAACf4AYAcAAAAIUAAACBAAFAhzABIHHwAACHAAAAgwAAAJwQAQBwoAAAhgAAAIIAAACaEAAAgAAAAIgAAACEAAAAnhABAHBgAACFgAAAgYAAAJkQATBzsAAAh4AAAIOAAACdEAEQcRAAAIaAAACCgAAAmxAAAICAAACIgAAAhIAAAJ8QAQBwQAAAhUAAAIFAAVCOMAEwcrAAAIdAAACDQAAAnJABEHDQAACGQAAAgkAAAJqQAACAQAAAiEAAAIRAAACekAEAcIAAAIXAAACBwAAAmZABQHUwAACHwAAAg8AAAJ2QASBxcAAAhsAAAILAAACbkAAAgMAAAIjAAACEwAAAn5ABAHAwAACFIAAAgSABUIowATByMAAAhyAAAIMgAACcUAEQcLAAAIYgAACCIAAAmlAAAIAgAACIIAAAhCAAAJ5QAQBwcAAAhaAAAIGgAACZUAFAdDAAAIegAACDoAAAnVABIHEwAACGoAAAgqAAAJtQAACAoAAAiKAAAISgAACfUAEAcFAAAIVgAACBYAQAgAABMHMwAACHYAAAg2AAAJzQARBw8AAAhmAAAIJgAACa0AAAgGAAAIhgAACEYAAAntABAHCQAACF4AAAgeAAAJnQAUB2MAAAh+AAAIPgAACd0AEgcbAAAIbgAACC4AAAm9AAAIDgAACI4AAAhOAAAJ/QBgBwAAAAhRAAAIEQAVCIMAEgcfAAAIcQAACDEAAAnDABAHCgAACGEAAAghAAAJowAACAEAAAiBAAAIQQAACeMAEAcGAAAIWQAACBkAAAmTABMHOwAACHkAAAg5AAAJ0wARBxEAAAhpAAAIKQAACbMAAAgJAAAIiQAACEkAAAnzABAHBAAACFUAAAgVABAIAgETBysAAAh1AAAINQAACcsAEQcNAAAIZQAACCUAAAmrAAAIBQAACIUAAAhFAAAJ6wAQBwgAAAhdAAAIHQAACZsAFAdTAAAIfQAACD0AAAnbABIHFwAACG0AAAgtAAAJuwAACA0AAAiNAAAITQAACfsAEAcDAAAIUwAACBMAFQjDABMHIwAACHMAAAgzAAAJxwARBwsAAAhjAAAIIwAACacAAAgDAAAIgwAACEMAAAnnABAHBwAACFsAAAgbAAAJlwAUB0MAAAh7AAAIOwAACdcAEgcTAAAIawAACCsAAAm3AAAICwAACIsAAAhLAAAJ9wAQBwUAAAhXAAAIFwBACAAAEwczAAAIdwAACDcAAAnPABEHDwAACGcAAAgnAAAJrwAACAcAAAiHAAAIRwAACe8AEAcJAAAIXwAACB8AAAmfABQHYwAACH8AAAg/AAAJ3wASBxsAAAhvAAAILwAACb8AAAgPAAAIjwAACE8AAAn/ABAFAQAXBQEBEwURABsFARARBQUAGQUBBBUFQQAdBQFAEAUDABgFAQIUBSEAHAUBIBIFCQAaBQEIFgWBAEAFAAAQBQIAFwWBARMFGQAbBQEYEQUHABkFAQYVBWEAHQUBYBAFBAAYBQEDFAUxABwFATASBQ0AGgUBDBYFwQBABQAAEQAKABEREQAAAAAFAAAAAAAACQAAAAALAAAAAAAAAAARAA8KERERAwoHAAEACQsLAAAJBgsAAAsABhEAAAAREREAQYGEAQshCwAAAAAAAAAAEQAKChEREQAKAAACAAkLAAAACQALAAALAEG7hAELAQwAQceEAQsVDAAAAAAMAAAAAAkMAAAAAAAMAAAMAEH1hAELAQ4AQYGFAQsVDQAAAAQNAAAAAAkOAAAAAAAOAAAOAEGvhQELARAAQbuFAQseDwAAAAAPAAAAAAkQAAAAAAAQAAAQAAASAAAAEhISAEHyhQELDhIAAAASEhIAAAAAAAAJAEGjhgELAQsAQa+GAQsVCgAAAAAKAAAAAAkLAAAAAAALAAALAEHdhgELAQwAQemGAQsnDAAAAAAMAAAAAAkMAAAAAAAMAAAMAAAwMTIzNDU2Nzg5QUJDREVGAEG0hwELARkAQduHAQsF//////8AQaCIAQtXGRJEOwI/LEcUPTMwChsGRktFNw9JDo4XA0AdPGkrNh9KLRwBICUpIQgMFRYiLhA4Pgs0MRhkdHV2L0EJfzkRI0MyQomKiwUEJignDSoeNYwHGkiTE5SVAEGAiQELig5JbGxlZ2FsIGJ5dGUgc2VxdWVuY2UARG9tYWluIGVycm9yAFJlc3VsdCBub3QgcmVwcmVzZW50YWJsZQBOb3QgYSB0dHkAUGVybWlzc2lvbiBkZW5pZWQAT3BlcmF0aW9uIG5vdCBwZXJtaXR0ZWQATm8gc3VjaCBmaWxlIG9yIGRpcmVjdG9yeQBObyBzdWNoIHByb2Nlc3MARmlsZSBleGlzdHMAVmFsdWUgdG9vIGxhcmdlIGZvciBkYXRhIHR5cGUATm8gc3BhY2UgbGVmdCBvbiBkZXZpY2UAT3V0IG9mIG1lbW9yeQBSZXNvdXJjZSBidXN5AEludGVycnVwdGVkIHN5c3RlbSBjYWxsAFJlc291cmNlIHRlbXBvcmFyaWx5IHVuYXZhaWxhYmxlAEludmFsaWQgc2VlawBDcm9zcy1kZXZpY2UgbGluawBSZWFkLW9ubHkgZmlsZSBzeXN0ZW0ARGlyZWN0b3J5IG5vdCBlbXB0eQBDb25uZWN0aW9uIHJlc2V0IGJ5IHBlZXIAT3BlcmF0aW9uIHRpbWVkIG91dABDb25uZWN0aW9uIHJlZnVzZWQASG9zdCBpcyBkb3duAEhvc3QgaXMgdW5yZWFjaGFibGUAQWRkcmVzcyBpbiB1c2UAQnJva2VuIHBpcGUASS9PIGVycm9yAE5vIHN1Y2ggZGV2aWNlIG9yIGFkZHJlc3MAQmxvY2sgZGV2aWNlIHJlcXVpcmVkAE5vIHN1Y2ggZGV2aWNlAE5vdCBhIGRpcmVjdG9yeQBJcyBhIGRpcmVjdG9yeQBUZXh0IGZpbGUgYnVzeQBFeGVjIGZvcm1hdCBlcnJvcgBJbnZhbGlkIGFyZ3VtZW50AEFyZ3VtZW50IGxpc3QgdG9vIGxvbmcAU3ltYm9saWMgbGluayBsb29wAEZpbGVuYW1lIHRvbyBsb25nAFRvbyBtYW55IG9wZW4gZmlsZXMgaW4gc3lzdGVtAE5vIGZpbGUgZGVzY3JpcHRvcnMgYXZhaWxhYmxlAEJhZCBmaWxlIGRlc2NyaXB0b3IATm8gY2hpbGQgcHJvY2VzcwBCYWQgYWRkcmVzcwBGaWxlIHRvbyBsYXJnZQBUb28gbWFueSBsaW5rcwBObyBsb2NrcyBhdmFpbGFibGUAUmVzb3VyY2UgZGVhZGxvY2sgd291bGQgb2NjdXIAU3RhdGUgbm90IHJlY292ZXJhYmxlAFByZXZpb3VzIG93bmVyIGRpZWQAT3BlcmF0aW9uIGNhbmNlbGVkAEZ1bmN0aW9uIG5vdCBpbXBsZW1lbnRlZABObyBtZXNzYWdlIG9mIGRlc2lyZWQgdHlwZQBJZGVudGlmaWVyIHJlbW92ZWQARGV2aWNlIG5vdCBhIHN0cmVhbQBObyBkYXRhIGF2YWlsYWJsZQBEZXZpY2UgdGltZW91dABPdXQgb2Ygc3RyZWFtcyByZXNvdXJjZXMATGluayBoYXMgYmVlbiBzZXZlcmVkAFByb3RvY29sIGVycm9yAEJhZCBtZXNzYWdlAEZpbGUgZGVzY3JpcHRvciBpbiBiYWQgc3RhdGUATm90IGEgc29ja2V0AERlc3RpbmF0aW9uIGFkZHJlc3MgcmVxdWlyZWQATWVzc2FnZSB0b28gbGFyZ2UAUHJvdG9jb2wgd3JvbmcgdHlwZSBmb3Igc29ja2V0AFByb3RvY29sIG5vdCBhdmFpbGFibGUAUHJvdG9jb2wgbm90IHN1cHBvcnRlZABTb2NrZXQgdHlwZSBub3Qgc3VwcG9ydGVkAE5vdCBzdXBwb3J0ZWQAUHJvdG9jb2wgZmFtaWx5IG5vdCBzdXBwb3J0ZWQAQWRkcmVzcyBmYW1pbHkgbm90IHN1cHBvcnRlZCBieSBwcm90b2NvbABBZGRyZXNzIG5vdCBhdmFpbGFibGUATmV0d29yayBpcyBkb3duAE5ldHdvcmsgdW5yZWFjaGFibGUAQ29ubmVjdGlvbiByZXNldCBieSBuZXR3b3JrAENvbm5lY3Rpb24gYWJvcnRlZABObyBidWZmZXIgc3BhY2UgYXZhaWxhYmxlAFNvY2tldCBpcyBjb25uZWN0ZWQAU29ja2V0IG5vdCBjb25uZWN0ZWQAQ2Fubm90IHNlbmQgYWZ0ZXIgc29ja2V0IHNodXRkb3duAE9wZXJhdGlvbiBhbHJlYWR5IGluIHByb2dyZXNzAE9wZXJhdGlvbiBpbiBwcm9ncmVzcwBTdGFsZSBmaWxlIGhhbmRsZQBSZW1vdGUgSS9PIGVycm9yAFF1b3RhIGV4Y2VlZGVkAE5vIG1lZGl1bSBmb3VuZABXcm9uZyBtZWRpdW0gdHlwZQBObyBlcnJvciBpbmZvcm1hdGlvbgBBkJcBC1JQUFAACgAAAAsAAAAMAAAADQAAAA4AAAAPAAAAEAAAABEAAAASAAAACwAAAAwAAAANAAAADgAAAA8AAAAQAAAAEQAAAAEAAAAIAAAAlEsAALRLAEGQmQELAgxQAEHImQELCR8AAADkTAAAAwBB5JkBC4wBLfRRWM+MscBG9rXLKTEDxwRbcDC0Xf0geH+LmthZKVBoSImrp1YDbP+3zYg/1He0K6WjcPG65Kj8QYP92W/hinovLXSWBx8NCV4Ddixw90ClLKdvV0GoqnTfoFhkA0rHxDxTrq9fGAQVseNtKIarDKS/Q/DpUIE5VxZSN/////////////////////8=";Ou(Po)||(Po=h(Po));function Mu(d){try{if(d==Po&&z)return new Uint8Array(z);var E=xa(d);if(E)return E;if(m)return m(d);throw"sync fetching of the wasm failed: you can preload it to Module['wasmBinary'] manually, or emcc.py will do that for you when generating HTML (but not JS)"}catch(I){vr(I)}}function vh(d,E){var I,D,M;try{M=Mu(d),D=new WebAssembly.Module(M),I=new WebAssembly.Instance(D,E)}catch(ie){var _=ie.toString();throw k("failed to compile wasm module: "+_),(_.includes("imported Memory")||_.includes("memory import"))&&k("Memory size incompatibility issues may be due to changing INITIAL_MEMORY at runtime to something too large. Use ALLOW_MEMORY_GROWTH to allow any size memory (and also make sure not to set INITIAL_MEMORY at runtime to something smaller than it was at compile time)."),ie}return[I,D]}function kh(){var d={a:Pa};function E(M,_){var ie=M.exports;t.asm=ie,A=t.asm.u,Ei(A.buffer),Qr=t.asm.pa,RA(t.asm.v),LA("wasm-instantiate")}if(NA("wasm-instantiate"),t.instantiateWasm)try{var I=t.instantiateWasm(d,E);return I}catch(M){return k("Module.instantiateWasm callback failed with error: "+M),!1}var D=vh(Po,d);return E(D[0]),t.asm}var Dr,Ae;function Do(d){for(;d.length>0;){var E=d.shift();if(typeof E=="function"){E(t);continue}var I=E.func;typeof I=="number"?E.arg===void 0?Qr.get(I)():Qr.get(I)(E.arg):I(E.arg===void 0?null:E.arg)}}function Yn(d,E){var I=new Date(fe[d>>2]*1e3);fe[E>>2]=I.getUTCSeconds(),fe[E+4>>2]=I.getUTCMinutes(),fe[E+8>>2]=I.getUTCHours(),fe[E+12>>2]=I.getUTCDate(),fe[E+16>>2]=I.getUTCMonth(),fe[E+20>>2]=I.getUTCFullYear()-1900,fe[E+24>>2]=I.getUTCDay(),fe[E+36>>2]=0,fe[E+32>>2]=0;var D=Date.UTC(I.getUTCFullYear(),0,1,0,0,0,0),M=(I.getTime()-D)/(1e3*60*60*24)|0;return fe[E+28>>2]=M,Yn.GMTString||(Yn.GMTString=Fe("GMT")),fe[E+40>>2]=Yn.GMTString,E}function Uu(d,E){return Yn(d,E)}var St={splitPath:function(d){var E=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return E.exec(d).slice(1)},normalizeArray:function(d,E){for(var I=0,D=d.length-1;D>=0;D--){var M=d[D];M==="."?d.splice(D,1):M===".."?(d.splice(D,1),I++):I&&(d.splice(D,1),I--)}if(E)for(;I;I--)d.unshift("..");return d},normalize:function(d){var E=d.charAt(0)==="/",I=d.substr(-1)==="/";return d=St.normalizeArray(d.split("/").filter(function(D){return!!D}),!E).join("/"),!d&&!E&&(d="."),d&&I&&(d+="/"),(E?"/":"")+d},dirname:function(d){var E=St.splitPath(d),I=E[0],D=E[1];return!I&&!D?".":(D&&(D=D.substr(0,D.length-1)),I+D)},basename:function(d){if(d==="/")return"/";d=St.normalize(d),d=d.replace(/\/$/,"");var E=d.lastIndexOf("/");return E===-1?d:d.substr(E+1)},extname:function(d){return St.splitPath(d)[3]},join:function(){var d=Array.prototype.slice.call(arguments,0);return St.normalize(d.join("/"))},join2:function(d,E){return St.normalize(d+"/"+E)}};function Vl(){if(typeof crypto=="object"&&typeof crypto.getRandomValues=="function"){var d=new Uint8Array(1);return function(){return crypto.getRandomValues(d),d[0]}}else if(g)try{var E=require("crypto");return function(){return E.randomBytes(1)[0]}}catch(I){}return function(){vr("randomDevice")}}var qn={resolve:function(){for(var d="",E=!1,I=arguments.length-1;I>=-1&&!E;I--){var D=I>=0?arguments[I]:S.cwd();if(typeof D!="string")throw new TypeError("Arguments to path.resolve must be strings");if(!D)return"";d=D+"/"+d,E=D.charAt(0)==="/"}return d=St.normalizeArray(d.split("/").filter(function(M){return!!M}),!E).join("/"),(E?"/":"")+d||"."},relative:function(d,E){d=qn.resolve(d).substr(1),E=qn.resolve(E).substr(1);function I(_e){for(var ot=0;ot<_e.length&&_e[ot]==="";ot++);for(var Bt=_e.length-1;Bt>=0&&_e[Bt]==="";Bt--);return ot>Bt?[]:_e.slice(ot,Bt-ot+1)}for(var D=I(d.split("/")),M=I(E.split("/")),_=Math.min(D.length,M.length),ie=_,we=0;we<_;we++)if(D[we]!==M[we]){ie=we;break}for(var me=[],we=ie;we0?E=D.slice(0,M).toString("utf-8"):E=null}else typeof window!="undefined"&&typeof window.prompt=="function"?(E=window.prompt("Input: "),E!==null&&(E+=` +`)):typeof readline=="function"&&(E=readline(),E!==null&&(E+=` +`));if(!E)return null;d.input=OA(E,!0)}return d.input.shift()},put_char:function(d,E){E===null||E===10?(v(Ge(d.output,0)),d.output=[]):E!=0&&d.output.push(E)},flush:function(d){d.output&&d.output.length>0&&(v(Ge(d.output,0)),d.output=[])}},default_tty1_ops:{put_char:function(d,E){E===null||E===10?(k(Ge(d.output,0)),d.output=[]):E!=0&&d.output.push(E)},flush:function(d){d.output&&d.output.length>0&&(k(Ge(d.output,0)),d.output=[])}}};function ds(d){for(var E=Y(d,65536),I=Et(E);d=E)){var D=1024*1024;E=Math.max(E,I*(I>>0),I!=0&&(E=Math.max(E,256));var M=d.contents;d.contents=new Uint8Array(E),d.usedBytes>0&&d.contents.set(M.subarray(0,d.usedBytes),0)}},resizeFileStorage:function(d,E){if(d.usedBytes!=E)if(E==0)d.contents=null,d.usedBytes=0;else{var I=d.contents;d.contents=new Uint8Array(E),I&&d.contents.set(I.subarray(0,Math.min(E,d.usedBytes))),d.usedBytes=E}},node_ops:{getattr:function(d){var E={};return E.dev=S.isChrdev(d.mode)?d.id:1,E.ino=d.id,E.mode=d.mode,E.nlink=1,E.uid=0,E.gid=0,E.rdev=d.rdev,S.isDir(d.mode)?E.size=4096:S.isFile(d.mode)?E.size=d.usedBytes:S.isLink(d.mode)?E.size=d.link.length:E.size=0,E.atime=new Date(d.timestamp),E.mtime=new Date(d.timestamp),E.ctime=new Date(d.timestamp),E.blksize=4096,E.blocks=Math.ceil(E.size/E.blksize),E},setattr:function(d,E){E.mode!==void 0&&(d.mode=E.mode),E.timestamp!==void 0&&(d.timestamp=E.timestamp),E.size!==void 0&&pt.resizeFileStorage(d,E.size)},lookup:function(d,E){throw S.genericErrors[44]},mknod:function(d,E,I,D){return pt.createNode(d,E,I,D)},rename:function(d,E,I){if(S.isDir(d.mode)){var D;try{D=S.lookupNode(E,I)}catch(_){}if(D)for(var M in D.contents)throw new S.ErrnoError(55)}delete d.parent.contents[d.name],d.parent.timestamp=Date.now(),d.name=I,E.contents[I]=d,E.timestamp=d.parent.timestamp,d.parent=E},unlink:function(d,E){delete d.contents[E],d.timestamp=Date.now()},rmdir:function(d,E){var I=S.lookupNode(d,E);for(var D in I.contents)throw new S.ErrnoError(55);delete d.contents[E],d.timestamp=Date.now()},readdir:function(d){var E=[".",".."];for(var I in d.contents)!d.contents.hasOwnProperty(I)||E.push(I);return E},symlink:function(d,E,I){var D=pt.createNode(d,E,511|40960,0);return D.link=I,D},readlink:function(d){if(!S.isLink(d.mode))throw new S.ErrnoError(28);return d.link}},stream_ops:{read:function(d,E,I,D,M){var _=d.node.contents;if(M>=d.node.usedBytes)return 0;var ie=Math.min(d.node.usedBytes-M,D);if(ie>8&&_.subarray)E.set(_.subarray(M,M+ie),I);else for(var we=0;we0||D+I>2)}catch(I){throw I.code?new S.ErrnoError(lt.convertNodeCode(I)):I}return E.mode},realPath:function(d){for(var E=[];d.parent!==d;)E.push(d.name),d=d.parent;return E.push(d.mount.opts.root),E.reverse(),St.join.apply(null,E)},flagsForNode:function(d){d&=~2097152,d&=~2048,d&=~32768,d&=~524288;var E=0;for(var I in lt.flagsForNodeMap)d&I&&(E|=lt.flagsForNodeMap[I],d^=I);if(d)throw new S.ErrnoError(28);return E},node_ops:{getattr:function(d){var E=lt.realPath(d),I;try{I=Oe.lstatSync(E)}catch(D){throw D.code?new S.ErrnoError(lt.convertNodeCode(D)):D}return lt.isWindows&&!I.blksize&&(I.blksize=4096),lt.isWindows&&!I.blocks&&(I.blocks=(I.size+I.blksize-1)/I.blksize|0),{dev:I.dev,ino:I.ino,mode:I.mode,nlink:I.nlink,uid:I.uid,gid:I.gid,rdev:I.rdev,size:I.size,atime:I.atime,mtime:I.mtime,ctime:I.ctime,blksize:I.blksize,blocks:I.blocks}},setattr:function(d,E){var I=lt.realPath(d);try{if(E.mode!==void 0&&(Oe.chmodSync(I,E.mode),d.mode=E.mode),E.timestamp!==void 0){var D=new Date(E.timestamp);Oe.utimesSync(I,D,D)}E.size!==void 0&&Oe.truncateSync(I,E.size)}catch(M){throw M.code?new S.ErrnoError(lt.convertNodeCode(M)):M}},lookup:function(d,E){var I=St.join2(lt.realPath(d),E),D=lt.getMode(I);return lt.createNode(d,E,D)},mknod:function(d,E,I,D){var M=lt.createNode(d,E,I,D),_=lt.realPath(M);try{S.isDir(M.mode)?Oe.mkdirSync(_,M.mode):Oe.writeFileSync(_,"",{mode:M.mode})}catch(ie){throw ie.code?new S.ErrnoError(lt.convertNodeCode(ie)):ie}return M},rename:function(d,E,I){var D=lt.realPath(d),M=St.join2(lt.realPath(E),I);try{Oe.renameSync(D,M)}catch(_){throw _.code?new S.ErrnoError(lt.convertNodeCode(_)):_}d.name=I},unlink:function(d,E){var I=St.join2(lt.realPath(d),E);try{Oe.unlinkSync(I)}catch(D){throw D.code?new S.ErrnoError(lt.convertNodeCode(D)):D}},rmdir:function(d,E){var I=St.join2(lt.realPath(d),E);try{Oe.rmdirSync(I)}catch(D){throw D.code?new S.ErrnoError(lt.convertNodeCode(D)):D}},readdir:function(d){var E=lt.realPath(d);try{return Oe.readdirSync(E)}catch(I){throw I.code?new S.ErrnoError(lt.convertNodeCode(I)):I}},symlink:function(d,E,I){var D=St.join2(lt.realPath(d),E);try{Oe.symlinkSync(I,D)}catch(M){throw M.code?new S.ErrnoError(lt.convertNodeCode(M)):M}},readlink:function(d){var E=lt.realPath(d);try{return E=Oe.readlinkSync(E),E=ju.relative(ju.resolve(d.mount.opts.root),E),E}catch(I){throw I.code?new S.ErrnoError(lt.convertNodeCode(I)):I}}},stream_ops:{open:function(d){var E=lt.realPath(d.node);try{S.isFile(d.node.mode)&&(d.nfd=Oe.openSync(E,lt.flagsForNode(d.flags)))}catch(I){throw I.code?new S.ErrnoError(lt.convertNodeCode(I)):I}},close:function(d){try{S.isFile(d.node.mode)&&d.nfd&&Oe.closeSync(d.nfd)}catch(E){throw E.code?new S.ErrnoError(lt.convertNodeCode(E)):E}},read:function(d,E,I,D,M){if(D===0)return 0;try{return Oe.readSync(d.nfd,lt.bufferFrom(E.buffer),I,D,M)}catch(_){throw new S.ErrnoError(lt.convertNodeCode(_))}},write:function(d,E,I,D,M){try{return Oe.writeSync(d.nfd,lt.bufferFrom(E.buffer),I,D,M)}catch(_){throw new S.ErrnoError(lt.convertNodeCode(_))}},llseek:function(d,E,I){var D=E;if(I===1)D+=d.position;else if(I===2&&S.isFile(d.node.mode))try{var M=Oe.fstatSync(d.nfd);D+=M.size}catch(_){throw new S.ErrnoError(lt.convertNodeCode(_))}if(D<0)throw new S.ErrnoError(28);return D},mmap:function(d,E,I,D,M,_){if(E!==0)throw new S.ErrnoError(28);if(!S.isFile(d.node.mode))throw new S.ErrnoError(43);var ie=ds(I);return lt.stream_ops.read(d,pe,ie,I,D),{ptr:ie,allocated:!0}},msync:function(d,E,I,D,M){if(!S.isFile(d.node.mode))throw new S.ErrnoError(43);if(M&2)return 0;var _=lt.stream_ops.write(d,E,0,D,I,!1);return 0}}},mn={lookupPath:function(d){return{path:d,node:{mode:lt.getMode(d)}}},createStandardStreams:function(){S.streams[0]={fd:0,nfd:0,position:0,path:"",flags:0,tty:!0,seekable:!1};for(var d=1;d<3;d++)S.streams[d]={fd:d,nfd:d,position:0,path:"",flags:577,tty:!0,seekable:!1}},cwd:function(){return process.cwd()},chdir:function(){process.chdir.apply(void 0,arguments)},mknod:function(d,E){S.isDir(d)?Oe.mkdirSync(d,E):Oe.writeFileSync(d,"",{mode:E})},mkdir:function(){Oe.mkdirSync.apply(void 0,arguments)},symlink:function(){Oe.symlinkSync.apply(void 0,arguments)},rename:function(){Oe.renameSync.apply(void 0,arguments)},rmdir:function(){Oe.rmdirSync.apply(void 0,arguments)},readdir:function(){Oe.readdirSync.apply(void 0,arguments)},unlink:function(){Oe.unlinkSync.apply(void 0,arguments)},readlink:function(){return Oe.readlinkSync.apply(void 0,arguments)},stat:function(){return Oe.statSync.apply(void 0,arguments)},lstat:function(){return Oe.lstatSync.apply(void 0,arguments)},chmod:function(){Oe.chmodSync.apply(void 0,arguments)},fchmod:function(){Oe.fchmodSync.apply(void 0,arguments)},chown:function(){Oe.chownSync.apply(void 0,arguments)},fchown:function(){Oe.fchownSync.apply(void 0,arguments)},truncate:function(){Oe.truncateSync.apply(void 0,arguments)},ftruncate:function(d,E){if(E<0)throw new S.ErrnoError(28);Oe.ftruncateSync.apply(void 0,arguments)},utime:function(){Oe.utimesSync.apply(void 0,arguments)},open:function(d,E,I,D){typeof E=="string"&&(E=to.modeStringToFlags(E));var M=Oe.openSync(d,lt.flagsForNode(E),I),_=D!=null?D:S.nextfd(M),ie={fd:_,nfd:M,position:0,path:d,flags:E,seekable:!0};return S.streams[_]=ie,ie},close:function(d){d.stream_ops||Oe.closeSync(d.nfd),S.closeStream(d.fd)},llseek:function(d,E,I){if(d.stream_ops)return to.llseek(d,E,I);var D=E;if(I===1)D+=d.position;else if(I===2)D+=Oe.fstatSync(d.nfd).size;else if(I!==0)throw new S.ErrnoError(Ro.EINVAL);if(D<0)throw new S.ErrnoError(Ro.EINVAL);return d.position=D,D},read:function(d,E,I,D,M){if(d.stream_ops)return to.read(d,E,I,D,M);var _=typeof M!="undefined";!_&&d.seekable&&(M=d.position);var ie=Oe.readSync(d.nfd,lt.bufferFrom(E.buffer),I,D,M);return _||(d.position+=ie),ie},write:function(d,E,I,D,M){if(d.stream_ops)return to.write(d,E,I,D,M);d.flags&+"1024"&&S.llseek(d,0,+"2");var _=typeof M!="undefined";!_&&d.seekable&&(M=d.position);var ie=Oe.writeSync(d.nfd,lt.bufferFrom(E.buffer),I,D,M);return _||(d.position+=ie),ie},allocate:function(){throw new S.ErrnoError(Ro.EOPNOTSUPP)},mmap:function(d,E,I,D,M,_){if(d.stream_ops)return to.mmap(d,E,I,D,M,_);if(E!==0)throw new S.ErrnoError(28);var ie=ds(I);return S.read(d,pe,ie,I,D),{ptr:ie,allocated:!0}},msync:function(d,E,I,D,M){return d.stream_ops?to.msync(d,E,I,D,M):(M&2||S.write(d,E,0,D,I),0)},munmap:function(){return 0},ioctl:function(){throw new S.ErrnoError(Ro.ENOTTY)}},S={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:!1,ignorePermissions:!0,trackingDelegate:{},tracking:{openFlags:{READ:1,WRITE:2}},ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,lookupPath:function(d,E){if(d=qn.resolve(S.cwd(),d),E=E||{},!d)return{path:"",node:null};var I={follow_mount:!0,recurse_count:0};for(var D in I)E[D]===void 0&&(E[D]=I[D]);if(E.recurse_count>8)throw new S.ErrnoError(32);for(var M=St.normalizeArray(d.split("/").filter(function(ut){return!!ut}),!1),_=S.root,ie="/",we=0;we40)throw new S.ErrnoError(32)}}return{path:ie,node:_}},getPath:function(d){for(var E;;){if(S.isRoot(d)){var I=d.mount.mountpoint;return E?I[I.length-1]!=="/"?I+"/"+E:I+E:I}E=E?d.name+"/"+E:d.name,d=d.parent}},hashName:function(d,E){for(var I=0,D=0;D>>0)%S.nameTable.length},hashAddNode:function(d){var E=S.hashName(d.parent.id,d.name);d.name_next=S.nameTable[E],S.nameTable[E]=d},hashRemoveNode:function(d){var E=S.hashName(d.parent.id,d.name);if(S.nameTable[E]===d)S.nameTable[E]=d.name_next;else for(var I=S.nameTable[E];I;){if(I.name_next===d){I.name_next=d.name_next;break}I=I.name_next}},lookupNode:function(d,E){var I=S.mayLookup(d);if(I)throw new S.ErrnoError(I,d);for(var D=S.hashName(d.id,E),M=S.nameTable[D];M;M=M.name_next){var _=M.name;if(M.parent.id===d.id&&_===E)return M}return S.lookup(d,E)},createNode:function(d,E,I,D){var M=new S.FSNode(d,E,I,D);return S.hashAddNode(M),M},destroyNode:function(d){S.hashRemoveNode(d)},isRoot:function(d){return d===d.parent},isMountpoint:function(d){return!!d.mounted},isFile:function(d){return(d&61440)==32768},isDir:function(d){return(d&61440)==16384},isLink:function(d){return(d&61440)==40960},isChrdev:function(d){return(d&61440)==8192},isBlkdev:function(d){return(d&61440)==24576},isFIFO:function(d){return(d&61440)==4096},isSocket:function(d){return(d&49152)==49152},flagModes:{r:0,"r+":2,w:577,"w+":578,a:1089,"a+":1090},modeStringToFlags:function(d){var E=S.flagModes[d];if(typeof E=="undefined")throw new Error("Unknown file open mode: "+d);return E},flagsToPermissionString:function(d){var E=["r","w","rw"][d&3];return d&512&&(E+="w"),E},nodePermissions:function(d,E){return S.ignorePermissions?0:E.includes("r")&&!(d.mode&292)||E.includes("w")&&!(d.mode&146)||E.includes("x")&&!(d.mode&73)?2:0},mayLookup:function(d){var E=S.nodePermissions(d,"x");return E||(d.node_ops.lookup?0:2)},mayCreate:function(d,E){try{var I=S.lookupNode(d,E);return 20}catch(D){}return S.nodePermissions(d,"wx")},mayDelete:function(d,E,I){var D;try{D=S.lookupNode(d,E)}catch(_){return _.errno}var M=S.nodePermissions(d,"wx");if(M)return M;if(I){if(!S.isDir(D.mode))return 54;if(S.isRoot(D)||S.getPath(D)===S.cwd())return 10}else if(S.isDir(D.mode))return 31;return 0},mayOpen:function(d,E){return d?S.isLink(d.mode)?32:S.isDir(d.mode)&&(S.flagsToPermissionString(E)!=="r"||E&512)?31:S.nodePermissions(d,S.flagsToPermissionString(E)):44},MAX_OPEN_FDS:4096,nextfd:function(d,E){d=d||0,E=E||S.MAX_OPEN_FDS;for(var I=d;I<=E;I++)if(!S.streams[I])return I;throw new S.ErrnoError(33)},getStream:function(d){return S.streams[d]},createStream:function(d,E,I){S.FSStream||(S.FSStream=function(){},S.FSStream.prototype={object:{get:function(){return this.node},set:function(ie){this.node=ie}},isRead:{get:function(){return(this.flags&2097155)!=1}},isWrite:{get:function(){return(this.flags&2097155)!=0}},isAppend:{get:function(){return this.flags&1024}}});var D=new S.FSStream;for(var M in d)D[M]=d[M];d=D;var _=S.nextfd(E,I);return d.fd=_,S.streams[_]=d,d},closeStream:function(d){S.streams[d]=null},chrdev_stream_ops:{open:function(d){var E=S.getDevice(d.node.rdev);d.stream_ops=E.stream_ops,d.stream_ops.open&&d.stream_ops.open(d)},llseek:function(){throw new S.ErrnoError(70)}},major:function(d){return d>>8},minor:function(d){return d&255},makedev:function(d,E){return d<<8|E},registerDevice:function(d,E){S.devices[d]={stream_ops:E}},getDevice:function(d){return S.devices[d]},getMounts:function(d){for(var E=[],I=[d];I.length;){var D=I.pop();E.push(D),I.push.apply(I,D.mounts)}return E},syncfs:function(d,E){typeof d=="function"&&(E=d,d=!1),S.syncFSRequests++,S.syncFSRequests>1&&k("warning: "+S.syncFSRequests+" FS.syncfs operations in flight at once, probably just doing extra work");var I=S.getMounts(S.root.mount),D=0;function M(ie){return S.syncFSRequests--,E(ie)}function _(ie){if(ie)return _.errored?void 0:(_.errored=!0,M(ie));++D>=I.length&&M(null)}I.forEach(function(ie){if(!ie.type.syncfs)return _(null);ie.type.syncfs(ie,d,_)})},mount:function(d,E,I){var D=I==="/",M=!I,_;if(D&&S.root)throw new S.ErrnoError(10);if(!D&&!M){var ie=S.lookupPath(I,{follow_mount:!1});if(I=ie.path,_=ie.node,S.isMountpoint(_))throw new S.ErrnoError(10);if(!S.isDir(_.mode))throw new S.ErrnoError(54)}var we={type:d,opts:E,mountpoint:I,mounts:[]},me=d.mount(we);return me.mount=we,we.root=me,D?S.root=me:_&&(_.mounted=we,_.mount&&_.mount.mounts.push(we)),me},unmount:function(d){var E=S.lookupPath(d,{follow_mount:!1});if(!S.isMountpoint(E.node))throw new S.ErrnoError(28);var I=E.node,D=I.mounted,M=S.getMounts(D);Object.keys(S.nameTable).forEach(function(ie){for(var we=S.nameTable[ie];we;){var me=we.name_next;M.includes(we.mount)&&S.destroyNode(we),we=me}}),I.mounted=null;var _=I.mount.mounts.indexOf(D);I.mount.mounts.splice(_,1)},lookup:function(d,E){return d.node_ops.lookup(d,E)},mknod:function(d,E,I){var D=S.lookupPath(d,{parent:!0}),M=D.node,_=St.basename(d);if(!_||_==="."||_==="..")throw new S.ErrnoError(28);var ie=S.mayCreate(M,_);if(ie)throw new S.ErrnoError(ie);if(!M.node_ops.mknod)throw new S.ErrnoError(63);return M.node_ops.mknod(M,_,E,I)},create:function(d,E){return E=E!==void 0?E:438,E&=4095,E|=32768,S.mknod(d,E,0)},mkdir:function(d,E){return E=E!==void 0?E:511,E&=511|512,E|=16384,S.mknod(d,E,0)},mkdirTree:function(d,E){for(var I=d.split("/"),D="",M=0;Mthis.length-1||ut<0)){var st=ut%this.chunkSize,yt=ut/this.chunkSize|0;return this.getter(yt)[st]}},_.prototype.setDataGetter=function(ut){this.getter=ut},_.prototype.cacheLength=function(){var ut=new XMLHttpRequest;if(ut.open("HEAD",I,!1),ut.send(null),!(ut.status>=200&&ut.status<300||ut.status===304))throw new Error("Couldn't load "+I+". Status: "+ut.status);var st=Number(ut.getResponseHeader("Content-length")),yt,ke=(yt=ut.getResponseHeader("Accept-Ranges"))&&yt==="bytes",zn=(yt=ut.getResponseHeader("Content-Encoding"))&&yt==="gzip",Mi=1024*1024;ke||(Mi=st);var jA=function(Cs,Da){if(Cs>Da)throw new Error("invalid range ("+Cs+", "+Da+") or no bytes requested!");if(Da>st-1)throw new Error("only "+st+" bytes available! programmer error!");var qr=new XMLHttpRequest;if(qr.open("GET",I,!1),st!==Mi&&qr.setRequestHeader("Range","bytes="+Cs+"-"+Da),typeof Uint8Array!="undefined"&&(qr.responseType="arraybuffer"),qr.overrideMimeType&&qr.overrideMimeType("text/plain; charset=x-user-defined"),qr.send(null),!(qr.status>=200&&qr.status<300||qr.status===304))throw new Error("Couldn't load "+I+". Status: "+qr.status);return qr.response!==void 0?new Uint8Array(qr.response||[]):OA(qr.responseText||"",!0)},Yr=this;Yr.setDataGetter(function(Cs){var Da=Cs*Mi,qr=(Cs+1)*Mi-1;if(qr=Math.min(qr,st-1),typeof Yr.chunks[Cs]=="undefined"&&(Yr.chunks[Cs]=jA(Da,qr)),typeof Yr.chunks[Cs]=="undefined")throw new Error("doXHR failed!");return Yr.chunks[Cs]}),(zn||!st)&&(Mi=st=1,st=this.getter(0).length,Mi=st,v("LazyFiles on gzip forces download of the whole file when length is accessed")),this._length=st,this._chunkSize=Mi,this.lengthKnown=!0},typeof XMLHttpRequest!="undefined"){if(!u)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var ie=new _;Object.defineProperties(ie,{length:{get:function(){return this.lengthKnown||this.cacheLength(),this._length}},chunkSize:{get:function(){return this.lengthKnown||this.cacheLength(),this._chunkSize}}});var we={isDevice:!1,contents:ie}}else var we={isDevice:!1,url:I};var me=S.createFile(d,E,we,D,M);we.contents?me.contents=we.contents:we.url&&(me.contents=null,me.url=we.url),Object.defineProperties(me,{usedBytes:{get:function(){return this.contents.length}}});var _e={},ot=Object.keys(me.stream_ops);return ot.forEach(function(Bt){var ut=me.stream_ops[Bt];_e[Bt]=function(){return S.forceLoadFile(me),ut.apply(null,arguments)}}),_e.read=function(ut,st,yt,ke,zn){S.forceLoadFile(me);var Mi=ut.node.contents;if(zn>=Mi.length)return 0;var jA=Math.min(Mi.length-zn,ke);if(Mi.slice)for(var Yr=0;Yr>2]=D.dev,fe[I+4>>2]=0,fe[I+8>>2]=D.ino,fe[I+12>>2]=D.mode,fe[I+16>>2]=D.nlink,fe[I+20>>2]=D.uid,fe[I+24>>2]=D.gid,fe[I+28>>2]=D.rdev,fe[I+32>>2]=0,Ae=[D.size>>>0,(Dr=D.size,+Math.abs(Dr)>=1?Dr>0?(Math.min(+Math.floor(Dr/4294967296),4294967295)|0)>>>0:~~+Math.ceil((Dr-+(~~Dr>>>0))/4294967296)>>>0:0)],fe[I+40>>2]=Ae[0],fe[I+44>>2]=Ae[1],fe[I+48>>2]=4096,fe[I+52>>2]=D.blocks,fe[I+56>>2]=D.atime.getTime()/1e3|0,fe[I+60>>2]=0,fe[I+64>>2]=D.mtime.getTime()/1e3|0,fe[I+68>>2]=0,fe[I+72>>2]=D.ctime.getTime()/1e3|0,fe[I+76>>2]=0,Ae=[D.ino>>>0,(Dr=D.ino,+Math.abs(Dr)>=1?Dr>0?(Math.min(+Math.floor(Dr/4294967296),4294967295)|0)>>>0:~~+Math.ceil((Dr-+(~~Dr>>>0))/4294967296)>>>0:0)],fe[I+80>>2]=Ae[0],fe[I+84>>2]=Ae[1],0},doMsync:function(d,E,I,D,M){var _=V.slice(d,d+I);S.msync(E,_,M,I,D)},doMkdir:function(d,E){return d=St.normalize(d),d[d.length-1]==="/"&&(d=d.substr(0,d.length-1)),S.mkdir(d,E,0),0},doMknod:function(d,E,I){switch(E&61440){case 32768:case 8192:case 24576:case 4096:case 49152:break;default:return-28}return S.mknod(d,E,I),0},doReadlink:function(d,E,I){if(I<=0)return-28;var D=S.readlink(d),M=Math.min(I,he(D)),_=pe[E+M];return be(D,E,I+1),pe[E+M]=_,M},doAccess:function(d,E){if(E&~7)return-28;var I,D=S.lookupPath(d,{follow:!0});if(I=D.node,!I)return-44;var M="";return E&4&&(M+="r"),E&2&&(M+="w"),E&1&&(M+="x"),M&&S.nodePermissions(I,M)?-2:0},doDup:function(d,E,I){var D=S.getStream(I);return D&&S.close(D),S.open(d,E,0,I,I).fd},doReadv:function(d,E,I,D){for(var M=0,_=0;_>2],we=fe[E+(_*8+4)>>2],me=S.read(d,pe,ie,we,D);if(me<0)return-1;if(M+=me,me>2],we=fe[E+(_*8+4)>>2],me=S.write(d,pe,ie,we,D);if(me<0)return-1;M+=me}return M},varargs:void 0,get:function(){Tt.varargs+=4;var d=fe[Tt.varargs-4>>2];return d},getStr:function(d){var E=re(d);return E},getStreamFromFD:function(d){var E=S.getStream(d);if(!E)throw new S.ErrnoError(8);return E},get64:function(d,E){return d}};function Ku(d,E){try{return d=Tt.getStr(d),S.chmod(d,E),0}catch(I){return(typeof S=="undefined"||!(I instanceof S.ErrnoError))&&vr(I),-I.errno}}function Xl(d){return fe[Rt()>>2]=d,d}function xh(d,E,I){Tt.varargs=I;try{var D=Tt.getStreamFromFD(d);switch(E){case 0:{var M=Tt.get();if(M<0)return-28;var _;return _=S.open(D.path,D.flags,0,M),_.fd}case 1:case 2:return 0;case 3:return D.flags;case 4:{var M=Tt.get();return D.flags|=M,0}case 12:{var M=Tt.get(),ie=0;return Qe[M+ie>>1]=2,0}case 13:case 14:return 0;case 16:case 8:return-28;case 9:return Xl(28),-1;default:return-28}}catch(we){return(typeof S=="undefined"||!(we instanceof S.ErrnoError))&&vr(we),-we.errno}}function Ph(d,E){try{var I=Tt.getStreamFromFD(d);return Tt.doStat(S.stat,I.path,E)}catch(D){return(typeof S=="undefined"||!(D instanceof S.ErrnoError))&&vr(D),-D.errno}}function Dh(d,E,I){Tt.varargs=I;try{var D=Tt.getStreamFromFD(d);switch(E){case 21509:case 21505:return D.tty?0:-59;case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:return D.tty?0:-59;case 21519:{if(!D.tty)return-59;var M=Tt.get();return fe[M>>2]=0,0}case 21520:return D.tty?-28:-59;case 21531:{var M=Tt.get();return S.ioctl(D,E,M)}case 21523:return D.tty?0:-59;case 21524:return D.tty?0:-59;default:vr("bad ioctl syscall "+E)}}catch(_){return(typeof S=="undefined"||!(_ instanceof S.ErrnoError))&&vr(_),-_.errno}}function Rh(d,E,I){Tt.varargs=I;try{var D=Tt.getStr(d),M=I?Tt.get():0,_=S.open(D,E,M);return _.fd}catch(ie){return(typeof S=="undefined"||!(ie instanceof S.ErrnoError))&&vr(ie),-ie.errno}}function Fh(d,E){try{return d=Tt.getStr(d),E=Tt.getStr(E),S.rename(d,E),0}catch(I){return(typeof S=="undefined"||!(I instanceof S.ErrnoError))&&vr(I),-I.errno}}function j(d){try{return d=Tt.getStr(d),S.rmdir(d),0}catch(E){return(typeof S=="undefined"||!(E instanceof S.ErrnoError))&&vr(E),-E.errno}}function wt(d,E){try{return d=Tt.getStr(d),Tt.doStat(S.stat,d,E)}catch(I){return(typeof S=="undefined"||!(I instanceof S.ErrnoError))&&vr(I),-I.errno}}function TA(d){try{return d=Tt.getStr(d),S.unlink(d),0}catch(E){return(typeof S=="undefined"||!(E instanceof S.ErrnoError))&&vr(E),-E.errno}}function $i(d,E,I){V.copyWithin(d,E,E+I)}function Zl(d){try{return A.grow(d-ve.byteLength+65535>>>16),Ei(A.buffer),1}catch(E){}}function $e(d){var E=V.length;d=d>>>0;var I=2147483648;if(d>I)return!1;for(var D=1;D<=4;D*=2){var M=E*(1+.2/D);M=Math.min(M,d+100663296);var _=Math.min(I,xe(Math.max(d,M),65536)),ie=Zl(_);if(ie)return!0}return!1}function va(d){try{var E=Tt.getStreamFromFD(d);return S.close(E),0}catch(I){return(typeof S=="undefined"||!(I instanceof S.ErrnoError))&&vr(I),I.errno}}function Hu(d,E){try{var I=Tt.getStreamFromFD(d),D=I.tty?2:S.isDir(I.mode)?3:S.isLink(I.mode)?7:4;return pe[E>>0]=D,0}catch(M){return(typeof S=="undefined"||!(M instanceof S.ErrnoError))&&vr(M),M.errno}}function wE(d,E,I,D){try{var M=Tt.getStreamFromFD(d),_=Tt.doReadv(M,E,I);return fe[D>>2]=_,0}catch(ie){return(typeof S=="undefined"||!(ie instanceof S.ErrnoError))&&vr(ie),ie.errno}}function Nh(d,E,I,D,M){try{var _=Tt.getStreamFromFD(d),ie=4294967296,we=I*ie+(E>>>0),me=9007199254740992;return we<=-me||we>=me?-61:(S.llseek(_,we,D),Ae=[_.position>>>0,(Dr=_.position,+Math.abs(Dr)>=1?Dr>0?(Math.min(+Math.floor(Dr/4294967296),4294967295)|0)>>>0:~~+Math.ceil((Dr-+(~~Dr>>>0))/4294967296)>>>0:0)],fe[M>>2]=Ae[0],fe[M+4>>2]=Ae[1],_.getdents&&we===0&&D===0&&(_.getdents=null),0)}catch(_e){return(typeof S=="undefined"||!(_e instanceof S.ErrnoError))&&vr(_e),_e.errno}}function BE(d,E,I,D){try{var M=Tt.getStreamFromFD(d),_=Tt.doWritev(M,E,I);return fe[D>>2]=_,0}catch(ie){return(typeof S=="undefined"||!(ie instanceof S.ErrnoError))&&vr(ie),ie.errno}}function gr(d){$(d)}function Jn(d){var E=Date.now()/1e3|0;return d&&(fe[d>>2]=E),E}function $l(){if($l.called)return;$l.called=!0;var d=new Date().getFullYear(),E=new Date(d,0,1),I=new Date(d,6,1),D=E.getTimezoneOffset(),M=I.getTimezoneOffset(),_=Math.max(D,M);fe[_b()>>2]=_*60,fe[zb()>>2]=Number(D!=M);function ie(Bt){var ut=Bt.toTimeString().match(/\(([A-Za-z ]+)\)$/);return ut?ut[1]:"GMT"}var we=ie(E),me=ie(I),_e=Fe(we),ot=Fe(me);M>2]=_e,fe[zu()+4>>2]=ot):(fe[zu()>>2]=ot,fe[zu()+4>>2]=_e)}function Lh(d){$l();var E=Date.UTC(fe[d+20>>2]+1900,fe[d+16>>2],fe[d+12>>2],fe[d+8>>2],fe[d+4>>2],fe[d>>2],0),I=new Date(E);fe[d+24>>2]=I.getUTCDay();var D=Date.UTC(I.getUTCFullYear(),0,1,0,0,0,0),M=(I.getTime()-D)/(1e3*60*60*24)|0;return fe[d+28>>2]=M,I.getTime()/1e3|0}var eo=function(d,E,I,D){d||(d=this),this.parent=d,this.mount=d.mount,this.mounted=null,this.id=S.nextInode++,this.name=E,this.mode=I,this.node_ops={},this.stream_ops={},this.rdev=D},ka=292|73,En=146;if(Object.defineProperties(eo.prototype,{read:{get:function(){return(this.mode&ka)===ka},set:function(d){d?this.mode|=ka:this.mode&=~ka}},write:{get:function(){return(this.mode&En)===En},set:function(d){d?this.mode|=En:this.mode&=~En}},isFolder:{get:function(){return S.isDir(this.mode)}},isDevice:{get:function(){return S.isChrdev(this.mode)}}}),S.FSNode=eo,S.staticInit(),g){var Oe=D5,ju=require("path");lt.staticInit()}if(g){var ec=function(d){return function(){try{return d.apply(this,arguments)}catch(E){throw E.code?new S.ErrnoError(Ro[E.code]):E}}},to=Object.assign({},S);for(var tc in mn)S[tc]=ec(mn[tc])}else throw new Error("NODERAWFS is currently only supported on Node.js environment.");function OA(d,E,I){var D=I>0?I:he(d)+1,M=new Array(D),_=se(d,M,0,M.length);return E&&(M.length=_),M}var Gu=typeof atob=="function"?atob:function(d){var E="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",I="",D,M,_,ie,we,me,_e,ot=0;d=d.replace(/[^A-Za-z0-9\+\/\=]/g,"");do ie=E.indexOf(d.charAt(ot++)),we=E.indexOf(d.charAt(ot++)),me=E.indexOf(d.charAt(ot++)),_e=E.indexOf(d.charAt(ot++)),D=ie<<2|we>>4,M=(we&15)<<4|me>>2,_=(me&3)<<6|_e,I=I+String.fromCharCode(D),me!==64&&(I=I+String.fromCharCode(M)),_e!==64&&(I=I+String.fromCharCode(_));while(ot0||(Sr(),hs>0))return;function E(){Ke||(Ke=!0,t.calledRun=!0,!oe&&(Gn(),i(t),t.onRuntimeInitialized&&t.onRuntimeInitialized(),fs()))}t.setStatus?(t.setStatus("Running..."),setTimeout(function(){setTimeout(function(){t.setStatus("")},1),E()},1)):E()}if(t.run=HA,t.preInit)for(typeof t.preInit=="function"&&(t.preInit=[t.preInit]);t.preInit.length>0;)t.preInit.pop()();return HA(),e}}();typeof Yw=="object"&&typeof GP=="object"?GP.exports=YP:typeof define=="function"&&define.amd?define([],function(){return YP}):typeof Yw=="object"&&(Yw.createModule=YP)});var i9=w((Tst,r9)=>{function jPe(r,e){for(var t=-1,i=r==null?0:r.length,n=Array(i);++t{var GPe=Array.isArray;n9.exports=GPe});var c9=w((Mst,s9)=>{var o9=Wc(),YPe=i9(),qPe=Hs(),JPe=yd(),WPe=1/0,a9=o9?o9.prototype:void 0,A9=a9?a9.toString:void 0;function l9(r){if(typeof r=="string")return r;if(qPe(r))return YPe(r,l9)+"";if(JPe(r))return A9?A9.call(r):"";var e=r+"";return e=="0"&&1/r==-WPe?"-0":e}s9.exports=l9});var cf=w((Ust,u9)=>{var zPe=c9();function _Pe(r){return r==null?"":zPe(r)}u9.exports=_Pe});var XP=w((Kst,g9)=>{function VPe(r,e,t){var i=-1,n=r.length;e<0&&(e=-e>n?0:n+e),t=t>n?n:t,t<0&&(t+=n),n=e>t?0:t-e>>>0,e>>>=0;for(var s=Array(n);++i{var XPe=XP();function ZPe(r,e,t){var i=r.length;return t=t===void 0?i:t,!e&&t>=i?r:XPe(r,e,t)}f9.exports=ZPe});var ZP=w((jst,p9)=>{var $Pe="\\ud800-\\udfff",eDe="\\u0300-\\u036f",tDe="\\ufe20-\\ufe2f",rDe="\\u20d0-\\u20ff",iDe=eDe+tDe+rDe,nDe="\\ufe0e\\ufe0f",sDe="\\u200d",oDe=RegExp("["+sDe+$Pe+iDe+nDe+"]");function aDe(r){return oDe.test(r)}p9.exports=aDe});var C9=w((Gst,d9)=>{function ADe(r){return r.split("")}d9.exports=ADe});var Q9=w((Yst,m9)=>{var E9="\\ud800-\\udfff",lDe="\\u0300-\\u036f",cDe="\\ufe20-\\ufe2f",uDe="\\u20d0-\\u20ff",gDe=lDe+cDe+uDe,fDe="\\ufe0e\\ufe0f",hDe="["+E9+"]",$P="["+gDe+"]",eD="\\ud83c[\\udffb-\\udfff]",pDe="(?:"+$P+"|"+eD+")",I9="[^"+E9+"]",y9="(?:\\ud83c[\\udde6-\\uddff]){2}",w9="[\\ud800-\\udbff][\\udc00-\\udfff]",dDe="\\u200d",B9=pDe+"?",b9="["+fDe+"]?",CDe="(?:"+dDe+"(?:"+[I9,y9,w9].join("|")+")"+b9+B9+")*",mDe=b9+B9+CDe,EDe="(?:"+[I9+$P+"?",$P,y9,w9,hDe].join("|")+")",IDe=RegExp(eD+"(?="+eD+")|"+EDe+mDe,"g");function yDe(r){return r.match(IDe)||[]}m9.exports=yDe});var v9=w((qst,S9)=>{var wDe=C9(),BDe=ZP(),bDe=Q9();function QDe(r){return BDe(r)?bDe(r):wDe(r)}S9.exports=QDe});var x9=w((Jst,k9)=>{var SDe=h9(),vDe=ZP(),kDe=v9(),xDe=cf();function PDe(r){return function(e){e=xDe(e);var t=vDe(e)?kDe(e):void 0,i=t?t[0]:e.charAt(0),n=t?SDe(t,1).join(""):e.slice(1);return i[r]()+n}}k9.exports=PDe});var D9=w((Wst,P9)=>{var DDe=x9(),RDe=DDe("toUpperCase");P9.exports=RDe});var $w=w((zst,R9)=>{var FDe=cf(),NDe=D9();function LDe(r){return NDe(FDe(r).toLowerCase())}R9.exports=LDe});var F9=w((_st,eB)=>{function TDe(){var r=0,e=1,t=2,i=3,n=4,s=5,o=6,a=7,l=8,c=9,u=10,g=11,f=12,h=13,p=14,m=15,y=16,b=17,v=0,k=1,T=2,Y=3,q=4;function $(A,oe){return 55296<=A.charCodeAt(oe)&&A.charCodeAt(oe)<=56319&&56320<=A.charCodeAt(oe+1)&&A.charCodeAt(oe+1)<=57343}function z(A,oe){oe===void 0&&(oe=0);var ce=A.charCodeAt(oe);if(55296<=ce&&ce<=56319&&oe=1){var Z=A.charCodeAt(oe-1),O=ce;return 55296<=Z&&Z<=56319?(Z-55296)*1024+(O-56320)+65536:O}return ce}function ne(A,oe,ce){var Z=[A].concat(oe).concat([ce]),O=Z[Z.length-2],L=ce,de=Z.lastIndexOf(p);if(de>1&&Z.slice(1,de).every(function(re){return re==i})&&[i,h,b].indexOf(A)==-1)return T;var Be=Z.lastIndexOf(n);if(Be>0&&Z.slice(1,Be).every(function(re){return re==n})&&[f,n].indexOf(O)==-1)return Z.filter(function(re){return re==n}).length%2==1?Y:q;if(O==r&&L==e)return v;if(O==t||O==r||O==e)return L==p&&oe.every(function(re){return re==i})?T:k;if(L==t||L==r||L==e)return k;if(O==o&&(L==o||L==a||L==c||L==u))return v;if((O==c||O==a)&&(L==a||L==l))return v;if((O==u||O==l)&&L==l)return v;if(L==i||L==m)return v;if(L==s)return v;if(O==f)return v;var Ge=Z.indexOf(i)!=-1?Z.lastIndexOf(i)-1:Z.length-2;return[h,b].indexOf(Z[Ge])!=-1&&Z.slice(Ge+1,-1).every(function(re){return re==i})&&L==p||O==m&&[y,b].indexOf(L)!=-1?v:oe.indexOf(n)!=-1?T:O==n&&L==n?v:k}this.nextBreak=function(A,oe){if(oe===void 0&&(oe=0),oe<0)return 0;if(oe>=A.length-1)return A.length;for(var ce=ee(z(A,oe)),Z=[],O=oe+1;O{var ODe=/^(.*?)(\x1b\[[^m]+m|\x1b\]8;;.*?(\x1b\\|\u0007))/,tB;function MDe(){if(tB)return tB;if(typeof Intl.Segmenter!="undefined"){let r=new Intl.Segmenter("en",{granularity:"grapheme"});return tB=e=>Array.from(r.segment(e),({segment:t})=>t)}else{let r=F9(),e=new r;return tB=t=>e.splitGraphemes(t)}}N9.exports=(r,e=0,t=r.length)=>{if(e<0||t<0)throw new RangeError("Negative indices aren't supported by this implementation");let i=t-e,n="",s=0,o=0;for(;r.length>0;){let a=r.match(ODe)||[r,r,void 0],l=MDe()(a[1]),c=Math.min(e-s,l.length);l=l.slice(c);let u=Math.min(i-o,l.length);n+=l.slice(0,u).join(""),s+=c,o+=u,typeof a[2]!="undefined"&&(n+=a[2]),r=r.slice(a[0].length)}return n}});var uf=w((Qot,_9)=>{"use strict";var V9=new Map([["C","cwd"],["f","file"],["z","gzip"],["P","preservePaths"],["U","unlink"],["strip-components","strip"],["stripComponents","strip"],["keep-newer","newer"],["keepNewer","newer"],["keep-newer-files","newer"],["keepNewerFiles","newer"],["k","keep"],["keep-existing","keep"],["keepExisting","keep"],["m","noMtime"],["no-mtime","noMtime"],["p","preserveOwner"],["L","follow"],["h","follow"]]),bot=_9.exports=r=>r?Object.keys(r).map(e=>[V9.has(e)?V9.get(e):e,r[e]]).reduce((e,t)=>(e[t[0]]=t[1],e),Object.create(null)):{}});var gf=w((Sot,X9)=>{"use strict";var XDe=require("events"),Z9=require("stream"),Jd=bp(),$9=require("string_decoder").StringDecoder,cA=Symbol("EOF"),Wd=Symbol("maybeEmitEnd"),yl=Symbol("emittedEnd"),aB=Symbol("emittingEnd"),AB=Symbol("closed"),e_=Symbol("read"),nD=Symbol("flush"),t_=Symbol("flushChunk"),Ln=Symbol("encoding"),uA=Symbol("decoder"),lB=Symbol("flowing"),zd=Symbol("paused"),_d=Symbol("resume"),pn=Symbol("bufferLength"),r_=Symbol("bufferPush"),sD=Symbol("bufferShift"),_i=Symbol("objectMode"),Vi=Symbol("destroyed"),i_=global._MP_NO_ITERATOR_SYMBOLS_!=="1",ZDe=i_&&Symbol.asyncIterator||Symbol("asyncIterator not implemented"),$De=i_&&Symbol.iterator||Symbol("iterator not implemented"),n_=r=>r==="end"||r==="finish"||r==="prefinish",eRe=r=>r instanceof ArrayBuffer||typeof r=="object"&&r.constructor&&r.constructor.name==="ArrayBuffer"&&r.byteLength>=0,tRe=r=>!Buffer.isBuffer(r)&&ArrayBuffer.isView(r);X9.exports=class s_ extends Z9{constructor(e){super();this[lB]=!1,this[zd]=!1,this.pipes=new Jd,this.buffer=new Jd,this[_i]=e&&e.objectMode||!1,this[_i]?this[Ln]=null:this[Ln]=e&&e.encoding||null,this[Ln]==="buffer"&&(this[Ln]=null),this[uA]=this[Ln]?new $9(this[Ln]):null,this[cA]=!1,this[yl]=!1,this[aB]=!1,this[AB]=!1,this.writable=!0,this.readable=!0,this[pn]=0,this[Vi]=!1}get bufferLength(){return this[pn]}get encoding(){return this[Ln]}set encoding(e){if(this[_i])throw new Error("cannot set encoding in objectMode");if(this[Ln]&&e!==this[Ln]&&(this[uA]&&this[uA].lastNeed||this[pn]))throw new Error("cannot change encoding");this[Ln]!==e&&(this[uA]=e?new $9(e):null,this.buffer.length&&(this.buffer=this.buffer.map(t=>this[uA].write(t)))),this[Ln]=e}setEncoding(e){this.encoding=e}get objectMode(){return this[_i]}set objectMode(e){this[_i]=this[_i]||!!e}write(e,t,i){if(this[cA])throw new Error("write after end");return this[Vi]?(this.emit("error",Object.assign(new Error("Cannot call write after a stream was destroyed"),{code:"ERR_STREAM_DESTROYED"})),!0):(typeof t=="function"&&(i=t,t="utf8"),t||(t="utf8"),!this[_i]&&!Buffer.isBuffer(e)&&(tRe(e)?e=Buffer.from(e.buffer,e.byteOffset,e.byteLength):eRe(e)?e=Buffer.from(e):typeof e!="string"&&(this.objectMode=!0)),!this.objectMode&&!e.length?(this[pn]!==0&&this.emit("readable"),i&&i(),this.flowing):(typeof e=="string"&&!this[_i]&&!(t===this[Ln]&&!this[uA].lastNeed)&&(e=Buffer.from(e,t)),Buffer.isBuffer(e)&&this[Ln]&&(e=this[uA].write(e)),this.flowing?(this[pn]!==0&&this[nD](!0),this.emit("data",e)):this[r_](e),this[pn]!==0&&this.emit("readable"),i&&i(),this.flowing))}read(e){if(this[Vi])return null;try{return this[pn]===0||e===0||e>this[pn]?null:(this[_i]&&(e=null),this.buffer.length>1&&!this[_i]&&(this.encoding?this.buffer=new Jd([Array.from(this.buffer).join("")]):this.buffer=new Jd([Buffer.concat(Array.from(this.buffer),this[pn])])),this[e_](e||null,this.buffer.head.value))}finally{this[Wd]()}}[e_](e,t){return e===t.length||e===null?this[sD]():(this.buffer.head.value=t.slice(e),t=t.slice(0,e),this[pn]-=e),this.emit("data",t),!this.buffer.length&&!this[cA]&&this.emit("drain"),t}end(e,t,i){return typeof e=="function"&&(i=e,e=null),typeof t=="function"&&(i=t,t="utf8"),e&&this.write(e,t),i&&this.once("end",i),this[cA]=!0,this.writable=!1,(this.flowing||!this[zd])&&this[Wd](),this}[_d](){this[Vi]||(this[zd]=!1,this[lB]=!0,this.emit("resume"),this.buffer.length?this[nD]():this[cA]?this[Wd]():this.emit("drain"))}resume(){return this[_d]()}pause(){this[lB]=!1,this[zd]=!0}get destroyed(){return this[Vi]}get flowing(){return this[lB]}get paused(){return this[zd]}[r_](e){return this[_i]?this[pn]+=1:this[pn]+=e.length,this.buffer.push(e)}[sD](){return this.buffer.length&&(this[_i]?this[pn]-=1:this[pn]-=this.buffer.head.value.length),this.buffer.shift()}[nD](e){do;while(this[t_](this[sD]()));!e&&!this.buffer.length&&!this[cA]&&this.emit("drain")}[t_](e){return e?(this.emit("data",e),this.flowing):!1}pipe(e,t){if(this[Vi])return;let i=this[yl];t=t||{},e===process.stdout||e===process.stderr?t.end=!1:t.end=t.end!==!1;let n={dest:e,opts:t,ondrain:s=>this[_d]()};return this.pipes.push(n),e.on("drain",n.ondrain),this[_d](),i&&n.opts.end&&n.dest.end(),e}addListener(e,t){return this.on(e,t)}on(e,t){try{return super.on(e,t)}finally{e==="data"&&!this.pipes.length&&!this.flowing?this[_d]():n_(e)&&this[yl]&&(super.emit(e),this.removeAllListeners(e))}}get emittedEnd(){return this[yl]}[Wd](){!this[aB]&&!this[yl]&&!this[Vi]&&this.buffer.length===0&&this[cA]&&(this[aB]=!0,this.emit("end"),this.emit("prefinish"),this.emit("finish"),this[AB]&&this.emit("close"),this[aB]=!1)}emit(e,t){if(e!=="error"&&e!=="close"&&e!==Vi&&this[Vi])return;if(e==="data"){if(!t)return;this.pipes.length&&this.pipes.forEach(n=>n.dest.write(t)===!1&&this.pause())}else if(e==="end"){if(this[yl]===!0)return;this[yl]=!0,this.readable=!1,this[uA]&&(t=this[uA].end(),t&&(this.pipes.forEach(n=>n.dest.write(t)),super.emit("data",t))),this.pipes.forEach(n=>{n.dest.removeListener("drain",n.ondrain),n.opts.end&&n.dest.end()})}else if(e==="close"&&(this[AB]=!0,!this[yl]&&!this[Vi]))return;let i=new Array(arguments.length);if(i[0]=e,i[1]=t,arguments.length>2)for(let n=2;n{e.push(i),this[_i]||(e.dataLength+=i.length)}),t.then(()=>e)}concat(){return this[_i]?Promise.reject(new Error("cannot concat in objectMode")):this.collect().then(e=>this[_i]?Promise.reject(new Error("cannot concat in objectMode")):this[Ln]?e.join(""):Buffer.concat(e,e.dataLength))}promise(){return new Promise((e,t)=>{this.on(Vi,()=>t(new Error("stream destroyed"))),this.on("end",()=>e()),this.on("error",i=>t(i))})}[ZDe](){return{next:()=>{let t=this.read();if(t!==null)return Promise.resolve({done:!1,value:t});if(this[cA])return Promise.resolve({done:!0});let i=null,n=null,s=c=>{this.removeListener("data",o),this.removeListener("end",a),n(c)},o=c=>{this.removeListener("error",s),this.removeListener("end",a),this.pause(),i({value:c,done:!!this[cA]})},a=()=>{this.removeListener("error",s),this.removeListener("data",o),i({done:!0})},l=()=>s(new Error("stream destroyed"));return new Promise((c,u)=>{n=u,i=c,this.once(Vi,l),this.once("error",s),this.once("end",a),this.once("data",o)})}}}[$De](){return{next:()=>{let t=this.read();return{value:t,done:t===null}}}}destroy(e){return this[Vi]?(e?this.emit("error",e):this.emit(Vi),this):(this[Vi]=!0,this.buffer=new Jd,this[pn]=0,typeof this.close=="function"&&!this[AB]&&this.close(),e?this.emit("error",e):this.emit(Vi),this)}static isStream(e){return!!e&&(e instanceof s_||e instanceof Z9||e instanceof XDe&&(typeof e.pipe=="function"||typeof e.write=="function"&&typeof e.end=="function"))}}});var a_=w((vot,o_)=>{var rRe=require("zlib").constants||{ZLIB_VERNUM:4736};o_.exports=Object.freeze(Object.assign(Object.create(null),{Z_NO_FLUSH:0,Z_PARTIAL_FLUSH:1,Z_SYNC_FLUSH:2,Z_FULL_FLUSH:3,Z_FINISH:4,Z_BLOCK:5,Z_OK:0,Z_STREAM_END:1,Z_NEED_DICT:2,Z_ERRNO:-1,Z_STREAM_ERROR:-2,Z_DATA_ERROR:-3,Z_MEM_ERROR:-4,Z_BUF_ERROR:-5,Z_VERSION_ERROR:-6,Z_NO_COMPRESSION:0,Z_BEST_SPEED:1,Z_BEST_COMPRESSION:9,Z_DEFAULT_COMPRESSION:-1,Z_FILTERED:1,Z_HUFFMAN_ONLY:2,Z_RLE:3,Z_FIXED:4,Z_DEFAULT_STRATEGY:0,DEFLATE:1,INFLATE:2,GZIP:3,GUNZIP:4,DEFLATERAW:5,INFLATERAW:6,UNZIP:7,BROTLI_DECODE:8,BROTLI_ENCODE:9,Z_MIN_WINDOWBITS:8,Z_MAX_WINDOWBITS:15,Z_DEFAULT_WINDOWBITS:15,Z_MIN_CHUNK:64,Z_MAX_CHUNK:Infinity,Z_DEFAULT_CHUNK:16384,Z_MIN_MEMLEVEL:1,Z_MAX_MEMLEVEL:9,Z_DEFAULT_MEMLEVEL:8,Z_MIN_LEVEL:-1,Z_MAX_LEVEL:9,Z_DEFAULT_LEVEL:-1,BROTLI_OPERATION_PROCESS:0,BROTLI_OPERATION_FLUSH:1,BROTLI_OPERATION_FINISH:2,BROTLI_OPERATION_EMIT_METADATA:3,BROTLI_MODE_GENERIC:0,BROTLI_MODE_TEXT:1,BROTLI_MODE_FONT:2,BROTLI_DEFAULT_MODE:0,BROTLI_MIN_QUALITY:0,BROTLI_MAX_QUALITY:11,BROTLI_DEFAULT_QUALITY:11,BROTLI_MIN_WINDOW_BITS:10,BROTLI_MAX_WINDOW_BITS:24,BROTLI_LARGE_MAX_WINDOW_BITS:30,BROTLI_DEFAULT_WINDOW:22,BROTLI_MIN_INPUT_BLOCK_BITS:16,BROTLI_MAX_INPUT_BLOCK_BITS:24,BROTLI_PARAM_MODE:0,BROTLI_PARAM_QUALITY:1,BROTLI_PARAM_LGWIN:2,BROTLI_PARAM_LGBLOCK:3,BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING:4,BROTLI_PARAM_SIZE_HINT:5,BROTLI_PARAM_LARGE_WINDOW:6,BROTLI_PARAM_NPOSTFIX:7,BROTLI_PARAM_NDIRECT:8,BROTLI_DECODER_RESULT_ERROR:0,BROTLI_DECODER_RESULT_SUCCESS:1,BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT:2,BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_PARAM_DISABLE_RING_BUFFER_REALLOCATION:0,BROTLI_DECODER_PARAM_LARGE_WINDOW:1,BROTLI_DECODER_NO_ERROR:0,BROTLI_DECODER_SUCCESS:1,BROTLI_DECODER_NEEDS_MORE_INPUT:2,BROTLI_DECODER_NEEDS_MORE_OUTPUT:3,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_NIBBLE:-1,BROTLI_DECODER_ERROR_FORMAT_RESERVED:-2,BROTLI_DECODER_ERROR_FORMAT_EXUBERANT_META_NIBBLE:-3,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_ALPHABET:-4,BROTLI_DECODER_ERROR_FORMAT_SIMPLE_HUFFMAN_SAME:-5,BROTLI_DECODER_ERROR_FORMAT_CL_SPACE:-6,BROTLI_DECODER_ERROR_FORMAT_HUFFMAN_SPACE:-7,BROTLI_DECODER_ERROR_FORMAT_CONTEXT_MAP_REPEAT:-8,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_1:-9,BROTLI_DECODER_ERROR_FORMAT_BLOCK_LENGTH_2:-10,BROTLI_DECODER_ERROR_FORMAT_TRANSFORM:-11,BROTLI_DECODER_ERROR_FORMAT_DICTIONARY:-12,BROTLI_DECODER_ERROR_FORMAT_WINDOW_BITS:-13,BROTLI_DECODER_ERROR_FORMAT_PADDING_1:-14,BROTLI_DECODER_ERROR_FORMAT_PADDING_2:-15,BROTLI_DECODER_ERROR_FORMAT_DISTANCE:-16,BROTLI_DECODER_ERROR_DICTIONARY_NOT_SET:-19,BROTLI_DECODER_ERROR_INVALID_ARGUMENTS:-20,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MODES:-21,BROTLI_DECODER_ERROR_ALLOC_TREE_GROUPS:-22,BROTLI_DECODER_ERROR_ALLOC_CONTEXT_MAP:-25,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_1:-26,BROTLI_DECODER_ERROR_ALLOC_RING_BUFFER_2:-27,BROTLI_DECODER_ERROR_ALLOC_BLOCK_TYPE_TREES:-30,BROTLI_DECODER_ERROR_UNREACHABLE:-31},rRe))});var hD=w(as=>{"use strict";var oD=require("assert"),wl=require("buffer").Buffer,A_=require("zlib"),$c=as.constants=a_(),iRe=gf(),l_=wl.concat,eu=Symbol("_superWrite"),Vd=class extends Error{constructor(e){super("zlib: "+e.message);this.code=e.code,this.errno=e.errno,this.code||(this.code="ZLIB_ERROR"),this.message="zlib: "+e.message,Error.captureStackTrace(this,this.constructor)}get name(){return"ZlibError"}},nRe=Symbol("opts"),Xd=Symbol("flushFlag"),c_=Symbol("finishFlushFlag"),aD=Symbol("fullFlushFlag"),pr=Symbol("handle"),cB=Symbol("onError"),ff=Symbol("sawError"),AD=Symbol("level"),lD=Symbol("strategy"),cD=Symbol("ended"),kot=Symbol("_defaultFullFlush"),uD=class extends iRe{constructor(e,t){if(!e||typeof e!="object")throw new TypeError("invalid options for ZlibBase constructor");super(e);this[ff]=!1,this[cD]=!1,this[nRe]=e,this[Xd]=e.flush,this[c_]=e.finishFlush;try{this[pr]=new A_[t](e)}catch(i){throw new Vd(i)}this[cB]=i=>{this[ff]||(this[ff]=!0,this.close(),this.emit("error",i))},this[pr].on("error",i=>this[cB](new Vd(i))),this.once("end",()=>this.close)}close(){this[pr]&&(this[pr].close(),this[pr]=null,this.emit("close"))}reset(){if(!this[ff])return oD(this[pr],"zlib binding closed"),this[pr].reset()}flush(e){this.ended||(typeof e!="number"&&(e=this[aD]),this.write(Object.assign(wl.alloc(0),{[Xd]:e})))}end(e,t,i){return e&&this.write(e,t),this.flush(this[c_]),this[cD]=!0,super.end(null,null,i)}get ended(){return this[cD]}write(e,t,i){if(typeof t=="function"&&(i=t,t="utf8"),typeof e=="string"&&(e=wl.from(e,t)),this[ff])return;oD(this[pr],"zlib binding closed");let n=this[pr]._handle,s=n.close;n.close=()=>{};let o=this[pr].close;this[pr].close=()=>{},wl.concat=c=>c;let a;try{let c=typeof e[Xd]=="number"?e[Xd]:this[Xd];a=this[pr]._processChunk(e,c),wl.concat=l_}catch(c){wl.concat=l_,this[cB](new Vd(c))}finally{this[pr]&&(this[pr]._handle=n,n.close=s,this[pr].close=o,this[pr].removeAllListeners("error"))}this[pr]&&this[pr].on("error",c=>this[cB](new Vd(c)));let l;if(a)if(Array.isArray(a)&&a.length>0){l=this[eu](wl.from(a[0]));for(let c=1;c{this.flush(n),s()};try{this[pr].params(e,t)}finally{this[pr].flush=i}this[pr]&&(this[AD]=e,this[lD]=t)}}}},u_=class extends Bl{constructor(e){super(e,"Deflate")}},g_=class extends Bl{constructor(e){super(e,"Inflate")}},gD=Symbol("_portable"),f_=class extends Bl{constructor(e){super(e,"Gzip");this[gD]=e&&!!e.portable}[eu](e){return this[gD]?(this[gD]=!1,e[9]=255,super[eu](e)):super[eu](e)}},h_=class extends Bl{constructor(e){super(e,"Gunzip")}},p_=class extends Bl{constructor(e){super(e,"DeflateRaw")}},d_=class extends Bl{constructor(e){super(e,"InflateRaw")}},C_=class extends Bl{constructor(e){super(e,"Unzip")}},fD=class extends uD{constructor(e,t){e=e||{},e.flush=e.flush||$c.BROTLI_OPERATION_PROCESS,e.finishFlush=e.finishFlush||$c.BROTLI_OPERATION_FINISH,super(e,t),this[aD]=$c.BROTLI_OPERATION_FLUSH}},m_=class extends fD{constructor(e){super(e,"BrotliCompress")}},E_=class extends fD{constructor(e){super(e,"BrotliDecompress")}};as.Deflate=u_;as.Inflate=g_;as.Gzip=f_;as.Gunzip=h_;as.DeflateRaw=p_;as.InflateRaw=d_;as.Unzip=C_;typeof A_.BrotliCompress=="function"?(as.BrotliCompress=m_,as.BrotliDecompress=E_):as.BrotliCompress=as.BrotliDecompress=class{constructor(){throw new Error("Brotli is not supported in this version of Node.js")}}});var Zd=w(uB=>{"use strict";uB.name=new Map([["0","File"],["","OldFile"],["1","Link"],["2","SymbolicLink"],["3","CharacterDevice"],["4","BlockDevice"],["5","Directory"],["6","FIFO"],["7","ContiguousFile"],["g","GlobalExtendedHeader"],["x","ExtendedHeader"],["A","SolarisACL"],["D","GNUDumpDir"],["I","Inode"],["K","NextFileHasLongLinkpath"],["L","NextFileHasLongPath"],["M","ContinuationFile"],["N","OldGnuLongPath"],["S","SparseFile"],["V","TapeVolumeHeader"],["X","OldExtendedHeader"]]);uB.code=new Map(Array.from(uB.name).map(r=>[r[1],r[0]]))});var $d=w((Fot,I_)=>{"use strict";var Dot=Zd(),sRe=gf(),pD=Symbol("slurp");I_.exports=class extends sRe{constructor(e,t,i){super();switch(this.pause(),this.extended=t,this.globalExtended=i,this.header=e,this.startBlockSize=512*Math.ceil(e.size/512),this.blockRemain=this.startBlockSize,this.remain=e.size,this.type=e.type,this.meta=!1,this.ignore=!1,this.type){case"File":case"OldFile":case"Link":case"SymbolicLink":case"CharacterDevice":case"BlockDevice":case"Directory":case"FIFO":case"ContiguousFile":case"GNUDumpDir":break;case"NextFileHasLongLinkpath":case"NextFileHasLongPath":case"OldGnuLongPath":case"GlobalExtendedHeader":case"ExtendedHeader":case"OldExtendedHeader":this.meta=!0;break;default:this.ignore=!0}this.path=e.path,this.mode=e.mode,this.mode&&(this.mode=this.mode&4095),this.uid=e.uid,this.gid=e.gid,this.uname=e.uname,this.gname=e.gname,this.size=e.size,this.mtime=e.mtime,this.atime=e.atime,this.ctime=e.ctime,this.linkpath=e.linkpath,this.uname=e.uname,this.gname=e.gname,t&&this[pD](t),i&&this[pD](i,!0)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");let i=this.remain,n=this.blockRemain;return this.remain=Math.max(0,i-t),this.blockRemain=Math.max(0,n-t),this.ignore?!0:i>=t?super.write(e):super.write(e.slice(0,i))}[pD](e,t){for(let i in e)e[i]!==null&&e[i]!==void 0&&!(t&&i==="path")&&(this[i]=e[i])}}});var B_=w(dD=>{"use strict";var Not=dD.encode=(r,e)=>{if(Number.isSafeInteger(r))r<0?aRe(r,e):oRe(r,e);else throw Error("cannot encode number outside of javascript safe integer range");return e},oRe=(r,e)=>{e[0]=128;for(var t=e.length;t>1;t--)e[t-1]=r&255,r=Math.floor(r/256)},aRe=(r,e)=>{e[0]=255;var t=!1;r=r*-1;for(var i=e.length;i>1;i--){var n=r&255;r=Math.floor(r/256),t?e[i-1]=y_(n):n===0?e[i-1]=0:(t=!0,e[i-1]=w_(n))}},Lot=dD.parse=r=>{var e=r[r.length-1],t=r[0],i;if(t===128)i=lRe(r.slice(1,r.length));else if(t===255)i=ARe(r);else throw Error("invalid base256 encoding");if(!Number.isSafeInteger(i))throw Error("parsed number outside of javascript safe integer range");return i},ARe=r=>{for(var e=r.length,t=0,i=!1,n=e-1;n>-1;n--){var s=r[n],o;i?o=y_(s):s===0?o=s:(i=!0,o=w_(s)),o!==0&&(t-=o*Math.pow(256,e-n-1))}return t},lRe=r=>{for(var e=r.length,t=0,i=e-1;i>-1;i--){var n=r[i];n!==0&&(t+=n*Math.pow(256,e-i-1))}return t},y_=r=>(255^r)&255,w_=r=>(255^r)+1&255});var pf=w((Oot,b_)=>{"use strict";var CD=Zd(),hf=require("path").posix,Q_=B_(),mD=Symbol("slurp"),As=Symbol("type"),S_=class{constructor(e,t,i,n){this.cksumValid=!1,this.needPax=!1,this.nullBlock=!1,this.block=null,this.path=null,this.mode=null,this.uid=null,this.gid=null,this.size=null,this.mtime=null,this.cksum=null,this[As]="0",this.linkpath=null,this.uname=null,this.gname=null,this.devmaj=0,this.devmin=0,this.atime=null,this.ctime=null,Buffer.isBuffer(e)?this.decode(e,t||0,i,n):e&&this.set(e)}decode(e,t,i,n){if(t||(t=0),!e||!(e.length>=t+512))throw new Error("need 512 bytes for header");if(this.path=tu(e,t,100),this.mode=bl(e,t+100,8),this.uid=bl(e,t+108,8),this.gid=bl(e,t+116,8),this.size=bl(e,t+124,12),this.mtime=ED(e,t+136,12),this.cksum=bl(e,t+148,12),this[mD](i),this[mD](n,!0),this[As]=tu(e,t+156,1),this[As]===""&&(this[As]="0"),this[As]==="0"&&this.path.substr(-1)==="/"&&(this[As]="5"),this[As]==="5"&&(this.size=0),this.linkpath=tu(e,t+157,100),e.slice(t+257,t+265).toString()==="ustar\x0000")if(this.uname=tu(e,t+265,32),this.gname=tu(e,t+297,32),this.devmaj=bl(e,t+329,8),this.devmin=bl(e,t+337,8),e[t+475]!==0){let o=tu(e,t+345,155);this.path=o+"/"+this.path}else{let o=tu(e,t+345,130);o&&(this.path=o+"/"+this.path),this.atime=ED(e,t+476,12),this.ctime=ED(e,t+488,12)}let s=8*32;for(let o=t;o=t+512))throw new Error("need 512 bytes for header");let i=this.ctime||this.atime?130:155,n=cRe(this.path||"",i),s=n[0],o=n[1];this.needPax=n[2],this.needPax=ru(e,t,100,s)||this.needPax,this.needPax=Ql(e,t+100,8,this.mode)||this.needPax,this.needPax=Ql(e,t+108,8,this.uid)||this.needPax,this.needPax=Ql(e,t+116,8,this.gid)||this.needPax,this.needPax=Ql(e,t+124,12,this.size)||this.needPax,this.needPax=ID(e,t+136,12,this.mtime)||this.needPax,e[t+156]=this[As].charCodeAt(0),this.needPax=ru(e,t+157,100,this.linkpath)||this.needPax,e.write("ustar\x0000",t+257,8),this.needPax=ru(e,t+265,32,this.uname)||this.needPax,this.needPax=ru(e,t+297,32,this.gname)||this.needPax,this.needPax=Ql(e,t+329,8,this.devmaj)||this.needPax,this.needPax=Ql(e,t+337,8,this.devmin)||this.needPax,this.needPax=ru(e,t+345,i,o)||this.needPax,e[t+475]!==0?this.needPax=ru(e,t+345,155,o)||this.needPax:(this.needPax=ru(e,t+345,130,o)||this.needPax,this.needPax=ID(e,t+476,12,this.atime)||this.needPax,this.needPax=ID(e,t+488,12,this.ctime)||this.needPax);let a=8*32;for(let l=t;l{let t=100,i=r,n="",s,o=hf.parse(r).root||".";if(Buffer.byteLength(i)t&&Buffer.byteLength(n)<=e?s=[i.substr(0,t-1),n,!0]:(i=hf.join(hf.basename(n),i),n=hf.dirname(n));while(n!==o&&!s);s||(s=[r.substr(0,t-1),"",!0])}return s},tu=(r,e,t)=>r.slice(e,e+t).toString("utf8").replace(/\0.*/,""),ED=(r,e,t)=>uRe(bl(r,e,t)),uRe=r=>r===null?null:new Date(r*1e3),bl=(r,e,t)=>r[e]&128?Q_.parse(r.slice(e,e+t)):gRe(r,e,t),fRe=r=>isNaN(r)?null:r,gRe=(r,e,t)=>fRe(parseInt(r.slice(e,e+t).toString("utf8").replace(/\0.*$/,"").trim(),8)),hRe={12:8589934591,8:2097151},Ql=(r,e,t,i)=>i===null?!1:i>hRe[t]||i<0?(Q_.encode(i,r.slice(e,e+t)),!0):(pRe(r,e,t,i),!1),pRe=(r,e,t,i)=>r.write(dRe(i,t),e,t,"ascii"),dRe=(r,e)=>CRe(Math.floor(r).toString(8),e),CRe=(r,e)=>(r.length===e-1?r:new Array(e-r.length-1).join("0")+r+" ")+"\0",ID=(r,e,t,i)=>i===null?!1:Ql(r,e,t,i.getTime()/1e3),mRe=new Array(156).join("\0"),ru=(r,e,t,i)=>i===null?!1:(r.write(i+mRe,e,t,"utf8"),i.length!==Buffer.byteLength(i)||i.length>t);b_.exports=S_});var fB=w((Mot,v_)=>{"use strict";var ERe=pf(),IRe=require("path"),gB=class{constructor(e,t){this.atime=e.atime||null,this.charset=e.charset||null,this.comment=e.comment||null,this.ctime=e.ctime||null,this.gid=e.gid||null,this.gname=e.gname||null,this.linkpath=e.linkpath||null,this.mtime=e.mtime||null,this.path=e.path||null,this.size=e.size||null,this.uid=e.uid||null,this.uname=e.uname||null,this.dev=e.dev||null,this.ino=e.ino||null,this.nlink=e.nlink||null,this.global=t||!1}encode(){let e=this.encodeBody();if(e==="")return null;let t=Buffer.byteLength(e),i=512*Math.ceil(1+t/512),n=Buffer.allocUnsafe(i);for(let s=0;s<512;s++)n[s]=0;new ERe({path:("PaxHeader/"+IRe.basename(this.path)).slice(0,99),mode:this.mode||420,uid:this.uid||null,gid:this.gid||null,size:t,mtime:this.mtime||null,type:this.global?"GlobalExtendedHeader":"ExtendedHeader",linkpath:"",uname:this.uname||"",gname:this.gname||"",devmaj:0,devmin:0,atime:this.atime||null,ctime:this.ctime||null}).encode(n),n.write(e,512,t,"utf8");for(let s=t+512;s=Math.pow(10,s)&&(s+=1),s+n+i}};gB.parse=(r,e,t)=>new gB(yRe(wRe(r),e),t);var yRe=(r,e)=>e?Object.keys(r).reduce((t,i)=>(t[i]=r[i],t),e):r,wRe=r=>r.replace(/\n$/,"").split(` +`).reduce(BRe,Object.create(null)),BRe=(r,e)=>{let t=parseInt(e,10);if(t!==Buffer.byteLength(e)+1)return r;e=e.substr((t+" ").length);let i=e.split("="),n=i.shift().replace(/^SCHILY\.(dev|ino|nlink)/,"$1");if(!n)return r;let s=i.join("=");return r[n]=/^([A-Z]+\.)?([mac]|birth|creation)time$/.test(n)?new Date(s*1e3):/^[0-9]+$/.test(s)?+s:s,r};v_.exports=gB});var hB=w((Uot,k_)=>{"use strict";k_.exports=r=>class extends r{warn(e,t,i={}){this.file&&(i.file=this.file),this.cwd&&(i.cwd=this.cwd),i.code=t instanceof Error&&t.code||e,i.tarCode=e,!this.strict&&i.recoverable!==!1?(t instanceof Error&&(i=Object.assign(t,i),t=t.message),this.emit("warn",i.tarCode,t,i)):t instanceof Error?this.emit("error",Object.assign(t,i)):this.emit("error",Object.assign(new Error(`${e}: ${t}`),i))}}});var wD=w((Kot,x_)=>{"use strict";var pB=["|","<",">","?",":"],yD=pB.map(r=>String.fromCharCode(61440+r.charCodeAt(0))),bRe=new Map(pB.map((r,e)=>[r,yD[e]])),QRe=new Map(yD.map((r,e)=>[r,pB[e]]));x_.exports={encode:r=>pB.reduce((e,t)=>e.split(t).join(bRe.get(t)),r),decode:r=>yD.reduce((e,t)=>e.split(t).join(QRe.get(t)),r)}});var D_=w((Hot,P_)=>{"use strict";P_.exports=(r,e,t)=>(r&=4095,t&&(r=(r|384)&~18),e&&(r&256&&(r|=64),r&32&&(r|=8),r&4&&(r|=1)),r)});var xD=w((Jot,R_)=>{"use strict";var F_=gf(),N_=fB(),L_=pf(),jot=$d(),oa=require("fs"),df=require("path"),Got=Zd(),SRe=16*1024*1024,T_=Symbol("process"),O_=Symbol("file"),M_=Symbol("directory"),BD=Symbol("symlink"),U_=Symbol("hardlink"),eC=Symbol("header"),dB=Symbol("read"),bD=Symbol("lstat"),CB=Symbol("onlstat"),QD=Symbol("onread"),SD=Symbol("onreadlink"),vD=Symbol("openfile"),kD=Symbol("onopenfile"),iu=Symbol("close"),mB=Symbol("mode"),K_=hB(),vRe=wD(),H_=D_(),EB=K_(class extends F_{constructor(e,t){if(t=t||{},super(t),typeof e!="string")throw new TypeError("path is required");this.path=e,this.portable=!!t.portable,this.myuid=process.getuid&&process.getuid(),this.myuser=process.env.USER||"",this.maxReadSize=t.maxReadSize||SRe,this.linkCache=t.linkCache||new Map,this.statCache=t.statCache||new Map,this.preservePaths=!!t.preservePaths,this.cwd=t.cwd||process.cwd(),this.strict=!!t.strict,this.noPax=!!t.noPax,this.noMtime=!!t.noMtime,this.mtime=t.mtime||null,typeof t.onwarn=="function"&&this.on("warn",t.onwarn);let i=!1;if(!this.preservePaths&&df.win32.isAbsolute(e)){let n=df.win32.parse(e);this.path=e.substr(n.root.length),i=n.root}this.win32=!!t.win32||process.platform==="win32",this.win32&&(this.path=vRe.decode(this.path.replace(/\\/g,"/")),e=e.replace(/\\/g,"/")),this.absolute=t.absolute||df.resolve(this.cwd,e),this.path===""&&(this.path="./"),i&&this.warn("TAR_ENTRY_INFO",`stripping ${i} from absolute path`,{entry:this,path:i+this.path}),this.statCache.has(this.absolute)?this[CB](this.statCache.get(this.absolute)):this[bD]()}[bD](){oa.lstat(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[CB](t)})}[CB](e){this.statCache.set(this.absolute,e),this.stat=e,e.isFile()||(e.size=0),this.type=kRe(e),this.emit("stat",e),this[T_]()}[T_](){switch(this.type){case"File":return this[O_]();case"Directory":return this[M_]();case"SymbolicLink":return this[BD]();default:return this.end()}}[mB](e){return H_(e,this.type==="Directory",this.portable)}[eC](){this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.header=new L_({path:this.path,linkpath:this.linkpath,mode:this[mB](this.stat.mode),uid:this.portable?null:this.stat.uid,gid:this.portable?null:this.stat.gid,size:this.stat.size,mtime:this.noMtime?null:this.mtime||this.stat.mtime,type:this.type,uname:this.portable?null:this.stat.uid===this.myuid?this.myuser:"",atime:this.portable?null:this.stat.atime,ctime:this.portable?null:this.stat.ctime}),this.header.encode()&&!this.noPax&&this.write(new N_({atime:this.portable?null:this.header.atime,ctime:this.portable?null:this.header.ctime,gid:this.portable?null:this.header.gid,mtime:this.noMtime?null:this.mtime||this.header.mtime,path:this.path,linkpath:this.linkpath,size:this.header.size,uid:this.portable?null:this.header.uid,uname:this.portable?null:this.header.uname,dev:this.portable?null:this.stat.dev,ino:this.portable?null:this.stat.ino,nlink:this.portable?null:this.stat.nlink}).encode()),this.write(this.header.block)}[M_](){this.path.substr(-1)!=="/"&&(this.path+="/"),this.stat.size=0,this[eC](),this.end()}[BD](){oa.readlink(this.absolute,(e,t)=>{if(e)return this.emit("error",e);this[SD](t)})}[SD](e){this.linkpath=e.replace(/\\/g,"/"),this[eC](),this.end()}[U_](e){this.type="Link",this.linkpath=df.relative(this.cwd,e).replace(/\\/g,"/"),this.stat.size=0,this[eC](),this.end()}[O_](){if(this.stat.nlink>1){let e=this.stat.dev+":"+this.stat.ino;if(this.linkCache.has(e)){let t=this.linkCache.get(e);if(t.indexOf(this.cwd)===0)return this[U_](t)}this.linkCache.set(e,this.absolute)}if(this[eC](),this.stat.size===0)return this.end();this[vD]()}[vD](){oa.open(this.absolute,"r",(e,t)=>{if(e)return this.emit("error",e);this[kD](t)})}[kD](e){let t=512*Math.ceil(this.stat.size/512),i=Math.min(t,this.maxReadSize),n=Buffer.allocUnsafe(i);this[dB](e,n,0,n.length,0,this.stat.size,t)}[dB](e,t,i,n,s,o,a){oa.read(e,t,i,n,s,(l,c)=>{if(l)return this[iu](e,()=>this.emit("error",l));this[QD](e,t,i,n,s,o,a,c)})}[iu](e,t){oa.close(e,t)}[QD](e,t,i,n,s,o,a,l){if(l<=0&&o>0){let u=new Error("encountered unexpected EOF");return u.path=this.absolute,u.syscall="read",u.code="EOF",this[iu](e,()=>this.emit("error",u))}if(l>o){let u=new Error("did not encounter expected EOF");return u.path=this.absolute,u.syscall="read",u.code="EOF",this[iu](e,()=>this.emit("error",u))}if(l===o)for(let u=l;uu?this.emit("error",u):this.end());i>=n&&(t=Buffer.allocUnsafe(n),i=0),n=t.length-i,this[dB](e,t,i,n,s,o,a)}}),j_=class extends EB{constructor(e,t){super(e,t)}[bD](){this[CB](oa.lstatSync(this.absolute))}[BD](){this[SD](oa.readlinkSync(this.absolute))}[vD](){this[kD](oa.openSync(this.absolute,"r"))}[dB](e,t,i,n,s,o,a){let l=!0;try{let c=oa.readSync(e,t,i,n,s);this[QD](e,t,i,n,s,o,a,c),l=!1}finally{if(l)try{this[iu](e,()=>{})}catch(c){}}}[iu](e,t){oa.closeSync(e),t()}},xRe=K_(class extends F_{constructor(e,t){t=t||{},super(t),this.preservePaths=!!t.preservePaths,this.portable=!!t.portable,this.strict=!!t.strict,this.noPax=!!t.noPax,this.noMtime=!!t.noMtime,this.readEntry=e,this.type=e.type,this.type==="Directory"&&this.portable&&(this.noMtime=!0),this.path=e.path,this.mode=this[mB](e.mode),this.uid=this.portable?null:e.uid,this.gid=this.portable?null:e.gid,this.uname=this.portable?null:e.uname,this.gname=this.portable?null:e.gname,this.size=e.size,this.mtime=this.noMtime?null:t.mtime||e.mtime,this.atime=this.portable?null:e.atime,this.ctime=this.portable?null:e.ctime,this.linkpath=e.linkpath,typeof t.onwarn=="function"&&this.on("warn",t.onwarn);let i=!1;if(df.isAbsolute(this.path)&&!this.preservePaths){let n=df.parse(this.path);i=n.root,this.path=this.path.substr(n.root.length)}this.remain=e.size,this.blockRemain=e.startBlockSize,this.header=new L_({path:this.path,linkpath:this.linkpath,mode:this.mode,uid:this.portable?null:this.uid,gid:this.portable?null:this.gid,size:this.size,mtime:this.noMtime?null:this.mtime,type:this.type,uname:this.portable?null:this.uname,atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime}),i&&this.warn("TAR_ENTRY_INFO",`stripping ${i} from absolute path`,{entry:this,path:i+this.path}),this.header.encode()&&!this.noPax&&super.write(new N_({atime:this.portable?null:this.atime,ctime:this.portable?null:this.ctime,gid:this.portable?null:this.gid,mtime:this.noMtime?null:this.mtime,path:this.path,linkpath:this.linkpath,size:this.size,uid:this.portable?null:this.uid,uname:this.portable?null:this.uname,dev:this.portable?null:this.readEntry.dev,ino:this.portable?null:this.readEntry.ino,nlink:this.portable?null:this.readEntry.nlink}).encode()),super.write(this.header.block),e.pipe(this)}[mB](e){return H_(e,this.type==="Directory",this.portable)}write(e){let t=e.length;if(t>this.blockRemain)throw new Error("writing more to entry than is appropriate");return this.blockRemain-=t,super.write(e)}end(){return this.blockRemain&&this.write(Buffer.alloc(this.blockRemain)),super.end()}});EB.Sync=j_;EB.Tar=xRe;var kRe=r=>r.isFile()?"File":r.isDirectory()?"Directory":r.isSymbolicLink()?"SymbolicLink":"Unsupported";R_.exports=EB});var vB=w((zot,G_)=>{"use strict";var PD=class{constructor(e,t){this.path=e||"./",this.absolute=t,this.entry=null,this.stat=null,this.readdir=null,this.pending=!1,this.ignore=!1,this.piped=!1}},PRe=gf(),DRe=hD(),RRe=$d(),DD=xD(),FRe=DD.Sync,NRe=DD.Tar,LRe=bp(),Y_=Buffer.alloc(1024),IB=Symbol("onStat"),yB=Symbol("ended"),aa=Symbol("queue"),Cf=Symbol("current"),nu=Symbol("process"),wB=Symbol("processing"),q_=Symbol("processJob"),Aa=Symbol("jobs"),RD=Symbol("jobDone"),BB=Symbol("addFSEntry"),J_=Symbol("addTarEntry"),FD=Symbol("stat"),ND=Symbol("readdir"),bB=Symbol("onreaddir"),QB=Symbol("pipe"),W_=Symbol("entry"),LD=Symbol("entryOpt"),TD=Symbol("writeEntryClass"),z_=Symbol("write"),OD=Symbol("ondrain"),SB=require("fs"),__=require("path"),TRe=hB(),MD=TRe(class extends PRe{constructor(e){super(e);e=e||Object.create(null),this.opt=e,this.file=e.file||"",this.cwd=e.cwd||process.cwd(),this.maxReadSize=e.maxReadSize,this.preservePaths=!!e.preservePaths,this.strict=!!e.strict,this.noPax=!!e.noPax,this.prefix=(e.prefix||"").replace(/(\\|\/)+$/,""),this.linkCache=e.linkCache||new Map,this.statCache=e.statCache||new Map,this.readdirCache=e.readdirCache||new Map,this[TD]=DD,typeof e.onwarn=="function"&&this.on("warn",e.onwarn),this.portable=!!e.portable,this.zip=null,e.gzip?(typeof e.gzip!="object"&&(e.gzip={}),this.portable&&(e.gzip.portable=!0),this.zip=new DRe.Gzip(e.gzip),this.zip.on("data",t=>super.write(t)),this.zip.on("end",t=>super.end()),this.zip.on("drain",t=>this[OD]()),this.on("resume",t=>this.zip.resume())):this.on("drain",this[OD]),this.noDirRecurse=!!e.noDirRecurse,this.follow=!!e.follow,this.noMtime=!!e.noMtime,this.mtime=e.mtime||null,this.filter=typeof e.filter=="function"?e.filter:t=>!0,this[aa]=new LRe,this[Aa]=0,this.jobs=+e.jobs||4,this[wB]=!1,this[yB]=!1}[z_](e){return super.write(e)}add(e){return this.write(e),this}end(e){return e&&this.write(e),this[yB]=!0,this[nu](),this}write(e){if(this[yB])throw new Error("write after end");return e instanceof RRe?this[J_](e):this[BB](e),this.flowing}[J_](e){let t=__.resolve(this.cwd,e.path);if(this.prefix&&(e.path=this.prefix+"/"+e.path.replace(/^\.(\/+|$)/,"")),!this.filter(e.path,e))e.resume();else{let i=new PD(e.path,t,!1);i.entry=new NRe(e,this[LD](i)),i.entry.on("end",n=>this[RD](i)),this[Aa]+=1,this[aa].push(i)}this[nu]()}[BB](e){let t=__.resolve(this.cwd,e);this.prefix&&(e=this.prefix+"/"+e.replace(/^\.(\/+|$)/,"")),this[aa].push(new PD(e,t)),this[nu]()}[FD](e){e.pending=!0,this[Aa]+=1;let t=this.follow?"stat":"lstat";SB[t](e.absolute,(i,n)=>{e.pending=!1,this[Aa]-=1,i?this.emit("error",i):this[IB](e,n)})}[IB](e,t){this.statCache.set(e.absolute,t),e.stat=t,this.filter(e.path,t)||(e.ignore=!0),this[nu]()}[ND](e){e.pending=!0,this[Aa]+=1,SB.readdir(e.absolute,(t,i)=>{if(e.pending=!1,this[Aa]-=1,t)return this.emit("error",t);this[bB](e,i)})}[bB](e,t){this.readdirCache.set(e.absolute,t),e.readdir=t,this[nu]()}[nu](){if(!this[wB]){this[wB]=!0;for(let e=this[aa].head;e!==null&&this[Aa]this.warn(t,i,n),noPax:this.noPax,cwd:this.cwd,absolute:e.absolute,preservePaths:this.preservePaths,maxReadSize:this.maxReadSize,strict:this.strict,portable:this.portable,linkCache:this.linkCache,statCache:this.statCache,noMtime:this.noMtime,mtime:this.mtime}}[W_](e){this[Aa]+=1;try{return new this[TD](e.path,this[LD](e)).on("end",()=>this[RD](e)).on("error",t=>this.emit("error",t))}catch(t){this.emit("error",t)}}[OD](){this[Cf]&&this[Cf].entry&&this[Cf].entry.resume()}[QB](e){e.piped=!0,e.readdir&&e.readdir.forEach(n=>{let s=this.prefix?e.path.slice(this.prefix.length+1)||"./":e.path,o=s==="./"?"":s.replace(/\/*$/,"/");this[BB](o+n)});let t=e.entry,i=this.zip;i?t.on("data",n=>{i.write(n)||t.pause()}):t.on("data",n=>{super.write(n)||t.pause()})}pause(){return this.zip&&this.zip.pause(),super.pause()}}),V_=class extends MD{constructor(e){super(e);this[TD]=FRe}pause(){}resume(){}[FD](e){let t=this.follow?"statSync":"lstatSync";this[IB](e,SB[t](e.absolute))}[ND](e,t){this[bB](e,SB.readdirSync(e.absolute))}[QB](e){let t=e.entry,i=this.zip;e.readdir&&e.readdir.forEach(n=>{let s=this.prefix?e.path.slice(this.prefix.length+1)||"./":e.path,o=s==="./"?"":s.replace(/\/*$/,"/");this[BB](o+n)}),i?t.on("data",n=>{i.write(n)}):t.on("data",n=>{super[z_](n)})}};MD.Sync=V_;G_.exports=MD});var Bf=w(tC=>{"use strict";var ORe=gf(),MRe=require("events").EventEmitter,js=require("fs"),kB=process.binding("fs"),_ot=kB.writeBuffers,URe=kB.FSReqWrap||kB.FSReqCallback,mf=Symbol("_autoClose"),la=Symbol("_close"),rC=Symbol("_ended"),or=Symbol("_fd"),X_=Symbol("_finished"),su=Symbol("_flags"),UD=Symbol("_flush"),KD=Symbol("_handleChunk"),HD=Symbol("_makeBuf"),jD=Symbol("_mode"),xB=Symbol("_needDrain"),Ef=Symbol("_onerror"),If=Symbol("_onopen"),GD=Symbol("_onread"),ou=Symbol("_onwrite"),Sl=Symbol("_open"),vl=Symbol("_path"),au=Symbol("_pos"),ca=Symbol("_queue"),yf=Symbol("_read"),Z_=Symbol("_readSize"),kl=Symbol("_reading"),PB=Symbol("_remain"),$_=Symbol("_size"),DB=Symbol("_write"),wf=Symbol("_writing"),RB=Symbol("_defaultFlag"),YD=class extends ORe{constructor(e,t){if(t=t||{},super(t),this.writable=!1,typeof e!="string")throw new TypeError("path must be a string");this[or]=typeof t.fd=="number"?t.fd:null,this[vl]=e,this[Z_]=t.readSize||16*1024*1024,this[kl]=!1,this[$_]=typeof t.size=="number"?t.size:Infinity,this[PB]=this[$_],this[mf]=typeof t.autoClose=="boolean"?t.autoClose:!0,typeof this[or]=="number"?this[yf]():this[Sl]()}get fd(){return this[or]}get path(){return this[vl]}write(){throw new TypeError("this is a readable stream")}end(){throw new TypeError("this is a readable stream")}[Sl](){js.open(this[vl],"r",(e,t)=>this[If](e,t))}[If](e,t){e?this[Ef](e):(this[or]=t,this.emit("open",t),this[yf]())}[HD](){return Buffer.allocUnsafe(Math.min(this[Z_],this[PB]))}[yf](){if(!this[kl]){this[kl]=!0;let e=this[HD]();if(e.length===0)return process.nextTick(()=>this[GD](null,0,e));js.read(this[or],e,0,e.length,null,(t,i,n)=>this[GD](t,i,n))}}[GD](e,t,i){this[kl]=!1,e?this[Ef](e):this[KD](t,i)&&this[yf]()}[la](){this[mf]&&typeof this[or]=="number"&&(js.close(this[or],e=>this.emit("close")),this[or]=null)}[Ef](e){this[kl]=!0,this[la](),this.emit("error",e)}[KD](e,t){let i=!1;return this[PB]-=e,e>0&&(i=super.write(ethis[If](e,t))}[If](e,t){this[RB]&&this[su]==="r+"&&e&&e.code==="ENOENT"?(this[su]="w",this[Sl]()):e?this[Ef](e):(this[or]=t,this.emit("open",t),this[UD]())}end(e,t){e&&this.write(e,t),this[rC]=!0,!this[wf]&&!this[ca].length&&typeof this[or]=="number"&&this[ou](null,0)}write(e,t){return typeof e=="string"&&(e=new Buffer(e,t)),this[rC]?(this.emit("error",new Error("write() after end()")),!1):this[or]===null||this[wf]||this[ca].length?(this[ca].push(e),this[xB]=!0,!1):(this[wf]=!0,this[DB](e),!0)}[DB](e){js.write(this[or],e,0,e.length,this[au],(t,i)=>this[ou](t,i))}[ou](e,t){e?this[Ef](e):(this[au]!==null&&(this[au]+=t),this[ca].length?this[UD]():(this[wf]=!1,this[rC]&&!this[X_]?(this[X_]=!0,this[la](),this.emit("finish")):this[xB]&&(this[xB]=!1,this.emit("drain"))))}[UD](){if(this[ca].length===0)this[rC]&&this[ou](null,0);else if(this[ca].length===1)this[DB](this[ca].pop());else{let e=this[ca];this[ca]=[],KRe(this[or],e,this[au],(t,i)=>this[ou](t,i))}}[la](){this[mf]&&typeof this[or]=="number"&&(js.close(this[or],e=>this.emit("close")),this[or]=null)}},tV=class extends qD{[Sl](){let e;try{e=js.openSync(this[vl],this[su],this[jD])}catch(t){if(this[RB]&&this[su]==="r+"&&t&&t.code==="ENOENT")return this[su]="w",this[Sl]();throw t}this[If](null,e)}[la](){if(this[mf]&&typeof this[or]=="number"){try{js.closeSync(this[or])}catch(e){}this[or]=null,this.emit("close")}}[DB](e){try{this[ou](null,js.writeSync(this[or],e,0,e.length,this[au]))}catch(t){this[ou](t,0)}}},KRe=(r,e,t,i)=>{let n=(o,a)=>i(o,a,e),s=new URe;s.oncomplete=n,kB.writeBuffers(r,e,t,s)};tC.ReadStream=YD;tC.ReadStreamSync=eV;tC.WriteStream=qD;tC.WriteStreamSync=tV});var sC=w(($ot,rV)=>{"use strict";var HRe=hB(),Xot=require("path"),jRe=pf(),GRe=require("events"),YRe=bp(),qRe=1024*1024,JRe=$d(),iV=fB(),WRe=hD(),JD=Buffer.from([31,139]),Gs=Symbol("state"),Au=Symbol("writeEntry"),gA=Symbol("readEntry"),WD=Symbol("nextEntry"),nV=Symbol("processEntry"),Ys=Symbol("extendedHeader"),iC=Symbol("globalExtendedHeader"),xl=Symbol("meta"),sV=Symbol("emitMeta"),yr=Symbol("buffer"),fA=Symbol("queue"),lu=Symbol("ended"),oV=Symbol("emittedEnd"),cu=Symbol("emit"),Tn=Symbol("unzip"),FB=Symbol("consumeChunk"),NB=Symbol("consumeChunkSub"),zD=Symbol("consumeBody"),aV=Symbol("consumeMeta"),AV=Symbol("consumeHeader"),LB=Symbol("consuming"),_D=Symbol("bufferConcat"),VD=Symbol("maybeEnd"),nC=Symbol("writing"),Pl=Symbol("aborted"),TB=Symbol("onDone"),uu=Symbol("sawValidEntry"),OB=Symbol("sawNullBlock"),MB=Symbol("sawEOF"),zRe=r=>!0;rV.exports=HRe(class extends GRe{constructor(e){e=e||{},super(e),this.file=e.file||"",this[uu]=null,this.on(TB,t=>{(this[Gs]==="begin"||this[uu]===!1)&&this.warn("TAR_BAD_ARCHIVE","Unrecognized archive format")}),e.ondone?this.on(TB,e.ondone):this.on(TB,t=>{this.emit("prefinish"),this.emit("finish"),this.emit("end"),this.emit("close")}),this.strict=!!e.strict,this.maxMetaEntrySize=e.maxMetaEntrySize||qRe,this.filter=typeof e.filter=="function"?e.filter:zRe,this.writable=!0,this.readable=!1,this[fA]=new YRe,this[yr]=null,this[gA]=null,this[Au]=null,this[Gs]="begin",this[xl]="",this[Ys]=null,this[iC]=null,this[lu]=!1,this[Tn]=null,this[Pl]=!1,this[OB]=!1,this[MB]=!1,typeof e.onwarn=="function"&&this.on("warn",e.onwarn),typeof e.onentry=="function"&&this.on("entry",e.onentry)}[AV](e,t){this[uu]===null&&(this[uu]=!1);let i;try{i=new jRe(e,t,this[Ys],this[iC])}catch(n){return this.warn("TAR_ENTRY_INVALID",n)}if(i.nullBlock)this[OB]?(this[MB]=!0,this[Gs]==="begin"&&(this[Gs]="header"),this[cu]("eof")):(this[OB]=!0,this[cu]("nullBlock"));else if(this[OB]=!1,!i.cksumValid)this.warn("TAR_ENTRY_INVALID","checksum failure",{header:i});else if(!i.path)this.warn("TAR_ENTRY_INVALID","path is required",{header:i});else{let n=i.type;if(/^(Symbolic)?Link$/.test(n)&&!i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath required",{header:i});else if(!/^(Symbolic)?Link$/.test(n)&&i.linkpath)this.warn("TAR_ENTRY_INVALID","linkpath forbidden",{header:i});else{let s=this[Au]=new JRe(i,this[Ys],this[iC]);if(!this[uu])if(s.remain){let o=()=>{s.invalid||(this[uu]=!0)};s.on("end",o)}else this[uu]=!0;s.meta?s.size>this.maxMetaEntrySize?(s.ignore=!0,this[cu]("ignoredEntry",s),this[Gs]="ignore",s.resume()):s.size>0&&(this[xl]="",s.on("data",o=>this[xl]+=o),this[Gs]="meta"):(this[Ys]=null,s.ignore=s.ignore||!this.filter(s.path,s),s.ignore?(this[cu]("ignoredEntry",s),this[Gs]=s.remain?"ignore":"header",s.resume()):(s.remain?this[Gs]="body":(this[Gs]="header",s.end()),this[gA]?this[fA].push(s):(this[fA].push(s),this[WD]())))}}}[nV](e){let t=!0;return e?Array.isArray(e)?this.emit.apply(this,e):(this[gA]=e,this.emit("entry",e),e.emittedEnd||(e.on("end",i=>this[WD]()),t=!1)):(this[gA]=null,t=!1),t}[WD](){do;while(this[nV](this[fA].shift()));if(!this[fA].length){let e=this[gA];!e||e.flowing||e.size===e.remain?this[nC]||this.emit("drain"):e.once("drain",i=>this.emit("drain"))}}[zD](e,t){let i=this[Au],n=i.blockRemain,s=n>=e.length&&t===0?e:e.slice(t,t+n);return i.write(s),i.blockRemain||(this[Gs]="header",this[Au]=null,i.end()),s.length}[aV](e,t){let i=this[Au],n=this[zD](e,t);return this[Au]||this[sV](i),n}[cu](e,t,i){!this[fA].length&&!this[gA]?this.emit(e,t,i):this[fA].push([e,t,i])}[sV](e){switch(this[cu]("meta",this[xl]),e.type){case"ExtendedHeader":case"OldExtendedHeader":this[Ys]=iV.parse(this[xl],this[Ys],!1);break;case"GlobalExtendedHeader":this[iC]=iV.parse(this[xl],this[iC],!0);break;case"NextFileHasLongPath":case"OldGnuLongPath":this[Ys]=this[Ys]||Object.create(null),this[Ys].path=this[xl].replace(/\0.*/,"");break;case"NextFileHasLongLinkpath":this[Ys]=this[Ys]||Object.create(null),this[Ys].linkpath=this[xl].replace(/\0.*/,"");break;default:throw new Error("unknown meta: "+e.type)}}abort(e){this[Pl]=!0,this.emit("abort",e),this.warn("TAR_ABORT",e,{recoverable:!1})}write(e){if(this[Pl])return;if(this[Tn]===null&&e){if(this[yr]&&(e=Buffer.concat([this[yr],e]),this[yr]=null),e.lengththis[FB](s)),this[Tn].on("error",s=>this.abort(s)),this[Tn].on("end",s=>{this[lu]=!0,this[FB]()}),this[nC]=!0;let n=this[Tn][i?"end":"write"](e);return this[nC]=!1,n}}this[nC]=!0,this[Tn]?this[Tn].write(e):this[FB](e),this[nC]=!1;let t=this[fA].length?!1:this[gA]?this[gA].flowing:!0;return!t&&!this[fA].length&&this[gA].once("drain",i=>this.emit("drain")),t}[_D](e){e&&!this[Pl]&&(this[yr]=this[yr]?Buffer.concat([this[yr],e]):e)}[VD](){if(this[lu]&&!this[oV]&&!this[Pl]&&!this[LB]){this[oV]=!0;let e=this[Au];if(e&&e.blockRemain){let t=this[yr]?this[yr].length:0;this.warn("TAR_BAD_ARCHIVE",`Truncated input (needed ${e.blockRemain} more bytes, only ${t} available)`,{entry:e}),this[yr]&&e.write(this[yr]),e.end()}this[cu](TB)}}[FB](e){if(this[LB])this[_D](e);else if(!e&&!this[yr])this[VD]();else{if(this[LB]=!0,this[yr]){this[_D](e);let t=this[yr];this[yr]=null,this[NB](t)}else this[NB](e);for(;this[yr]&&this[yr].length>=512&&!this[Pl]&&!this[MB];){let t=this[yr];this[yr]=null,this[NB](t)}this[LB]=!1}(!this[yr]||this[lu])&&this[VD]()}[NB](e){let t=0,i=e.length;for(;t+512<=i&&!this[Pl]&&!this[MB];)switch(this[Gs]){case"begin":case"header":this[AV](e,t),t+=512;break;case"ignore":case"body":t+=this[zD](e,t);break;case"meta":t+=this[aV](e,t);break;default:throw new Error("invalid state: "+this[Gs])}t{"use strict";var _Re=uf(),cV=sC(),bf=require("fs"),VRe=Bf(),uV=require("path"),eat=lV.exports=(r,e,t)=>{typeof r=="function"?(t=r,e=null,r={}):Array.isArray(r)&&(e=r,r={}),typeof e=="function"&&(t=e,e=null),e?e=Array.from(e):e=[];let i=_Re(r);if(i.sync&&typeof t=="function")throw new TypeError("callback not supported for sync tar functions");if(!i.file&&typeof t=="function")throw new TypeError("callback only supported with file option");return e.length&&ZRe(i,e),i.noResume||XRe(i),i.file&&i.sync?$Re(i):i.file?eFe(i,t):gV(i)},XRe=r=>{let e=r.onentry;r.onentry=e?t=>{e(t),t.resume()}:t=>t.resume()},ZRe=(r,e)=>{let t=new Map(e.map(s=>[s.replace(/\/+$/,""),!0])),i=r.filter,n=(s,o)=>{let a=o||uV.parse(s).root||".",l=s===a?!1:t.has(s)?t.get(s):n(uV.dirname(s),a);return t.set(s,l),l};r.filter=i?(s,o)=>i(s,o)&&n(s.replace(/\/+$/,"")):s=>n(s.replace(/\/+$/,""))},$Re=r=>{let e=gV(r),t=r.file,i=!0,n;try{let s=bf.statSync(t),o=r.maxReadSize||16*1024*1024;if(s.size{let t=new cV(r),i=r.maxReadSize||16*1024*1024,n=r.file,s=new Promise((o,a)=>{t.on("error",a),t.on("end",o),bf.stat(n,(l,c)=>{if(l)a(l);else{let u=new VRe.ReadStream(n,{readSize:i,size:c.size});u.on("error",a),u.pipe(t)}})});return e?s.then(e,e):s},gV=r=>new cV(r)});var mV=w((nat,fV)=>{"use strict";var tFe=uf(),KB=vB(),rat=require("fs"),hV=Bf(),pV=UB(),dV=require("path"),iat=fV.exports=(r,e,t)=>{if(typeof e=="function"&&(t=e),Array.isArray(r)&&(e=r,r={}),!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");e=Array.from(e);let i=tFe(r);if(i.sync&&typeof t=="function")throw new TypeError("callback not supported for sync tar functions");if(!i.file&&typeof t=="function")throw new TypeError("callback only supported with file option");return i.file&&i.sync?rFe(i,e):i.file?iFe(i,e,t):i.sync?nFe(i,e):sFe(i,e)},rFe=(r,e)=>{let t=new KB.Sync(r),i=new hV.WriteStreamSync(r.file,{mode:r.mode||438});t.pipe(i),CV(t,e)},iFe=(r,e,t)=>{let i=new KB(r),n=new hV.WriteStream(r.file,{mode:r.mode||438});i.pipe(n);let s=new Promise((o,a)=>{n.on("error",a),n.on("close",o),i.on("error",a)});return XD(i,e),t?s.then(t,t):s},CV=(r,e)=>{e.forEach(t=>{t.charAt(0)==="@"?pV({file:dV.resolve(r.cwd,t.substr(1)),sync:!0,noResume:!0,onentry:i=>r.add(i)}):r.add(t)}),r.end()},XD=(r,e)=>{for(;e.length;){let t=e.shift();if(t.charAt(0)==="@")return pV({file:dV.resolve(r.cwd,t.substr(1)),noResume:!0,onentry:i=>r.add(i)}).then(i=>XD(r,e));r.add(t)}r.end()},nFe=(r,e)=>{let t=new KB.Sync(r);return CV(t,e),t},sFe=(r,e)=>{let t=new KB(r);return XD(t,e),t}});var ZD=w((aat,EV)=>{"use strict";var oFe=uf(),IV=vB(),sat=sC(),qs=require("fs"),yV=Bf(),wV=UB(),BV=require("path"),bV=pf(),oat=EV.exports=(r,e,t)=>{let i=oFe(r);if(!i.file)throw new TypeError("file is required");if(i.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");return e=Array.from(e),i.sync?aFe(i,e):AFe(i,e,t)},aFe=(r,e)=>{let t=new IV.Sync(r),i=!0,n,s;try{try{n=qs.openSync(r.file,"r+")}catch(l){if(l.code==="ENOENT")n=qs.openSync(r.file,"w+");else throw l}let o=qs.fstatSync(n),a=Buffer.alloc(512);e:for(s=0;so.size)break;s+=c,r.mtimeCache&&r.mtimeCache.set(l.path,l.mtime)}i=!1,lFe(r,t,s,n,e)}finally{if(i)try{qs.closeSync(n)}catch(o){}}},lFe=(r,e,t,i,n)=>{let s=new yV.WriteStreamSync(r.file,{fd:i,start:t});e.pipe(s),cFe(e,n)},AFe=(r,e,t)=>{e=Array.from(e);let i=new IV(r),n=(o,a,l)=>{let c=(p,m)=>{p?qs.close(o,y=>l(p)):l(null,m)},u=0;if(a===0)return c(null,0);let g=0,f=Buffer.alloc(512),h=(p,m)=>{if(p)return c(p);if(g+=m,g<512&&m)return qs.read(o,f,g,f.length-g,u+g,h);if(u===0&&f[0]===31&&f[1]===139)return c(new Error("cannot append to compressed archives"));if(g<512)return c(null,u);let y=new bV(f);if(!y.cksumValid)return c(null,u);let b=512*Math.ceil(y.size/512);if(u+b+512>a||(u+=b+512,u>=a))return c(null,u);r.mtimeCache&&r.mtimeCache.set(y.path,y.mtime),g=0,qs.read(o,f,0,512,u,h)};qs.read(o,f,0,512,u,h)},s=new Promise((o,a)=>{i.on("error",a);let l="r+",c=(u,g)=>{if(u&&u.code==="ENOENT"&&l==="r+")return l="w+",qs.open(r.file,l,c);if(u)return a(u);qs.fstat(g,(f,h)=>{if(f)return a(f);n(g,h.size,(p,m)=>{if(p)return a(p);let y=new yV.WriteStream(r.file,{fd:g,start:m});i.pipe(y),y.on("error",a),y.on("close",o),QV(i,e)})})};qs.open(r.file,l,c)});return t?s.then(t,t):s},cFe=(r,e)=>{e.forEach(t=>{t.charAt(0)==="@"?wV({file:BV.resolve(r.cwd,t.substr(1)),sync:!0,noResume:!0,onentry:i=>r.add(i)}):r.add(t)}),r.end()},QV=(r,e)=>{for(;e.length;){let t=e.shift();if(t.charAt(0)==="@")return wV({file:BV.resolve(r.cwd,t.substr(1)),noResume:!0,onentry:i=>r.add(i)}).then(i=>QV(r,e));r.add(t)}r.end()}});var vV=w((lat,SV)=>{"use strict";var uFe=uf(),gFe=ZD(),Aat=SV.exports=(r,e,t)=>{let i=uFe(r);if(!i.file)throw new TypeError("file is required");if(i.gzip)throw new TypeError("cannot append to compressed archives");if(!e||!Array.isArray(e)||!e.length)throw new TypeError("no files or directories specified");return e=Array.from(e),fFe(i),gFe(i,e,t)},fFe=r=>{let e=r.filter;r.mtimeCache||(r.mtimeCache=new Map),r.filter=e?(t,i)=>e(t,i)&&!(r.mtimeCache.get(t)>i.mtime):(t,i)=>!(r.mtimeCache.get(t)>i.mtime)}});var PV=w((cat,kV)=>{var{promisify:xV}=require("util"),Dl=require("fs"),hFe=r=>{if(!r)r={mode:511,fs:Dl};else if(typeof r=="object")r=N({mode:511,fs:Dl},r);else if(typeof r=="number")r={mode:r,fs:Dl};else if(typeof r=="string")r={mode:parseInt(r,8),fs:Dl};else throw new TypeError("invalid options argument");return r.mkdir=r.mkdir||r.fs.mkdir||Dl.mkdir,r.mkdirAsync=xV(r.mkdir),r.stat=r.stat||r.fs.stat||Dl.stat,r.statAsync=xV(r.stat),r.statSync=r.statSync||r.fs.statSync||Dl.statSync,r.mkdirSync=r.mkdirSync||r.fs.mkdirSync||Dl.mkdirSync,r};kV.exports=hFe});var RV=w((uat,DV)=>{var pFe=process.env.__TESTING_MKDIRP_PLATFORM__||process.platform,{resolve:dFe,parse:CFe}=require("path"),mFe=r=>{if(/\0/.test(r))throw Object.assign(new TypeError("path must be a string without null bytes"),{path:r,code:"ERR_INVALID_ARG_VALUE"});if(r=dFe(r),pFe==="win32"){let e=/[*|"<>?:]/,{root:t}=CFe(r);if(e.test(r.substr(t.length)))throw Object.assign(new Error("Illegal characters in path."),{path:r,code:"EINVAL"})}return r};DV.exports=mFe});var OV=w((gat,FV)=>{var{dirname:NV}=require("path"),LV=(r,e,t=void 0)=>t===e?Promise.resolve():r.statAsync(e).then(i=>i.isDirectory()?t:void 0,i=>i.code==="ENOENT"?LV(r,NV(e),e):void 0),TV=(r,e,t=void 0)=>{if(t!==e)try{return r.statSync(e).isDirectory()?t:void 0}catch(i){return i.code==="ENOENT"?TV(r,NV(e),e):void 0}};FV.exports={findMade:LV,findMadeSync:TV}});var tR=w((fat,MV)=>{var{dirname:UV}=require("path"),$D=(r,e,t)=>{e.recursive=!1;let i=UV(r);return i===r?e.mkdirAsync(r,e).catch(n=>{if(n.code!=="EISDIR")throw n}):e.mkdirAsync(r,e).then(()=>t||r,n=>{if(n.code==="ENOENT")return $D(i,e).then(s=>$D(r,e,s));if(n.code!=="EEXIST"&&n.code!=="EROFS")throw n;return e.statAsync(r).then(s=>{if(s.isDirectory())return t;throw n},()=>{throw n})})},eR=(r,e,t)=>{let i=UV(r);if(e.recursive=!1,i===r)try{return e.mkdirSync(r,e)}catch(n){if(n.code!=="EISDIR")throw n;return}try{return e.mkdirSync(r,e),t||r}catch(n){if(n.code==="ENOENT")return eR(r,e,eR(i,e,t));if(n.code!=="EEXIST"&&n.code!=="EROFS")throw n;try{if(!e.statSync(r).isDirectory())throw n}catch(s){throw n}}};MV.exports={mkdirpManual:$D,mkdirpManualSync:eR}});var jV=w((hat,KV)=>{var{dirname:HV}=require("path"),{findMade:EFe,findMadeSync:IFe}=OV(),{mkdirpManual:yFe,mkdirpManualSync:wFe}=tR(),BFe=(r,e)=>(e.recursive=!0,HV(r)===r?e.mkdirAsync(r,e):EFe(e,r).then(i=>e.mkdirAsync(r,e).then(()=>i).catch(n=>{if(n.code==="ENOENT")return yFe(r,e);throw n}))),bFe=(r,e)=>{if(e.recursive=!0,HV(r)===r)return e.mkdirSync(r,e);let i=IFe(e,r);try{return e.mkdirSync(r,e),i}catch(n){if(n.code==="ENOENT")return wFe(r,e);throw n}};KV.exports={mkdirpNative:BFe,mkdirpNativeSync:bFe}});var JV=w((pat,GV)=>{var YV=require("fs"),QFe=process.env.__TESTING_MKDIRP_NODE_VERSION__||process.version,rR=QFe.replace(/^v/,"").split("."),qV=+rR[0]>10||+rR[0]==10&&+rR[1]>=12,SFe=qV?r=>r.mkdir===YV.mkdir:()=>!1,vFe=qV?r=>r.mkdirSync===YV.mkdirSync:()=>!1;GV.exports={useNative:SFe,useNativeSync:vFe}});var ZV=w((dat,WV)=>{var Qf=PV(),Sf=RV(),{mkdirpNative:zV,mkdirpNativeSync:_V}=jV(),{mkdirpManual:VV,mkdirpManualSync:XV}=tR(),{useNative:kFe,useNativeSync:xFe}=JV(),vf=(r,e)=>(r=Sf(r),e=Qf(e),kFe(e)?zV(r,e):VV(r,e)),PFe=(r,e)=>(r=Sf(r),e=Qf(e),xFe(e)?_V(r,e):XV(r,e));vf.sync=PFe;vf.native=(r,e)=>zV(Sf(r),Qf(e));vf.manual=(r,e)=>VV(Sf(r),Qf(e));vf.nativeSync=(r,e)=>_V(Sf(r),Qf(e));vf.manualSync=(r,e)=>XV(Sf(r),Qf(e));WV.exports=vf});var s6=w((Cat,$V)=>{"use strict";var Js=require("fs"),gu=require("path"),DFe=Js.lchown?"lchown":"chown",RFe=Js.lchownSync?"lchownSync":"chownSync",e6=Js.lchown&&!process.version.match(/v1[1-9]+\./)&&!process.version.match(/v10\.[6-9]/),t6=(r,e,t)=>{try{return Js[RFe](r,e,t)}catch(i){if(i.code!=="ENOENT")throw i}},FFe=(r,e,t)=>{try{return Js.chownSync(r,e,t)}catch(i){if(i.code!=="ENOENT")throw i}},NFe=e6?(r,e,t,i)=>n=>{!n||n.code!=="EISDIR"?i(n):Js.chown(r,e,t,i)}:(r,e,t,i)=>i,iR=e6?(r,e,t)=>{try{return t6(r,e,t)}catch(i){if(i.code!=="EISDIR")throw i;FFe(r,e,t)}}:(r,e,t)=>t6(r,e,t),LFe=process.version,r6=(r,e,t)=>Js.readdir(r,e,t),TFe=(r,e)=>Js.readdirSync(r,e);/^v4\./.test(LFe)&&(r6=(r,e,t)=>Js.readdir(r,t));var HB=(r,e,t,i)=>{Js[DFe](r,e,t,NFe(r,e,t,n=>{i(n&&n.code!=="ENOENT"?n:null)}))},i6=(r,e,t,i,n)=>{if(typeof e=="string")return Js.lstat(gu.resolve(r,e),(s,o)=>{if(s)return n(s.code!=="ENOENT"?s:null);o.name=e,i6(r,o,t,i,n)});if(e.isDirectory())nR(gu.resolve(r,e.name),t,i,s=>{if(s)return n(s);let o=gu.resolve(r,e.name);HB(o,t,i,n)});else{let s=gu.resolve(r,e.name);HB(s,t,i,n)}},nR=(r,e,t,i)=>{r6(r,{withFileTypes:!0},(n,s)=>{if(n){if(n.code==="ENOENT")return i();if(n.code!=="ENOTDIR"&&n.code!=="ENOTSUP")return i(n)}if(n||!s.length)return HB(r,e,t,i);let o=s.length,a=null,l=c=>{if(!a){if(c)return i(a=c);if(--o==0)return HB(r,e,t,i)}};s.forEach(c=>i6(r,c,e,t,l))})},OFe=(r,e,t,i)=>{if(typeof e=="string")try{let n=Js.lstatSync(gu.resolve(r,e));n.name=e,e=n}catch(n){if(n.code==="ENOENT")return;throw n}e.isDirectory()&&n6(gu.resolve(r,e.name),t,i),iR(gu.resolve(r,e.name),t,i)},n6=(r,e,t)=>{let i;try{i=TFe(r,{withFileTypes:!0})}catch(n){if(n.code==="ENOENT")return;if(n.code==="ENOTDIR"||n.code==="ENOTSUP")return iR(r,e,t);throw n}return i&&i.length&&i.forEach(n=>OFe(r,n,e,t)),iR(r,e,t)};$V.exports=nR;nR.sync=n6});var l6=w((Iat,sR)=>{"use strict";var o6=ZV(),Ws=require("fs"),jB=require("path"),a6=s6(),oR=class extends Error{constructor(e,t){super("Cannot extract through symbolic link");this.path=t,this.symlink=e}get name(){return"SylinkError"}},oC=class extends Error{constructor(e,t){super(t+": Cannot cd into '"+e+"'");this.path=e,this.code=t}get name(){return"CwdError"}},mat=sR.exports=(r,e,t)=>{let i=e.umask,n=e.mode|448,s=(n&i)!=0,o=e.uid,a=e.gid,l=typeof o=="number"&&typeof a=="number"&&(o!==e.processUid||a!==e.processGid),c=e.preserve,u=e.unlink,g=e.cache,f=e.cwd,h=(y,b)=>{y?t(y):(g.set(r,!0),b&&l?a6(b,o,a,v=>h(v)):s?Ws.chmod(r,n,t):t())};if(g&&g.get(r)===!0)return h();if(r===f)return Ws.stat(r,(y,b)=>{(y||!b.isDirectory())&&(y=new oC(r,y&&y.code||"ENOTDIR")),h(y)});if(c)return o6(r,{mode:n}).then(y=>h(null,y),h);let m=jB.relative(f,r).split(/\/|\\/);GB(f,m,n,g,u,f,null,h)},GB=(r,e,t,i,n,s,o,a)=>{if(!e.length)return a(null,o);let l=e.shift(),c=r+"/"+l;if(i.get(c))return GB(c,e,t,i,n,s,o,a);Ws.mkdir(c,t,A6(c,e,t,i,n,s,o,a))},A6=(r,e,t,i,n,s,o,a)=>l=>{if(l){if(l.path&&jB.dirname(l.path)===s&&(l.code==="ENOTDIR"||l.code==="ENOENT"))return a(new oC(s,l.code));Ws.lstat(r,(c,u)=>{if(c)a(c);else if(u.isDirectory())GB(r,e,t,i,n,s,o,a);else if(n)Ws.unlink(r,g=>{if(g)return a(g);Ws.mkdir(r,t,A6(r,e,t,i,n,s,o,a))});else{if(u.isSymbolicLink())return a(new oR(r,r+"/"+e.join("/")));a(l)}})}else o=o||r,GB(r,e,t,i,n,s,o,a)},Eat=sR.exports.sync=(r,e)=>{let t=e.umask,i=e.mode|448,n=(i&t)!=0,s=e.uid,o=e.gid,a=typeof s=="number"&&typeof o=="number"&&(s!==e.processUid||o!==e.processGid),l=e.preserve,c=e.unlink,u=e.cache,g=e.cwd,f=y=>{u.set(r,!0),y&&a&&a6.sync(y,s,o),n&&Ws.chmodSync(r,i)};if(u&&u.get(r)===!0)return f();if(r===g){let y=!1,b="ENOTDIR";try{y=Ws.statSync(r).isDirectory()}catch(v){b=v.code}finally{if(!y)throw new oC(r,b)}f();return}if(l)return f(o6.sync(r,i));let p=jB.relative(g,r).split(/\/|\\/),m=null;for(let y=p.shift(),b=g;y&&(b+="/"+y);y=p.shift())if(!u.get(b))try{Ws.mkdirSync(b,i),m=m||b,u.set(b,!0)}catch(v){if(v.path&&jB.dirname(v.path)===g&&(v.code==="ENOTDIR"||v.code==="ENOENT"))return new oC(g,v.code);let k=Ws.lstatSync(b);if(k.isDirectory()){u.set(b,!0);continue}else if(c){Ws.unlinkSync(b),Ws.mkdirSync(b,i),m=m||b,u.set(b,!0);continue}else if(k.isSymbolicLink())return new oR(b,b+"/"+p.join("/"))}return f(m)}});var g6=w((yat,c6)=>{var u6=require("assert");c6.exports=()=>{let r=new Map,e=new Map,{join:t}=require("path"),i=u=>t(u).split(/[\\\/]/).slice(0,-1).reduce((g,f)=>g.length?g.concat(t(g[g.length-1],f)):[f],[]),n=new Set,s=u=>{let g=e.get(u);if(!g)throw new Error("function does not have any path reservations");return{paths:g.paths.map(f=>r.get(f)),dirs:[...g.dirs].map(f=>r.get(f))}},o=u=>{let{paths:g,dirs:f}=s(u);return g.every(h=>h[0]===u)&&f.every(h=>h[0]instanceof Set&&h[0].has(u))},a=u=>n.has(u)||!o(u)?!1:(n.add(u),u(()=>l(u)),!0),l=u=>{if(!n.has(u))return!1;let{paths:g,dirs:f}=e.get(u),h=new Set;return g.forEach(p=>{let m=r.get(p);u6.equal(m[0],u),m.length===1?r.delete(p):(m.shift(),typeof m[0]=="function"?h.add(m[0]):m[0].forEach(y=>h.add(y)))}),f.forEach(p=>{let m=r.get(p);u6(m[0]instanceof Set),m[0].size===1&&m.length===1?r.delete(p):m[0].size===1?(m.shift(),h.add(m[0])):m[0].delete(u)}),n.delete(u),h.forEach(p=>a(p)),!0};return{check:o,reserve:(u,g)=>{let f=new Set(u.map(h=>i(h)).reduce((h,p)=>h.concat(p)));return e.set(g,{dirs:f,paths:u}),u.forEach(h=>{let p=r.get(h);p?p.push(g):r.set(h,[g])}),f.forEach(h=>{let p=r.get(h);p?p[p.length-1]instanceof Set?p[p.length-1].add(g):p.push(new Set([g])):r.set(h,[new Set([g])])}),a(g)}}}});var p6=w((wat,f6)=>{var MFe=process.env.__FAKE_PLATFORM__||process.platform,UFe=MFe==="win32",KFe=global.__FAKE_TESTING_FS__||require("fs"),{O_CREAT:HFe,O_TRUNC:jFe,O_WRONLY:GFe,UV_FS_O_FILEMAP:h6=0}=KFe.constants,YFe=UFe&&!!h6,qFe=512*1024,JFe=h6|jFe|HFe|GFe;f6.exports=YFe?r=>r"w"});var pR=w((Sat,d6)=>{"use strict";var WFe=require("assert"),Bat=require("events").EventEmitter,zFe=sC(),$t=require("fs"),_Fe=Bf(),hA=require("path"),aR=l6(),bat=aR.sync,C6=wD(),VFe=g6(),m6=Symbol("onEntry"),AR=Symbol("checkFs"),E6=Symbol("checkFs2"),lR=Symbol("isReusable"),pA=Symbol("makeFs"),cR=Symbol("file"),uR=Symbol("directory"),YB=Symbol("link"),I6=Symbol("symlink"),y6=Symbol("hardlink"),w6=Symbol("unsupported"),Qat=Symbol("unknown"),B6=Symbol("checkPath"),kf=Symbol("mkdir"),dn=Symbol("onError"),qB=Symbol("pending"),b6=Symbol("pend"),xf=Symbol("unpend"),gR=Symbol("ended"),fR=Symbol("maybeClose"),hR=Symbol("skip"),aC=Symbol("doChown"),AC=Symbol("uid"),lC=Symbol("gid"),Q6=require("crypto"),S6=p6(),JB=()=>{throw new Error("sync function called cb somehow?!?")},XFe=(r,e)=>{if(process.platform!=="win32")return $t.unlink(r,e);let t=r+".DELETE."+Q6.randomBytes(16).toString("hex");$t.rename(r,t,i=>{if(i)return e(i);$t.unlink(t,e)})},ZFe=r=>{if(process.platform!=="win32")return $t.unlinkSync(r);let e=r+".DELETE."+Q6.randomBytes(16).toString("hex");$t.renameSync(r,e),$t.unlinkSync(e)},v6=(r,e,t)=>r===r>>>0?r:e===e>>>0?e:t,WB=class extends zFe{constructor(e){if(e||(e={}),e.ondone=t=>{this[gR]=!0,this[fR]()},super(e),this.reservations=VFe(),this.transform=typeof e.transform=="function"?e.transform:null,this.writable=!0,this.readable=!1,this[qB]=0,this[gR]=!1,this.dirCache=e.dirCache||new Map,typeof e.uid=="number"||typeof e.gid=="number"){if(typeof e.uid!="number"||typeof e.gid!="number")throw new TypeError("cannot set owner without number uid and gid");if(e.preserveOwner)throw new TypeError("cannot preserve owner in archive and also set owner explicitly");this.uid=e.uid,this.gid=e.gid,this.setOwner=!0}else this.uid=null,this.gid=null,this.setOwner=!1;e.preserveOwner===void 0&&typeof e.uid!="number"?this.preserveOwner=process.getuid&&process.getuid()===0:this.preserveOwner=!!e.preserveOwner,this.processUid=(this.preserveOwner||this.setOwner)&&process.getuid?process.getuid():null,this.processGid=(this.preserveOwner||this.setOwner)&&process.getgid?process.getgid():null,this.forceChown=e.forceChown===!0,this.win32=!!e.win32||process.platform==="win32",this.newer=!!e.newer,this.keep=!!e.keep,this.noMtime=!!e.noMtime,this.preservePaths=!!e.preservePaths,this.unlink=!!e.unlink,this.cwd=hA.resolve(e.cwd||process.cwd()),this.strip=+e.strip||0,this.processUmask=process.umask(),this.umask=typeof e.umask=="number"?e.umask:this.processUmask,this.dmode=e.dmode||511&~this.umask,this.fmode=e.fmode||438&~this.umask,this.on("entry",t=>this[m6](t))}warn(e,t,i={}){return(e==="TAR_BAD_ARCHIVE"||e==="TAR_ABORT")&&(i.recoverable=!1),super.warn(e,t,i)}[fR](){this[gR]&&this[qB]===0&&(this.emit("prefinish"),this.emit("finish"),this.emit("end"),this.emit("close"))}[B6](e){if(this.strip){let t=e.path.split(/\/|\\/);if(t.length=this.strip&&(e.linkpath=i.slice(this.strip).join("/"))}}if(!this.preservePaths){let t=e.path;if(t.match(/(^|\/|\\)\.\.(\\|\/|$)/))return this.warn("TAR_ENTRY_ERROR","path contains '..'",{entry:e,path:t}),!1;if(hA.win32.isAbsolute(t)){let i=hA.win32.parse(t);e.path=t.substr(i.root.length);let n=i.root;this.warn("TAR_ENTRY_INFO",`stripping ${n} from absolute path`,{entry:e,path:t})}}if(this.win32){let t=hA.win32.parse(e.path);e.path=t.root===""?C6.encode(e.path):t.root+C6.encode(e.path.substr(t.root.length))}return hA.isAbsolute(e.path)?e.absolute=e.path:e.absolute=hA.resolve(this.cwd,e.path),!0}[m6](e){if(!this[B6](e))return e.resume();switch(WFe.equal(typeof e.absolute,"string"),e.type){case"Directory":case"GNUDumpDir":e.mode&&(e.mode=e.mode|448);case"File":case"OldFile":case"ContiguousFile":case"Link":case"SymbolicLink":return this[AR](e);case"CharacterDevice":case"BlockDevice":case"FIFO":return this[w6](e)}}[dn](e,t){e.name==="CwdError"?this.emit("error",e):(this.warn("TAR_ENTRY_ERROR",e,{entry:t}),this[xf](),t.resume())}[kf](e,t,i){aR(e,{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:t},i)}[aC](e){return this.forceChown||this.preserveOwner&&(typeof e.uid=="number"&&e.uid!==this.processUid||typeof e.gid=="number"&&e.gid!==this.processGid)||typeof this.uid=="number"&&this.uid!==this.processUid||typeof this.gid=="number"&&this.gid!==this.processGid}[AC](e){return v6(this.uid,e.uid,this.processUid)}[lC](e){return v6(this.gid,e.gid,this.processGid)}[cR](e,t){let i=e.mode&4095||this.fmode,n=new _Fe.WriteStream(e.absolute,{flags:S6(e.size),mode:i,autoClose:!1});n.on("error",l=>this[dn](l,e));let s=1,o=l=>{if(l)return this[dn](l,e);--s==0&&$t.close(n.fd,c=>{t(),c?this[dn](c,e):this[xf]()})};n.on("finish",l=>{let c=e.absolute,u=n.fd;if(e.mtime&&!this.noMtime){s++;let g=e.atime||new Date,f=e.mtime;$t.futimes(u,g,f,h=>h?$t.utimes(c,g,f,p=>o(p&&h)):o())}if(this[aC](e)){s++;let g=this[AC](e),f=this[lC](e);$t.fchown(u,g,f,h=>h?$t.chown(c,g,f,p=>o(p&&h)):o())}o()});let a=this.transform&&this.transform(e)||e;a!==e&&(a.on("error",l=>this[dn](l,e)),e.pipe(a)),a.pipe(n)}[uR](e,t){let i=e.mode&4095||this.dmode;this[kf](e.absolute,i,n=>{if(n)return t(),this[dn](n,e);let s=1,o=a=>{--s==0&&(t(),this[xf](),e.resume())};e.mtime&&!this.noMtime&&(s++,$t.utimes(e.absolute,e.atime||new Date,e.mtime,o)),this[aC](e)&&(s++,$t.chown(e.absolute,this[AC](e),this[lC](e),o)),o()})}[w6](e){e.unsupported=!0,this.warn("TAR_ENTRY_UNSUPPORTED",`unsupported entry type: ${e.type}`,{entry:e}),e.resume()}[I6](e,t){this[YB](e,e.linkpath,"symlink",t)}[y6](e,t){this[YB](e,hA.resolve(this.cwd,e.linkpath),"link",t)}[b6](){this[qB]++}[xf](){this[qB]--,this[fR]()}[hR](e){this[xf](),e.resume()}[lR](e,t){return e.type==="File"&&!this.unlink&&t.isFile()&&t.nlink<=1&&process.platform!=="win32"}[AR](e){this[b6]();let t=[e.path];e.linkpath&&t.push(e.linkpath),this.reservations.reserve(t,i=>this[E6](e,i))}[E6](e,t){this[kf](hA.dirname(e.absolute),this.dmode,i=>{if(i)return t(),this[dn](i,e);$t.lstat(e.absolute,(n,s)=>{s&&(this.keep||this.newer&&s.mtime>e.mtime)?(this[hR](e),t()):n||this[lR](e,s)?this[pA](null,e,t):s.isDirectory()?e.type==="Directory"?!e.mode||(s.mode&4095)===e.mode?this[pA](null,e,t):$t.chmod(e.absolute,e.mode,o=>this[pA](o,e,t)):$t.rmdir(e.absolute,o=>this[pA](o,e,t)):XFe(e.absolute,o=>this[pA](o,e,t))})})}[pA](e,t,i){if(e)return this[dn](e,t);switch(t.type){case"File":case"OldFile":case"ContiguousFile":return this[cR](t,i);case"Link":return this[y6](t,i);case"SymbolicLink":return this[I6](t,i);case"Directory":case"GNUDumpDir":return this[uR](t,i)}}[YB](e,t,i,n){$t[i](t,e.absolute,s=>{if(s)return this[dn](s,e);n(),this[xf](),e.resume()})}},k6=class extends WB{constructor(e){super(e)}[AR](e){let t=this[kf](hA.dirname(e.absolute),this.dmode,JB);if(t)return this[dn](t,e);try{let i=$t.lstatSync(e.absolute);if(this.keep||this.newer&&i.mtime>e.mtime)return this[hR](e);if(this[lR](e,i))return this[pA](null,e,JB);try{return i.isDirectory()?e.type==="Directory"?e.mode&&(i.mode&4095)!==e.mode&&$t.chmodSync(e.absolute,e.mode):$t.rmdirSync(e.absolute):ZFe(e.absolute),this[pA](null,e,JB)}catch(n){return this[dn](n,e)}}catch(i){return this[pA](null,e,JB)}}[cR](e,t){let i=e.mode&4095||this.fmode,n=l=>{let c;try{$t.closeSync(o)}catch(u){c=u}(l||c)&&this[dn](l||c,e)},s,o;try{o=$t.openSync(e.absolute,S6(e.size),i)}catch(l){return n(l)}let a=this.transform&&this.transform(e)||e;a!==e&&(a.on("error",l=>this[dn](l,e)),e.pipe(a)),a.on("data",l=>{try{$t.writeSync(o,l,0,l.length)}catch(c){n(c)}}),a.on("end",l=>{let c=null;if(e.mtime&&!this.noMtime){let u=e.atime||new Date,g=e.mtime;try{$t.futimesSync(o,u,g)}catch(f){try{$t.utimesSync(e.absolute,u,g)}catch(h){c=f}}}if(this[aC](e)){let u=this[AC](e),g=this[lC](e);try{$t.fchownSync(o,u,g)}catch(f){try{$t.chownSync(e.absolute,u,g)}catch(h){c=c||f}}}n(c)})}[uR](e,t){let i=e.mode&4095||this.dmode,n=this[kf](e.absolute,i);if(n)return this[dn](n,e);if(e.mtime&&!this.noMtime)try{$t.utimesSync(e.absolute,e.atime||new Date,e.mtime)}catch(s){}if(this[aC](e))try{$t.chownSync(e.absolute,this[AC](e),this[lC](e))}catch(s){}e.resume()}[kf](e,t){try{return aR.sync(e,{uid:this.uid,gid:this.gid,processUid:this.processUid,processGid:this.processGid,umask:this.processUmask,preserve:this.preservePaths,unlink:this.unlink,cache:this.dirCache,cwd:this.cwd,mode:t})}catch(i){return i}}[YB](e,t,i,n){try{$t[i+"Sync"](t,e.absolute),e.resume()}catch(s){return this[dn](s,e)}}};WB.Sync=k6;d6.exports=WB});var F6=w((kat,x6)=>{"use strict";var $Fe=uf(),zB=pR(),P6=require("fs"),D6=Bf(),R6=require("path"),vat=x6.exports=(r,e,t)=>{typeof r=="function"?(t=r,e=null,r={}):Array.isArray(r)&&(e=r,r={}),typeof e=="function"&&(t=e,e=null),e?e=Array.from(e):e=[];let i=$Fe(r);if(i.sync&&typeof t=="function")throw new TypeError("callback not supported for sync tar functions");if(!i.file&&typeof t=="function")throw new TypeError("callback only supported with file option");return e.length&&eNe(i,e),i.file&&i.sync?tNe(i):i.file?rNe(i,t):i.sync?iNe(i):nNe(i)},eNe=(r,e)=>{let t=new Map(e.map(s=>[s.replace(/\/+$/,""),!0])),i=r.filter,n=(s,o)=>{let a=o||R6.parse(s).root||".",l=s===a?!1:t.has(s)?t.get(s):n(R6.dirname(s),a);return t.set(s,l),l};r.filter=i?(s,o)=>i(s,o)&&n(s.replace(/\/+$/,"")):s=>n(s.replace(/\/+$/,""))},tNe=r=>{let e=new zB.Sync(r),t=r.file,i=!0,n,s=P6.statSync(t),o=r.maxReadSize||16*1024*1024;new D6.ReadStreamSync(t,{readSize:o,size:s.size}).pipe(e)},rNe=(r,e)=>{let t=new zB(r),i=r.maxReadSize||16*1024*1024,n=r.file,s=new Promise((o,a)=>{t.on("error",a),t.on("close",o),P6.stat(n,(l,c)=>{if(l)a(l);else{let u=new D6.ReadStream(n,{readSize:i,size:c.size});u.on("error",a),u.pipe(t)}})});return e?s.then(e,e):s},iNe=r=>new zB.Sync(r),nNe=r=>new zB(r)});var N6=w(pi=>{"use strict";pi.c=pi.create=mV();pi.r=pi.replace=ZD();pi.t=pi.list=UB();pi.u=pi.update=vV();pi.x=pi.extract=F6();pi.Pack=vB();pi.Unpack=pR();pi.Parse=sC();pi.ReadEntry=$d();pi.WriteEntry=xD();pi.Header=pf();pi.Pax=fB();pi.types=Zd()});var U6=w((Dat,M6)=>{var CR;M6.exports.getContent=()=>(typeof CR=="undefined"&&(CR=require("zlib").brotliDecompressSync(Buffer.from("W1YWV8M2Bm73erNK/X8Ao59vhTJuj9A5ts0kuKSIx2QXjDzsGUs8PbdVZG5L6XYdVdXEZDLGumN1mwNUlCz73iKPJZC1igLZRK2zc13JaeOOPfeI2pEQlNZxCCqfcByDjjBMiKtBg7utoxYaTXZNuOE10KOQ8BnumEiaeYZZ1yOG2/yN3T9Q6UbzsAqJzf62LV/qfysaEstGqsaGu18PWSv9ilJB+HenKQgyx5MHJk6bcH05DqofPku3C5V3rL8N/hZQYNx6JTAkU5btGjpyS8/xyie/f75Ov36pjkul8GI6pmLhjLV9Q4a6yM+q9EAONZuZ5uu75Hg4UCXZgJzjYjowVu5wb6e97Ti9aFQ7qZlDPY1de830PV3T4NfglBN12SWPCeLe7jdBQJmIbojYfPmS/FfRvt5S639/voyaC70MjgvNQ3TI0EYiVdY83TB992jFNvsxWXaziwaK0ngRXD97W/b1a1qWUx0Xzr1H9Txa5lwyp4A9vcjx4p5JzxEj2mRFsE01s6r1CQYtQ5eGlDYULOTFHrBF/fGiPoeBf1padjprg5Y/vGbuEl8U9qi22qHbIVM43DYHsb9+5enaSelrwkdWlJHM+KmmBJaKKLDwVamvp6s+y4LQwmOy88wfbKHoxLN9o6iQqJAc4rL9pa9V9a48W6XLXYiJ5aNnTgENiA2+ai86rTkjEI7264UiqX+ZQ4c5o6P+PRd7pdU0fX0DpeOEqu7YKoRAsaicay2Q9A2kBF8f27QhzGmSEnqj7ZexnzXjUViEw19mfh2vvayvygcsIZuKQA6gfKe7Z+T7qmscWxkdqgz97hKkTV8y5eHQGjy+lavj1V3p9fjlMkSdYlHo/QbMQ1bMHgbHOWzN/+Phb5q8jWMmkgkulKVkRm4or2hhaPv0iwDPU+J1PDjoUiuO789Mkkk7bX81JW4EvwSxYg38+MbW8dDBX1mLxzfDAKNCDUhFN8L8Gm2ouPsli/K3+OPhfUCXddljdU/PZPKlM4QQb4AyxIiI6Ma4mKn1fOylOumxTwAEJdqSE4hhE+fXehoQomyldWZk2o8SZ/+/XLz3fgRwZ5zlOdUluYQsUzIi4+wc51Kt+GcEyEmL+FLifDoedne/C/kt//ik0dia2AY5mRBzS02RjSmhdYqh82u//peL+z5gMWdcQqlJyR0wMd39ZiDF0ZMfwoF1Ua1cmyBEwM8BtP7/92ba94cRMAlK5Sh9Y8yo3ZojAZCq+t5M5ohj7iaeiRDDACICyBQTmVQrMymWkpRYhlV1z733Rb54EUgFgJQ+Ekmpkknpy7VhiW30jZWqqr2dDdp/N/zDQUrtWG3c6A+GPexh//++qdVGe+77v1AogBIboBy1zTbGxgWQ7Wy0zgShFd679z/hv/d+CVW/CkQ5SFWF4ggoEqfLANtkgf1/Ad1TBVGzRVI6A7LNgGrNHmos1aM9h73rCYDcESm1lTSGWmNcbIzNrQuiidYE2YbJnk0SmySbBJtmxvr/35ta/fTe915EAAJJUSpKWenU1g6GEQGIsm2sHRffPefulXj3vtcJhOkmIgJdJMwq0a0lEmIZKfPeFwHyvQBSGYCYVQGI6gYoZRWlNItSO7Xxabq/cXYyLTfq9cd/9WT4B0N9r8r+3g2G35fWem1BuFpRx079myT7CgDGV01w7rjAmoX6wtutq//vfquIn06khW3/NhbDt4dLpELS6lKaWMiEnIicEU8//Z8Ne8Sbe9QvCoTjG5VBIXUe9YYaUKjqEA5XmhAaIw3XbFEF2zQ2suJjMIidpxdaJh9kP4BuFf8XlehQ5JXdtVE1lNaTUCTUFpCm0KVEpVl25jVQynv/DzCn33+TYPJrGu92m2Ya3QyNY4E4QNSIE8QBuEAUBy402W3fvqHf1ne4suXL0AaxiVIExEDM2NB9UR9f9x05AzPDNaRjIZcOP3jxooOKiIgoObMuvr+dJ3DwYGGqLjAQCAQCgUBDIFV/CBrkr64+3s0s/UfZ9cCAAQEBBgEBkWpVAQYGVuVWBgEGBgaROu0Nz5vr/+F9u0eY92l7KVesWEoogaYQEWjEiBRGhBJoRAoREREjpjBixIiI0/793OL/wbf3Ctm2X1yxYpMiEAgEaRAIBII0CAQCQZoViBUIxIgRR4xYi2zzZ4+Y6OH7YrAhR9S70EE605KKgXdb/5A4H8hV00qNkGpBRtQQF8QC2JqYcvXxC7p4Mfy7Wf83WCl65b2Z7kwCwSwBLxA8QAVCBW0JUqyCtUVGVj5+jlRc1+SfEt2s8Vmm+TAPi1+qxLn+PXGSNjp7HQChKNGoFvjYOwFFFr52fMXI9tgoJXozQG0OilPul5dsgikHOYWK6PNUrDqGdpTfxvcyvOX/Lj7XH7FZqT3QTRbWgLVz8HcqltO+berFSnrpNubwPvbYnm5s+IC9Q7UBfrbMwROJlnYfG3N2B60DL/o4V3hZ2A4JRrqlIzUP/pCIJ778GKjnM0ETp793edX4ZATp+1gz2ZeoCH9NNHhBF8VBBtp6KlpNMvJ9UpYMTdMUe6rb8QzJEmS6DSh7FtRcPyEPAyqtYh754XUkEnN9xC7L4vP785fVmWkQxo7SrurZ0tgtB9/oAxUUAI45CcOiKJh4HYRdz5d/rMZRnlH2NpRQyYNnO+7yPmMeg9oTXD9DrP+8pkoHdRmfnZwro/kMVXwtPJX8kwIWCGL3H+nOMCV7Dq2n6lgMe7w3e9rab3lvSdPcGnZe+PFVfEPrGPjHOx+bOH3+kd0em3M7/31Q45WivLl6zv5VfFTlF5eOah0yCWQhyP9yfvpUBKwE2jtP6KO8M1j16vpVWTQizteBynkHPHB+ebqSURn+/aexQd+AuRLCVQ9BVz2S624kRHGLzRiI45ZXtjy/3xWtzTlVnFMKV2KRjapziO5C3d1zzXZnQLdfr+Nz+X25c1V+NrgJxdaoFTASsOhRYIqV9L9hq+9POrnHzJ/nDoeUeytC+ADCyzewtuLJ0bHMB9j3RY1dAZGS2fAw0dwx+MkSKevMHwBJwwnrTyMZnmRS4VHBLHhWkNpC35Stx8VlbOFsC2dbPNvw7Mn3bEiFh5tkeLqmmj4/ZWnlKLu0Zq8dYwDsT+BYUAnaG1tyQnVkHM/S1avUkXIvLV2d99OlrT0zY5aIQ5ZSXx4gIMyiZAU99pwxCy32z0GdNVqTNkSw4Pb5Q/LMEL2ml5eAwDa9gUDTlIHz9hi+Pu2d/5Ir9pCXicf2NqhgdlSheTSr+EBXyrxFDQHc+tkVnFQN0dTm5HBjB4ivxpVhBOlOzaDjWeUa27YZX7GiI2P8zG3J2C0M66ynEqar4mx19VXd5wehI3GweV/jTdnSH1yKOPQSfrxdHE55UgpRiDDtLoQLHzy0XZEjuHEOLzQDVqRE2HU2lxN5G2DTuYSZ5E2Io86yZrIcmvuIhV6GMj+IookKUR/U2hAVpAIzmbAGUsPFMMeFiiInrTgmQJRDYriITHfCjLgI1y9R40CFKMxzyoqkIKed+1DEpAXL8YxInKx/+yV410/NTbtUQORAvoZsdD25ZzgAQoudlEkKlyWKw5cwS/jUY0690vf27fqKAMOsd9b39XyJS8kVsDTX9oUszCHVKfoYg+7L3pcBXPIrHQF4RC2Pcrz2zq50vqnirPBlNrPnxss9r7qoXkixr8/zu43ttZyXDFvOLRn7WB5b72YYetj992bElhxEMaEmfawj55rQX0DNvbNutjX0cavTr5sZv6/RPtfuI7pWElzcPfGnhXua7DtAeZ+fpNp/RtFmq1xE79dHuxBs7er6Ci0zCGKP18LpQMvRPQoP4q3Ehj1r4jsRo9lc/xUgkhM5kPAuTXIviCAO5GTLGy3e2XzEZKt9OtFkPco5b8xWXw3k/dsAbs1JHBwJTT4YUamEGejooimdEiTUs5QRvFiww0BqTOEKHCGiL7ZAB3ctJzsZ39BMS7O9dq/o03e8npWHPqdEoe+u/R8UwWeMz9b+L+xTCU6T8ubBZVbDtxUvHsWJJ0bH7rkzwtajoNOcjH9hAf7cyy+E3BOnBTGpn+4If97hVxV+uezshKgeb+35j6+wz1XZd6jpgt8fDctNSwu/GWYgUTL5cLXBlbNwBODU8+3FhYscf6Sqq1ckBve+CQGgX/uYpl5YPU+o/Eni/RsDyRo1Qv58niyyKAQP4ML+LckND0SxQjKbKN3JCxevKi4u6iWgIeqK2LxOi5KxO4w+peT08PMZxBgN7lZiuIcY/iO477N3s03zi4TATaF6g4sRYmgfHsMFmj22NTscveBLH/NEpSQmPVYQoAbc6YYI9xGrW7FDB17jG45jzDnCTv1C73Lpd3FWu3H9CWJLxLQSYVlGQFGjxeOwnnWO6gVtbBni89rNxvQaL6LkvvFLSR5xKFox18nHu/4XWfdAvexgt/eQcFskU5lK9XWDsCDtj7YWhxB2hK9PWg8s/I5jvEY+PdzOkT2i/waXE/IarTJEVLKRnnmEdQYhC9G9tczVmm16I6D4WPf5+hRKSo8dsY/uB20yFwbvx5C4n4dG9rR2h2Ru5lUMb8JB5VtQe+TvVJlQDX5+kmkJVle0+UD+l1qYvLKceGduGEd5F0TAo2ESO8GIofXQ5ewytv32OWZtRisntPmkvHMcUn+pvdsDoUBPUN6a5CFOLQ8vx7lkM3HsTtDigICO4JCMn5oTgrtucpG/0qlvFwqkOxQ8gd1f0rK2z1SYOSBZXScRwbpE9dudyyMOll3R64cIzQAXlAa31Cx3QaXpgkTwwn2dwHDCrM9dT5banqepsDqu5+SXIZRnU6xEl6HRQuaEp4cYUbogaeulGBh3Tzq4k/QByLzkebCVdWuaOdQwMMPhnVzUUDP52+E7Ti41SF0BYp2hbbchtsoENe/BDcZUjceTA1fZE6sNHNmkE4KFwFererZNtxCSzp6aXMmwXK+fPvtVHrqq+COuXr369qd+Z1I0I4WVo1YdhmbglaUe80GOaLuzj+82+jbrS38sI2gOYh87VV1xLctVC32YgmzV8qwdrnigNpkupw2mdTPza+SHdqbWwr/yIThnHiCxVYME9E5tZ0zePosxrFq0g+ovLS8oOAbJi8fEz47LVotO+0DctM3qPqkKg0DidKXJrhix5DoSRu/0wExQeJt6xHHyYiPl+XxitTSerXqaG0UPzBFmT3vzo1rR71CXiDbfU2IuffcAKMKDqoBRjnh+RFGSi+7XDGA41hLfwopMLxGz5XFmi5ydJPrgEvPd5xIiIMO+AQtBnXdiWGLEOGip+JClbQr6x99AAbMmGsIYOWYoCjebWtSPqRIvJ/pBM9r4fdlZpY3Z8Ja7ylURGS7palZJ0DvW+kt0VpBId5ZKNpOzRJg5+Rltcbs0KM7fF9UnwfvMtJu2jii0vKfQuMb/ybGv3af80gMdAWAeh3WRXz+PIa+SF7pLi83QL4uwdF+doOL1eZ20Bt1CODxDDnMjpIXnrddkTVc8nWEmGsNkaCw8UkWe7JTcp4yd0gddffQQwOuJgPURfJofkxVAsSXFUHTUKPoJOCZJIQQHFI8xlXV7BNZOaCIVwOmQ8xArV5f4MXsIb1aehRst1A/Zs0f9AyEvBXR5nktvoMsrzmAJ0VJcocFcvFp/06XTnZZrf0WuvsS0DQA5+/7rAH1EhOLkdP3KcWVHIjwpcZ87/A7TZz2NngqDszgaGv1ElMeuPBTER+3oKTs70j3QbcUm/YRZ5r5PJQz3MxSCpLAz+7igOAwn6h/PQhdmDb5X9poTxphOOOm+DVMPBSOMwewPTvRhNHiMRlI80560FEsKJyrLIgLlPBteg1+cW1zAgxgoe0qcbdkjnJ3hV2hn3O3fqk7c3w+bHDDii1smoUe2PL7zIkb3zm1cS/c2ujMr3j2SYDltgAsqEXzf4QxFRoLXER6IL+PcLrH4R3ed9059MFRpaifDSLA2wBdxL6Z8xJYdNfo1Mbu9XRcJEJn4vAlobJK7Hd3fm9YFgbhXFm0T8eMvgtOCgFph05unaIvoPs90NDzTkU6MwSPapcOWxliCQe52CMdUVU5EHjV2/EU865kMbNnpCEw2lBYnqCoxHccaPy5bwjHsdHq86muWsY0hHF5H/TTPMMFBR+PdVqOZFI3xa1BfnlH/p87GwVt7Vw2BU5dpxnbHrfKiD6VkdF712uc/vIWiY/D5khmpI4WNXwtICaUOUU4WGOKqUDcpj+8qyQDJSO7Tp4rwZdLLo4kHeArAIElgCvx1bKHLJMk+BL9JFI7Q0K7xx8vx9sWcaKpvS1mCIOak2B6+fva7TcqX+ktKh+uN246dKmqP5BBSlE/L61GLjgF5w+afnVbK1X+lNHWEIdWsxa4XEGpI0KEZoQAo+bqkDjBfrWOMwtXEDRaCDuuwDzfr4BqfGWwIH0F0HtDKgarBLYIqMJsJRsSokNggDZC3BxpM0pITcTsy8ZreNNUWdYhLVbBoUNrWsXFLkt57tW+a2tEvIK0xie1qKGnk/Hh2Ea42N2alQeqpDB2wnGLViBbMnaJJLM+o/uk2ZGF55xv4ppq6vY/ZWqkZoEYAyd3RMm48UxdAJX4OLB2/voiiOU+4/uaDDToZ9tjT2B02eognvwkVP+vZLoCLIHXWgSrPXbC5mwPphY1ChHMinJVRU0qMt6SuNVmLAzPi4QNolZFFmcckuYdH2SjXwAGGnF56+37ujl5B3KV2azlPeSnO8EUPat/WfHt/ZjxYYQCq1Z4Nqt7j+AKd1Czjc3SVeFtWtPtyRS2pOxPXEJ70QRMzdXVSIlNU/9ZSHDr2XOVTFWkTkKmrUXlxNuLKRlHhRhMK9Vq4W+Sjq44MiTdEAgJfmu/+mIw7Qcf81KBKsvqhylMqw08HibrktGD6epm5zAU6JcXVhM82Z4BorUG9K3UdQU5Opm6mAWAIOvxE0DtK5U7ADwO38A9F5OvnTAwKrys64ScP1LMhMiiwC2EBI9V0Yl/AGffdM+951wfi+g3weqBny7qcO6qZb1Bar/71COgBjoAVmsF3hFOThCtQnfpUYQV2c9QuujW7aUpIWnScaKTLFVcVvRbKruoVoxWGXcrmInUkGPcObrLFyQCtdI2hqPXdGbqSkhKtOm96pdJyIwAPe9fuuA29UQaj77/3nHlZQi2elDOPFAS5OfMzRq0kpT4yXRwmngXhpme00oJgX2W2TII72AtZ9vVYINCXVvf5jELb9Rq9iYOlarpmBpL4AkI0pnfA+ss+7QPe3/fSntiT0Enn/+sEk+NcLQ6GsTnvIm3P1IURyibKgDQ5DffJpru3C7iWjk+JNVmqhac9gh0/IF51I28tCRE0cStHcS+h1f42Eeh86rgzqVdiRGXih3rVyKWa5BDCKcVuaZUrI56RR1NOZICNzkX4krHY47b+XfC//n/v3GtzfNUFbWzSb53njnOETw8NyN3DjJfWejCI8Emvm7NASuyzScCjosd+co/Ycm5af6CvgFShiiqo32WHdiWoPx+mSR5kun4hJTVBCPdy04I5TeGINM5bI4KRCEt7pOrkVEsHiYYpHl9QJM4VpBE1Z4/NM4Wiv3MEbzWEKhBkFA0Ogp4Wud4yvnl8I+GtPAwIhJrF5cgVWOi37j8TYbbd//KIhqtE9ljkLEirv+gIsR/Im70HGryvsULUVmvbECelyrtCCICQcObbmWn3E0x5izqBUT3Jo8WFzeRIwNPSl6qQVHM4lHc5aeNRQGaCTAPNwWaxk7q7Q2NLzau89ibBpWNzGafIopk68YqIQaFYK43KJhR7CJIfFj2cyJmtksT/SMnqmsakSmDiCgEBC7f7m7GDa1/W5vdmf2NYIHaOGwJdkznoFVD+FkXiwg36A/zrPkeDMyg/4PnzzuuUz5i3DIKTvxzUMmkmj/0FAAP1qJq71Vq42IdVWUJCH6+f7S9ECB9Ubz5XVgpu4Fbg0zqkV6hsVPrkzLKtNqIPO/98q2p5qZgb3179cfdKh2hHBrWt5mmam6hsX7h+/Ncz69PK7eoH83Xrb1ntYIeKRH/wivRMklkFXvHjE21Gwe4XvW2gBqVErsqteJbAky8OBHTki2gC+phz5BoiIsP3rFpT2OByvPjDPiFntSTJUerND94Y+NWLtylAueHT9LhRcyj0IcJZWnF29++tOOCMMSYzOJVeoVFfVSgtc7VbfiqvyQ/2/z0aOZvNWaRYrYpQg23eASBz+dFyJlWodhVzluql6Di3Ec+llWkhxUMDMtA+/qWAqzw9yPAuXg1MlaNoJIE9nAW6mSRiFWNEjhvtxYnEgj2mH2Hc9JXKu1Hdwm9G/0ys61lPZVKDGR3yo5Rphvp2DCZfQnr9x325eQrP91+3hr7S5q5k519X1uzxtUx+KvTqFbncem6V3wZupe9KvAJ1e8jR5fLBcCoOn1/C1QtalFmwpN2VfD4C2cToOF78+beNxo9NuBiuzoJlDBwKURO/Q2bmodAn4lto0tN0rhg5z1LXSXIqTEQbgmw+r/09dW9ED/ymqFZMt2lFMGo9wcKqWWXZUa/vGoIjq7rkdUW6qhatGjiL1VKkSEHcg8EFgZlNE3uoaiCoEnEanHSbgDEikiXIDEFzgfHfeAN76V6ZOCmPUc3pxg8SQyqfUDhWkOm+hyPISD+HCkRxIycSPakJDxskLUQjmoyGLOMPNg1bY4zCtd2IkCA5ZWECE6lMQKyLnGJgLhC7s8i+AWKIAp5G2OXHXcdEDd+tTATekPXKO7fayRvPUHpp+Wu1gcDEjOFYuPAJ2rOrWAd70kI20DKTVCftb6QgIRawNqkRHCQfPPSBCHm6KW4kEO9cuLRrLxABPAlnioali5qG0EpJGfB2YtOccBqSPWEBbM9oj/GY06aoyXpOGQVIFAQ+DoXiHwUAdR8b8glO/tHmjPMCNOwT7AvPNQOzT2SBU4Ht6TRaVnzc8pgRKPJOgA/tiX6L4OQPJw2C6TuzwgeP8TBiWgnzpPMoWSd0sPvoKRcfrElvOvLpRmcvDlFCzebKKtC5Z3W1R3FSInz/FNaihsmI5kbRqVHSNDCD6u88qDBKmms7fZ1g5gx2iAoS8LA3wPUC7A50wUUYK7u+BLQjzxdjIP2w+4gevpHKgRqw6ztgzw3YVwMwWD6i6GAwOIxElm3GizzLnoRx6BEfjSjVkgGnw8SBis8XbEKzi6AMMtFZxtQ5ypvAix+STAwmaorc3l65iyHH06+zVx4AZCrGMOiWR23yCqzy0GQyXaDDrILohWIMVjqqUK/7zcZM1ggZE7spkWB4V0k3uzB8NK+FYstwGsYqbPefIVc5PPiAf9p2CFu6KfmPoh4hyH7SntKfYH1enwBdfQpbwyf0iQxoMzGM4Gm41cqeo41tWMsawK8cVDMxnEEmPlMmx4eiWskRv1pk43bnNvF3JyUuA36iYvQ6zOoqA21NOXn27odcwbgX/aM/6Mo9um1i74yz3jC59z4g+4SDBo0NSoCciyzszyPCpbHRcrCjGAdCMIS1o7O4AbbSBihej2mLEJSBBkCqN5D7OFbOl19RNg5IYhjmwhE2pPFTWBt1iimWo0pTQRIy6fmxjk89mTWBicah+JdpFiibswgaHh5IA5SZoUhaWf0r4+T7J3AvMsSh039cGgAPuxr5CSrMFJcr0n97S/OZXNPTNTY/aNseq7rUEXO4O1RDaD0tKVoE8JWfqdv32oEtUaj36FJpNIGkLKqIUREklXHjuEIBE/08tAtdvCk1wGPcgJPjVpcj7ioSDUxwhlT001ncrK1mzYe+iRyQ5MRm+Vg4Nq97PZ1osc4O4pwag6LcEQUHRymQ7/5Vj32DOoynh8KYgjih/E0WdVQgSM+jPZ4HkX9yGf738D30BVag9n5+F9loRyy8ha2pVzyi/FemmQHIBW9fZujcNsS6sgsDYpRbegW+UCOmuXBAcv/PuCMArMlGbhbXH3amvOIAZ6Tf3ATav7ULfuH/PegIAEOSc9F2oyd7J3VLd03cceSLd1v/mlb1vRpMrdO2/ZhCycas9sLPtPouZnVFrZvgXSxzw+8Jp7daeRvBnUZJAi83JRFkF81j54JGnqtvKuuy6xQmeUhULI5KmrqRO96Y8hVJQNwY8oOW/NzwUPMh9WhZvlIGSkPtKQ5BOEHUqqvgCwvby7TCxtfpTll+tC/jOD40zWKR9pVFZdpzNbzYPjigyEmkXZerJ2pUIgxhKnSFxkG3TohaqkUSMYl/Vspzi6cFD0n7l3lvo2Fr8+gsoXe8yPH9JJ2nad40g6eIuhdI2Cekxl4y2q644FufadchmJZlGwbaZZrbPZKR0fPfEqmZ3D4Fqr0AKXOajYZ+0jpnHeEBiaQ9uaEU7uJldkHdNTyt7s9tW68u68GNaeUl2qYnDyz7LpR5heb6VsjH2crvMk+TWFoU5RJ/JfkpRpWiTzJjGbG490vwlT9bsi+GOIeCLb9Sh1MI4wW5Mf//VKDQgpdxkPSBUVUNOTXGj/Wr5L0BUcbAy8o549Fup1314PDoK/o8WJJT05Th6E4anLq1n1WFpDYXrwdhfC2/mbAQW4hFGWKqKzrPEom4YjOhDqgnT2ZVpfJbJJ8xFxRx0meXF3S25X2UPXbiXu+euNzfxVlhc5tmcWZW0APk1PJWjjrU7F7mCk4n4Y4nZTAztHjFNzwdBCbVRfWs3kJsjprI3vwVESeIqctODxQu0fZPdkbRk9q1rJytIJPxEOpulEhBdbNrkcVD13LPw5fAkrt8dZ/hD8nXpfDumFSGi64G28VyMC9iEIzwODfmB7K6hGdBrcPMNrDErgp3AJtN1NSdYIxr1H628CziQ4BE45NknJxxq6hofIbYGFtF1KiFwi2x5X8kplx/3zH4UQ/q60L4tJU37I9NAYim/M04dXrv+IilrC/+9tMaZ1kDlicw5ygB6s3Zfy7HeyrUrS19vnZX3tJGZjUj6xoz5/JZ4+hAXQr03AZN0iZH2rT0j8A8OIGT3gjtD99cMJ4knP4YzxG7c6zAF+oXb49OQx2VYfu0/q7PXuvqt6H2ia6aHLhBY7AFQfBmbb6IGZt682UTuRXHVZdckrlCcTFvM2+NoC/eRA1KLrbFa+V6Mt4FL5b2v/NJltNG+bvAYQBnzOmC0hyddOryueIm6hmjoyY9yt1FpWZagbH0uAg0wwUdUKRyvRy6sEd8XuJqbuDE8wBPA/Rd+eCPFS0uc2m60dFJPYvJhj7SyAptiFeiPUyb91NzmPVxwsunqo57P3EUbA0VVZsnfYd1/cd56A8o27hb/HzfPNvaHBksoqgEMP2bu/crFSS/2Zj+bLWTN6AqqYg86DSNGytiLHytitwt7h9Vcs+QAzVp96Pd4qn9XF6Ecm9NmSyZaMDk4Yh+VIZMm3HQeFBqhLh7YuYt67cUG/xy6o9f71cJXizpyd9pgxz6scqmkIOpvOT3C/xNjVB9Lw/xen2tfESlmLbpI5BQP2ByqSSdqLB6UM6dzwVHC2eEAnN4k/WvfbuL+3mONCtMO075bDDNgkMyXQAxBrFmDa+NtCdodAHtsqpKsSjonBLgZPl7SMvYkfRXKaJlXGSaOazr7yl7n0pZMy8Fr/ZuYzGWD+3T0xdrWx42bmiOiSgsDKUL8aQLoUhuAKzGSYv6U3FDMJhOITwiqv0ntQ/UFK2KDbLPX8iBdGMZP04nrg5CIWaoq9haG1w8HPbP9AffjYhHcxYIUYUuvxkX3l7bBd5yRIuRdE2ltraUnmiycXCEisisXahptVGNgbmQJiRKUzbkilgXT7bv7cCRWOE94rp0J/Z7yh06ADCwXo3CjETDYsfqiTYYBCuymJ5wJf3ZppLv36b5y36QY9K3TX7qp9H2bsl34AvQyed8bGWco55f0deEqdsaUIuTNLQxJVyqk5FUMwjGysD0GN7YUpR+MAxrRu6BY1XNQW64S4WI0zZwD4RGKiFrqOqbhSI5yJoq3PdPE7mcckPU2P7/gY87rNpfUngiVRMMpNtCyANC/78IQMc/NF/v/H+z1KBK1WaJuRwc6yFbX5L3YiMnKF/C4eHyOxTTcyKw854grh/CjBh4+k7R0/SI7c022jOn2otynu/4bI9j+/3uDvmtQb8iyteyxrTA7tPEirQUsYL4Iefb0iGBEYvbRYvo5Ha5piUtbMmBLiZehj3FyLNN838vYkohD3vb197vBMrRUBl+vqNrQaTQT+ybhnejq1ul2d8V/3plk2l8SvFtmfEpLRzk24ocETClAwKeNBCE1oqu9z3dL23vpC5CNmvrjWTUhbKeakKgTNPOKNbaXpdIZ1NnB2oBGw8+qXUKhJFP9UCgoe4RCHCJhjeIMxzveXpqIhTqCp+C2T19Qn+8oVx1ij9m2jELWuFaWJIUoomTg9T4GJa5uXGRsTyBwn7EfXL1sT4K2qIrpbI83pBnGA1WEIRkKk7gR4iNZFBwOrtZONnOkkmhYC6wvHLlBuTLpm4wFKwsIa3qYzpUaqGUJSKCMEJbusKzDCwVvF+RoGCG/iTTT3Jt/hlFh107IjLnPAlMoEIQbY7b2HKdXbsrB4bNROZWTZwsH1Zdj09Zx3VMsKdVuA8+tu5ZjWS8PcYVIK1jgwG9Squfjinb+qmfnMtpYU+nJeow/OjI7GHOspjPHbWbyPLjtuTalO5KignmftabHPKanlqKOKd1iZyGkqszNifBcIhBCBGA8eyizvd/DlPPIE/ar6oGb1XcaYO50C2ASxlnb3AYwm8co+FqavH2p8f9iqjXJ88PP43J8hMOEByhxzQ6rb67vLLZnma9VUL4rKA4QZgQeGiWQB3gyMokZrcj2bXNYf390cqWHnIqTDwcQQvn6RN10ZVyd+4GIbinGD8NY9cBJNgRhfg4tOmJVg6l/Y6e7q9MIco8Ojp/olTCszl3JQDADq2B5uKbrlkrk5I+vOhiJvk+W5eJXcQLIZKweh1yMydBelpZDkUySzbnEFrnnUw8XWn9zRmWse3w7HkoPW1a40Ana7BmuncffjhHtFq2MEMjjcLjlD4AdekEP3GpUAQ4Y5AHNaiZNDiT9Gm4QAVdqGW995w+d3hnlcITsGUofoJn5HPsVPKe8M6HK6mLFHSe/lxL+xzPZ4Pg0Ds7rlwIXUM4Eqphnpxc7XQL2lAWIhLmOFrEITLjsNVekN6i2PDKc20pfHbkFwJF1RpxiFAEP8qyBz/x8om52qxJSWCP3OJWtBpaTEujhsCMhrk3EJ6K81Kwy8A8GMF5MTLteMdf+utLevLh/UMmT6A8CZ+CMNE884dOTtNDUNtfTfkXBJUyy0Uh2Q1eFhKo6kqRDB0vxQ+cYMS66c/CtwDiEuuzDM6s65xBIEEnRjSnqIPmc3ayzaIJ4l08EMcXBq8SIzZpbMC4S38QhovQfV/D5zHETGMw4PESBDjlcXUBPPBYjWQtGwDDNrzsba8g4dOZ5A0IKK5e7J/v5+39fIgn4ZLNY/lhC76hA6SeKur/WKzZ1nyrNr2LQN3Vyyb84MzfgKkGvEfeAU33wP+tGzp7r9wDx74HZt0DsAEN3RA1mOirbJhF0/zWD8upN3svlOa7rG+8NzSZG0/6MwZ+EVrriDnrMBT6JK4XGU4shZkvMX8Qmo8rdig6i4LW6iU5V4bSbQGTVsM/EQBTvV5VDfANjGcmDhc2dx3W03fHjaTlJRjibmGOu4bptHvV6abtnQcLTad5TbXZmStt9dvi2eQ+a78PpUs/HTXtp8J47v2+qD8HxdHKZZj4d0/6GBmfwStMpSa+MOt9XbL6ciT1x/PyULQ4dUtWvk3mdEJ/mLws3CVMRTK3G7UnqF9JWW55fWZu12fX144qpyFHm3SpMHD99ZaZd3rSnL78OdfLTl5x5mpAw51yaPWd6pQuFZf5cFWUy6zekbjGE85vKChnsr56kh0K5Wmh3Nhv3u/UNRIIpK7hEpS4GXLcwZ2Ibu606SKCCfJZRlynoY2IBi3qAzV+HBgG5sHD2DRYxs329p12C9enu26tV0zIe7LjnbIsykQu9X6MPvHIMCoM7lEGvO95FaXVMozLbzwLtm/rj+s/XR/kR7GGkjwuoRINw1GofnmUP6cfUYLojvdP6E76pkE/nyftoSIKIgVDcC/a5y3BYTuXYRHrMjBzsS5JGIQjbS9/fSSaniC3WNDvLMB7UIUZYCc8yuZnBhQccimS5ncvEFaeUL2raYyBRU0gYOCxzZkoCwdFF+PPtCKjgomEC0FATNUYDRoOw1V7C/Y6At1EZ9PKTsJhS9GKCEtjmA739eGmsttgxkRiE2ce1F+cfcZUwJrMJUojiKYY7Nh1QoIHbAqrSz1UHjkFhg+76Qvb1VTbpIrswAHnkExYt5q/BvGuaRFKo5MVOZgR9+ZJ3Il4yxAFSqII3myUQ9k5dz4cYbWmFbd4s4na0zCkIwY0PbGtMq+EAcqilbirjq2wRDpgpC9CrWfeZxu7PobsOMB4K5UG03xBzK8a/A57E3KYEIqXhtQoTBdqp5dvFiUIj+hLX+qt3il7FAWUxCmW2a0i1sDJxFL98oKIJaXsCWgNNR2tIYJOKomwuQ+u8shlKwqtIifi92TdLYkRf3naV33/Lba//CZREZO2kOH+B1n0Xug9CYTZTks845TYvs/esrIg92RpVdznMg5teApj+Sxc+YXR64VBneTsXNF2GMEPdZJmEYWRlNGunXo4xUMxjCfSfIzDlkzteaX8gIwBjFu1u8ZqPWjjj3xhu4ZiS1Ytap+lKQCl5Ry3+88Y/feSL4TJKSk2NaNRSqwJQo35wE0Db3/5SN/CK79DWCNZdhcV+VTNResWUVPwsjTomqgxBEM1lsf6DmJn+Zuvf7nnEhkCG4w05T7/hxm2DHhbCNJdiiZqpXDjBszkPYGc+orb4pdcpvU/QK+E724tEQrmk9QKAQbc9p8m3iGJ7WzR1GoqkGRB5eFUCmpi0STydjD5o2MHu+QyVvmZNoHXMj7klhIkzkPlestEc1er8uzPZHdmqoynnD5tQOr9jUQvVj5BpWkPUln+GeKBCdUGxiNAacTJOWdMk2Obf8h/8LZgHvhCWHZ5zz9W215svZC8zFszyCxTrfRkHgO0q5WLPD0rlP2dx/jMR1bvUmzzA6/k4vXuigJlzpF224oE64WPBqLlmRhf2Yyjl1VGpfuwqL+Xa+QUH9pKUb2R4fm5OTGnlSottB3bsFp7hkYb2wFYEUB8CUpoNm1YxuDgrsCSoFU98gax7JVKUh/sxDP1deALHmMMmsATDODcTJu0iWrllaehDL75+sjioiO5t+Njb3vTZbtKH8q7tz0+jiKyhj6sb9JetVp9BrFsiVXXv6icH7/ucrldceFxgRyaGzy2UHhlRbJ0beDxjTID16r5lD7vtmsZ8LaSpRCPpXFpfyp+NWvNcpm+8tV392or6VxFqiBPkGSrTE1PJBA5bSGE7cYJr5kzzDKqMKa+9iYqyyzdT1aMopG5W+EO+QVzJdrQabTvxbx/px19yUUnpEtpt6VCRRUdeoJaa/11r0fXl9cHjjcF0MS9s3uAM6BiT6BL1rl3hOZVgD5tYCvu9hoTDgkSAKZnUuEappY2SfWK07kAeKp/cy92jPvdf2WKmqzCmm7Q+tWScph8fdL+LNKYRX5ysobhxVNusoiX77Tga4fsfoSSrnXQQ8HxHEfAydIB7eKywTNS1Aj8LhThJ8zKHtQgj/7w78pRNNWH3+gotD0Ury2j+qCEJV+w+7KLmWb1DikYv1tobpv1HtUBYlnYQF/hxLYn9fCCu3up5Ozeg5XdX3si75ie2y0qzvS/7qdPHIfsfr9oDZIRnW9Tq2ylBvg+e2ftMi23e9QkRu1ZMy1eqv+ShDavrQT4c/peKrpJMR1p/10N61F8f2cmbICrFowGEwKN6/D+cnOsRRvm7KW5p4WyctlRcZxM6CRxRJDqhxT0ZXef83zJy5DHVCIev1h9KrdUA1ISJJqddFsTKAoiBOcTWknii7UfjuElHKL7NjGXKpXT4jiI8c0FF5U+b6Akn+ajHBWKecuURBAxAtNr+nmLXCkisqwxOw9eroRC8RA0bMXgAgY/JDzKrGZRON4emNGAe/oO+IzcA2j2ppi9a1JMRKjkTEhB+xL2Dx/rnXt4W+q4XyjrbIz00S7JrWAdpYamb5tBzVVfRoHY1k3fVEKRTr9MHzRXRMU0k5Cih13pYMFaTvppCsLie3XJGy5SSYRdTuAUTix9CKfxiiE7uFuWTQ97HycKFt0GKqlj2joX2H6QSVxcCe/yZ4UqJBTDgAGyPWB4I7oypNdrAAvTPUJFMxqk0hbSdLaxbzA4tskcaytumYCVVjRnYB4iuTo+8EvD6IEevarDhp75PSGZsoiU0AyYJhW5pi4lAoRm7YrgEh8San//ZdmVg0gb1azckeyfHnrm0MVKRK1uElv+mzPuHIbq7Fmy//FipYGlR9XwzzTO+M0QzNf0f1GTyJR22tldu2e/q+LT5hxEfdOpd0uXxBnaT0gK7Q1EamtbEy8+eRNyC/FWIR6qcwpTU+WtQnCtkrsUqHhoKvkM+6n9oBNs00yspXL3p/F7J3dvYoSFVz2fhqa+T7zPw3JjyKxol2/1ZWaQDWy9HrpYberY/1WeKk0LQHiXDICTZLgXucpML+tyFhjRHlB/WQ6FqnCh9xp2EdfSFyWFrxyD6tyKS8oru16unI/4SluQUbqz68FMyN+4jxZxyH1FtWAXGNscuovjg4F08rn7Wdeq0JAjw11qq6c/5Enyl8diOJCSc3K5HvRATaQErj5Ma8MWF0vdYx90a6i6FZ0Xo3o8DU2QwK+KHknh1YK+U9EQjf0kopqwq6LqGxex+6C1cPUMODoSocISRcZGKjIoJNhNi3RPf7onZsfCiG7M3sL++XJFhIt6wXu2I953WGgnyvV2rnvdl1gheWs3EvvgXKrmnOJx9gW90HMyhdfloE13bl9k+3xPlyQ4gaUkeEtoSRss4Y5o9amk3F9plSUcTgxsgbWT5LmKho/hmZHqSODexFWyJb3Udc2ezxLb+vhYhf3PnIJMEUVt0eOlZvZf12V5hHvnvbSUy6/lGmVWtyETfxgqvPjLtj+qZyz17Hbb3ZDMRDC2a+WOFvBA2TkednlB4LsYMy3KpDBlC5klflaPTE9EdPiYruXLSkn5EWjZnRKgTbGJEN6uTF620JTVGe9PdSzi36faFfhJqdWp/CpuWe/YaxwZtnZ1Y/DHO+n6bPA8BmcV4ew8ntda/51pNUwLK/o7o512wqQbz874vP/RL21wHh63VN5C47QTo0LbslSdjL62TBY+hyp7AkaLRJXoedb/kNQ9NbZ2984WdSt00YvpJVoevutT+cA+BIuHFd3cD751rSoR2Xp5iUWtP9eaF1zMAXXQ0o87fR06/GCZ8l2qDFbLVHn1zAO38regOur4RtkPmk32XK4XrwhurZ6bOedQf6yNAOBRxR856YclX05l6qHBq1XtTFRE6KOCdRb8Khky9u5O/zJWrNw84SdmXSQs9Ie9FPx4lbiFIdfsvDQd/ujyD1rU+YTbf7uA9ryeBEsf3dK+Xtai0PbDtIf2f41VAZTv/S5w2N8uKHad91HXEKh9JpiP+hhST8mcPSG6Lobt+RlpYwZ9lEyA6BT60b+LCnqiNrw3MyldKIjmUUimns/NxYVzG+VX8y1CvL24O2NaVJDoQRcP2lPdS0nyVRdfrylfMyFH39QTl0rFrDY5v8ijaDGTmRsHCnlE6UfBKjMsCXSHDZOMJ9VpXgLXsyUudqb51elo2zka+uTTzz6ijmzTdM6xwJI2NyHrJq4FsFPMF2sMdt9eLAcZuRK5NfCs9JMhc7brlTC+MGD2oHeQ2l3f/hAGRcitMS7Dq8XTBbp5NBAP3/s3VeU4n735e/+KL7nnA3f8k2+5401FRXqwFdMn7eJVufxrJZU6KgzxKK/7bc3sUJRxySxP0+c1oHnHYSvwU5lyNdV5albyoUmKTweWr71j8nb869xNt/l5Esc8q3x2V+cebi2fKzr6kOhuet6arE+30Gud1NBbnF90ruH2xzU5YrsWSyAwTp5pkwOTnhoyrmvQpdB3jx2UVW5N52vo2ycXoQoLa5BI7tSsLSXN9Kf30l/0nPMXVHEnUTJpVk4koFQ6OTmQ8bTPkrS8nk7znnGWVXgRi4annYOuxW2uPYnBfPjnq8B6V5piEXhQe79hiOpQbsI9L/PHb5BWE2+WksLnmpi1bbeeSSKmkuubB/4JiDGdCMlxpIy9UcoMKiBxoPKbxCfH5Mqp4n93COX42lvUNAdFKMu9oMWEWzLBZ0qUMBKOfEqUc52UMBa5lSrfQY2MzN5W0UVxIlpYswGnjmT7+K7JKqf5a1xLSk3y1D0wKmw2mZp2cZaKDZN7ARTc1w8nCpnAzTOfM66vBB2mbZh1iquX57kvgqecquXXjDWlcnJ/2XEkfnfy+KKgHHMcTNpImphgZid1qYktNEkJ7eV3XWsv75Z863fXLRU273k/jaUT9MLPNUsciQdNBPo+SkjFsjJxlJNLjSsT0HXw/aVXBq4G0Prt66tlxji1IcuNL9kfgP3eZeCJb+K7Z/sgx7fffs+N+Ybul3wI2Dg4+U2GGfDaZCbIKX++b56bzMGP9NP87e6fDEy/jj5af899BJvhvqgC3Zf/sn6tfYwR76RVJMqs70Y/1jU93t/8KnEqinuhH0aT7/5rTbf76SOhWf/iK0t9M7QtvG8gev8x5L1Pev/dGtozWE7PM/eTX3xBwPk8b1wLlc45qi5x6xBjYILrMed3gqrstmB6UmLz9l75k3j+aT0Od3hOgLz0RqH78gzfqWiHQWS4g9ydyZhAZ90KGtOcIvXocQ5WO03X4ukB+Ua13dwQ7xuBeeFF6j0WNOyRz/jX9PmpdTR82j7XqDpv/GFYCav7PzI8PXgaGf1+bbXvit/412bwdw/7Bw2Wv3caAOQF/9ZRHsMXxq+L47tgj0/I17GIezyN5PHk94V07I8dQW2/gVMLmdQXRweXbUiup74+JOvMHKnxBW9isXoSW+EVRkalN7HYDzF562zfi/hHfbS8M59IwZejthZcExepvV5+u3HDIOEpM1ivLMAhhr9p0cE+moRi12cYQgkii6m3b8x3+KXn7mGr0ml5APEXnizg2UowWB28WAFeLczbBqOFeT+Yf2vgAyYrwKcF/Lda87VMXub7BT8W5nfB7D8QxptlYD1jChocRinMg+TIHHIe8gwls3tA8sS6tqxYNOTIKBH6aKUNYsBDcJhHO7BIYhzLHIgc1zyxEos2aCr5NnJEAWrR0CHjJRqRrD5uQ+QnOghCTsTc7glao9MeTYimALO7Ms9IYOly8p4MMkmjScBuQYMTTYIlD30tOTIWz0GnyEyBdWwwAABDCW0pGipyV4m1QawTtlEgNmKQNwgtcBpCpUPjFCAFZyOiUQGwYMgIOpYwxylBtMzUxaNoL3Q4i3BiAz1mK7OFF2xKvswvuW4dlCdEhOueIXt6wDyj5RmbIFyPzDWIqle0dKRCaBO/JrTFJ7QsSZPQdvxokG12l1gZaYKjHXjJni7NLbByyXoQ2szCAu3UH9Byjc+yy75AS2UzCPvAEbG05jNa7tiMwl6ZE2Sv7Nl3VJTXpF7YGz8a5Er9I1reUs6FfcGpBmnwA9ocOvU92jx16u/Q5rlTf4s2Q6f+Bm1eOvUd2rx26q/R5q1T/w9txk5djzbvnfortPnXqd+jzUenvkWbqVO+OP/ZQ5O7exX8mPDdiXBJkzsN8FYecL3rXwWzHLg/Ef4S3H0VmFng+6vARQ58/yq4sMD3J8KFBvdQBc5y4IerwJkFfngVnGloVAOqYVlPRgPZinHOz2RLPnp60kVk7PlgWcmoznLmGAMb+TFvczq2+pz91wyPSQqrv3ie07nWE88Tuf9H6jqcEuTxp0C38hFjMkznvMNHwglmERO8siiRNBKzqMMviOnZzu5hRCZj/7hB/neKy1+FJ9SPOSUipVcMP2ICAIuehjkFEaXjjMJIcX4+eJyieMxQJKHO51BEwnWdxCaOCreCQr3+MO1p0Y7MoNiZowKO56GCEGw33SB6gQwqDLLdhhsr4fqsJgw0K+7MB8L1QF/tyF077hQ04LemUukH2OfVHqnqfxZdUNJugh/v9AqOAesSMLDwNijEMR2ZsQp7tvdqDRsjWKFk0pCsUdVN8ZQXyNLyDjYeGq0sT6aW5b97BQUULUk5BOYCVglGBBaKITUKOKx0CjuQJJ9CaQWFupdgkJV0MlFH9CKsEgy14pEIDQNLcBj3sCUUjUcNSVjZbOn8vfrkeQhhL5BejCEKuYJDIY72GTdI1ZBmDD0jDipWmmfvIoxQ7q9viqul3MKXQERZeDskimUUdUDKsjD4kB+jE8C5r9kly4yeP3aIJxJkpv8sTRK6kUMZKtjn+IYItN5AkR0p3vSvHRMjKIhw1Cu4RxBCG0DrDUJYAhV+EypogWqpBRJGeF1BAmdTRQk+xpEQzbiWR3MHJ0XtSN0JwhYGTqEu0LyEhbofIahtPYrt+LPUdWalmn9X4KZTHOgGO4eJx3mEwlsorxEZs1Bg4Z9P5oPNWYjAFaoZVaHGiEi/3iRhOymbFc6b6YznY+MOUB5WSr2YNFKo6cHxGHubB5Ptavr2obUC+9WCwYtjKTFDGazQLNQJCfZWZmXbHRI9vPmk0M1+4r/Ghx8LKrt+4z/j+/hxjL0jOS7vhx8gTzg2KFpshVCxJQmnngFkbw+F12PQ/Py1ngxwBUevIJplV5LAwZDicMVQAWmmnx+RcWtxvNiBlyQhIVAizj/buh5YyRNgC1WelBRw3I7MDG6vqQQqYMgDzXXcdFZGGYYzjzuMUHA1Oz4ukkt4IRFnDYtXa+Kbzd0rpXonzmTymTHIbvRbi+C8VxhyCyPE8PprXZKS4vujRwR3Ezz8XCaS0FYARaTk9XjrllzPFz2eolAFXgbKUy5KW23r16ivJsKGBMykxxLiqrbegbfiq25MYgJpXsmo8gOATHPBoTH+WieCyIMiEY7vZ4NEmTt+fBdhhZxRZEgBhUzCXkimUO58JfL1vjV7RLBZ4szUySCz8noK7aykk19K922oaUiu5I/RTKY3AjZgLaxocU4dqxIzxB9iT6OgiB2E7MPCdp02VYdUyisY+vWF+3GC4ywafO+jAYerwJJZnEhSjhPRoBCL4SjLDQnYrhh2MAyoBbC9zKhrYmOlRjjwJINHaYHaghnlrIA6G8RVKSgU1CXVZijMhQdYqDe7vsk3/3DMBXWxBvgTS9ShlSsXrP8ZgeyXDQzYzo0myNDeaCVqLHXbVmwzu7RusmA67e29eU24ViPKvi3+0Pi0DNLJtCdc+4FU1q/wJ+JSnHJ6CAqzJuI3KP5rqLKJN9G09+enjr85nojsieCJC+x0pSUIAwi6lCp2j0TSiLXnCUE8OO/37u3GHBUihgQzdZy9hwUKOp8nUJHc+eBhBkd7WV2TsykULM32otIdPG8nRbOXO7f9agrF2RQReLAlK1OcqEl1Db8TnZFGjyaMK4/vhKNMdIDH+TkHjYateNh7hQhVpFhjvImGORsKVpmNiKqM4S5vOmoABU8CA1hPE3NlET1FAYV5EXwgbATD/jIpRiuWUh+NkIhrOhdhbGnoqq13NkpnIc+MRfC6CTZCiujM6+9BB08SeoLpQeEwbcdYMiIqhTGcj5AJs+bJt5YLrWWApcEsDSg6+4y1d5Vl+wOBUbarmvXLEWB2vfWSrjSn6U3UZxJ4IfNaerqFWbLf5RjsPomY5Pb3zYIO9LkWFzdmVKCaBkOCQs9kGJzFGLyCZnXa2KRWoJmbMNk3OprLRAahnPiHxaQpVre7v1DcEHmBNubkuzVHlgkoqvQkuwYl2sUddF45OGiBFJxw2a4LDUkOZhSxBvNIYloaDT3KIydIoCuxKIALGpkxwvnPHgrwhM4MV3AMHK48fmhQEP2/+9zFJiU3zPD8/Wqy15r//2szWwY9DXWSS71+vn/v1oArcyh7fTMYOfvWi1t60AAd0serKoj84+LTba+yvWhg3cjjlj08MR6kWwzuGDXhUfluK5FjJwOaTsNAeX+h4QGWQ/v4lXBMJq+vbzKZtz1EyspTEIEaGtiFYIFiVO/AIXpw6beNiFGCRbD7FM6BZtODwWrmOZmz1h2DQkfAmngCdXXey4TuhzKSPE7kY6x8lePeAM7vJYNAU2Im22oFDkuAIMPgN2HFwZnzvx2B08pctjLBucUnRHodr8TrQN/tgm24gAU8ATpzJLMTVxDLVSXctXUFpZM6BZz8YYxYRqSc+RJvR3mujj3Am+58RFezn8riLo+kjzB0StsERVmcvFdao8I82RFAkHoscOXMPpam3aBAC3sDzksHpvzOACSRWEtG9kdwp4yH/WIcnGptFQrcX0L0X2fADIURTsPQSnomIfpj3GLDspvzPBovexIQF22zkFelms7HSnb71ciulZsUUs81JKHLEnwuV7T2kVq7Kbt3squsmPgo2cHBbNanu8moRw0jRhb0tFl6VKMjbMbZfTcOW+Z3c2RdzvTj4wXYhA7WHT44MaavP4J3f84uyiX8A8ONQPE9VkZfl2oSadpWj+exa07XL1du/cCIa6OU4Yof5r2g1g3wLPSZwKB2WNcdPCvG3tiRb+1SnFkX0xRSew2jgtpQVLRFbBFfr+xC1bFr/L9FPwBElElLtisGZ+4OxPpKBrR+QhHCFG3oVnKbp3RwiDe1LsUwcVdlp5Awn0hxtvVVo47ZpsKwZKMzGkERr5RvQEGSOgkGNtz3VXiCn+k2XsW7EX2V+uZwubICvNPCEKxDg6KpeoL+muvWsXWdR7bng4IiZrFJw1iCAWU70HUHH2339/6bq0JV1zUSaDA3dWyUWIkCdpbPkHbVsQBDbUMng0MRG9F1jXZVCYjt9LrappVOzGhRYUCHtGHE/+jbYrtDkojzr0eBERKMnEJxOKubn2bkWgG0EBpi2BogtFEChk6txdG5CNwiNq2tzUekxfcsqNEdDumr+/pvdpzbzpg4Mv4V84RAv0zD10twqtotJKRHFPJ1SszS413VKyQSfQVlK4iilM13DTyr3Ix6k7OKgr7xzlHRYZGS95zW6FXqDsXQ2gsZNv4GAm3KizdILCSmgSrL5TBkiGvs9BWf15RXeNr9sdT8X1UYqvuBVMl3GoXDKIgPhj/vaJp8L6VMUfOb3QxAjoWsp7WiLDbU0toTrbrOnj4zqE1adSNDEYnTI+lev5vyA4q40GuHJByXWBxtuqxka7hDqvIVpFdqAzkEEXXphxvP+eKE8o+9wecyTxyCAr1+VPbmg/dX0n1NYFRm+zfX7dpMBqdQg0/uxGdZQZJwJAKn4TrjFDvvefPgZ8VAvh2pnVwzoZGq52A1pEVFqbhWBgszWbJWAuvLNkZlip2toeYI/uJBwRha8wSoDY3Zfh3UkR190UGz+KJaCmHIwkvuSVheBQvI4Ekx4ukJys9MERe+0Cl0qxg1WF/qhSGdEf/2dYWjaDg17brh8810VGGkNrRth5IiO+dHMamin0WVL0eGVOFnqSliaxpJfc9nD3xGAY1H3n85eBqvHCz70Lh3m10C88ZFPtcycmzhAaXXRfeYuj62/K2ytGw4zY7qgT3nAKXjcMUzRIlJvLMChQDcTNLRhmwtkrsp2thOU/b6YGg2qWCCo42Yhiu3K2H6TPwxyf2weVTbWBHoNVq5YjFya+mdJxsVwcWBJ4TOHPw3DN29AWcUlnJ8wLFgUGwJPaKJjzHtscFKooSh3ctwSNO7GcDpxz254LcYAxFIFSSwVU86cymWCrN8rINsc8KAa97SeT9XBrDi0V4d9NMkwQXKuDRqtBbxh/TEdhkLY9Um1o3pAAMvtg4QwHPV+QKw+33sIa8noCGx0ydjMOSHI+mjE8DuPjoB8AKMizhYsMFm4HGJAbQ2/kA5GLTBREGEnKKwI7Z6hnjCTiwgdv8jsYcH0gBFCTDWnkNlDClKEbwAjqqZ/IjmGSVd0A/cVH+3Bo/xNO+2wSvWyfl6mNPRc/A8lqIDPtBcQtYfx28ijs0rszgrJXQf9ysUsJQirwadiMXNP6VS87B4N6jw3m2Hepwo+hOeTXSGaVuJ3+KO2WCt6nBriLHJNr51/FhX65JxcEghTQc9JYlcGpDGXmkSB4s4wQVxyoy4AqNb5Vx1GH9QXUid98skSgbWRkNCop8WPziGLvmYxANl0XoAdn8LgTRTdaye0ei8YRj6cQ0niIOQRW/ax9oiza2EaixPsLveNefx+EAk2p0ELpAjZ+SBzldUkDcdiIGkGgQ2oDFjBG0UgZV9jEj+XH459zScrjzlQ+CMfmRHVN3tlFdnefw+FbwdR9vvxIpIlCUn06ieFwddwpAcgZ2RPkzzhiWsDpXuNZqMlF4j0L2lGYbHiHPoqW+QBveLp5BIBWr/DFY3T6CBoetiMe0OFSl6iAxuB0ETyLsM7xeAYYS4DhhM67BpOQO3aNb2cdgjyQpsNYyAVQygPYkNbAxR8vgtxz9HArcIBqaNuimVEAIpJ1MHn+L3lgOJSA629yml/3AnNvpi2igm7RaWwNESedTHqxXYM0PZdYkTkpYtjKvHCC6kQBSvRxDvQjRuYahZIU3RZw1OEboPp8CYNdL4MIavWhu+1MpqX5WVYLuNSxbxoW/ccPNmwwQ1E+sZoxRZnjSEKjPd1YuuK6me62ihbagPCYTXogw1qbfHIjiiMJ4Om/cJITdCLCVI9iLMUAiiSrVse8RFmHd6YW+BVI2R1GyOg2fBlT6qwzhHUxHB4VKPp5FP0DSi3kJbxkyeudPVmjiqlDRP2PG8FZ1dJqUkTnI6KQLer0bFtoH1LDj7zNwBxaHUIbIKnyZIpGcILDQ0ghvQNOEMxdZn9hzoj7D9dAjg+U5gxSyaRB7jszjbFGp8nT1tEnuzVrG2MHlV5H5pktCdsmJGVYHR7itmicKUyAOfuv/9ozU6IAx4XzSmkaLoK8HSjiGO007rbV1H54MX6wA7c6DLUSJOob4KlV8IGiO6d47IJiP5oxdV1kQHJlGJZd89OuujB5ZEs5ixe4LN0cKywRXr0c7W6MrKaBGbYvdzsBv4Votk6Zh4vKims2mqKdAghsWbHlZMZkC00dLiiqJb+jmU1DKSWYWjZiqgilh5fnBDvhwOfosLQL4C8glrAz85qMr+3vfnEKu4nScQLGcRDAggQ1CtNUHiXB42XBni2AXhnQ6ScCzxAQRJPXaTC+YR4YWz8A085Vl4N4FziFVZyohk44O1DQK3iiCBjORgC+0AfUTkR5KnwCDzlYJloakbXfjUjiKA1s6bCQGgiTIQAGA+k3Z3NpqHMzmVM2VRVTuns4AuPJN/hiPVlP0xBT+lXr1HKfuusjZAkX79uGE36u+Qb9rBsTeIiD7+23ujgkdGPpLPzvA0zg3scljRKhKIwtXkLVE6OCCMqc4s/SNw2fnp++1lyDG2vWxncKKbTUO2J4jwVuo79OZbrjES559jtr+dZz/k+HNG+S9HGMiFGJDmjaF/lYFIbhRvOS2THtHOAV06UFdA2Q0lDRr7+p4l9ZpEgwIdZz6Ds/divz+iKn7PfxT0ooEoHY3nTwh2jXk7Svq3VhDUA8YHyERGTk+/LVQSEa7WwO7pyvlFUkJyammCrA6OAD1NQefjOVwNK9ea+AZd3ERT6Z/zZ9sS/1j67bqvTtGgWef02+YyQY68CQUqFt0MLXcf29YEGWo11GYBXioRJgd3ybEGU0YTBbTXCmBwV2Ecd/9v4FbfZ1ZOhI2VhzWnKMk80NdzZkLJ2L/8YeQPPb3TN9Rr3UFnnA3Qf0in4+7DTZBvRMXLoZ+05skq1vtsTutV/ZiOq8nXaXYDoe1LmCKxo5bjZT8yjvml2+RQRglUPLXIpWOGZ4b6D5TwuvkBFmuH4RLI/dOYTylzklscVTatoOtZhShKTpwFt9PoTysnTWgN+owIjdHqJs+v85WzLR04qHlISq1HbGYv+ZDS2nA2iEzSSkb4YBPsTiynBYxFNjLu6xqhFmZ/SG0A+8y6wlcNLmo0faCWKo0kyXG699LGAckkJiIhG0LDtoF9f8C1CLXHI5fI7mIjp8Nac1gQXXklRGFWm9KA5STgKPRjAUnDA/wpS9+sEHIxmhBWRU0bDKSHJQTL/B/YQRNCGDEb4YSOy2fmFMh1mM0FDu2EvlyvOQGN4FWsvCzclUNacUfsaG3iOR5ZcvoRTWVLvAOoPsvpeCXoivcun5xGAZZ3ca4qCJYDw8+3ge9AOC4QDfUsykJzQhlbhJ65LRLCSONNn/oKn4CBeBq7pj42ly+weaeQd8ic+0jZAnh/hZzFxqsGxtGw01d9wxUZMa6ChHbDIbnGR+ZGtl5xgJyRBcUFy824OexyCuqTVNcvxhqFPrFMqT4Rj3D5xVdHykJv5kZ72D06gA/3QtPHW2tncJtGRSbQCFPOcK/hYtDh0XKZIHA/n89bjjutoqhK8sQ+TnZ2VJ5WbROH0IoWGEegUtaZilFI8TV9hzRTUTLzvA7aSr+0d2BrGEEvE5I0DLNklv8cT4UsHxa2zVuem/R0OigXYdc94eYlx7l2s3dcs2AXb1t/H3dzg+762rcUp0cmIhAyT0mzVdhdZoiQY2SW0mHHCFCwxdjgGbK1Xs6NbLQTyS1ClxhDsBmAavbXWTcYiEDLXh06P0BFbGAfdp6nnpqa5zrK4JRmszKNDc0Lidx2vsLobSDmM4nghR4iib+QJjNYcgR151Nkn8HAChGykLPmSfFI3CW1fFTmv/Md5ZEJ+OQNT5+W2QpMR0RwsNgKx5t3zb6OjAOXr3Tvo6WeWYO0KTBDUYJIEtE8yRlNgyJ0kiyp9FKbW4T6ESuAREQgeF85HUC3X/4BA7UBVlHsEtcfYYBUcsrj1U+tmE47rzi6SVH7NvDQrCl6Ft1WVbHTycy8OOb+/s+BVoLjQHCsbRSqoR/45XMyKXEauUuhT3hy+mbv+9FWcIlryxtWeXMt3anyl0FazaK0S4cUlk9gGfpTNveUo7/aPLZn07FQ0tSR30ES6heroykQVbR5cW/eLBOlwVn8ScBVpZRecTOIieGuVBzII5jWeL7GLdJU8Qmhz0BNfQk2JfBzboD3QSfWxtrZITa5Xb14dlfZYLeZWb2C9tGmMdhbdItpbSMvSBJnW4TqJsando+3GVyTNDbJCZe6wBZntcHrJYF+x4RoI+utnKI5ouIO6zABEz4HB4k040/61EzMGlNR5HUARPYcwVcEO4Kby0Mturi10bX4lwfTYqvFu8hwUZYdwAoENDnZkwXQczpCEPRl+sxiZEYrUU3mD4PKEfQPMF4jtguhnWg2N7M3vREMpb3TmDcNwF/spCmNkwdoMtumzFf400eknbVrNypUNCmJTn9Uwz005JBAfWewTAKmsII54j+4rS7TOjRLdB+J3zhGClwFMxEBn2E7OcWVkBKYQwU0Z+9GG0jDtmtfCXSDI/v3pMo5IiDliMkdumU4lcg9LG4xrrPywDMfT9oNVXyu8J21beXDgmhRdrmxgM530+SP98Hx2zUrbLv6JA/Y3XDwspW9lKfd0fo7vkg9yIVlaylDfZ7tOQArk4lDSZEWPYIienTesitjk9YW+2/7pfwO0ZpsZe7HVyaJbtsLkc5thyWgr7di9Do57f3CSeYri/QP5eWYSGx0wiQApUa2bVbN5SANgs87ulxdj2+uBUF02eEnkoPSXexb1KJnGjcxufMOJmSygw+2vFI9LKd26bmpnLfqKhEzmW3gvQV5s0CKJRVzPZ7OziDOUH/dZnOZjgK6nMb+6BqQl/LCNLbn3XV5oo/qatJuc2gWXQVZbPHTDK39O/2Tu+r8Y+FXnt9HIv5D6XdJ9RCzMFTVOC/f/LAWEjxliKShF7AaZ80eA/RyLjFaXJWenQsVsFVNymUu7HgaAGYFNDs7LoJ9j+H4IqfV+A3f4zr7eVtsI9e44SNz/2UuAOo/Dwq8N2tC+PnaQWMFZNNbOe0RFhZT9E7PxP/nIxymn8wfYw8u7dFh/7rmr+K4cQZmoPcp8zbrKc3ma8QvnoAPfXA6jBSLu7W7+tko6CXMF01wCXtNI8MqeKX99IaJXJblk8y5+ORgQiTPj8zpt9c4iu/fnyiUs8W6WLbMlPLy1seHZWcalWvMgqKm8jxiR4iQj/J96wm900N0mK4isQmBiqxK0i+EicmVyXeBqLhen6OWDgk7snjPicrgs9VM0lk+YlZnJlenullT0M2wpL4f4oCd2lhJSfi5aJ7fWlespBAyAOFqJtHsRqLxxtidSppDTSxG3h+hCJGbcJKovxEkUJlMrfTMx3I6ItbFRyJCRAJikqhPJZqsvNlcBJoZ+tdUEYuDb/dXRGiStMcR37zWQmgUW3q4O8h4UCcRSgWaTHxXyyr/+nk8IAbMqndmvOvMEiqtSXIrQ+/jIHRBsg2UmxRqSSMHahIch79ebnCKaP/NvsGLWDshJdVS/31E0LqZFI6TNSXRsPJJEGgbPeaOArI7KQcPmEnSY20v2xHBNau7T+Dqf8Hz17Nun4WcFd/HSySXk322+mm5nP1fX6PFmmUHi4kPzMJFAnMb5JdXUkW1M//0nB02zXVrwDHHaDteQ5qxobvLD8rdJYUve7WwwWRma9kUcfJLUmxaC1Kellm84oHGLXQ/J4HuTxCmD9TiTnckQCvuK2vcu5VAqwf5DTCNkttFTdHrD9q/TBwccRpuGSaH0aekcjXbmNebQ0X7ORtV2PiOubxvm8Axcguxj8xtsk0fXjSOdEkYc24W6srxKB0w414lof9uljdFP+G8pb/VSG2UbMZi0UNtkI/oBfCf33CZI3Q5si0PoLNm4n0N1+kNCMaJldaShC4xN34ijzt8lNaGu92OnljrAzcP+voUy2hPk/cHfR/x4QDlx7bhGHrX2FQbuNLQEmflKU7mmoYgOeADGikfI8CfNVanFLEbvd12Tm4ce62fzR0+I1ArXLBj0oXqJTmpZ7+YL3DwVZX7cM8d07SHXxpvBh45p0xWDnWKLylTV+PKg7HNGUL7H9/8Dbeib8N3e+DI0yR4qMZF2goVxA2effYF3ZtcnXo07ZMq8naKGt6O5l83MWUNAuCPS4yYi/HUmnxFqOEnRyBNEoYJmLaJqWYNLa7Zfd22qQg9ORpWCE8dsGQV6F0c4Evn5NR1GkJU3SfGsfwIh16zmyWOo4EcefENKxPlPnESjYkvhlXduZ+5lbBKEGQrURyQRX184+/H00iKTSzQ2zEYj1njjrrwvVTqGcc8koAxMeoa5ieV/xv6PwWw+0Ip/Skgi44yy2ieAYbNan8AjtM855DfC3LcarwYt47Tv9vX5MSDgTGSbfUvnw42PsY3zppePObR4vTwUkpiU3y/OndNiqxES0IiNqzRlz0/+HrWz3DWpLxHfj+PLfyYyY+1TWJDrWi1TyKuOdrxEdasF4CMOOeUT6ZKG2UXgI8RneXhSCEgw4jTKiTiRAbNwciOOQa5bNuxkmZVKBM2UlOYtYwGV1YzYoiwME5d04rKlbZQCC8I8TsKCUoZoxxBO2EBX3vIs9PJv0f502lkVo5TSmHhRgrGWUZW85flzkeyM3D4NSBKu1L/B9c+YlDTwEz44Z0RajBbCC2ykjYY+vD7s7w6j2GTpxsrK2v+t4C2YwdqP8hKH6K+K+IDsLt4n91OWiyM6WP4NVagjMWJy8ldRQRir7eilbHf51D4tTRCCO8x+cVJ5jFInfnommJXKhOBm9iQC6Cgxf0Rr6hslzBOu1Stefk5p+NfoP2pj1Mm4tOnLFA9EPWQVvdeoEZkiMXxtXMCL1wz96Oj8uAbKyjqDm0TYdyfINBk2uGjNErbBJltWxNc1pdifUa+6QUrF8s2psPVdBeR0jAujo2chlfkV1Wnu/C4lYnTHCS4fHlwiOu5t5FhDwaa3+f0jXE+86/vNTEx8moabRBg9+iePHVsQoF2MUkewHvGpHGl0ObEb8Xr/cffOlBuSWVsI36PO/L2p7UXMsO+yYuEGLtNvsZHBCJi8rFUnOsu8VOtI4yf0VCBlaJvh3Jmt+sZiXMsBtZqn/nVXQOeSxSnL14UZEw6WaYwK09HRCAGxiRFT0VOSiIV1Ka3PJtNgFGq8pn2DaQkBW48L4sO64jZ2dFmKTJKrsC54akWXjYzCwv2IsyPI8OWtsBqePmokHlxB6vjhUk6wu0yIziBQm7STsCGRmCZxry3dTY454g8MZ0sV90bLdY7FJsiLpA5Oc68wXI1GxFYDsh2o0RKDHFqjpAxOKuIoYJTaqA5aK+yReoCjRGRQfI6NzimWPCVShEeeaf0GnMrmWMcaZM1VsMPDmBZM4FVoio9KFqm9bymTWEQYS3UoAkPwBccVdnHGTbWGeMIqQcfL8ebcTSysG8nwreSGkSV6fFK16cs2KG7mcxnDCfRZ8dEiXaut09vFvvKxEc244/Nz5R4f1bl4GKjNu3vcg7eXAg+qWjNtNFJEjBsYiiJqe4tZoSwECSVce+B0QLz5r55HZT3oXpE5sV6igSI8DQuU5pfKx3eAEQefhn6hjyvxHVfwLQZtXkzrPhl4PCu06n0QIlwOAT+veaIVz8AG6qu35rvIlxBVP/8JJ6Dm/pj5a9Sa2J4lwk7DggJFMYA5fFd1iLj34ih9dA+oe0NfQApKC01EEGE7HwHtHwWtjOg48fd5WeyM4CmJQVi9Q4iXRoVFaEjNeGRy+xN/mZ2McquDoT5vOj1ZlZksU0MiGoiU5fm2uRHxsWvFxEqfYV1kjYeKbN+e7yOD3I/C8i3TTLICGYlm8Wmtnr7A1VKVLgortNVmfUoBbAuPj6y+PFv+Ia91l/eEK6F6FtmXoY19d70zyNCY5cDc5ZF16YrWVOk2Vx9xxkZllHUaRE59mYGW+JdjZo+UxlJTX0YVQ3nEaTr/AQ0bc5kGevF2IedXhTUXMOXl7dV0zsHCKC0PwFgxoy7AGVFDFSTBRtrGW6VVcuxLQ7c2KyjK1/zrVJ5bC729AHcCC+QcQwQHokO6h4ph/v9IsnvZz2VciBmu2mQ0vQ3UDxQvoK9pLa0Vqw/VQXeLehsVGkqo5nTfYwNG87EmRfM18vRjBUDzwEkn8tmnEdOiUmZ0PLokfCayO8jAJtfWk8V9+ZxcL7Zm0NQt/W3nNkwSKT6gdMzLecUQ1GCJklBNNpTNdm07eNK3NRmmZlVsml2awPQMZNha8uXTXpRGuJ7Oamun0L6UXPM0s/16UNV/4T5c9tuO0MtWQfUz6slufMPI6RR3c5SL34gnOhXGxlerpsUeoWUaNoB9TqF2IGm+kOISNrL9BgypdafHAOTzSjQB5l1Hp2rrkijfII7Zh/4M6MF2y5/i8kJijnzjycsbf8ESMU4Q2RjghWt1t7G9lh2wFTRQa50+ro0BhhwJo8qYIkb5DEDE/CMm4ANrpAXDqC75kAywucpPc5bskwJijFJpbuoZ42l+xjPHwQdUBwRLbHRzeRvDAkO8rA7IK+Da+QRHzCOAfa4Cai4WR4fgDVulRcq4C3opiFiJZXw8ttqHVdpSGOCJUSjoUPsGB5uSQK3NvT6mX17qFhVCV1gCW2TrWVZO1kuKu78UnxLCoeLEG4uA0UYF1u0SxCtshckkniSv8tb1P1HIBMCPkWumhQt86oExUWCFuZ1DNykYqQEmKkbFMlARxR04cs6+oB9qvkSllSznyaBlIr9FX1CvYSNjowBXZZlyspCd3EHX3y0/um0en8YJARekFxKft9NGjjf8fkFOy45wZUdVCyHVZ6gT7LdkrLy9ACVuW2xNpmy5Pw+0z2Ykjg60h1gKetjkTFDd62MYdZ5stM5c8vQYyfbBpj60wyZVQXXemtxxOt4vepA2A8gOddKTXwXXH5DkuW56E0ogYhbtPOAH5y3XQNcjj2GlMkm63CRw0UOZuA4OzT9Evtzvh5ckZcKI8Jtk3A/rfhh5Ma9lbgiJuIveJFvhKjPkIZsIsJ9PSQ6CNwawew8K1ZRKrc4ZLxqcIzKHDMCNGf11YFZ7M8jLWJ1cvoR7CBnjesDvow0TQeyZEyueSGWO6O+zbYG83GHD+Ji4BqtYr/maQ0b3g68hRtBoskSrTqW1NDAeooKAsy/Ifgz889/Pr9Wup6Jt6T93x3KR4/mOzl3yKtBYtTC3e2EDqzi6iWhlesfTx1Fdx+TOrPjcgpTm57p9hFFdLMA8hCpn5lcnsonweB9/aWTV+QxWB65Zg/0nBemW47P1FbTE6M/7k2iGAu2ofCIn4VBz3tEypdkpobc6iAfRrtZ3b+MrYs/hfC18QUwKNeJlMCRfEBDW14Z0X4tzevlHhtda9KFKp7i6hpsUpmsuy1TP6n1QndBSybX1GyTTYLWtOhvhQqInI0DpXQmfU3aOBmYP0HWtyER3BJgwCSlQqFWY6EUJmds9f4g4KV9kWQhom69YyFEW1+1BlSRbtU93jVr+nyFQTL3QDWP0U6FJ+PmzhERcDJe+8IBU0X1DK821NMSWdJ6+WAkFh54jNFrMr9JHco4JnkLuB7HkgbZcH8gBEvTnprkHSS35+761gHRFyTE/yz/Pdpe9O08dsBRvwKdef1msXoHaoLTO9AYJWzes5RwjCmUivNT1ivP9Eol+Yvp5ys939cZzERP+qnW/+mrNhEpxnVBec6l3VaF0xu74B6Wws8w+rVA2l2DU8DPzwOF35hJuL+8UXF0p37BzGzWBuQu0TYzjwGw2kGsqbUFdOfUIOjSeymFpQEzb6BUmrDAF219i5o6YDRogZuIG6HITTVqT+9d/rOk8l5x3gzOHOLp4SSqk1XNJpZ9a/WfYqNQ002vZHbYilDXFM+y+0P7vCx8emy+F7QSt2KOsrk+Mv3zl2QjwG9Qhzf/a2Jd6uNLY8LoVLIP10lDrEaZdNwDYJ4EmcJIESz5QUiDbNw6muhrCAN7btbNNXSNx3DOsIm9JoHAC4a4ThVkKPl4PIl9wVsnSXziOKRwhfjlT7WXTJk8iW7MA1ngYngoGQQ6IA1nWuCeTCpu9lPYkKQUPaw2SNJBU12oiBqh7+HyuMDOnjPuk9irCI/jzi3gvfIcBCtI32hwiCBioYQIqxmhUkMWEFbjCC5lnwlfKTlqbB7aVb6aWWWzgT//aPwfrOkk4JbvG57j1rAbvbU6HqZXplQKwh+MY4PJ2Bhozp5n4UhTxB3+BquMG63phCytjNClgp2oNaYL5CO8MWMZ2PtxjVk8P6d3vlFJiJjQq3mGGT08vyop4RAwV2l6OQwGq29UuBS6YEQ38lHUxQkUM7LQ8K2cTR9qggEFzQ/zjj0zRxjgMXjOMSRf2plshiJkj5jqRauFoNXl4E1NsQ7EOSyF8kXEyd39WVa05r4OS4R46c1zYJKz41PlQ7FMF19d4mCFyKaVBjgRy4sZNROuftBRNfO16IhCdd58d30EFSD83ZICX21d3y4GVD35LYjUYFIpFrrvNa0MZ9xVjAF3RojAqhsEJSf0LYn6Z1EYcDRhaBFI4Z9KjvHVL5Tb0VKFz0J4rfaR9QhJqijagMnZTY7rlj4UA7t8CpjDiN58w8Xm0EkFdKU7Du1MSziSRUGElGoQa5yOk4jKdNKZi2xe0lRc3QzOoQHmS49xdhL2PqwGuUweYbDi4awTxsnU0vTwK9UlitW2C9KJsts8L5Oq1KFVSlrGLBxLK64GD5PrUjNye3aeNxhOPaIGTX8H9ddcY9dg8HvnBiNBjSTrrs8jh6rHhFe8EEdxvrWEV9vAXaubC3VFqDO9ijGOtmliDVSVQcopYBtwnvRiopwcomuuaCtzspaJ3IwQwlqvXtG96UZHiotcFKQSDZPIXO+Gu9tR4YG6VY+080BgYTOMxEWNXbU7uDKbaQJGBFch1mC2vCYVR4MR11PATj2Ul5Q+27k6YFYp6Uqlo93m4GyTknEJcGQ4gOY9cAazbxDizJ7s+uYJXSQfwLRLzHgaqWwfRu3HXcIDHFXNQZGbslwRtiyizzUvHOHBdrfMYbCnMeE5GLDt71swxt0g810yRSrbbxDZJ4w6foj3AR8zxOtASAxqLonz2DcmMWwVTRBk3uetB5TdRanOENUDdb8piYQnezvXFFb924NiFWHumL8AXfI8vUScDpc8AYX8JE0WEzE9AWxz5XMGzMpzaXUt4Vq/e/OfaxuHqrtntk6veE0PgFDt1EuCFTNeYQjtUlwAmGSneoQ/OxIti0V+RaXVGRFtU4bn+zO5Hep4guN5SbBkRfyx+oaueej/GN875eyYsQBWm0wmwVHek8RYO3MJlb6Al8TTgIq5W6svXBrTakcUNw5226qbkQjJlLZ79IQheGJmhU9FFHB01SEZ4THAuGMcTRW5MogwtS9OvgkSLRoyYqXUQZ+fVuXdjOhI9NJGg278gDBL+GZGj7fWp1l96Qc8rPCUAVH5FLDmbam3WKs7smWJrNfZd7vqg5EZl4tI0k+1sPyPWex/fYczSX2iC4cVwzRmRYQ7Uxv5OacgzDTpZ5VjCRlXoRzz/A4W4wpUY+Z36Eku4H1Sk6Jh+nBDbeZ5tA3hT3G5uwPBf9KL80yfZoUU1h+zx3g4rhcRttcEBh+Wr0DvXF7O5Gshh/xEwxfYcMaqA63bI3EnjU71DhxwvoUu61M6e9dSQoG9z3fct1tH6h9Mw7iyL/e9byWyJ9DB/QdkOKkO95EXvB8E++wHcCCK5cyqcw8X4j5Z/OUkCNQD98UdER1qBh7J1il7SvBNAr/kto9n1ReRMEx4C7ZkDLXhFZ4wdy/31BpkIWGgeERqtK7iAWW1ZYqU+FjOMhEGZ7mn8LvwDpZ01elgDUxAQYsKpSMB1JXlytlRYbqnGw1HR/hRWlr8F8yKvyqUdRUNG8IQXrDgPtvwq9CgEeuhD/AoDbjzeSaTGWRUb9fQ5Hz6il915dfaBnsJ3WB+6rBcYWSxwzN/kzuDe0S4yEHabizq2BK3eANyqC5BqESs58DOUKyvNUJrwFG/wTgfSo4Z4JK097Gx+HQhBw5be6QS/FOTx/t21U3WylHzZlJLEpKSgEQFYSih22DykCXo+IqwNyTY6R3fl+RL69j9AVxETZdaPsCBHaXlURLCWyeI22QWXEL3jsuJuB5lAJt+FpvRGmN2qXhsr6LGYN5DmbQAW+iT573O9LfH12AGob3AyKGUOHqES8q0ZL3pDRWyFt55SYjQcJzL5H/EITn68lpArjXi2JUa/Z7lw2Y8KFavlfQTydhxjL73SQS5pJ+Q6ZEFCOnMmBWG+BcVKuw6dP2lXdS6BJvBYNAR6WsDSVQYzMF3p9iUGuPB+7K39QfDKl7vpRAhQJibV95xy0WAP4+Etq1R+5qMONjLpuEvcOoVwn2+3tLZBJPAqRk7qapbJ7i1cWKZRI6v3eihbdYJ97cYRcRwqEieXH0YLonIt4oIC0WfF8qFznqnmy8I5WUPte949r3IK6TDFaN1zrI8KdvqvxmZdShe1ID23BSdvSwnB9CODdrzJm7Apkb5H0a3hvIo+R7vpthgLoGvyH4Ya6muWrb3OvH/I8Cr/nQ1s+qe1ZJZXvO6lfTqTCtXE3/TOmhgh5Y4bJUisijuf6r0Qhm22b9hGwx4akCKaxI80dC1C0ImU6Pxn/UFxFOCdOG7mwktitJjJC+y9znb+KUI7SrvfxcT+k//hxNZPHVraA4Y8XSEdajeb0wiXNd87/X4Z6OsBGEcNh3uX8dArTHmO5nP0v1Vu5hwLPm2OAtWPuwrRgPybEhy4F/X0TRj5rjCAPJEevIFDaU9PXZ7Qw6reep7F+iqPlndtYfMVdzuowZDiblZk4BMWBxi2gdm09RVRabPV9zkoIPRd1h40yiuBRGshj/fc0QjPAHfVlzVkD3/QhfSjA0Sn0x5rsUJxQOThMMAo8PLDu5ioYzIf/8Klsk9hnycBBisD0G66Z0SsTC8aLS8wjx6vmKTvlFbFZJYXzg/ZL/k4QYJpGFaqFmiAHkV4gLXCuIEh+KhQNrILN08CIh5teoyaSqLiRKIXXrTgt0NSULT84H59AFQ2NhamWl87ye3BHELJ6CST5glT0uy4bQKa4vLwdebgOfl/y5usAvREyPdoeqrKREDIxCotWEIBYlt3Ns8JiRogWlFiWeo8R5aUOTB7FamqYgpwdE7ITo8zmwiQf+v+TLQSZAAunxWJYB0Eu52eeykV4ED2d9VQDVB0RQTtY66ySe+iyQoSGmJikzD0MRMJQ6rGC9H2HQWbmaU5ItBz3RkI4FMsKhqnl/QeslKKhyJGze9t3uZfoXxWixk+dmli1PnB5ixPNKOCFjhdzPyN6IQiOtJABb+/NQTTLSXLekcK6DbllTelJyMwC8vTTIbpFdRJv6F1kQM0v3jKe+wjQz6G1cK/niBesq1sk9iUTPhuLOL86M3afFjiXehR8NOy7/z4gM6BNMIdsNJ6Q334A7/MNTrD+PuIqO6+R1VTnO8bo7zPCTmR6Lh7TWEedeNya2Sr1wU0JvRDPBNQZv3Za8TDz+lf8HfWt4k0ST095l7zXjdsMHHbbTdpyarqszYwELT83beJj3pnOTM9+pa9jUt9N6yMn7we1FeO1jZ4HtZhLpBojpBYXofImPfNaEbeQenHTVElDcdUbGmnspYG1yXsX2k2fuyMeKPhPkKgonYdl6Wk4KQG2zwQ7qR1mK7tXS1FqNPaBFuW5VSDkb0QDiK6LEeSWhHdCclfpViisgyMwHt5Hozz2ekuYTnw3XLjGO3MWlL9LZM1D6fHFWrai2zMVIFumYULezeXCwaYbHP97JAuMIhS4iirpYMPELx061ArfLOb8xFaoj2AzopK1pfNKkLBpV+BEQfbBAY+ZkhNjEw5lL8EXNF7efzwYcn5Ul62yZnJq+TdzeZ8CRxoiBWqJVRVAWqed/F8kGloRwPer6oferLr1rJC4/ZCz1R4tx6YBSsYLJocslflBAGupD0IE540nOQpTKKxCxlMrLFcavJWqsxvHnWO0IDaPCC0CVJmclKsqhKA07svNcgcdw9lYuQOn28UTzb4DD+aGVDFgPXOAFDXeAIQsMCgHURqj1xmAG+Nx3dHdaAuqT+Wp4+3/9cVBZDNo5/Li/5v082G6W3OTKnDhlUPfwOvB7hOHxfIkStqUwQpWwahSUDV31Rk6q1f4IiSeZBqGz83D7xRTjKpo5JsyWYt2VmCU/xrJWp+un7YrVj4OZQxRI2YhkFlGlnzQ2TFCr0Cj05/pmTWtx5RaSKpq7PMIqLNsZEAy+Fek3CYqZOpFLcaLMKpLhCVnyp1yIOErQ8S1cMXY2Ogz/wnLLtuNAYX8AouNZwJV2kVWs53AyCi6aK5nibe4bIKK0BYhkH58nXCRFQrh8JLdTSsnIBiYGnR5Do9DRcerEkC4jNEdpu0T2a0bgvgXSmFWybNiS1vsEBOw3GGCbe/Qh0bOLmEBX/Y15Bbf89sAnvzB+CYmngZIu2G0+1E9HoWT0PH7/Lp1UF6zxs0Y47YZ/hXcPt9Pq438mkUDnM5m7x+kOuVZuOmscwETDnt5D09hfXw6eiL1x18zkwXzg15f8IAeBdKW1hzYtTzk5xAwlLyvNZZ+hE81PrKoTVfhlE9ccEadavhxm5YTOW1/fZLmzqaVWRBo0jS8KsYkvpy6Z0SMyBL+rNEndK0kUbJKTCiTxX7CurR76LhtfNhx+24d6dxK10NEi8H8vfMMkFVPVRTE3AQqHYzHJUOortvUhPINL4BNwuv50C7D/TxOGTCcMins6xk4f9pWxd93okik30w6dy/lPukK4MPrLgiNZDw1UoQvaf3R1XUXmQYCLpdTFX57vdbU/ShIdaifixGxindSUJshDYLeG50Gbl7wJRqKJDFDR0qcyW3onyFFBzE9CO0aWS+VWssyi8WOpEqGai9SVKnwGtKRRgSYA79ds/m6Tk4GPdU+85NikXyvtsbVPMF337gMBqsesoo1qwxR3wOX2ro8x/72Ylc9mMypSHFHCxRl19rLJKThC5pvR01OKuWaPMgZ+P5PEvOuK8scdbn4WnLyH/eErcWIDZ7qBsyE8Sx7TalgPc1US0xR244COVk6BYgLzJ3fthzIS413od08OfvlEU60T46+pORBqkEDsTdjSWhv5ZMvD8mFBQWcZSjrByMUsH0yVHIyHspK6ZrVoRjx3UQpMsWqH5cjkrWyw9PVXS2RlK+TYy8TwF11nKJslU1I+UJoOXmy7JZgbxOUqxBpsvmUh4PRTwdC3kGBAvtLG3HSC0LdBW3caw1u5OBn5by7hiJc76zey2lgsvCj2m+RT6uMSHj1cAZUSPgir7ovhxoejUkxCQnGwvvosDXcpKCKh66HG2wTiJxCcQgvThufFmG9yOPZKBeXPHoB6bBGjMNVBPR1D1OVaWj0SeW8fGylCOU24S26Fi+mrXKOAyEnYL21XxfrE9IuQ6yhCVz3rlb3cYf5IYUtJ/SMe2ba2Awl+DC32qtNVMyjkKvvOVurYwH1yRLvTb4joyMoIXe++wOq8jPK+t9ize2qUQqxxGqEii9JXP5y9cBtML9Vbt39R536Y2/N0rI6aeixsWD6YXAsigQID5kczfINmLLMcZmkSShhaz7rCDMY1XwZVsNZsL+Q9FxbhvKc4LnXJQcPvkBlbChVAYSlGTLItUhH7MQXKTaxy0B7RlRP2K6jVbosuQz9LhiXmrgwbbgDBELP32aDb0ppH0nsVLf1KoyBRPjr25GgSGjtCW7ezlevkCFFlyyM7FEuk5PC+B6GlEKnIVMkItdFaC4JzHQbdOwxdVuE8BC3JF9NHWTBBe9z/PXlLG3GdYB60GJQAJk1I7i4MNNxP4eCKP8A27xnoLFDs4teyJhKdLkOEv0Bhg+WFhHiCH0i3PXpkrf6HPVh+ZMFAbvrsm1+ZhJbrkQMa7RWrF4uHIBR0XzkTErIlJfu1TmtiSoeyJeKYwRyqNi3k6cvRekn6B3cxmeWD/py2R56jQrWbMHRq/N7N76BnMIpAZSjjmjaHLN2HAkEanVNjaPfd2TP4aL5MLuONysd4wkuBC5UodS1MIrW432zf9cWOGFsEfC7GyMSYun9PD54v2fBcqULT91hDfVbzWrQ0nu0fsGzBj9V0Tx0Zo5e82QiSs7BoJ7Af6YtT02ZBgHxGyltAzGZoA42sM9xzjfX/Tk7W9E0V8yyd01sYdY+3YPHkwgEQZkO0JotusiPzNxBxWYxZHPOkSBofKnQ6AA0dCMkOb8xlzVb5czM17L+6w4u7O3KbdJC5KfwipDE78pXiSBGkJoO0Ugud6jGI2CtWhXmP5LtxGwqQASSTGd3oVu/x++NnopSy39vxgFgWkfiU9SEHI8qmtlMiRWSFGkPPc5hLDFBkgvUxX+jAAdrbjo1whDS9IEiL/BJzNFOrALTZb06UK0VL1BV9RGqqp0pvfEwMl4rVh2fTFbE9wmGeT7smr04PN3U7g5lHxlnnSiXqNfU7J/Y7QTBEFRAftWMPZN2ZgiRZXUGop6FF5gt6p4CnO7NxqwOR+A5ZA7R69SvTgvqKJwtc39UU8NjzypE87V2ZzmqpWsDOkkQgOofK0aqxebqS8xe1UVocbRQFpFDU5jA6Yf7fjiMyI66BxQJu8PeAnaTIihb3iG1+FiFIHfFK/2I5Ie6h3l0dd4GshsDyUd+ElSYbkqvhW7B0HygKaoIsB0jw5BFVZJFU+oGGI5rgY+/OwZlsKWT5eYXnGmDRARZC4A1YLRSm3TeGxFimoQOBo28S84Ev3RiO3Zc3Yylz3H47ubMQgMt7GVvafs3LkLD28CKx4NtpkCh48AqfapTJWE3uxhc7bnr+Frigw6lgnPbiAuUC90Ia+2/Mtw4mdPX0Vz+oBW6ji4EARXLVhm4zkvxusCIP0LrxXO/TOLVPIcXnqOxmg+R0XXzDuFh+bR6bIy0Khl+i5gk86TS4k9jRlCnL734mt5f7lOrQwgSbc5v2fb0l+yh2+5NStR/uG1c2TQ6VM79yuGHJKu/bkFy3UDLZ0BFzmXAIIY1LwFTZB5V7QMUMC5K34C/wO28IIElUu17APsJCoxbWXeGoh+Tk8AXO3FIOELA5IkPpWUgfdVSiMbs1yLUgMpgeBrUN0SjGJLWyfl4IFfZoPPc1rQ7WUPIL0xLnZvgzxt76i8rK69LrF8oH3jDtE4MCSORz74i6k9CKAifgXcl9sESNZgoyNk8QM46QL2iOC2yLQLyNCAy/nNlZPRZ94UhbOXBZ+bhSaalJDDQUlJ+4u7EppNuE1qTCeiJfua1uG3Nr+5603Yefv1ncy+6NHy+St8M5k/txYHaaHb3pEsWoLPTySAA8o8f4Gha1vYXPC372rh0oCbmFUnEnCaTzSXC7Iw2fVXDcUQ0MRaEQibFLBQpBWSJpAjGRPEjpU+glpiySeP9hhx4i0qKqcnOgxUaech1VtZcQ2UABDsxHZtOtu9LRjHinAvQisk7oUUQ2plDmfMVmdPj6wYxGORESlMbDBnj7Hnlbuhs3si08CUCMQUuV7Nx6RspVhqBTV9EBlQ+2dWKjq08tW0nfkKehC8NCr5b0qOY2mB3F6CrWufSOLkVjzJ67xgeogWc+MphIGlCSIqqo3wl9mn24uIkn896Jrx37vfosKcvyQrKRNJronLHd+wgj69A5at+YIyBZUscDD4z4kjrtIUpp3hyKlne6aZ/+zXYIIDAjyBAFWXho3GVC24KW9LQCXJwEHApZT/oW2GTTRnYuRAlCiEkM67Cbc/XChpp960iKoteOpdY8H+YSp6+2NCW9oEi95QVUhyaIvlAFLoJGSjeRBRZIVMJoM7VXJMYfoVOh1Ef+FnjkMs8Bo/IOvFVLz3YV43QhxUMxrr0o01m2tKQmBBmM7twgwjzXxpCEqzMo7KUMW3C52ZrfQ5SQCDConZ0DNBqJQaV7L5Xg/sWo7EeQ6Anq8lx/g0uMZadUxk22DHanvIOIbMWeFtJBGA29hr479os4CEzSdP3L5ZnqUdGmIKSSBy5A4WL8+knLYllkGGu6Ky1sLtk3BKu8FJTRzKQwVD1tC5E6/C4bp8BZwCu/SytGVdEDU1iLYHyhwFtcVUINWZkSmvhrYXhYrEcVNVxMvxzLM6hLLW2mCP/3c5ECTwyloh2UzWRksyD24JAEiiVLWJHxZdN7WGjmUquJ+4FE4z8B8zA6X59IcygNgRkSIFlTkyYeHXI3ZRXaB7DhB6yaGBbZkhtpPYHwVU/yUUrKilxTIUogaBPYyZz1MP5zFEOVa0cEQy4vU7JTKAX5GLDHfleTshye2qbfN9s3G3Acbv4jRPurjtkiNYIcCtmvMFyaKL+8LUUmoVW7SjavuPnQvcjic2qGmuf49X7NWanKepaLIfEJKDyISlCTTG+3cCqZfx4UvZUTtgZb1cDxp2hpKbNGqlyy2EuAaVhUsoqYZB7RPQy4AHVNPUBNPg87E2AJ7una7MSBqdx4CI6Y8AWNzWbjjd3A+U8oCiJSnF3K/6fEJXBvYqjKLLfeIAZBlPQ19NJNRI8isOCXY2r4gGxr2Fkx1s4x0TOb/xuzVxgAUppQksS0JOyEUcDJILUUrgaoRwoSNadMJnrBrSTwGk5RtYx8T47ywUwqfuLBZ30LPhsK8LwSHYrF6pqP0OV8mdBpFoOalgCYA6msvkzb1ambifPNehiZ1Xk4NIqa9GKPzWOEBUXoZSfwphpd3IyUVoZ4Bo71BKKHypZr576skkV9DlHitI4XlGHHjanP9ACRaTrY8rQHiSxm6kx+AbdJt4S0gh2u3Fs9H7MU7STuCtrvU1dw+z0TFmRlGTQK1wQGdAvZ5YCUPjv6kJdakW8BeL2Nrb4uH2BICY9sO3SlGxh0wTCXU4Ckd6Bd40l1H76+EeE+HbFmTuleLokvteIHWfU5mS6pjQ4LiXF7rl7oeH42hstypA9IRPJVZaSuQU9sC9xfy15GhAbMC2kqH0Bpym4Q6EwsFuf5kJC1NAMqigEE20rOUDmObhkkhS1j/YmX1f85gaPknfLDeWzRVjnKw96I+CeBVDCNjX+r3WvDVY5ZLSEUoL6zykkaiGDf/PgW1xicWUvRei3751rzzz71RGwlgLc2Ss+u+NYZa8UBSHAtMtKW5RvEfIRs1hij+I2/JHDoXEJ5wjZyF36MV0mhPqpTrGLnclKWtVCXZ+tbxcxLS6Uc8VJ5zK0SgM6c8gZ4pP2idGs5B5hkOt8xBQe3Cd2hOE0TNt3D/rrTyaV83x4bcJjNWfIua3u7VEUGy6WejRZpOQCX9blrTHeic+sIyWvzLJR/EN03s7/fix8cbR/joZLQtaNr90MXjRApO5xTPe/bl4Ll+YAmkagPpC7Yw8AS/sgLalXSE7gjYfdEZI1+VsX+CWwYIvpMYxjZZdMvKaKVsMWoE2Dw0KUXVc0wMKYerRZ3XT+YaGhn39PNY/AWgEQ+OE71jZozYmF9K8UKW5ZtJ30tDYSjZLoDskFJ+Ao6enefB0CdlzVmyPUDqkQPxCSakt0rvurHFWGoq8rr+/dgrQKnk+Pa4T4xs/uC1M5/QcSKwNUGLT7Srj05m6ld/pyPym8mCmEofo0mTRgT+bY2DlqYuyFH1Y2U/vaZoRsOc7P0NHEuIUtVEsbHADuoSYIrHd0WOMOxuqGjMong7sU+ddzdnFUY0D60TN52YLqkIoYxzaPnQQCqRF3P+k4zs8oNHgkp1Ghb0d05KsJKl4gltXWJOd1spLpScxQzwzvJ1jKiDGcbk21u3Y7+EBOTMWfDkyby4NF5scceGxPUo2TYOHvLd9DvBSLId6V2VFtlbYzppUGwyexDKAsyBEQYzb7v1rVCswk4x2XNuQFlWSuFGEkuC4bwiZyjHc+GeTR1ImK7mcWa2j+ribV8fQdhgEzd8r40JQ3etta4++DXUZK7Ysd4GQ3I+s3i/P1XFSEqHKoykEOf9HKRFDudeI6E/vOnJxtTPPTti0SOd84mK0drovF2yNQUbwOvtEqHJ9+GgiKtDJNJR9hPDrxK636i6PZxgW2tioMpteWBHiCaxRRDA6scDRf6NBi49W0Xal8hRut6jffRYK3AHXLv1zrZ7tS5SX9oY2r3uWIokZ6brOEVpCeGhGbOHZz2HBNzuaJxmN0RmbBUeMsw3pqrCsH4bEu4Ui/sJsA0cEap4IG8gw4qTAVHOaBT/EoSfBnBOeBDWWCbB7Fv7AgroewTC9Tz26DSTElvZJkvMMHKMf4HUYQm7gzqz5GzQTntlZidRVB2U9blG3ZS9IHGmdX5LRuiEGFywPs2vQc4Z7q2niXbYDHUsvDai1a5Z1tiedvF4fjuTSwWBTR1oDVrXmGaCEZajJOp96yAvjcdnNGsxaNiaKTSu5tXdRNxMMu20IIkv0XCGeKi8jFnuWIaTZH2aQM56pF3jjiI8gdmF4MjErlcqdHZ7YUFcvXTgUGxB8inID9/71ZgW9s9LXO4k6VdSRN4fLiusEOx3Nlu5OfSwAslr+cjwRY9b1ePDZmte9H/OCU5ihiIuzRr6wtzM4rsEf9ZkZMjN5+QERLq6ZTLmz5jwnVGnBCPEU+UN4YPCJSQ4ArxwT8N0WTmg9YKwkEkJDOkmNYlHHp9LwOvQGOhXxpBC3BNpCy+7Mh7UhD5fkN9D8wCzyERu50RFs5hormmz9wVB9JEUSiYPLCmLUZEQ0urMaEbbHX2ziGz3bc5t5XcExDG5ZBMNc2Jdfj8oHkjl7q5fXo73TGqOdMCSgT4U8VOW4j0869oBJQ4l6ewhlhP9SGsGgmNahsYiWUXjA9ITVgckAk94+ifF+jJ/LOoyfwqz3xSGy4e2n50SFUXr8PT2NJcSWQY8IoBlHIK89uSbhFvEukA8lM32JA8jMtro9qA4yZK7wwd+FhEFZL13JG923ZIviXaA3jk/kDph6HCxHgx03KD4C3j8Q/84GEc6yENvnn8gSJ5qwRilwKQsd5XLViqtonkjbp5fkEhDq0IPvRmmGncgQnRYouHciQKu3Av4p3DBM5Puw9ovnoV3sAv5nXI3KkR5Ic0DGrofeJhI6OVmS+kvrAOUJn3GCZ/UmWyx+NfaZ5GQYw+LqWWxlUH1FAgi+aDzWzBKd+u5jw8Co0WgNzExnGbXJKmMUBHVGjMcdvI1DVsgDDwzSjP5itgHIfr1Dmy50HvyPRnURXOZ+7bdBn6EE6EhFFTpEHVDxWW2C4uDiFSdbW6L4AoGMKjGSIPLgV+0solX7wFSxgngzE2oyuruS+oKhUR14IABGTCwGqzEB3pREKICknx1ICiE3GAFcOtb9ICTyWFR7X07D7rPJzzL8ZVgKbW7pPql+0H26I3Jowa9swEXRTmyQHkMcO4Klnp0i2zTa2U5EubhMBJTtmVvQBSoST52n5GQteHds2ie5/lJj1O8A9E4++aiU/0e3pIEtXPe8JobEaNN/wN5RhkwnjZBCT33hqztMJ0h5kU4gZLQi38QCdNtstIPRbXkvCfInzggcwGEfcV3J+EcHtFItgq8MYkbrrA6G6yJA9xenC+t+7U2M41w8XxJs/5RAnT4WIVCGJ+c4jlwI4CEurvHRT687pxB4qU1psAeXaY/Myapn8DccU1u3et85mzRM/tSpIbwu6qWpKU0HrSrqV96HZFEaSsWtCIFfwLvDcvnmMyiAO7kbp2fjjPPFpUpUfqqLZw/Hcb6UIPJMpwiJRa1MLL1NMvaXFitDboPbaDXhSdq1CTqmH02pJdE1Vtn533nNNi0TvnL58BXRN/AG/K4FAPvWEgyvxfEWt8MzNLjVu/w09vo7fs+PnhYAf2YrzpcH4+/qg4TvorLXuDT0Tr3mmbHO9DpAYLWK7iLJZBIthp+0uxdqnz83bqR8HlMJo7NHX6Oc9lBmR2gZQJ3CIydnxIffh0O3jnm5/5MFVi9sve5a920IjLeuZPSt6tmmFhF7P0g5NRiLUoJZSOtjqLETNRPZjSDG6JXNLXusqoxZnReV4NSu+u+tmA+z5XIsvzFN5LErtJhLGS1sUpuVAzbOokGTumjVA1FHFRz442Ofgo9obfeSu2RHAOJu0vswtyhK7gnPH4KB+W0as2hu1aVjUHm0XTtc1cFEneKvd9pJO3O98mBAOe+hoBJwNU+tnHEFDIopijPwbBK+QtIgedBqSga+DSoJlJRLFwmN9y4cb1vfB2/wrTuPUrlj61hBW7slY89LVq3pjuvtezyNLY2oUCy6JBOj+yMYsJPqlDn4dbWix+dKqhjH/TX703Uo3sicjTd41E50yKLVIQIvXzt8TMNHce5jDtjBPm3aznOIirmNyeyvhaHH+4oyGdM4Uizb21VCVIR15jmQmw/ZOHLZ3UHLZhZjYkFRcJyAA99B/lzD+TRF/R7NOTZ41vrk/1Cx3+Ck7mLK0SNsAfzUlNyBl6+4u61UmjmTPSymKXIYXUiHHrBdGB+hPwUFauOjsn2Gon828SO4u6wq5PrfaslpC+wCzp0u8D77A+l+fX0+3CyXX3ePguz6gmrOIZNltaj6lsGfypfdnyfJ8nJ5ObmbHmZO6gR8cvrc9k/nOFxxVOi3PSB5UesUMyX8bxr28b6PNvk+u4/IVUXCD9qD//zv829H8JVq1MW13XTOxd8P1V8S155L3upRz7MWoP3xZivH3S6U6vYREuQZOzFomKHqSE3iI499ovvJ3GRb7fbEZdwIW8mJrfcGCOnhTsjydd4ybBbQMS73aysff6Sd75CgNSfuTyMnC1XqJ0mWM7pylXYTjidOx7crjtrnfJqgnjG20Wf1JHKStlsMrG25waRSNQzaz7wwvyMNnBaHvFOcoLpYqcrDviuxfeVnqBxxVgRL2qGK4YCfkZzmrFk5oU9E65RVISodK2Yev20+GlARhKFCmswz1zBrUtIBSevs8VA7+QnWXHFgICBE+PLXJDO9E2XB9S2EuEKnUmTlYf2S2EzXyuSoy92BCy5AqlQHVWgu0eoKtCp8HHsR6rerdUYnGBJzXNbw2IwQRbDwg716FytsdtdEn8V1Fgz9oozjy3lkmdGEmgB1uJHQ6+iBfBB3ihQL89xR/RQQO2oQ+0gX/RiNn39ciyb6t8tmGexNnnRizTE2LtgcfCRtIGA+qE3Z1MUBRrxtrOr2OcVeOnM3zwX3nrp3MiI0VEXE87eyT6STj1NQsx5G2/wiCOApGM9UUHE8u4z9gfUWKlsvjVULgR9sxXhJSlsEGDytjsbzptKycbEkM6v7xA9kcCHHb+6N4V6NTqtIqW0aTvndlVyDDha2wzlyEx0kMQtiasC0W93SCskVZ8Ze79MzPfTm54cix8SRbOz/4xDUwZCuPbVkUsn7m16iUtMFCawZG6QeGbzuzfNnbh46WLUu/KLv2Dzdwhg5imxOkjSnnuPmTkmq1Baf7HpRPuwIIUAA4xDenL/7qozK3Dhrk83LbcHLgr0SiJ36Bxs3PURnEg6O2xQ0lMkSTjsE8tWI+65CYzk0HYGxbM0VkHJP6zQ5SkCNaNf1SmewPvY+oTOfhYAF//1O9vLErYElJkWL2RqforZS5m9yqRtTzfw6BpP6XgB20939q3BYOoXABwz6XEx3c7yDPA2jvtZB1zWIHF2zQ/StVisVMS1QFFIJXAX9AVtvFmBEW5YhfZ2Zq0TEvWHZwZsbLNSGMc5sFRR+w0rpzFXGdavxlKs+758oYJ4o5Kjh8xDyzN4nT1ylhuW/DyOEQv40TOfK9VD5orhoTgpcnBHMbta/mhCb6RxhJaS9HCxSFXaYVMdLCW4R0ICK9+Z3+HWq2Y5zy44cKmdbGsIPc+RVyFIT/IHgVOoOQ+tDurWHqQsdAtuKugOC1tQV5tQuBHDWMgpj5rSo9QAEDxFbdpnaKdq22CIDhfOc3jtmUdVoJVhORH6o5WsPrIFqh2NAVgJONERksC5xxKYB6dxaEPMbO3Q3H8NxZhIT3tIIAvXg7FWpkzQgO1jGCn7Dcs+pRMhbWuh3pJIjpafM/Gxuz+WNuZB+rXAajq3gKNs5YeyuxczkJIQFwlCO9xr8oRmeswkY7ZQ+t0VZRPAu8T7XoRS7dUlWj5xj4+I6QniI0nkQWpzwyEox5lKAkU8c7zaTtG5W0dHgxDRIi6zLB96kjnoLFjQk5RhK7Pk95uyPv5Yns2KfAEMLjwU4/4GE5ngnamFSFbWt9tZwJKhX3kIjqWUEXPVS3mz7ZurWNo/fIXkpkvbMCE9J6YbASJ+h5N3r3reA76POJCu6MmR0uMPicF7f200SxAqaeCja+5A+UrHLt8EiRHLrWHnx6HV9ejaSkYc9apjmvLY6qkmwV618bl/PCf2MWCOj/yjNA8qwJ6PAUvmswNvKD8ho5wvxFCcYKeGBGdKnGl7w3ZDa7YtehC5dnihze3512In5UQnFnmUqaXtBfn0N8h2GMjIe9fq2xreUaN6AUmhBawoBZCcgvlef1FHnbzH9VZ1qMPTqrUl04vAk/sAHCzf+bs/FR7eqjus6KOkfUCvxEz7PQgpgwcObfz+gQk1r3YXgvb3KgUFw3Z8MSgvL5O6Yxu2O16W5A3k4LuuCdBrNObT5vBNJiMhf0bdYr88a+igcA4y5BmsnN0eNVYg34wAr9mzfNxKMVZfYcidZN5N0P+4Uj9pNsZy7kmfYEBR4JgBH5dzncrAEVA21zW3js/wmj0yg6+YcRr0yYlWCIW2rCxmt61P/gIew8rpTZ4IvcnBGGH6G1ImfLhAjYS7bH4pOBu0bvoNbh1LbS2k+4xCtAu2XiOpR3zhYimi6po3PrC3g+r/6cD+lhpC/WAKMXPwTbnKQEU2j8ZgsCEkdlGYc9GVrhdUF3xV3dPQ/Aqh2LaoYqNmiO0HI+HhFHgb0zt47gUdZNOkW9AkfWN8Hns5Dz1f/rWwg1waLz+SEeCVIfuAcr4vc8Q8HlmApYpuhWyBvtA5+NT1xd94UhNMfqjWvSuPXRJZVJrNRh0xTJ6ex5j+FXm/gG+rwvyVvhqOExJMWEulymU1VHVNV/8K/4NUF3WNxOnVuJRUwdRVL3iW5GK+teik9DauBNlSsit1UO4kU+bajSADAodsAnIrV30lkf/qvJ0OCd1fkRqKxmszIwLhBLTjEZuAlbIjOxSxiuYWNcq4ccYd1IBFjvl91/iLDyTs5jLvPij12T0bgZ9jkLZRBIJAnZeALkNktRDlqL9DtSNDTVvrcbGH1QAmNNvQp4IG9/F0PggSjV+/RgTH1FQjOOHkt6OBCh747h4oekFXu4vL4aYuzKK4t3KucFmQ7cN5w3kwkhRH/kIg6TndHDAhec8B20cDy86on9dv6PT94vDkGPtpCP1dOYwV0om5o1ABK14VQ/TVq9GQ0skxWVpjItG0uF2LN4xQlJqoQVJ7R/27sXngx8iGhie3MWIkONdYEuP+QOPYaILRp8zU6AUckedgYM4lyDXgRS+10dJn3AgyPqgv1/e8YGR6cDpPn5egXEvZ/opy3aXUK0jZaBZZAGPu4s1/jA+ket6Ga/gAkNSU6BJ6icBadqp81fUmzRjZ7xeyxG/oBu1UjAfamps8cYZd/5NGn3zLsD6q3D6EpndEH36rHwlBkp7NdAMZa6xIBAtY6rElrTeVMc007tNrWw8yqMLJuHbDYv4ztB+iBO6ljtHtrldwik+8X3yb+TGIYkSG+uqjK2VPjr8MDpT1+ndVhtuL+7087Ah0O21W2RNrgxXj7RmbR5wGkqI1WJ9u1q0ivObAUHARasU2dLobXO2gkBQLkgHI4NDnTya6ZocCttNou2P4Q/oBy1gRKEtZfRTZt1B1bXjUXWHsYnHrlyjZfP7ZBYYyvD9JcucfTa7Uk9CNXcGrhk3kM2Isbm8XXUkqgW/YY+WsNXAhXPYdtILA/06bkfnhQQ138W8VaSq7tnxtyMkrIBZai6BkU5lpS9OVvKYz8bYHsU2dpCExgQj7/dciaXvZKgw1v70OejEvhmK81Lqe1afzk53jje+ID+T7w8rsD/8Wm3O1xn+yV9wS/I9ETyiXzlU2edDSBOoaDRW/qTuAywX9sV/vrVKSNB/X1dPVpCnPlp2PYu+1X9UevV8UMXFwQpf0R/p4tjyiULKJsEBHXbZ0KtBZ0ZeMxjX3N3aIZLWFlw3hQ9A7VeNp2QGxwzPv7b0Cmp2jXKj/XCIkaIb5EscoBj85SCTts+yQ07ANQyruxIkKVzoASIKkqhEbWEzF0MqJfdIchSMvGVvAQbYffgE1p92V1tFZZSo17hd18ihZXvFgCG6dLNaxiNOr27FpyoexI66xTlEp4+4b8Kswv4+M9qN0aCFKkoS0a3FJwOT+XsPM4smOgwDHi8hYa1km2fD47lBYmaSFhnW6BuG3IfWVk7ntS/z6krGw+gyf7DYKap3iR2wfGZFaS1fQ1FytEQkqFdj/ZZqtT/Q1qrDObjMG3KLtUXgfemzJQ6rdGW4I9YtTgD+4NeqWmRyfc9U5kcLzEJ0HFMS8jJ4czGHB9PXQIXed7svLFiw8FWA3SW5rkU/5hitKZiDA9J6QQ281DYPsPuh+z1Eq4nVemkns8gwFhOoMA8s91zyozQV41hbRIctxl0pGNBObnRMkGDn5Nxx+aenqscRTEZOhGgkapnq7bbRGAQFm9yUSZGQPOusVCMgGnHGOMEfbOzNGj1Xt952jg4H0LLf5adN6XQgH7Vl5pwefPc4oaF2Fg4Db4nRFhTRGJTflSAdg9xLCR4cPokw1AzyUv82Amd6jBslNB0+sjkD35aGPNft7zEQeK+f3BRtb3Apc4PEPlzAG/hVgekx17KuVh/DIjc9V2rdQvQbicKFVWJzJgFomzlEOAISp1I4zLmqJv/p5hi4YUGt9MCxxaYv/58Oya1tDY7Tk8Uog+rslOEbb3+cd45LIbdaKOVDlkADNJYouqFDI9x5WF6V+MkIsQ1XQAitu6aKkKQqXuGuXhVSBV/fgBglaju0t2gdkFMHigOCEYY+QchPH6/rXF4z4tYwA65rMKN5A0Frca1hkQdYhCxO7eq+lTnrdCoTvMt3g3X+DmTa9HFwynhcSni8Ha3J+WpUTEZgo1abg9j4fyqNN+up2S4KsCz/A4rLvQIYyXTzQUBfeITYAKdIbyml0MWXsJQr2yRIaVGMOVgWwLslB2umIQ4k3fZQWA2XO/EKanq71osVLFrLplJA84kzBLbIiP+/RGDDe3Cx+FhU501jXkHXa+rhf9WUysefwN4THxBZi9i1KcoDKNGu4IGoW19ez78pBdJKk9/HnPEOub5FZ+O9jkXobICR9UWFrc4TklkZxpbB3w/Odfvw3MdIdvDVGmWj81TwAzGDlYeNZZ5F4zizN4aWyYOZ6Cg3QTXo9Xe1tPaV0k+wegaB9a9Dxq9E6eR7sui8q+sSWcYCIGp+LGX4hPS6tFx/bJFHL2P6pN7U567KNhNe8EC4WNyWkYhRH3ry+oAtWxU92yXNnLOqxvh9Bay72oi9wWeOjpqIav4Rrn9GTPpbGPc65DMuyvRdVa5eEFg0Za6ajJiN92eupbBDZ9aocHwSq7MRN3VnDAlEGYzHdfArtmkvvK/Kd2tcEPfn4SlVETbPsS67h+vMIFQHw4k+vQt2OOUvknUTFpOJMl4RQWCm60gTmiX2D8WvZDMuqjI0J+7HUTpZgHIU6sQYOHjjOiqM9my+6ypfzUXQfbURvppxQoSLNSzUJbfkRPSGO8oMs9W0wncqeczO6rgCljTGZfySkg8KqQKOUeUtbUesQvgTlmHA4tfFfsuWITHh6iv2QGu8+Wka4XPugUJE4NUaT++qysYR3nc3onqv+DzK4Cn3WQkl87VR50StFtFoudGK0pLJ3/AWo6uDZ1smS2cs8urUn8UPjmmMM3HqM4DcYxhb0o2x8L5aMijwUdIFPD3PI5lqh/gJM3cn5BvzvctCH9s1cWhObKM9jiUUDUE3qe9Qutuez3kON6byrLWRXnHd2NQCOWkyUXsnMKlZPr5ACRzvnP8eJ4a5yOTEgMb28liNyoXGSvHNIhT8s6UFLh80kevrkN+ybE7Z+8shrPjm5Wg7X3/HU9j+thd5kr89UTwQBG1qqO+O9U7SYc89h1vObTqi/uM7qiCkZTsVzsmReivfrmkfNVPKZsRrg/Ja+LIjIKz3rWjhJX5ODrq7DuRBN/K/TrJfTueRAHg7cUhC9qBsXeOG/5PoAlZhevOYMm8i/KO5t5RYT4jIzyO4FUZjgoPU6mhJls5efN2F9jDY8eE+NdzwYx2JjzUoBmGUBHZ0x7chhE15du3H/HpPNfDlTrgHhWZIWPCJhB/fZa3nYpaPRDmDJOiTY02pj7/qZRcJ0vZGbgo996ZkdMHcfsVfYwHE6hkn4XF2Z83AOp2jmr+CthxYG3ZLxh/WFOCdjxTQSbSk9N38mJ1VMw59Y7YzucXs6Biq4VI9rcED8dvwtkQWAfJAgwL1E0txstIlzLwwF4UUwuwW14EYGgkDbciEfNvzcq854FgJwr7MAasTTc1/I7YYyWh3A5EezoDutVbP8sz0GB+y+/OrJAQedv+BbP123QPqND7hgvn5PxjvKBXBD6czcchd08LoWq5gx8tgyZTN8iQOTOuD0SLkMObDWzsFPOSf3yHAq/GadXm5VKHJSJKY32HrVrQ7D4+yMsHvNqt//mPgcNnOvhBPbvl2NbKTrg8JPEGGfQSNWyIz6Jcw5Z2UZUrbe7ATWc+nhzjZhjrrrDZBkh2fpSBfNF1kEVTxahsMoaJrESTeUi8mylHBC4k8pOhwk6iUz2OsM4mQ3XcmI+ZFvjunXscGhkRvtoC6GNBNnev24033y/AACMogQtSEXL3Y3EyrQrF3Dw7pPuhQjUQrzG2soCQIo/9KlT3lpKNzj9loOqe6UpwLvinBdGhPGiAl0YLk/X9PdHqzOOfHRnlF3/Il1aaLqCF6gysS+PzLgnBFf1Tr+Yv7cEyLaM2EXV4+cKYqro0g+A2M9cWj+Qa5yQEfbs8yGhxkwqrxWI8YLQNeyZl/iqpNaeNMLsMv8/V1Q/APZk7Mmh12IdfkLWIK6nLmwUdDSlqR1VltPLgrD0hLZjWmzNN95oLddGsoijXAiX9ARWtwRGEbYTIjh9COviGDEYz+FNJbR9MSxqO5EWEI5gAlTEywcKO25UTYPRXZR/vv9HrOHZHqG+YxUWrQYD6xneTD3upjGjDD0JWJ7Erp7IAIezQ10/e2IdtZqtAlO1NRiX/OZL0vrW9lKCwf1cDUUDyTYyeqR+XKfucgaoyp1kW1TjZFpjmnP5J0npbHgMFNdln39d9zMq9WOMZ1mrcmVtoAgGTkFkxe2VeTgZm4z1MwcxG7qt1+uppOQ7HkjSgqKtA0kukzG8Wqg5DrIMLsHB4F/CO1/dIHeJwe2x/E25V4X2eJ040XYwO4JFhWn6kX2IlQjtqrLkubsoLK01Tt75nfHDLaUKdO2DgdbKNs0bPqkpwMC02fMuKws1oUZyFvhFNQCLKRtG2888WJcrHYtKQ2lV5jUjogoUH5kNEjWOBFU9EM0balyhx2eixGddL/33SIz8Jkx5gQmO6i6kXTI4XjXEKzokGhBbKZsjRIsNUQeisvpCKQxC5uLRwBcE6AsBkbpDmpUleMIEM8t2GUr9r3TeaFhnnO17lbxzdexFhZ7rEFf8CV//OZX1/PDyBIFT7RSqcIcOXljIHx2rBM2O6znKNVd5uu5L3D29DVsNL65nwMojhNR712gRp15omm8Zp7rIGz/3TDG9ZCl32yavnIMSTwLjjrCHr8xvJsnzAZkkM6gnQFSKeJa4HQDetN9OBrey66hJ/LV3WxFsB1Ard+fSBrQIWRUk/aKGAs9iOXANgwUUG3kAQS29CNjotUL8FCrhJl7+yFdGWrSF+GC7Sn3qzqiAd9BombZvLS5jzhGd9f24MGmGN/LYPMi9dwtQHRQFBCLwxYXINb/IUBE8sJ5JI6bRu1py0MJdVicFzUqABwocwMxC9Yk9qccw0uYEhIImC+I0+bkKhY+fTB5DRIXX6IzJnkOKm0wRvX0lgAZgfALLE6EiUA3LBCBiROhu9wv+oCSOW2/ploYUHspKVyJFjF1sm+Hi/1zu3zQgzvOFlxvt/YOx0GZStbXrvO0IRyaHMvMiqD7qO0xI+7At8Gwm9+dc3ChI4W7RjB3Km0KsFojObgdRkuSN4ilNCkwtu4+cbhbtJsLBenlOhsTA3gOx131ydTLGgfLcczqRCzqqnwi16cYJqCLlqku+ZnmWDBMmn+BBSwLLaVx8etMgZ1jLGzUxn/IalO5qPXF+jPBQtl+K4GzrwbB3TIY9vqJFB96aKW/I/sUrC79XI/jJ9avfyncSQH7BNjmWlt9ab8gr7DuPCjiVrZImhJ6ORcrnX1xQLKRlueY/UVtQfb4l8cdCtK8HQ7ATf98Hhh9sriGcVLQnfNZKcMgnkOSQkFLan/bQmFmk3TtnBSuuFEXESDb4DITx+HKDPPiUZH0Cn+Oa5Ko0GDZyoME2SWputzkA78xQMfNyquBtnn7mrAbh4fQd6zIdlQe02JqU6YLaxGTkQw2WO61vhuZ32Hoz5HW9LZhLzibDOpUj06rVhdnj+ifNr72pnqrXt7BHXQxoTgcGiRGeoyfU9mrbrdNzfwlX7LY4MT9ABJZhvHLCsRGipr87OlrWXViZc7Y/CaCX04cRkZzudiYJytlJWT8rBqnXY5hCyqkIp/VvpWXb/KxuydlH1azKe68tOfHPQ590Iersko4RnuHoNJiXzhqkoefo9+yeMcWPGTwSa1qe/Knt9soFO5YZUTROgXdcqDis7FO2JlkfnO6UYQtEynXmLQ1988uItYM0mia9DAV/WBDpLrNY8Qlr20/QQGNQgaF5gMSzkEPanOJj/hb1IZMSQ0qdrFbB+aSQeebl6yHI72/OYWvP32+wxe81Aqz/ddmMzftzCo/kSCVl0W1MBB6/SfyaYans3oQiRQ/43MOohA8m5oezC//NxXZLTFa9rhbq1A7C1Ef8oKDhbz/jfiFiKjn8TLl8DUNksfooaVLAyX9cUwgqwlnXyTvYWl7DKZIyS5LVWmyfTIvU5kzxi3jOfdLJmCZgSBQR28k+FeUFYNaHKbgrTeBjlCnSGeKEIKP/eRluoolLI0YqSTlaYfA4o8ly/lqv26ScyM/Hgmrn9q/rx/qKeAgx83UGnXkHIWZ2ewMt8wzTccONXHFVy/YFOCIuwiBgp02M/G2KYGr7wdOs12Lzq6SE3g5EjogdV9yVr6RunU0Tn4h5Sg2r2uskK7ZJ+WjpfsXx7OJuSx892tP/1jtNg5YtroaFyqWBH6/CGS//bmUPOrb4afJukutSQf0xr30Vng6H4CajhVL36YUD33dmZuPhLUFBIlNeX/8KPxJbjps/o1mgMi17a9C6GaZKkWZ2gzrgCKrC0VVT0fgROorkvqSEBsMf9PzAwDbSvv36bSWpI/zs3ca/sIVMKczUZahKyZN0GbLkhwz2cNJ1JlXdj1E/IQ4159ERJlQW678G7zFT/QtVaguX4wipu+U3aKU0DTPF00n8Ft2Ymr7+9mljvf1GPF4jnIXeD3DY/GjpRRsEXeubH6sKEIulJTsi5IRFsjs3aKbFwEdhF58y2+Oq6sh7RGIipS0VVjUyFudx2Wr3+tuWaoqRPxZOIN5N4FLeQT7pbhsnn0FOBRiI8D40nmvrngyPaYEruUKclPYcrDbRYKdcWF6P6JfEc8qzntjR6w+blK88yEhgZ9MPPd2XZE/vIMDpLLv10sZxo9D9VyRC3CLBgo094GeCiwVQK+wfNOlAilMOGnZGTCXfqS6igGtfjZcdh2osdUBnhGTmI2u6/WHLh8LTvBNPBPhx2kPulP+/hZk4O++c2y7PfJMmlBgynfQkRiHvH7gkLFwFhNpFNtqA54n6W0A/G2YUZRz5PQVHMC9SmJtj5QXSjmQ+AWJgAD0zkGu8VnRJBY2V8N4+c0HIO7Z/mLPHuzfrbyZm9khmqwjyVnMozm5Ec5pTG6juXucfsO9//O2G5dfUAAYJr+4XJ9uHG6b1908+9r5aFvJLGo+B8FpuUVa4eVR6f+czF9qadGCXrlfYSH6x5sT2tY0c27t2LDTqHYycRgOqaV4ISqcvGmeHL3SXTpcRo+9Cccz5BdGRIkRdjSX4NGR6ZJpT50cj8y079cF1NU+62Y7LrqoU9sFerZj37o1fWq3zyjhhej9S3tPTLm1Csl2z0MbNniqX3LwXqWgW9qCU/FqEyWv070v8RIIvzA+VolQYZjsHAlBROeUqj7sZXBRN04NPzXXc0nRhSRZfn+ajylBWcyEheYjMJLECrZX5Dn4N0Ui9+mb37qO4L4uaL/vq5u6mM2A43CxKpW8n8I0VbHF0fcvO29Gc8UBeetjb9QEUh+FALRa5AHveyl5AQo2niWe1xRii4wMnJ68qkQo6loSELn1AsTtvAEUeML76K+L5if8+aVEH2aN+6KRKBSUoAy1m7U+hX86u34Z1dGBvKvCdSz66KdQxE1PrivDBipdaLkQ2X6X+1qc/BTPaXDQa1kdAL2hSn/pHenrdSpiINuhRCGChL8Rdair388r+zaTtrQOTjcp0z6qrzjP0FIAMnCtd7m5GQ2r+2fIspoY7XXZsBhmq6JluskQl//emzkIThG98GTnqh63mS9oeAKi3LRhTRvWYvb2RNEUX2aPv59pIft9uxwFwSU/UhmY43x9whHSpC+uCVoCmWTIuZA9iDIHlKIDWDKz2QMFQpvzEQUDu9ROz0LhHax5iLyhYYdazFgJzy1Z8m8v590WsyGDJqLP9iAjbdvTik3SzLjdQ9qw0Ky9VDXHxsQ46+H3pH+CbiAxbwShFYsURkudPKalpUYga45yZUjnunL7tClz2gpsTU452Ou5lDXiHCtSpJt9D9u3zXco+dO4SX0zSYpzj7kdvkzyVPS7k5eccI9je0IutqrqFCBmonmHf4VyIAHSJmtsbyYEWKCJkEATEndgD82W0z4TIhbhYdcBtuaIrauP470GqTadzHHTPNe4O025CaGujsMBFNZcRV/x/q4WutniKy2GNgYwHRbD9ADgc5R608vuF6V2BBpSobXRegUQoWGxbP61uSXBgsgOWvh6lReJAl9mpvJtkE2e19YPklTC1GrYqhmasGSd3qiPdLyGXbHXUcOsB8JLkxlKoIxuUlk/dT9tK6lrpLCZPBZhu6i0l+Xxe1pR3+ytwcRKHtMsuhc9bz/3IKh/YuoBMVxj7SVA6gKInMWVM8QLLgXSop7w1liIjaH0ypCa0x5gtgTEOHvUoX7QRKNZM7dtXe1nfeUubRTYZwTQcZ0s3OqdtmfWyGW628QI2vdvBIhz2/TTc1v5aEPh9IoWaElymdVMaXf2ZXPUZHzQMorjJrH6SYnkwaLMg5W8TCppzdVHhT+2fk03zy+SyyGOHhOLAbfh3xg3feWtUVUx94SROst/Y40pD9YA5wVvc/+oACg5zGm5mkTzkKDqpL2EG+FHucR8oQ3DDecSlFxaUl9kRNVk/kq3Z8mcr52iq/36lVnhEHzevLw7/whbkJ6UykuU89HfbkUDJu8rpNg2fWxZEL99XtAzbwgAdyEKGmTsXmf7Ym0A2Be025WLlmf09w5zmSlstKariKjifYzktq4uNeeMRrDL+9IqW0DRtZhWF3Y50egExVkgfMVyM6tMHsHUxpyDtonBtvSVx4j+0NUN8BYJS8eKo9qyk1ke6KU7J6wClRYUMtEjEFDRbvv6+XWmHmRl3/Z7u1UidOeg+d/wBdgXAKdJMi55yPa3V2P302YW/O76hyJ52AZIpmaO2IDEqLoL8soResD63AbIwLv89cR7SIjWj6R82twj0cA4OH0GTed0jfiT1GyklJEQsxxBNuRu43/o/CLFZgSZVcxlyLyjXeEDDoNKd6KlBROpWFrYhZG5+tRt9QG7x8i6u8Z911IOT9z2M8uaHn4lrJbcX+LH+3aeISMx2q9uR8JcckPpzxgNCpRoPJPlvoMytTQnA+3/f7IiCOHKY/mdoUqhPnNIM3qQSfmjBq4ByFapNKI2NRAIAf6E7N23Qn5CK7JToHI+YWno8Ncg0uoqPenRr2h7MsDV2c6g2/dUBJeJDpBqDx4SjeQ26HvnYAYjVrIGOvnwg7IzeR/sug6tyiYFax8mL0by8JOporhum+MvkCVQGvBaI2bafi6TvfuCNWv0aYcJiuNl0u3tF4HLzWkOVzygJZR3T86bet7K4QkOoTitwrsqqfwGV/KVD3cnuKzUBq4yt/n5fPo584ksqoMNz+ckz1lFHlKh9YLJSARJv27ItAgsqqg7gVV02P2Rx26b1saWexiIW5Y+FEQNBEjpKZiixFLlX1BXMEjYG9tcVu+Y/LxdTL9Ne6mFjLX/Xyjf7Q7s5JwGhEziyRbYJlFT5E5pkRpD/yJdkze9pYmUnOQvcvsA0BK8A8HteE8PI8m60mJ6KxCeB9pBAQKnwwbgIHYcTtOwHDtoIbe3Da4F5B7SoSp2BMNomCDPfwLzjKKolnuwGor0uVcrPnENzgcaE1m+sZE2/icpFkG9ZsRe36ZSqLy/C7S7NFop/bd/+Wckz4OlcIxiNo3sQrrqm1kp522K/wquCamaZZPfz4McXUbIJWVptGanKifHjvF5gRcdHxEyCuwTxXV3LBwjWJ3kfA4RIKIC65HgLVtGRWqxWdXtptzn4QIgejLHybUVhnUAS1DAUdqPASgOZ+xw4kE0bCoRpylmt/a3ZAiJEAc2968b4IKP5F1YE7Qzqf7KkNp+/wAIY3VL/mxkw0JZ1gUkxweIm8/Qk30AXdpPnRepuDSZ797bPsg+GT5IpeP+5qoPEIjIJ++ejXZP0a48iBk4XD89l31Vgd5NNFZMmyfOcrSatj/VLWWBNPpMUnDRqH3ezkd2cF9qfjDHyMfFNBClaxx6cYOtQ1RD3tr3c1dL3HmB+TNv9z2x4/wj/jnszQ8BlYdF0JElR+gQfR66Wfn8iUEZm7BJaX9dzihm+KVU4qnRWrtlKn2VugfP6PUEgp04lxVIuuBHB7blY8BMG8JiVl2wfyZo/MNuWUJ+h1Ber/URmWJxFpk7gKJnlNsLI3M5Etan5ka0hkEpr9bcbGelJqTWjdXLyuWd72zuzORM+nUbuyGCH66FkS77sqg+E2yV0Ad5X5CabxLzGTaO2SFj8TC0F8W7zSn1RRmJtuiXUPoCE28kmYRrj+4HDuvFSc04ZMpalfAYCINCFXrZlvxs/rtIQFGbTQjdh3F17rcj8hqHg1KXzlLFUr8d/QxDtPBq4FzAgADLo/rkxA96eIyuBC4nxuG40sOQOW5GT03/opSI/3iNLGfvCUrGDm1ZMhB720k/tN1/VBlj8zfi6HxFZTvnosVorLH0GJyOK1BSeNJobc7Iqn1p3NOj3e3hQBoidsN0qpL8p8hhtKh0CO0axMLUtzmkm6CFb+R6A5HprzDACxrrFaCZhsOjUESazz5l6GPBlUDnkbmdXvLnvAfv+RtB5isjrvsHO4MxkpzL+NFrfI0wB8efq1G0Yf99onnc4V84DM1omAuf2s5hwTR1hd/Ui2T7E1Raz57O4vI4Ui02kOcc/MKLOQzJj2AJaXzvt5NuEMHpWU2zekXLv7RY0lBU10u8+bEE0XcER0k6ZfB1g34WFS/+rCnyGyCk+bofBgeagCftHhD3A+dNCJisSOS0vO0JkeQHeIJHFFrPSH7l0I0tAOQSJdwblHgwR27/UygMYaPHAWVcNBxQAEVl/MNxJqJnRH6NDd7jPbyFxMU3bEwdpgLSLs3S0SGlgrSTIq9jfetMPESFrZBW5+0kPjOuQ/3gRtlZxaNjhQiFBj/Lqv7xPgujC1fiKF8hJkuAe0Yo4dAri2RyEkYvZl2BiOTwLCWUR+cFkmSGwLKq9Kvg9tJ3XK+ceY49X7rce7MHpvl8+B5aVEErJ2THNBHSaZP2U53YXFjCxIIgFU9Mt4l1BRBMrLe1CbD0FQe+B66xXjhHEdo9TOIf5cvJI2exyJl+YbDtZFWWXkc9PE9eUIepi9Yz/cBynLVFUypio6kU1GBqyV7hZwunNSghJ7krlccKe+ezkydLXrFWh9CuwMMdWU6JwpusdAB8rjP1hToRjEADSG6oRBOMWCEBBl818B1Lt0AoDhHZ4kdkgTcoDa8HhPJwZjPHAxyOyMnGemOgXNrN56hlEdssrzN/fT/PFD+holOAIs7MvtYobzGTAx42i68GfwFKzRjSpWTfSdHhe1yUgaAWtS4B0ValGiGfh2Rz3r2bb7ZE+XAJcNovgCR67f50uYctTc0OUo1AOo49lDwqnYIUzHDrlYXWdS0FAVDZVGy4ByOEX7zw3uWHmbgOgUxiGBfEziVVZucloSDQjHnVnEmmxaJyTlVDBcpTA7KVBzJAQU2Oqf4nePTbklAHIjqmBbb/KHmREpRC4LTTg+yif5BgLAz01syBd5IYhHPtTX20KxRVP7AMQeskqNi5KtsR1voGAf6+8UYK+Kg+8Jb2PTPZPttnsIuso61WpxvEq1qcNStow9eqcY8AieCtY+fwyYRI3RiCSvwOK6YUyYbQUJW8AeIduD1ljjyaje6OaL8iDTj620VjyOth7THYpmrlq9Zi/RU/fuKq+OoEGOrnubEZPHhiI3K/uaerrbeTk50yrXbdMYgxdRM/k0rOW3zbHnPD8YF/sWbnr1zV5JRBfT7qWUgvLLRHrH/OqqlA7lRkDyLmHXTGA7FM8pp9JEyzqroDsRjmPOMKvqXU30a5ofC8gIeyq8fqrG+IiytvD82QrFSL488i/vKu3e+MOwDdvO0WvzLGZOKYgEo/Etz8FQQHLVdz/mT+6ICrgIBlbpRI1hLyKoPmsCjfGO46V5y/Uxfy0kTaM5OSysLws0FiAjU5xRQ+giYfXa+YQXauERt3yY9uYQzGvFQ+X32wnuS27Fe/Xkub7Zz3aImviIlUNrs2romHfyHdX5bc+uR4UehNJ9MkfFIYrHD0psz8gp3iyUrn66dWSEwlXzHbHra855me/7hdFxMTIU6oFeiVuD8q/MeH8abR0wwaJIcrwvgI1lFTkDjAQIXUxKTdDd4vIBxqLS+/ohImxM5QO1hIxkZBj+yDcx1MeMisR91UhswIPV18nb1ii5tqkT+gzVFmlBXz/RzKtEWGYffJHBkrNJzYXmVVnrEdt22CrN0EcBCM/CMkfxcdzo7xTdL1al7ZiO41MHCgazPnUp0U7+LmL3xVjGGgQzK2XmWfb6JQPfIOlMcB7z5MHk7jDXbgKVrmKhhnxfDmXMoUP6+cZ9TKHEupDcnihOyIjje8U6a58QhupO+307wUi7SbRZUKIUVhF0wWLo6+wD9nSKyfXt4pW1f9oWsNuRJBJK5pfMmxB2bXajA0y1U4ZTpxoirKryJRJZdj3cvNDktgt3K8fl4UavXUyR+6dlTBH6yz0lEAFzoFtb23H76Si4RYW6dV3016dsh5xNqAw25HsRzIsbrVSrk69KBBWbfY7mxCiC6qrSibjcUUzhcJyvdreXDSr4SCoOE7OFnV5fbidDAiWtT8f8SuxAsy1dO1bTZEURvX0idGWk0v7zjKwtaMlAwCWOp0Sqt+iKXRC9g37ITOmO2Z0FwbnmIlYbBJvBURp1wyuPFLIS0LoXternPZbVmvhY4QMZnKsy1Cz0kVvlGFYWZrClIVCWItE+YtsbzpRaOMcrFP4E98wQqZVyK8phXQMXkwa/gPYVWtcoKJQ6DwbLLOtRrb1B565w0WkaB+ka6k17GJU632MUIFdmBBadiS9Ex2TrUjQ/VtWf19Ja6XGfjCtDeSbYZ7Hn6NadZSYl04+Ui20H2aUZWiyHHYHq6V49FFpedNqcnTrq1oN2PDoj3gosLhEZMVzth+2WJbIhlWO0Cc62gncngEVn5XauKvKQIbfRG9PH3owXnXeerCok7MiV2V6HSWFYz/2TufwvzIXgGBOTY3WpHQTnMweuvdMclUdz4PpO/SC1x/do53u0cA1rWEBEbss7910IHEmEeDVRoOm8Bee4EUjFW2MWzPqzNGUYXGqG0uEI+vkGAXKmW7g4G0SHbeNdjOvhQvzj35fQtokmLuJxQ8ta/rT9nDky8LSxqKyc8kwMHdK8hLXnmEQsX+glsin3gaBxD81cQoSRxTeSygmCYP7yXlROYa40knz1m2Ul1L4yltCk3PrYt7k7hNdvxIS2FK/0SeHmZP7MyardvQNcJV5kELTJcWtX5VeDkW1EhziomDa6XjP9cq1esKA77sbi7tBxKk7ODkUFnOgqnkxPdoLo10+DUdOydInM0n056cQ1cco5zYa3W4xXzTLRZ8SMe1mI2WcsDQAq/youz2K1DMU8Fg5/g2XQumXY3qGjHWSm/ffK723Gt0NnPe3CiVNrXtR3U5wI94T+qS6gQPRbNxx1uySlUSCxHlpmJ/Om7+gw/X5TIuyjNdiKDUjLE4ZuxJgYxw+sbktPl9BEF4mqElNDzBE9nLCEUiHQSoToOYvuDDAL5yUryngyfmWUx+wXeOe6r+SI2dMS2awaXWZSamsdXo1pD0+NF+vOZxeC5L62EVt52dNX1Ek7UnoZILPgvj+yZQ72N3CS9JxYlM1CTY6PDaCLlzR6E4X1j6arBZFmnfh/UKDY1bvdOQWqVf5EctjF6fFPqmEG1Tj1aTQIlM6BpXM+iOkru+0LjdEOlHCHyX8GeAPVRjXaXy4kN2npNscqKLjLXC5BRQjTtfHUrOLSQCLCdm+vRwXvYxvzqm7zarGHhysidhfrU346/7fVerCahd7XZy3HwabpNnQSo8HFyLtBuZtufPauSUpr8ofoObiVdZJiIIxal+VH922oHRvfC2IEhkYdnL2uCiuiTJzaqZoSpUNt9pzSAxKCQGFRb9wYhm8fBofxjGR62f4vd4CXU1gwNDOmaASXbaZnzLGtrGYsueHEd3KCe9DNulVNeKp0ikp0tbdwBWbGtGMFR6YgSB5hPPRy7TE3ApfWx9jG4gArvzUEDVJ1N+T5jj4LN/0IGDTVM/FigwwzGDWtmzYlRkGHxYq+wzfNhjzkFn96DQ8g/RNJxGDdOz+pW9lRNZDMEv5YTFcOj+L+tjwVCnYq1FB6V+zisM6TQndEpSFHMiO/bNoLDH3JRB3mzrkLCL+wC6ugHA/I3WkL2o9fvbG6l3cKB0WFW0vBwCu2KUU9Iq++6Uv+zGPzzffMjQ9IbtgYw6cpij5qg9EUsuqinKhJ9tV2sseX6hG5v/lAvRKPmvlU+fNK7lLnne1V38SddVa0nh7Vnlm1c2zHSYUMmOsNRGmgGjqF1hl1VMCs/+4GdA/qSZFedSsgEwJzPVWt3RNrtw3OUntOVdlnljp6EtRFTh/g/umDTqLfipIxcZBbJIqnD3B+//Lvnj57G8uNwuSb441iUionD5gxvIaha+dXQizlZXLcIK5z+4YgzowlZ3ErS9gz5MkSTtcoVezOFZ5tWmWWHflYPcq0HbqCO3j5a9ubsyFUtzcUixnqufejQHaRve/oODKymDps88hVppsK4AagXuM5jitIXtI8ekc0z2V40PenRs+HE60wiVidyoo2S+pmihrtB5qwsod2EkzSpxWpNTJwc1NVPnJuHaRXF5D4aBPUi977My9Ng6cKEIQCTg9s6K6pKljCVA584QJe5cYQ2CGNCC1MVKEAMpT1kF7W0bC+GX1GtE/7lhLljcj+KSOb5K4r64pc8DuEJ8Hdg4Vgd8T39kD7DON23YTn+zSXHNpJPZNqL2jkgbcs0auOqNVvEVPQ2r928fvK3WCu35qSZVL+VQnHforUO+wJXPNYWCP7f1s5O6BLXdBsXMu2WavmyOODfzbCzJRixLdP1JDaMVj1/ZhsLV7yMLTvayl3pDUbRomhoW8w3zDY3D2UnzDA27szTsYtdN5ktPPjUfTIxYfEdvI4aEIaDpxBYU5xY3Y1bOZn+ezdu3YtoUETrL/MM2DITfTYz8W3j8O+t2tpr8MGGV37qqab5Gm/1ivG1lWF+YVJ5/FnM1lQr1GVvP4y1dPLNqMS/0HYYw/EGwsTcvaI2kzBu47Gk82+WlLx92mM/eYHeWdoX0ub1JQrfDB0Hg8HzzIVPT7+HZqG62mfOmPSK3Aw26Hdgn+bxhpE2dJ1oC2TBC/iNrCPX94OB1/PC9MezpnXfR2ND4OOXVwPMY/Zl5iOqGYiHyLVz+zrqdBRvJWRuaZh2LLRcUBo+FtNRmtSii+ktQn8G3iGeZNNZ4nW7BeVfjPtc8/0/w6M+6ngVTQpebb73gNZW23yDNYc0K1b1Sh+Z5ZTdU9TWCJsBuEOe3cPyddTxZQnjbrfKm9N7HX3/gypBNuNXr2LqUPuPl4jmLWDOfm4Cr4YAZDomVhhWj1jE+eP7gNOAxBPcwMqNWscqy2diiEFvCOqBMSxw1Ld6RyVIo2gDniD2grjdc9+8ZIjjiseWSjRzy3Naf0Vxua1jecXv8i1CuU/IrUVp9nuZOffADN9CeCEK8o/8bgCkX9jSgdXufy05DgSDV+eoQ9JeBsFwLgsnCWtBGQAFF+Yfhr8vGPzXuf63tysjB3ExPa9NwlYff8Aq/yphTiHvHTv2V4UMm43aZsBW8wj6PvmEMhGciSEm7FfGCPCnkwJz00YLA5JzAHV7mM5NrXNqNgQ7EWQEKvO9tFQ9VFOdKhEHz4ufSjBYsnkLOQWEOU6H5pu9MxnEy5r2jjL7dz3gWdBFRjRL04GINkSWgt5QfzgT5r63msTMMEb5xiyCBis9zT5eXWGLFniRQY77I3Ge5ZxPgu0ULx/wzKoqBnfDlIqZz+oJEJLKPEOQrhvd3yMpt2iyS+yvU0oUCNBV/yRaUYL/wlUx6emzg5lp7i3Idg7MeK85/cQ1h/3b29YHenBtWHsVYlTBkJqKDHZOKwxNjn9RjtcqloQ88EgDijYCBA0j4nveI9c38CAUAzv4CEADW5MHdtHJdQfNJwpGeguBmRwnsQkPmRQNjJ9YVJvmzquUW5MZh/F037MTMerHn7HlHtoxH9AZqwHExUzDCNdGGSJsqIFdE+MjqQwkH5/QdOf0zf55vR4SAppcl0TlOIeraG5YGqp0Uy0scgfDniI9Fo2JjrNExnbsU5mc+XLAzGNXnHV0a2Josg42oAAxAlAWfC8iuhDd5z78vnS872PtMhuIEle19k1iZLKJf1OEOSFyEumkkaKxpLZOEwnVdUA6HrIDF1TB/4bX65V2cMu96/sjGAGnHRHsmai8HkVulIljNF/4nKQuTuvEsdFDCl0OonqWm10LZcYIb+kBOoQmbJvJ+veIhiU0ZqGWqTCJ8iG25/UPkj23EBb1+TY/CDKXZB2lIYR8hcrKVuWjClwiBiRDpY4aLQLZCz1SQ0ODmMjO2vBWHslScU4oxVZaUJC5U239lKvhC1UxWvSZu4OmwwEfp6yyv6zXlmYiu4GNHiAULsMOTgDYvnedjK77jDiLV0psfrcmXPToeaUsqp838ccMCqapaR8oJvBJ+XFEyMzZJ7hISpSblFHkhfUMqctLH8nGK0fuevTzHJuLAohj6s6KBVWlOuJ6r/JxoABGf8iyguT4rmwZ5MrHOz4ddLumYYiBtbo6QnPXsKBB9uYkeEGiDaw9PwirHxyS9yER4V7WqI9rt3RTJmbfCjwRX2lBOIp42MELJhAOqQyBQ8yjFK9UdASE2SthTNfCTyWx/K+2VDf55jt68Jil4nWF+mduQj6w/Tjnk8rp27ra4jemGRB3ndvn2X8PRPCLYypfT94ti909KWTpU6MI9Lo6lJMVLdGWIueqIhFPqDxEq3ceHgBOoDSeRKqVazqgzDL9cNZZRaFiEi9JpgyutgvKj6MyaEVf0VEUSOD/kRWIjhLCwOaR0rZBXh3i1KDVtQIJbDfZDNvqLD9gnh/kCKqnBZUCZd5zSew+6wijjBzfE6mr2fyHufG0mcd1tkZXo0PHG3n/4W7FcgJ7g45b9vPmPLTiv2S9Lrw947Azrw1LjDRnyaaAeJkevWNkzjChWp+0K6IYTDjwjW+4VDWPdrV3gmRCmqjbxO2h5Y/ePVRlNd7/wb0ZAa2TM+ZYEeZHzW7On3rYw2T5B+BNsyBFZLTmsWWdCP2CAoj6DOS1MKke0IqfYUuE6sQSl/WmzN5pvD3Ig4pqGFdqiuc7AeYIwyE58+Ud8G3dYga+UPYjfAR/WarOlxH0zQqYmnY9BvqVazvselwK2mswludqwVeaHo7UHhLqgw+5TxoaF2HFBqh6S3jD7mjihcj/xlT7Iv2brREieZ+oVTnIscUxno1MiacKvV/l3JNldVrykD48wGFlM0UlF3Xbg8IN0FAHCnb+dOSwTb6+NcEzVAEvnzN55ED/sZNQ4kNTmybNTqKyBFx5Reekq+Xx8wYnFuGxkx4Y6ksCPYHwXqFwMYlWBtS5rGg75RuScRx7mi0EYjpusiYlVXmrYOy1S3/RfhkMOXvykVwReGfk+JufrCOAzBzvBE8QJb2Hkp7EE/1cCDqugFI2XwPTOSaRIYxmh8LdCxPFsiXB6v/69YkSFt88ZwFD5DyKHgFhCMdHMTbY2NYSLpN3TNuVPy7Fs8db1nL8Xq1ab1tXrPYdz33LM/naMSw0p/c9SB3du65t6q9cqVJ/I4mgS/iNJ+wRwEOh8AJ/H4Hm2iAqWat7JqribAJF5IqlPm4ODQceyPIMBRXkx2F68z24vCAqzK0zjd9/gVhHfX4Hrzq68Bl/rRwr8vhHVUxkD09y9KlEzO6hh8ARDVNi4Shb14zOR4DPYNM3zxLqDDevWu1IxGyKIMNx0wwTW0svYC/NWCUPo21jQY/wN0zECgMGkqA8zlIWArnGs9XVXSQ7vRAoCTs1ss2iAYhLjIJDgU2iHAepUnIwhGjju0u6VqOxDLSmpE1R8iFxVs8zHoCsPmpeACGqnIoZYP5zmD5PrYjrwSJoF1xt9N+JvHCyG0x6BIq6SVZKqfvp6znaehZ9DrZvUfVP/tgIUt6/yvVH4WflnSPw2HVh1elSAM+u6j1D27X2jRTCvpppUBgeHly4pKW0Mn5gyzqxrZQAl6WYRraVzmaz/7iMrGEEP2L3MtxTcN4rqgE0HfSog2APjUM6Xxa9z745PxfIKaWriech2Djbd/BH1xOjf5k2u/a4CX4pk4DGPyVmmY2O0ULqAWq/YgbbTDWQ4lbdgabEOnGMKmTAbtcEaK99cUpd0NmUeLliTA1Wz0idOBKHKH2OjQHVZpXRw/xu1hCNZWsD9hvYEA8Sw1JMuaWL3FtZBMVxZ/JrQnExBwH0AbEw4x39Z8Q+/Z9TL3q+wC8YoipgvREF3teOMBG4PXhO+g2fH6VF0RHFeNTxPV1O/8zdON5UEhnFUPmuTCD/JEP00D7UI7nxYj2TOrcspa02TGBkwN+VQJVZRRRNqJvPJvH19VkemKSWUrEN++GhFU06xl71WuupbjuHOvWJP0NGf0WJyqNm2yVNyB0loPD9Gimx/XWpxxcoVZxk3t6SA+I5TjdFafOg2Xk6lPPstpSTlXbIbVR5C6hq3PBXqQ+RJiRs1xViuIg5+nRDH1M4j+XVUnr3hA9CZwHIGtTH1xTkohfEJkkHOowlwoPgw9z3iSnNWFDIw/Tralg4QeX8EwROIguK0bpJr+6z6cEA4PLS0lZ5m1oyu2ILM2ZDMCFSMV94Y/ABoYrk7ktj1G3kPcoAWu9uT4G3IMHlbWTBQd4qJu35Tqz0mS97JarNOj9S467qPpLiTBocdLaMm7urvZ7n0yPx3ZjEemGXpEc3Tgz/DjgrF0BDHG6FaaAS0hyLNJAs7DzYWR2HYJOUFMcXmrYfFf4QJ3030zf4fVWBWkDcX/Siuf72Xckp4XvAO8hd8OnJkRZj8OzqomKgbammJNDq5e7uCAh3mT3LytOZnNQZ/E0YUPQvfb1ADtr9T4EwTPJ7gm0wqnYQRwXazDs7Y0M3fuTWKwh38lnXnuPj/754AzyDglOpwPngkwAFLAwYSUBPwDPiTUTS3FsLZ52xT/moi1iHCw+hHtOo2crdSzcGHb3j32vK0GAuFtCuIkejZo53pwYMUtjmCxCpJqSwetTPslWksvXSm0xCuLLBLZvBh0Z66gyOvEvtBgsXkUY9i0CfWMrjeJmaQGXghHHK9TUYBC1tMxqy2uzpJHYLisqSUft8RFiToeHnOR9IcfQN8kDrsAcDl+V6Qz5jHd3VwT5DKggOuL44piF0SraR5jVFE2270c+iWFve436dCADbSLh6H66McDwWfEXMHVrsCJspABcKnztqlXmyGHS5G0ED1RBpaEDTmYrQdvL+cRBDvSLCJDjKF/WzY6hNN9QpyjXsk/THffy8J9Zm+pDS18RY4QVbHjMoOeVmDJBiSednQ10pFAVBK+/YFoTRNIp2cuKmLZoqfzBV9aNaq6YgSLLGcxjQg/bBluzg5eZ6aGa5ahlHNBkD9U/jSKcwBH1220Jm2ofgJZweU1hwIE2AVmw4yESzJ2iYnT3crLY6AdrtTWHbSspF+C8RWw2yBLNTKY7hpTzI73ALgQy0sJKtDvfobgqY0WG5C0dVk9vzWzwXrODXksskcS5WBrOgGZcicyyUpHJGCaQ2iRp1tKBPQDK2buw3SA5pkpGYQ8nHbHpqXQYDY5d5vC4fNDRaE/4m/QfIFclsTO76jYlD/U73aJn6uhJHT9JZBgmgRfKUa2SyGTHnI5Lh7hyXURZct61z2M4xWbMK6eA+N4rib+OH+vUMx3OFDBKC6xkH0ucm1Ya1GIHNXzD5Q1hQBQF5NgEcVusO4tSvlqn+po9aag0R0B1wiSkgUBrKjEiNrufhEbBgDG8fIRB7hqIUyn0AnyrV0jRih3boEpALfgJgIvz0Owfi4ZQD5q8pSU9DC3f9fAaa1LTQYGvycyYDrM5DyZR8Nqohtd8qDIMoa3hKQrnl+umFxkINxWN8JET9vrxhVnd9hVdO3fM6m1+VdiQltmWbhdVkqyQMpp3G/FQ0hSsaTgIkGsIJDOUqSDsQaQEiZNAPGvxqChVRv9ygKu0J5rOPwlUeIofHcPk//P2OexORnxNaAWkA7n9S6ij6XWkQZSAc9QEp7WyLcfrwPaDU8QsDj0jWQdsDwXGgITGTvVcrmva/gpz17Cd5QqPp/P1fU95ZH82ln0mdo5uZIFPhtfZJv9Cg4Tn8Zpt2+kNLsT6kf9BtXfMYnHRPDU5rhU1p9tu0FXISzNLRSqXnf3Rd4q6BtPTRwKQXAYqdWH0x8lp2wxBZF4JuJ7tMMssPxICEhwu6koTR5ZxYXddPwSbOPVEAlkE/vBIgEOm4emvhbSCqnom6ncnR5e3+DJIQY5mAGYg8CxaJLpdI0y6ur6BN0WmOMpHneLJpGI4aHUK0UgIJeB70PtUE2nH0wdE4ON7k3Fz3d0QkKGAsGdZcDr6YXkYcZbjKEcbZA2w4dfUbx+sNSmPcIlr4N/aqyypLLI5qv0WQaMCn+iSvIY04yuLmU7r0K2xxs9BdJDkAdVEjh6THwtMbzjuQ57jxzmYX6xnRMEvlm/iI645O0w7N/h0ZowtWFXFwSa8DgbTLVqZqq0iBrY4tuGGX9x8Nt5XMcr0nKqWCRMnAdv48hfsFO+EqpP1Jg6c7MeiTAB48PNVwldqA9ZqiKniqIwtTptoJVRSGOiOL2IDYj5YD6LtaBeNWzXFTNcmM0eByljpCZhCyiMExz1+l9CbYtjEUocPFUlFs5FQWV6QNILAIaAHRSWK6QjQwDpO8I/wq81k15QaEWlqfOjhmckl9C/b/gdr3+0GA3wRy8AQesn/wKdpQ35K2CzHmfWquczNq33XKmC7s2guKaiFqRTq4Bat2GpgOlC96WHYgUeRvQAN0CqHzMv9StLwShK+/AfGTPYkfWRZGuFHwnNvtvoOI3aMGAQ8tumBBs3YrQ32XgOLnnAGvNdw2LGugNcx9A3zVgdjGilY6kkvfRkTvvc4nFGyhBJctNXBHBcOchVqYQHEjPQX0rzeyWPsEF+rMV91PIedPVV2cWat+p+kZoetfyZSlWgnVn/kPqzA5xtq+3rddfP37SzzkJudfXteHCjY/LxyLstMSYESzezSUwbhoNMi3Eyu3cBrI/liCylkxLJ7HIWb1pH7BD1Ub9pEKdn1YkqeW2GXleqvv8hK4MK9eJYiPZqNWNkmYKdqid/g/YOaUVsFIIHruiCDx//xLRtdGvB1zOnhzmaM1BZ7xdSn95v7F1Lkb5YsQW1YELCLv4taBGcOMDOiZPm7gpiDldrw3TgLx6Mg2FKXXoos3ic8fksHvcfsSBtlqDh5Y8ZXr81XZspaQACQUYYj9571UGpTD1rmOOHJCJMhYDQjH7BFIp2M44hezoCuxzWIfYJyWJKh8ZvURZ/h6/cHaUNnzoHESBl4GXpEpXrEaaZp+BOhZFZWw/HPbCnQupzMpTq3zUxuH2l7zfwC2plqvK3qtpJqTiwqoExD6UJBCycoK7QXlPl4j/I4bWsA4PCJcOnJB9sgBP2ITXFi3LNQdYJKkDtTqB7/Ud3zGyJ5yqNhF/Qp1rhkbyZbuAYzzZneJ/OZA9uGp/qOGoHiMBr7pfWSYNrW5hFDdk60jPNDqUk8aZVxXlZXBgJkl+ykOeuU8Ccrs/iF1FcD7U07KzJYKd8xsJK9AZtHqSOhsvrLaERLdOrHkbiDcPw4q0DH5niIM1lyeuuFeyhfpuz5+BwsTyNFqqpEnngmy6/1UyjYRsWETAwTrm7rjklxmYgtHgGWfuH0JlnWZuGZrVZQT2C8k2LapWCpRRP2prIeZqY4ROl+zpbGIshcz0ygKvNUDHl/a/K+2W/O8P9Z2gJ7JD1f585vJVUNjeT8W1y7rwuhxtdBrziMPX1oqsqVhoWHmUEDi+0Vsop7dyROMX5fggWUfeIFxat+dANSlYKlzM7BZDl5R4t8RKBHUCJULvs54cjzFsfZ37MubKWAK91V7crEcs3SFgtkl8EQ0Jj6AJQm+Zsd18XKHLIxWEwISufAEX1p84bPUBnHBebvZM3rH9wj+HXUL2MzVSPr/fn3PjghE12pvvg5JGWm//g54xK7/79s9ig1+EntfM/NZiVHIG2rTHW+01vsVOdIASJvLav6xTBaZ+6bqIr5ePWVroumtM7i7a3hvubg/fGeEXXoso3Cs62O5fsWGJ+z8ocZCkwW56RG3Qf2Yjo5uUM8Cb0FP2iG1P22AEFRu+tMV/jte6m4PEZzeQXnQ3WzX/GzZs291jbahZ0B64EkTfQxNr653n4OH9NuGmJ9D9nCrtIwH7+qt6mbgtZEZYNiuKcWGVqJjsRScwIK/nN8pdre0essGhkMtrPIjnkpm+cMXKC9dDflLBodHNBWCGjWkMZ7ZTk6W7YEEQgYA0ug5bUA7uJPxJD+iLN1reD2tBcgDLprpdfdiSEK0vbPgrEIiwCNXghfAUMWtlSZfBEssc3pISrN4a4uz4xDOKc7pue74X+2aIvTm2Mqy36eBkSFh8iokKcYRVPcv9SJgrvbeZLL5dCTanbNjHXvlkth4+YYXYSGDtM5EXHDyon79BI0+XB6yA+LrCv/cSl7pp7EX6MAtofVxxaGWsfuNKzGZWiIZd3vclvKNZOS/m6YzCxDomfzEDrWlD96dD+6ZquDoxpoytU7Z4TGUITIECYicdJxVedwaWNoB3tuNKqIW4KiIXUU5IM2dlI947lEr9nDYyqqDz1co9wpeRNQWy/so9xEA2bPrbQ1piY4ljVwM8FjQcf5qTAqdHcESPECFR9t7c9EhYFKn4oqtzvRP8+Dl98U4QM6sdxCOa2HDTjhgzrrR3O/PdxDiFTb8d4A4OyZVzBAWrh/jUdcRA4UoN04cw3ElwvQ7sGtAowmJCkUPXhyNPBjyiBX/jtc0sLO1QGoaHl9c46j9rnzykHfgWHPOMD8Y4NVEnOI8bXPAsq+JI5/DaKYwUKYcc3OD2fH3rehBvnmenXhzj9J6T8t14tNvUuHrPtwewbz5t9N3z3+8qOM+m34XCdLdiACqgVAAydBqmRlzAMRSJ0l5b6+rwIqkOpegf+XS/QFgMKYFEoOS6fYmKqY0mT2ZMzqTZP2dOd5I8SN9Q9cmEkRKetbeYuvg5Wc/l7dJ1DvYzxY/fQbp9r4j34kmSBbkdXRZbsreU1Qu51F0sD1HCeSiRSCqYUvjUcqGQJ+9tpKFt04lkYF9/hxjxaHbx4c6KrHPJM/phXInIAu5UIJzNLxFkG1QvpGbiSJ33KQ6t5AqOAYybTWuzZmLpmBU9YmAFzmjvOwUuHISToWT3T9QbXsC/6YhUab43auE62XHdyKJnTxtUs4yIcbgWvAIu863xMYNQOrvjqniLiRAYSIZU3z5LNy17gC6DlrjrjChAKeXy3qMhqUuiQYdOSHkLZNx62rGwvzwBIEc0PC9zX86GPwOplfrCok+GTlqfLIw6xkyMGiLGHm6rqbTA7Qpa8jCwp4Pr7KwQDrs2e0udgXMTmfVV3wzUuVWpox+3qF3cC91vZDeJz3zcWynqwt0fdWhUTzsTmtFuQCf3iY4RWESHOuIxCcAC/sSX5sgyV51N8i5E/ADpTqitCct2mt+OfBVSkLsjWhofjxUFwckZNjJ2GBn8+Oi0y+dX8cnDmSqLmRaSOw221LPzyiHrMVwYoprnAZGHuE9EcP1OLfX4WZr0SntQsfu3BhyvU7/t2G5Y47mQWN2ybiEGvOW8r33mDFYd77JC+GG3JxmJqtVjN9NKJg/3k/akNnjEGGqtzDeeiEOB2Z8ShZ5CjpkKUuswLu+7tMtO+walle/0YzSdI+x63qNci8x1QiQ/nVt5qvSPMiM+GCPwtFmhFM2p3UJKtZhZo/UZ7iCsA9YGvNrh38eH8o07GPBiRSd1fovvuFtaREXggvNbDMD4ec3FyaR2j09QPpHx53AbyG4w8vwZtMaMFffg+ncC/dShUFBNlVmRNqcRV+IyaoXoPwcr26iNsJzRyXm46OZDjIkvaqucsWz/NeMnkh5J3osh8e8A3URJbKcxB6WNjU8v3S/khqwzZUJP3LRDAV6mTURg1pGY9HRPdSTD7xS5xla9ps9CnmtV5tzP1OPK5CauVFEf4zxnY653ehQywjNA+I5rw3IkplhHIgbgkQNd15acxoVHW9yfMyf4hzxVI9+h7zqIidw9SY47AROqQcu5QyBQ3/EX2uBsdmL8EJgVAuNpD1lYR8R/76cgbn5QLDKVIvsHlqpJO4WMIFSSu7OR8rANjHJU/lz0XqhHEx7Wi0rdcDoSYQgcPFKyH+0OdHJ9mTzxfDItX4gXNX+DLH47c9xizmFUiDag5vcQOkDJDSlJ4kYtFyx5x3jzgxVD/jdEzyjIMUIgvKT2NWHapYGONdIkYbbIaJPANbXJtOady2kmHsQ2RRTY9RGaTFLR7uqQnf2WzkwRuw9ETVBg6VeJrz0Lx9RZJ9txe/68NQdzqAeyte9Q8Y+VJr4lUeDpDuy+BzDjYVSx/Uh+yZuLo5HdVWqXdv3nR5u2lXvpNpCPVYaLvdCorHnh2CatniPDCZb8pAow3eFQDCsP9rRE+04TaY5H8OpLpX38nEI6Syz9dKpDAEvC9PR5nnOr/qlw//UWr2Q9JvCiaMMFWL4ULI/G/CCGbmlCfAll4EWhdMnNcVh5ZeQSrRma2kxCAAn/KfLjnxm0dI5Crnnea3jGHveKKVI76+ZcWnJ7LD4nYhK9Y7praAqXJvwx+jHM/mzXs2f3XWhy4e9zVgOljL7p0/7El3cqmRPPZ1p1khIkkrfMP1OEP/KkwL3jI/qQplMfJZP/aCQcAFI1kF5UNX2KBuRDhxbIaQF2xso8MuVR5qvA30lS1VXKEjwg4dnfdg3/X1dQtETxSDrg7NM7NSIQjabjeoQDAkwAtmAlFVUgLUWSGcGasO1UvWjrTjIFg5F2c7FUHAYJjRK4RaE/SKzVfkRZqSo4zYy/Tiy6vtdusDuXFiNwNXVnhuZ3A9HZoOVfNa63qZxborCuc6KuNjSasECSVJ8VbrJOIuKw8n3ERCCQldqfYt30DwGEfcepfJAXZfCvaCuPyBLMCsIOwWzbuAnOykWTvmnCbDTzcq/NaWqxZypTnUqeXLVeq4yiAHU5FxUPuDu/FhKfX/5+fuirZHb7fkT1IDiGiWI6zQsBo1wpePExojnLpRHnjRH4M+B8pQTMpKkdqIl7nrdEnxz2OZ1/Hhf6XduHe9AJM7PaHXzwzAtIOANyOqcRQgpbsY+PsAvKczunHIIw8gx1LhZhtNJUKxprbRmAKW2/heG4X8RtJRybPdaXzcABIdI9P/ece7h3AgyY1k7hhlh7V26dBt1clWGv6KnlRgLopm2FlgVwYUHcjNfc9g1AiN25yKzod+EO26DsAaeavwlBq2YsQeGmVKDIiD4OoUegs/wsu27ZUpiLGsA+JAZRpcjHhmp6zQYyxvNVFW4mmoYaudGnqnE3xAojtL3geFCFwNd5ceH5ZXmh1voMfFj6ApjwhIUPxeeHxFGbRpV21XFMeYi523XmbmL/pCdKa+dNJX8saTiYDCbdebCNSAj7FC3mgrajaZkuTX8+1ynVNUQVIpR617GollDT0vsHYDfsDLwOnG9uQDXz2IWajUcaXU2LlFV8s5+vRB0Squd3M6F2AtvLmEkGHvufc/SUpG9OXWNnBHNsNyygg+9BOLOiw8RTBnXhk1UchxtC9PGLbLEFOhycWj4visl0YrNiY6uVmo5owtUs2pPwThkEi9NJdX3BQEFLQvSa+tBwkwFYGI7ZhXQPw7EQEWHoZnaohMvOnIfOEi6Doi2neggJJBiImZOPbFB5dQNk0GmEvDLSQmNNca/O2MXRP6+lQTwt4SOGm1+MmWst+rRGC4/g1ghQJEG2Ehn7gdTTPae02qddmULJhAeu1CwPSYqYqLykWSIyRReRSH/fdaQFKcylUdfBJnAm89LWivk2TWyMOqTK5HW2NOQheeMo4xparIkb/iJxA26wvzigD/pXLI+UD+BOwxFtznj9UM96EuJgBoELPq22jlNjED+78vJihF0Q9ecPxfVn17cPAFWoFNkRAQUcaflmqpKWhkKnAXFEMJQ8iRgc2mUyJEGwhhd0YD/Hk1lJiSWqwROZoOfXMJykKEpBoELRqjBpRO44Z/NsYCKpK+ncwl372CG+jPBy0cpL5eXiRty+89wQiEtPeRE4XnIk8tITUGfajutSfnwz0RyO7TCkMGdAT5UOvkagIE5hSonO78CzqY8eT6Q/uab9QGLcTs5sxKmUr243345QMNihXIG/G/uHckSXIj2Q+56HGnhPtlkxLlmQh6DX9FD0HUpY1NynopQtx/BmrVBMGeDxFWx4hndAOZKZlmam46AO9dSgXK6LYZMAJaxLPc469VkSmtz31/Aut2fEY7SBR1fmQ5/4kiN8sUxQ26q7nNSNuy64nBgrDLWF1Uzk5lGmYKhZN944pMK780oANPrzlStCpWWefct1ZTLB/j3ucoNRQNbF9ReoyFKO0tKpvKjX+4N0rsMgUJiZ4Q6SBjhOmWACI3DMKem87TrEIm27ovTNugI2EO8qTfqCtntJ4Dt9iKTxBhhk44lYV16Bcu19gpycXnIGPVbJQ0gVTdnrJZ+Q22LVYOEmQingAxLu3zJ9fO3sJOfsm6BXnu+5kHmP9HMzDEtxU2EURYzsRqwxxN3YMY7yFDYTPbAhyjHyCjUd2fTmdGtpXZYGkyFCk5mXJZbmr2XyOJHRn8iPkuxKa6Zk3OtFKW8Eh/b72Rcndc0aLZPWgGRqkovGsOomCPUZNuqu0MD0/JiRamNkR0DRWOe3iST3JS0jezsmGdcZv53D5yileRxRJcm7eg64vPyIlJLjFNUB9DfCF6Dy+ykki6uPebQOCW0vs38UdGnD+kN9o8j1A4QoA1ZgaIsydeNqOhCEJN9hBhJDRVFh9BOu74vz7S3KRH82K8LbvKvCJbRUtOpj/6yoct0u8NErsCLoKe7XoCES9wWzhKQqVkWNNZ5FLTfm+nUlZ40tCqCDpCaxk9nLGL4bolnbyuP02qSc1z0QM45NhXna5AOyat1uz1DuZSHtSRfkfJ4GBKxXMc8WqxYF8kJqlRfCVDrheA5xrJTii5fKkZiAG5YpNAfBeDZGHHDWaYYwxY6LYhJjRZoWB+DPee7GGuvavbyurWZwnq7vphESvJQd0u1nzewNb5FLhaGnUQLike5sUkdPzwWN4jwtE833yMPZ9JwEcHE+1tBg5R4Y1VAKZhdTQmVGdtIxVmI915yMABywlKDbaZVWeD0yVy49aSZIXoePu3l3CwIwxpiTPsvHD+Diav4DtDXNC2L7DysmxqCoBmmjhiIfaWCRwmNw7O7ciCw78C3uhnP9j2dIK3SqnlEOQMRTuBrpfle80uit6Nmo0EUH0rFp23P2jTgpcuKgbJ/1GhMi5H9uC7kSOpHcVdwhGYJHqLaVyTlFA5iyF+pIZNWW1I+IMSvzLDA07W2xUPqJJGNFFwbY9ozhCUjpaXoML9JBL64xUmJqHaGF7H4BboLpkYuuhy2DcGvDFIgIEMKVdiDFUePCP8THNdkOgn4TDqrkSFpAwffV3VeUQRFkMw3AZuC3wjB6cQ85+zuYKZFdKmvfzjQnObH7tax7pdapid27VvMGUPzo8ecRqZzrZwiX3mzCMj/rfwd/oSuBT7h9VF8ymu1Oy/ZBp6sZN7jIfVj+2wiDvOZO8+43rvt67+li6QYd+fbG4mnAYiBCtvF+LMsfSCLIIdUzGnoKpqbD9sZmcZSeTeVMB6TLaf1VRqKfe+5GR9BXie9eedjqJK8QmwOy11XBEsTPJrjgCoHoxh2mQ3NJcxAxeldyc/D0/dnQsVe0rI8zjwo/pzQYjw1/4a8PyyLCbIHTlaUf5mw4hd6wuMHQuXkFRmbaFqQ1UExR1HDwm4edQN3uLA6DzTdmJ1Kgp2rNafKYvyfoh4tfHjp3l9p9cB2mF86M2s8nJJbFoJNP/J9NBDV1an8NHR0fFwUOvoJ5ZBhsX+LfPgzsPbuHzD0YQRcdZouZmf7yOUDtIEEqzFBwSYvCD9OaApD2+IpoNZ3ej92ec3vtB2JEVdUewRPrwSkSBYDj+d/bpFuBkzBRXJ7TkRtdpWsTlZJuMGuyGa5Y7SE+MZGlp+uUJZhHpgYyesuhDbQcYr2CL6YPGEB4guxYPFPHEEYA/nfEy2pt1Dwpn8t1MBq7uVKwq7JL9jqVos85HxAW6rb1872o9+yPjGMO9N5P4g4fQ4audfcNOXU5PfAzKTBHpqJIURjdI2/rOg8Q2L8e7/fyXpb/YEV/Hg5JXjeyi3ppK2nEeDa9XEBIZiB++eWn/wff28/nf0iIsJOdtR9Nu9zjdQh+Vnnv4O9la2Cd1zCTUUvzLSz7GFLbh3trv5qiIxxlKnnJWhHLSo4cWijtk8le1mlPxM2WsiotbdGsOJlWb7xROP1ccXH37ZW0/tNy+xhjma7e21+vfCk3/Lf1jmtttjX7K/EBy5h6aXpFhTRhcukJ+bxdXSbPOUP2tEym+niweZkc0pivYmib1+mENSjfw8Y0PnQaLQ80d2qAqI1JbRookTxpOZTCtvmBstdxykgbAesvpCYaoqBu75KlR3Cm7AvnAzYGlz9x8W+m95ljD8Ugxw8juOtpThUAD/s7BTqVukLEODHTAuJ1A6zdbuafovli3V1vrRqJEczX9mlR3Nst3VkmdYohs+dB1zNaU1+CYmH3ybaU1WGuvLVSXJiiGN7fcdYqxtYYPGUuN41CO5+UUSr6Wzi06RrfJGE6ZfQ25Obr55Ki+F/p+G/1P86KMErekaVfSxJ4MzQX57s9vl2xyTphH1rVJsSS5ZekpbJskuGiYpY4ryxjbGp+VHBT22B0lVKte7QhxpNtZNDXtun249AvaCTV1Qzt2i5qqf6C+32tBcy+74G86OTbFo793N4TdmGANT7rPb3fmS06M/W8BYOL6fjEGFER1LqkjErR7EomtfFQjd7TKCVK/7fcZkFi2pkHDOaSh8gTUYXnyknD3um0c/9UP6QDy8kESGHwp+kFNGbBEhKQVtDF8P30jCNQl4+n+rvpOVID4DlPq3gcZ+VbwxfEYhzLXccKEZyHxxbft6OBcasYNSW37wW2L+xzK7Nja4zoAUgZx3gX7sHlPzozbzqy/RAJyHpLZVBQDDH8Cn2cBpJ93mI8sDOjrPtym6hoFf/aVly/30e2fwFpye8ifoSOZt9X05V49SiNrKemPExsbevcBr0ZaXn3yih0NrklH4rDDNWIxux7OQ0bzLYPHqyPWswiCpKpW/agyMoDdLrn/w7plMSxuZ9mRvhobtM8V1J8IaVD5Altub/WxkgAAAA2afGLv/m4//FRsKcv4bNKxfOUkjvza+Mm9Ir/unTSH1v8j8uE4hL4aV1E/v3tu4mwMY6xwbVWgMhlGCsonU46Do7XoULGSAGd4r930+UWDkXE6SmDGnNQFIGKvo1y2qEl367Pd2if9yJUW12Y+0g7dNNhmjcMC3vBugjtDBEy3DM7W5n1vWNNKXsH6v1fdEKkzjlFgbe1VTlmxqXUkr6gbSbMUF27TxxLBqtSAHSaBOTupToc5E49CrN40e4DO/v1JKdlkp6uOtDZ2+XhcPAhndC2UXb2tuBuwqXTi7js3BFnMs1jBXvrdp8Law8X9P4l9Eo1mMsR/kGDCNSuAQwzVhrPt06j074FDOohqT5jyepUUKmWkyOzhAWUbOJ/S0Sd7mjz8EoU0r8GCK9azrQvxO3MaJVO5hQNMHJGBnhluBO3u/muDdqmFVLjjD07qn2Gwblv7fJkO56wU/z34UqZdhjNWp3UH3xj3RHEh4g2x6is68OoSqiezzK5Hyc2s+NufzGElaxlm5Q26IBiHgPDtw0GUlYJjjxXqAPgJHH7d0UmzVFJLx7GYOIMjdqpQ6vtpCe2wdg+vJbBGQHSfCSq8hsYe0UXmCrmP/i9f82biRkEv7j203gkv+nIxP+KeQjjdnddJgmiKOBiagajErFdcC2P8qY0mOxHh3pkifk6YZaKrm7enO89SdBVCqdCPpAKBVeOauEAqxOIfwDhdm8O7BRqjdeUW0yAbY/BTEwnguLyaExcBsUKBV3zOjwWWyvpdBjjCSjwQOdXeSNX2Jp9W3uFnVa+jpk9wWxOtTDf9PhUqYcU5/qy2xTPYJHn6W3R0NMyaS04cvlmUkfBtoM9ZMH6hloNbWepfFPUv0a4zbpXHBTNnla5SrHOIP18657U0S82TgvK6DD94y0m6vAP+I2QNdL30oMD9zyg8hI8F43a1fKSIb4pqYA6s2uOLLTn5Tr8xmM1j4lZohxOplw+xsHPbeF1gCm9nJV6sy99MkZsA17p0abbvC398tyUNrcYPDX9Y/L3kiW2KKlht5QaRYtkuPaupIKxiT2UjufljfBBxyEwbEH6DA6OmANXPimUreIpz98vlRs5S6d4Eny27AQ5xUNJxOT8bnxD9kH1kpGn6CZszRZvJs1aaUl4tZz6xpSR7gvDd/RA8d1cFLISTNVn5KuOIA/OdPley+VbouYWmylPaB4jbsgxqTWc4Z7YPiO/SHC+DZSXv+qgJK41JL5GklserkQe9hMCmuUAHWZFEy4oHm8s+QOtd5EgsuXpfKcVaQEts2LDpoAtVON1YUaS7tJwcKnbem0HT+5C/JwToint9xh8vMVHDydfJjbl+WO3VWzSBmVZCEJGuSovoGmq8Epo/cs2r++ulOYLJ0YQVg98sr5aiRX4uyhNGdjTVWG/UO2XjrAK/oodkcMRtv14gYwNpKpfi/YppLY9mPK0dQv6YK+v0knG5NhP2hh1n98AW40FOeVA6JRFki+TEOfyFvZOLnDXPiceROl0ofKcu8VJNRsYzMR1Xiv9/DtH1imckp6u6jEQHcDKVtFkoI6yJZQXuv7yTUtDswVDIja5iCC/xJLwuAUad09oeZgK1DGL39kyweLnZ5+/18Hp7PWxdXdL18OAMUPAfXK552/poxpEiPJsnM6rsg3AwhLuRNPh/yyYcYEXq47Vd5gRnn0+k8XjFQYZwzj7VmR+9JyGMHQLsCspzRv0sC3/yy/g24Az9b2FrDOyvv3LBvlLx21D+xEp+K16C7Kd7/EWkSvibroqPE48r2KtTuRo0VrsxGtTGzZ877sH1dCpnQPnd8CPYpjJRvegh/9pcSf6BMgURki+P1oXWFFjJR0pWcA1aNhNX4MGBUfVWhD19Vz6wF0Puibf8XvQdbvDfu0KbmKGqs8u1PzB1CjYqODhF7jmwRh40SCsvdBEny4Ewq7fM15C6HSf/G8lS5rrSOHGlGGqeJRzDW6K8DUJ81MK5PPu0yjSrS9Pl187jLqE/xFVwJ7dIiQ/MUkj3V36m+evK36oeCGgXRyJuqKF4nNS86O4YNKT2SJpNQvxHEiw8HA1A0q83vVk9tEcYCwpD7fs9kDpLWpsQmelQ7BdB9HzfK6Hl8nJI7bhgjmxe6uuKgKgaxcHo2WH668HqUcc7B4rduXp6JI/rC7nSJ91bFX9jmJPogaDUPlgUgSGI+eTn1h46kP+ZBYjgynL9nLTtjUaYC+jbsEyS440ta8q408L+4y5h7zIJggz/BWS7AP8o6giWyFpdigRfKbaWNFiuIA8LMFyz4JmS7gsDqkrI/y2ScYAuMdUj7yRimkZ4g9tncltsEEZqv14WueRwTw2sO2VX5bioUQXPMkC++SokWt9EQav+fv/n9gvyJkrGGjEP7NZ/2Adt+7Ty6IOoVFkSMDHw06jWGNvjFQ1gJ754cZ7BZgGsub/lVweDOEhOehSlXj+KxCNw+6Vgp8YcBNjM9DBjESSNRZlEoQyrnc6szlPlzOsRobpjEsmWUv7kCRhC2mOJBOAQEi8sJHm5B7hZMNz6360YwiqFFiNKoQll1IFmzJVqQvF1/AB+DKdij0nEc4JmFUUVu8nmmXrE6U7poa47XSIj/ao17PcK9QMVMDALYeKxSIC/MRibUxF0JNCSeRFiGFymZn7xBtdDinBUx9DkZ0mGHmmZaxVZ8sAsPvWwuIOs1EdUomQZpLszzyzZs0h9MxY8OB4738xpiS9fbR27CihEuliubpKB8UBt+pu7um63jCd0LOBY/k3XfffVilw7KPNsNG0h/0yrtwV0K7gEuuvsEKSxHUvib/KcN/C7xU/kx9TOqZnJnASObvhRojm4UXx8L1RTge+0C85IX2+PYI8rE+SLGHfyUDD8iYAFk18yI4jkFx75UXJLpZwmUhMWVl+uPAdY/pnVKV58Tl6zpCzWfeVcpypZbx//bVHMIng8dyD23yW0YQsqVIgXJypBiEET5moK/s56w2aE7tLlrGjMiJbOB5XIOjD27Q2tBx6BbrIO82+t8q7YkLCiKaqs7rbT/NiP8o3tSte7+C3v7G34WyYaIwn/vgfQOiVHs0SWlBhu0HRXWFXogblm0XwQngGfLFyoUY29xFFAhMdkJpd/jW5eahJ/ponlDmGWl7kxvEKmCw5+fTj3Am9DPBjiVXChLC4hmifv0cU24iGDI76zcVeKO/WEwHS14CzRuFf/yMKIVyjRdsWeFMu8k/kns2bN+tnsI/9Jr7B5mMEbu0KETuQbdVk5q9/71iEiIC39ScceKtAGnVJABKLWouMGfjhP78uVQq1Kmq1hZCR9b13gBs0Cgn8fQCalQRV0LxpMykh0IZPUnPkn2xURPNaEeVKrPxBTb8tGw7k2XjDD5qshcQe10wNE6guu+kiuAbQq4PECb7ZSjOg3LbdNpYZ1ObIrR8AJr9BtPuBowOLkkHkjPunOfV5BA/9xRnz1XtyQQd4lthZx8L0zcITtstMwnoGqa3ZSn8O/26wD6CtcrLTNdntr+/F/tJjSVwLjhdIzjenhbzEweANITaiWwcAcFxO9CfR2yC8XtKB/gasCr71dJN//PkKliq2BnsbvXgRQThEhrMovqY8ajdHeSFnBguUwVqjgk8piU7Rx6RzmmYFQdkHs8rSC2DQanDJArULRLso+blSPFeWvLtczOF00zM0zgX6GaaC4bB04uN/odJXmsOTT5Q+XVvJPal/eQUWtOEeJlcTafSK6P+SV6/pTmW56KXMq1W4EoQUvUrom0RWZU6Uzhj4vCTKJcu29OhdXOCADgqHCIN/ffY8Ex1NGyQLuk+U8F4WIdVSQI94Hd1qbb8b+WTvvTBFIwSxLsr4aNPgpAV9uIS3qmhEIHUoRK8y/acfLTIj0+WZmsa6wFXqaa7gZDc08N31hltnjair4ZRwo4Ea9JYnZEJz7bRU17W0oxulUM4jlvAcwUmKqp1rh8Rs6V/LQYvJmwGJJJI3/pfwE7mDZjD5g03o8uY1idnMKUZU1bwqd0x+JQiYleXVKBpNLFVfsMBLV4cXYLmdMSKEjF545UMivv90lvtbh6iPHxHXvTxVylSLRJj09UNP3pz+SEBm8r+J7zIF/+FZQz9M0CX0wzNY/NBaEY11DDJXzAGS/IpxK2xYFaV9rmcQUq3BucOYU2vtKqEf8tLE44FjQ8sruZ6ZTBdtMBG+qQk0yqIx4eHHvHE0jaDSVBEID5HtN1zLBQbWDzLGsTR5GFJMWrmPs0cfUgqb4ge8FG27uKa4FbDCx1jlsp/uGEZaIY2D3VUSTxfplFot3t7KEt7PajxR7KeZflELOzm3PrQo8R1HSZCPJ0gwec92qI0hiLt+UpfUg36KJKi0bC+5GRbVzXIvzP/Mt5xvKKVEVi1j4/lNCC/0kpvaPUPZ/McVCUXQSBOLuht3aKPs4WhFGDTwQWvcqUe++S0hP/iNS4phXNIEmnmhAj1xQHljm+cHUric/5xn7IElU0/GzmMXnOlxwklJ676Q8Xg9rXVXpnheWHtSFA7EV3mOcVU7P7cg9U+mad48t663speMr7Gr4HSM9YBnMWYmuCFOdwr+7M9rtJCvXIOhztMvga7PrusCDJIw3+/5HoIcKreAOyIy6W5pQy50Cfj8sk0glNveJFJB7h2aqdvbizFSMDS5G3U3uaEmRqC68/Hu6fnSaZzjku5/rdC64xqj3z6d0uewsXcXqIj8lHmCU2oAoBOg1ZbCE6TNm/YSw6WUsUZENlVzELMtHWEtWdF3lTm6Oqd1EVk8ZiJ5yL5ztPUu+Q4BHCL53sUhTOrU4pxlKSLX/caHaZtj9bsA6mxPIhmSJPqBXCAShvd8zXf27/uCByOncYJs/lt8S9O6v7wNE75tSJhEdPYvcpSn6JYhyI/gddPZCkx+ICq7uHbSATkDZ7s3UbxlV5JCWIMLcc/BcEMh2ZXp3vIiE6o6rOl0Sb7Yi//7rIh0Dqfk+OHA46iXrlS5S/2Jy3EGV4szA90XW31wSU5QHzCcBsGLy1Q9SMLCx9Un/Fug3f/ntr5T108ZqgDKv/dWOXXWgVLmeePmw0Wq24md3jCpYSM0nGS4VlDg5KTNmIvzho+2DGEjI+oyH2ZXW8a8kDmszkndi8ukrY0cccLGUjOuseqhuHyZZY7xf2BXrK7rp6bYaywFyI0sLY/olvX+SwedxCx26pnHDRSZNE2ViNQEFtAgjW6sCJzXQRyid9gbMkoMfYvvMO21PY9uqPaxcxzs/uT2oAhc3DkNFnvMggAlhRkII/YBvRvPP0hkNyjAI3lnlykECUgRTFcuuMV31ApN+oo+tVmeDUdB5roOO0fGpctKoRN5VYVeOdfa4wxnvPgNnSapTFhCp7icTAOVvi9GlC9Ca8NeLf9CkxDIxblLnk8oEFhNRww+xYKsFjOxHl1YKO5F3TVjQa53EPWCvCyjZ8xg8UY6QauOSxKz7xjZHdAicOCp4+G6Dwqe7l7OYW0paUio1SZZqHWGWCZOrNfQEB23CNQsiojrwNFptsROGvlWhaOQWmosK9wgPPEmwFF01My1IsxanL2gnPz49yJYwU+2/gjgS9AfzDwj6AZ9x6tROJtNg51T5QC/pfeSZvk89FR8efGfynLc9iL3YKEUMfOkM6jBpue6bL5z7dzcwnMTa8iaG6qEJyeko41IOyeOF3O3TB1OI80cOIs24DGsieYidp3B1dZteiE6uHZLXIF9wXAaQl422YsQVoR6Hr5cUdgjdXwfPNdPGVVR7DbGlNrmYuArOfmRsL0v+MP1UvNxxCIsKyo27dCw1kCEd2i2zUBYja2/5bLd1XcGkQNCoqW+M9rNexLL2CO1AEh4dCw+8eFKpg1xEMGMhxk4JaWAO4KqhA9umTapuNTazSKlWeaj+I1ELwmfPQ6GMI+SUGQOe8Gir5X8EiklR1ijITSRRKXQx3toWAeSgyfK0dgphyHP882HpKY928+wquMuXDUQKDPhwvRft+V9Ulk9Au+QfjuGpE0KOZm5JVK/VzD+7WaI6Ft38/dLXdcnZy99jAcool0OCwY7MBrjcWHwh73dqH2gR1x3cWYx1wq3l6yjYFTp0jafdQXw3FrWhUTtub90fQwZb1KLQLlplk9GJVcWxF5JV97gIzOuGDBmeePy7BR3SjvkyJR2Xr6NZDjcDNMV4brYb9cqvaA3kqZ5mMHYe5tvsBjWvPvrbR/XnlrzvN7uu6us/rvB4uq0mn0wIvhtrpmC7w+R9NvJWW6lI9bXyLzwjfJ1lZeR/f7ejFb59hKN17xwznc2WMxAfp4oMOilacS4p0Jc6M8H5eusWZMX0VZXpXb+x9uc3+E1CQhzwme0h0prar20+8Q+Pt3czG5KZl36z10NNoB6gJEUPfEC3VswQyelKrKRq9qzgCynniOTBwpmey/mEiE4kA8X/fPDEE+bOO67R8h/X5qMMT9wKL3xG5dO9wnV98k1vrr9ct7W8xeZDAoNCtRlUnhcAF4K7qIARuxYpbqpDDauxD+Pqghsfc1Kg6oonMXNwdowxRxUp12ggFGML1+Eg22gJtxHh8s3/n5zNfTAfcM5iVuoM587mR7mZoiMj6Q85sCY+K3tXk2cC7StW6Ab11yy7lharq6f9CmFJz/+6aw5YEpC5r46KRzTBLG7+yNiHZHQweHxC1H1ObPTkuh/N5vJ5P699y7OmedpgAmrdanK2eOuUK29fNeNrO2fdc+zbBBOq7zzizSsyOnoG9Idls39E53v1WT9hoMzmC13J4/4qhW528Tna7Oik4h2dhOnacDPtd0aMWxlNlJhOpkXP2ec75Tse6ZcXAP6/2nu50LEeNU8ZdQeEPlbvU+MXHw48QcvXEY2CQRCTm9JtAI66MQuDChyVh328/AiApZHZgrS7GYoxpYkNRfahIvc8kAZwbmomGFsMLV3BMOM5l1X2ZqBduvIkRjvf1EjjkOi0oXQv/y+nwhlfbDMuSwwk2TGveS95ZOOYVDKVP+FTXGo+xYfa5vvzcXaxahUSXlDkDo0cvbBCAsJTkQMQ2GT4aQst/ke5+Osa9TIgE7SyfJ+xBz8d7qc/2n6ToSrvS/PcgKLQemGdMiR45LNslBKv3gRAedtesAVXcLhZkWcMqF3FNlB6+ZOsVRF9GOIKyWWa8SAf2JEJSPKbSX4J3zK+90lSDo0j0CHp+Cr//pFSKTdXsMTbqdeyoHTJg+XhUeixrSqCKgsqSrMYPeuKo7GkaRKHZfo/ClWUkj0PEXUG3dxMD22MbnStCdOndDgzzP5xmvqs4DS4JSR0dDiQDis8svUmoFJ5cWRU6vAbtrHp0af9DyGfpmGqCD6kIpd/hteoF1Spd1XS25TGr7ppIz8iFV6206JmvbPJcUaDXDrh9/zcqPhMcm7pknXnJvRrG5Saio9TJS5DIqpFN9bV7soLw6EIxqhwEgVSflQODY3oIokv91QTuyHt10mcOeYqRRr5YJ+C/dKMB3yP7igKb8JIWddZSwGHpTpnqTnn3DkBzI+jXoySbGovVjW8NjoXa31/rlkzUqTeYKnF0fzSyaz4/8AWiCrX5xFcSrZp082/5+ACmhiTeTDefq7s9BmIVt5wcsUDdLYGvFvuvqT4h1PVTRy40OMffu+acrlSzbEvpx31+33l3TW7EXaSer/YEXLEkSQdmA+FK7zn3pCnT5M91RuIEQR4/W/R/S0c7dPbaPNfVuu+tU8q/qbncLnqO1p/N8JK0jfDJtCQbs27SUOMxKqCJb9XIZOetl9sX80I1llNnZNl52L7Zfec8WZ+vkHorovkgfXZu9pXNbg/hqiU0R1vOr+6RKvc3WM7utzB6r8HghUt/Ym4SdA3SqNkHsKTvEZBy8tNw/+0IWExQaDxhdYOlj6bZUOwbosK9TPF6QMG2f2gpwi0qm2+86bBTrVJt5Bs7dhMXpxY1IZg7Oxy4Ek5DWs//BSBhtFT32sPF9ubIf9jIkiO+FeJnkKqelruNxMZOtYcD24Xb9HUojUzH657S6K4sOc/IFbE13VvN/PaI2BAvlVtmXXDZQL34zSOlJnPVmxBqW7ryx9dYik8rhnidpHORNFite6sjx6VpallZWoaMDaoG5QjYuK/v7qT5UMc7eRNUOgtvpqgFn/O4X1UNeVT3vWYSR5puk/1MOfTsWLk/uavKeNUxvuJRK3JWMozTkV1C8Xa1m9uhd9YEFZH5tLmLrNbDn1XIJFa7pvO2Um/Yj5q1EtpDowQJtObjEZWlBzhb3tom/wEQv9Q6p/HNUWutcR5v4ddo5T9Jom+/iKo7c6b+IJLatx+2j3vgMLUQtNr0WBNlfXi1NHiqTCSZVOUZ9kY08Sh/xmo3Oy5Jnfdic4sOdKESSaqDpABEdnmj4RQJ/+1d89aIHDSbv6oamAIfdXfSqXUNCNq7fa9c17YE5PsU/q6tvTKKYSwXqV7ZyTPJ9vn0e5RFLsjjqTn3+mn8yl8AN9I9qIqPM4BhOJcWMFkkvKPWmnpkLhd+h0lw6bZ6SRwh0T0+rMBZR4hlWRvtvA8h5ZuC3RD+34B52gHbQdDi6gZoj8FOrh//lUz4GyzkmTlgZuX2RzoURlIYun26Upz+En1jteVgc1q5N0l8F3fQ/C1WtL+XBWf9CxfU6r43Tp29dhZvA/77Z5qF3l+21K31L4D91NYrEOLK6LuzmFW7J0iojBOIYXUvFwx9rNT/lNwl8R5mHr4RoDze3O+X5Ld8tJJB/rcV/EHh60Dts5JTfYX/Vu7E/iBl3w2538nfe+om4InarTqkHG84pUgth0KIdAs8e6EUgjw4X4PXjntq28E/vkuP01/9ybYXqYzgu3FdHIl3EcMqJMhpmYc0KMSXBPv/ruAY6vgz1tsBbtLrmQB/E4Ykdbjc2NjN3pcSxGpgbUX4a0BHLPMaIVAqrXHwcE9owYFGTeALKOyFohcybwERs9g2WC86lPoLOKCF5a4DZB7d1u5y/ZC9G9KOizS6DbuVweiF4v9eoaSFOt9pQgVlbrCK12Khg2rOl1meQ9nStm5iBzaVd82HolOL23lRtZlgrd7BQNkDcNF6kNq5kZT59sNtVCJyOd02ZqL7MJM8P+NivJX4KQ9KCz6aIlCQ93AAQap+rq5TGAyhROTXa4R4aHD1OfGaamnwBbVeFyMvHER2F5fjA29yZNeWhaxe6tJJPYlGw3qfV0rWZbyGwonSXGdSbXVozlXPCzSGF6DqoF2y7itWaWDMmGBeeS4SNBowx6VHmdu+4J/MlmJ0+0e9aM9JOtejHcAj8qZ8z3xMfk5MhrFJeOPDFxL11Wb41tv5abCDgE8x7Df2Ilo4F5+laLb99Bwr8l3qENwmgRL4Bh9gEI7QNn2sWSloXS2SJtKAPdbj3GZutBEfm+f+6m4xjJAZpN2+GiG1z8V2bWt1Jtbw+S6uuNBw6Ls+L6blEzUrq2IewqVsUdzefIlDSPKSsVTW3WXgq4pVkI3Hn0qxZgBPDMnswgif+UF79+EgqEFnjhY1hRGBzJjX4wDdc44ugMnsNiy5D/4++1OyH05iXVYzWe1EjJZKQoUSApyhGPmaeSqlBtnTkjr3gjgOoALe+l80sx+K85Re51as0lFDdkw3e7/9g7L3bczYbzdHsjzVA2wqmot1OPt9u3YBNS2V4ZyfDPf/9QOL3twVod8djTWiZmRD5QyN4uhzD9gcuLUQwfFPjEnVBIqqil/mhrlWjVipVZhzHGeWFRhla6o8TLnXf1XdeSUj8YIw8hIy0dGYG3Uq5hpRCmS0e1XbuCgGAJmmOEIZsmE2x56v8nthNz+8G5rUQsbfq1MRPmUonbuDQr7kNUZvilwtTQLOTl+L3kgOb3licynQjrAErGkcfonCkQXGvLhiijYEZPk3TPDueN8whhuaEpL6TXlY4SOVwp6zuujnBlty+SZ02/xIYb+d27nf6LDWG6RmpoNjfCblpHvK6Q89DHZw8IsjSEtCpkK1He6KRXvWj3avR2qbt9ephQ61wPXuJOQulvEer8wRuF/7tq7o+tecJzsTY4+qEzvXspfWgoxTCOwcUYj0Np+OETL16APbBXpseZyQF68GH7k/WSet92Wbkz9X8tDPf7E2L/NW/5DPxuG1NiIuOl4/4fvCkdOo+OfG8NFxDb33RKT2997Hw7AbC2olJKy7hHncqdIo/WKoR071gjcQZT5OY1+P/FB77wV/3g8yzO0bjZ08VKGfshBHcQUc5dNBK/8lo+vPGXn0cOYe9EWVVPvaOS63Qxj7wu4jD1UZAWdLnW9m6WAiPxk9jVpdyhyw9N6MnqkJft7rbTls8PE8QYSgU3H9ruC3KYGFNecy3iCt2/iTbQmE2gyH7MYobU1cf9m66YwfK7lsvKou05ov9vZJD0HwvP/5z4PfaBCBaZT8MtDkwQTbORAPyva4CLhiHllvplOqKmDdR7mSi+V0TEfrT2MuJyiXMg3kj5oBPnpxhZCEYuf66PKFU9qOQiB+u52FFyBY8krPwovPvQmduG0CTRU+0pU0iI0rbj4ZL/mkxHWh6Do7DuOQUsHILqWol6kzuIOOE/Mw15/qv0sbkVm7aD09hawOcFkcjDwZbBainZOzl3K5vXrWXhA0odo1oIDWh02OZ7/OBMQ2NHFfR2aZ3dG8lTbP9bCKbKUcuF5hl66hy1IjgSDsP/Q08np448fKwnd0uZLIZQscl6K2LbCQMwP3wqYKhvVYezWx9yG1ZciKJ+9BjvKr1FLFnuMe8K/FuaICO2k8kM1GM7Ds8khL5Vt/0I70iqpAIz49RRFPTz5RP+k4lILffM8o6gpIengzLldlK/TtF78kpdL0QXbK9r3Sd+YExDx5/jzgwE5H+XafqDk6zkvOav+ubja1FgyBzytWI47YfjLq03d54mcZ4+ikTDipzsTQOCPVNqwyA1krCRZY/DJyViAXDy3gYq4vgMMu4i69o6juT+99fpcioxoRD2g9VZPokUOMgD6TwHYAA9SbMNPSTXGs5fibVDeVVVix4sybro2ATR4sCU62XKzUyErr+I1cpNZjytg4UVyrLwtKuWbRY+w70U7/UVT4ppU2mSbFfHB9TbTfPWMwVztufwuNXNLopZkb7/AFaWb1Eo7hx5qa5/KHiMDyPiko20JdkA38M+lL3FaQXQ0pkNV543tWqRCavWJLqYyL5iW+34R5Nbx09R3mXG9RI3pIONwYtIJ9CkrI/M3deVdllgRRzFwmexUJ46aFqW8ooqBLq0vvlaJRNJwJFOMYd6PuF+q5w3JJf1pD3HmKxOQzLSPR4P6gPwp8psHF+jIshR0swCv0PFA44pGAoLs5L2n4uKw0iWW3E5T0kTuayMcMG0TuPfPdS57pEUxv6KfaFtcRbSjFBAi6x4OdCBzgk9cyyDIzQ5hi1hbJvtXhhFEWfZZT27T9I0zskuy2irliQYD4W7KMtrGxYbdr0KQh2pKcYjfCQCWHOhDcCaWDA1/yXXrJW7wyqoneUeXR4uMBg3ksAmT/CcosRwGxBEbnW0UN/F72qIvQ4af+LsU4ZN3O0zm3N5kuoyiuydTN497s+pw58/METK/LeNiqKD9FIPHkuAOgoEH4zTW4LIDpEioG+3XO/C1FWHOrwFx+yfthsCJfOAtQ0XocfZhcIlfJLNKgu8xbD6GFn1GcJa58uiMrQqxDt/DUPEAHbml1rBtZhdDjEAiwynzDVRAowycOeJ8KQyvEmKrmsseaeSOgmJXNkyKooAQmrwEbLytxsNJAZelSfIkFWFsXGb5BL/0wDHIqKSKiGf5GdT/wR0lSNiMqJXQQ2V2qZ7Z6o09lIX9e50T+HxpJIciPRdRIH+BpCbdGiTY+j5KrMm/Y+UcQEQ3ay/oQaw5EzFPqikX3Aa88gON1HrBPmXLtH2u3p+cxS+ztCmotOm6Odllz1iMpEPq0Umetqw0ZfiuNG8Ka6SJBWUvmbvXM+sfsy4OVK9C71c0+HuBrDU1dSPn9mbfT9sehENSuQui1IMtYVIH8MOM7Y9VZ9FPkUq7AHZeUNsZAoJErT9wrqvyUEHI1PxKGuG4vjtciV1tUcFcxYxNkTI78Nxwxd0hh6LkPp5aMRO9FbrhNQcTDyCkH9yJfXT6/daWTsFkhw6GwR0IgaznZjlGz2UrOmmUmov0CSG7LNc/Jtmkke/uI7kInx5CSMMoyV1blLLLMuMZ3SxekFfECRB7aRg+SifnRanqPJhMve0UpyDIPMoUkrrefr7aso7c+jGwxMvPu9PnIYSxChR5hD1GrKCPSwOBWK07wgHHxlboOUOXoA602WrQAujTJtpKef7pnT+MyCeoxvSGalj3N923ZsdqwHowC//VCb8H756KXzcz3yLNKUOdjSfygjvZWCdL2DXh7OQ0K7oifoqKjjE+hjGcN6QMok4S+7qBfQuKvnsoMScEghzuQIkd07vbMkVyuuCUqFjjfshtXtZIebdPfnvgV9+1PXim4jjHDvTFG3rJc6LfVLV5FPtlYZLIpMatSu3h6rVBCUVD3Q9Y94PMC43/cI39Mp5H6H1D7HmEdKXj/ZSnqrDRfv8caTva7P78md5V/pQOh7vJ9X8rmCD3zYAkKk3UxkclXpORqECtZ2q1FBUKudqwywf3BVzqV3klN91YwY7MPTUWp2keZu3sg7tTBmGKfntRWCVt0jNUHDHH4CdE94WveG6eBf/f8ojWdu5o8Pf8gRPcXpl3H/DNxFJekM92Fa0e9plLy99AHMWX4aUZ/q2f5H5dVa4thDPpcbkGT+j6F+YUsXi00I+QkCO+J9a1EzqDknrkg7eUeXD3VG8trkcOgWncI2Qrt8AmTpAJFOAVdvs24P8rMuynsMy7+K9AUPmXqFKX0V9YoLZHrRWv7oMKokYAdsBPbW++xJyqTM/SkLoG3DVJLko5x7TWqGvsGxYlMB6pmOc9os9n6vdYcLm0oScQf8ax6DnSPRWzyR2TJFSOmY3hWCEAbPU3nfr9nLaNOoPlHM38YluoZ3RSSR57Iblad17qFuuNHWqlOA4iXkczHn9T3vWTrvYRk9HEW3Q7cgIAz0dpBqLia7NN6s81GSxy8US70M15DBU3NQrH8dY0cj5jIIEd3gMmrK5Z2QGzrw029ZzI/9KDKqp0AK32pLLG6sEpYzSksjySvnMC6CAGPVTSifUvbCIUnGy5g24/h1npaX26c/lRAvjZ3pMUzSsttORsAFWsKAEmIt975oOV0x3hrzUxdMZOhWiMyMnNGiQs5FGT4tUeBLrOH1lz7s/IFQyCgbiympQ5cTaGa8vZQIxEPEmRFqkZ8Fq8InQpdDq13HO643u+PEaTE4z86VQget1BGB/K1NKo+3wWYSNj3fUY7chlvqOp4IFRJllp5orFSi35V996ETduF31oelmnOdPDIQdj2GH6gWQuF/MOwVCq6a8pb0soVEW+zAVVnXCIifSYCM5zGGJjkLrsgcEyhtj15xb8WzhXLJa353Ccn2vzEKPcS4E3Uqz3a6XZF7/tAIo21ogjeJaqyeSSkS32tBW1bGMMmFiXvN9ghi+1326EVh8Jb7xkAF9+AnUvqvUKNL+iaLkgQghOrotEf45Nn9AcR27+N1ibbr0+JDRAyCX0HxOYQmXx1xf1ub9Hs9S5KlmimwnCffGFE6pga4NQ6Iyb0iNkXSleSMNxu6VXbDTvmNZ8iAp0e2rpaihV4sWJCzXqjOr6JaxX72zSqxjsmzTn6CtxTrqGajkPWX/Lgk0KWrEmd+62mJvsUb/zbl6DDazpbBhTgsL77pi2/SENyFTkqhf79G3EvJJR2/cSK8p0AvKL1PdLuFWbU9RQZHlJbr/Sn5x8VJsr3axyIdh6zPYG3nhn7e7JGIdPgVfkLBjF28w+Yd9Xmna/GSP4ETM9tAGHfGdCq7uf4Dp899WCgzC2qkENtECGZEoXd6gb1wTLLat8kTAdRzgMv2tJclRL7RpFlrJwuqdX0446MAhrIkLWYySuPnic48dI3J5QjdL6xsY3kDdtcvmwBXfX1YAfaRWWoGmJLTemltpANnHpmlTq9P5rGbNObQmXKdOwizCJN8cKy72PLsRr9uJrrgrbEmFS8j7AHuu/gX7r9hRRcKSsIGGp4TYuzRji84teT7mg1ShWVTzIfLHCKiHgGwD8crX8iGgGW5RKfJAof3G7Yy4NMwoIlaCPmaUefwo2a5sVZtTzebjOmqqETfRBptHG0pDqqMPnOunCv4haWRZY//Xyi9jLXhomwXycG4dbUp5adsna++TuiE9G7TJiRb6lC+pU+avuZJWNrRFLh2aUVbxbcU+aSOERSxZ1Jlo+BBBoEL1/AghRChUWIXLXEW7uRwCjSS5rhf6NNkLa2BhC+JfCQHbrzy/9PmEL9+lhjT5awtYhei9XWS/4QSdOH4xDVpUDxzuuGk6xdNr7yClUfgH3O6IRoT33hmedBztiYToVySIw+ukJu2SDWHHjQYtsZkP44hVP72DXWFcArKr1zCjuEOUNHZhnWCcgt7ZNXjNHj7okNoQqg2O1P64+Z2V7qmnp/RxePsZKbX/QyZTY4/wGjw1WbHnrokvtdfU39BHKiEEf9Ca8JMnXL1tf5HCZyOtQOY6M4j4A+f6rGOUt+gcgxIg66FRfP4sRcyxvL7JYy0mV5Z59viO9mr+JYQT5F5/UJQGdzyI7mvD/RyOk7OfahE+p43wYiskjH1nWKvaBjHgE0uKm/WMwvk/up7A9MwWV+q3dlbRfb05RSEulr+SaAKePBZEo3vtPGaiurnZKy2QK9IJAKK0imupdj6YBj9BKtpMG/9Mk6VX3Ll5u5SDVVw8sOMyvUxEf2eRDleqlDDRZBM+fxxpX47P2y3aDEyL76rMYjr73/m7S6oOHvQrqV953NLBbskEmzaQEmrBuA9TMEIwSaRqaUGqV/fpoXsn9QFTsVUmdeh7WcnS48BYQDJbAxlqT++J28xqTiWjHd6ykaexIHfTfPoWZG3q3GYV+sx6TR3X8sKEu8fDMjPKtiAxtkBr+X528VsiZKEPuUsK4TpGlGBeMgT4LHpuUWwnlII76v+KurByGVcFK7d6T/J2olDIw/mqBb6Z8wvPjIRRtD9miY0247wrKaoD5P6TC8pei6wQZjwh5VTlaYLBgVKgkG2A3QxDOyfHryLRvsr4fmK7aV99FRIz+9kDHW7/5+VvmPzIl5OUFn3/OZEI0hq7stGPogo/Xl3FdlKZeyLBGPjkDSCoNmscR4e/CemlXjz+fGvuQuN30smqQ9TEh0vitwyQQiAfPw/11qYy+Pdug81I/iwGlrDfAOtl4CsemJTEqwM7hXkSBHoNZM0/vAugBxkkrmZlxUZKW4E2xw8NxEelNCc86tzIwvbKEZ8enySPRHYZ3Z84p6CB6Lt0LOyu6Mro0Zwop7nrv+H21KbQlpx9aI9TdX8E8rrCeYsWfKp5e8ykZps1CeCyzZGQBylVwuOFROJGM05pGh8ou5B88fs5cg4Zn0N021Cx0D752eiPPjkhzHv1jeHM2eQ9zNcsgQ67+RXpljnflsGOzMkduQHoPREVuy8zBTgTQUHBVw46vYsUL8W1NEVgjl33WfevY8iuMw2/t9hB5C+YutKVHT8AQtYeXzh8G82ccIoptkTXRU1nMl0ypPkBk25uxrGEgZe5ErvwwE+rnBPF/4m8FVhBjpLeYA4hGJC7evQJn/Z8xiNHSbZemU9IXIIbUpPKekctV7f1+yMmQY0duylJqu/TjeWXRnOmhidnpPfi2TsTMYicixWmKYSfCW9ys10wQWJYyAYvy3n6ojC7mxL7aVGGjgRCYuggIl53p54LgDVL+fORWAdFYzm7jgNAUe2iYpR4Mu06IItWgQS7bvT1cKgwVceMVPZhm0Y+69C/h3z4hQ5+Cd6LfaAIxruCWeJfP2lMmHYr175tOWHOffU7CSNkwsbboMB292Z7BgpFLCxBavs1/W3b7cRoZRs4vIknmbB3s8z5KjEEymhPLSWVlqa1GMqhK3ulTLInCDk8l2P0Atjx+SbRlM9yGC8VrJ61SaJbTvUKHi3Xdp3pnnmo63zNMsGWpwzMybM62idkhl2tNULnkgnMyVnih+m4lGxzXJbgbsykGFRxMOcUG+ayaIldtjX+eU7YYI40CKGPimykCVciwQlXhP3QkgKko2SS5rfRFHIyKWnWpO4gi+ii2FcEM0PeK0DA5TiFG9QkZe4GKEX2D++9jnidZkXMEErjt2zCVXyFaxE9NLlMQZmRfajCPzQWWQWHGfqSie48pyjVriZVCTop5MurhRjNUctVZE3+H1iOAQUhkl9Nu5ojVonG/pBDg1SHyDEgS6vpyS/HkZNUlhptmyT0NSS0zp3r0SJ8kY2ug6XevyU7JUQnhZFuyveSFfqkHOGry2e/aYOKiqDbCOnRfp6eanAKU0lDYebW7bwpXTiosC4nODtf3X4u4ib0KqJyI9GY5MZImHp0OecGdaGOEmRKpn11RBI1fIaMSacwzUZpLIy3u4aoUUv21sKmrjm+2Ar+YPfNrGEQoVeTAJtACl50/W5JoLzpXaKB1JCZRRp67ybiNPPLWzjGaiA1N3t2ye2eIyBi2Tvz0WeHAOjmeLXKlYwo9rlFSqYsDwlZ5LAvXAiUVe2AztuEol0DXu9PZNCfvxxCaEM1GsoHQZuhPSNRqsE4z+pOxTYtTG06d8Rs85gL2s+3+wl0CucxSNAbs6PtuQ2EFSwpaukvbjZh7zGkHISdQhc7qRgEJi65skAPKWkOERGVGn0cBUtQ1JHEhcdxXDzeY7uwZCqsKnN5yiq7yq4uqmxfDr4GpI0bmJ8tBvYh+5ksXd6r/DuFqbyKYQdzJxSYxso1vFY0Gqv1uzZ477nLZ/E+vDU9SIPSArJfolW+gv6sr8QoXW5fryavtZKgJs1iKUXtbf3kA+MYKPxWeGbFcIn5+fr/Bc2a502BXQ5roDzXRtysqu+7Uz0opZMQAnEEDShyKYdVSPaEYmItJiikbezNPp0XicW22pAjPgFb2flKFtvKApvs7frbGpsMVMpGMdNOcXgtUzJB0gfG9NO0MBhzk+bIK/L3rrZEm7TrugqrG5gw1hfgsE7wvWsxOCZ7OWFYRYvRZliEOa4O7PwPhg5KGMJ3Pkm7qroHZUoCfJGrKrS5OirFoAWlIyxnpvaReGZMrvD69jQkHQxHnsq5cjhInqZXxqDdQPRxz5xgOJuozveLmrXqlfbgxYbFOxNCbn+vNmCzK/TKKShy1ElJ9kTrJ9T7YdCcW3E3pVyZFptJ6AnpDMVeYb+imD4SY3NYAlJYDjLYFQYbtw/r4KW+f5/p4n5rmIASpg46xjKSM6xw97beFGyU7/r9zF7LXL4IpTOJXHDtveHCvJZhOQErpZsFB57zGuCwkl8htGNc5ZCedx76N5uj8hdDB6pqO0tJvPJBd6VoDYg0OvMQlN7tIB0T6qFA6e/VgoUQTqbZBDl3dWzqkz/0KRlZvmpnEt7dwigU9sKxYqXzUc3Ki3fQ1AkxUKe5UwydTki/hI3bgTVXgiEo0n11MJpqJ4UtSWmLAWtO0863XkHtmI5+/XWnenzf7k5SPHyGRfkzG1fuKL5AoTmVXdyVfhjUV47R5hY0+Wl3tj/isGDKh43LV8JAVT2PCZpz4uUuOWHF+VlPKlEi7U9c5qRdAjvOC9R0Kiyk+pKA+g7vqK/tWCnHsBhGjpmiGMLjY+6Fa9RcQRODK0gL3PMHQ57BwlOLWvKtzglv6nBnFrCpq1Ixwo4aLs1Bl2YLFXdnhC0m7tZXpQhBqJlfU4Lm0aJbWWWKld9LTmBd3/8/6facINvA4IQZMj62g44XEqYT4dYXYnFaqLzM9UsxMYR1t5j6xNem5sVvGaYEQ8dK71oJC2Ghx92dwUJTUOYc3w/EX6/Mqwh3Cv5xrLNxmFocLqcr3IXFCMUVJALxhcGmC5uNxSCGnOtXaE3CYkp+3LDeFHMqYWROFEtZaKiXfIshtGEsLkrf++kQsn3VuQp3QjZDM/NMJ9rzOr/wRC1XV/9qakPSMkcaNJzruViVOQzkN4EIWksyMJljsAu2ZIOWeWqEjl3euRy3VfGB/VInZra78p1YonLkVVXU5GO5YU1CQhan0Xw1ZGUfPH0GH7+4K2ZXwTzn18ppvicP/EBMgMhdFdt7z4ZnGU7So+u/t57B+tq/+ru+ZgZ43EQmj2CP+BohWHem3VQ5zGiwshTr0YSpEhO9zK2buYesFcHo/f+RZSzB30gGH0TQhAjOicI7DcRZCb4UhbscAa81+HcBGnpAqsF9icJcO0hTBJw6QLUH5RiAOxMDfW5ACAHkWjK4woD0Ebg3MTA3AzZ9BP5YMvjJHVRWghfJICLyKroMGgAEAAUIAEKoipKpAbihegTJijw9R/q/+KcBvY0b93cb1k/defPanm+ebp6bV86x913TT6/31d41A+dbe/1onvP5y15Cc9MeNs6dsOj5cFdjfAyHYXKX8YZD/bNo4118/eLW4EA0XmClUV7hngze4ZEsc4NnLI1/GEjKDyaxgj/4jlVmgR+ojD/4zEo5YlOgiBAimiUQWrYmNcEIKgvCxBpZEebssihloDXZUSp7lSvKnha5oRxps/TYGdcmjzjoVJ5wiRvkBddxk2XEDeyMPnCZvcp/XEEPfZNE+kwz6pZ7o1Nq41nlL/XEEbqgnsPTpmaTeVD/RKXM8S9UPQ/mP6iMP9n/p8p8Y0rSwG/zFRvlwvwPq4n/mD8m7TjFPZEl3tW9xTTKXP0nq8SR73xTBv4Z/2VZENx6Ip/xBR/IJr4yNm1By9is58rSlBsu5TOGwpSl7GIITDmXbQw20/yfU8HN0GZzLGw2Nvuk8mhmc9nEwI2WYYiEzb+Vc6QmNzb0kTJTlbKgMjb/c/kn0pjRZgktVL5YSmiJzcLSO0SfuL9mBn8kZ3t9WeOEn6fFyxF/9M17OHzTjTfL5DCRtK7HHAzJ5Eo20fTfEMrT1QboV/fJNDR+q2Nnu9gEgzHGmBa20M3xmPtME3ldo6BO7izkTmXT0vqYWdgTmRWmKvZ15mGtel9ULrnCFacgpRZr0IdwOUwdMSbreT8PLgIOkuOcL/opOrt5soRTNM7ei0vD5pIRgF/moQTDti+3pcWHa3sIFLdkiEsAkU6Txjh2A10wrp/mTNonP8gG7sDkFFRRJIyMW3b1LZdNzPAroqqDKoe3hTpghZtuHdTLyEBb6205/fCYdJWDj4lEIB6/tV+fm81Xhr/aLSgTPt/ULqV95/lU7RcGHSUmKGtFnGTnJmPo5IT+1c1oSiPi9wDjnXfVxSgLpzRiTOopbyQD4Vxvi763/opGJna7HMms7cHTubIsKidSUR7szfHexC0OXAFqm4uvTEYIu8XDjVM/WO+X5RjkivdxhgGsjZkiSW0sLD/MbKZOO4KUEXmHwRGLBMihIDr3ZBIP2WyeNAktRtI/Mg2kXL5Gxr1Zmum4kHJsusjTQwodIi88Yu+ADv2Uq/7Vcgl3nodmYm1SEPaOvumTkSR9foG7p79CjP71WM+RsCCNKLTHY9nQeeG7q31D8GluRDTyoDleppApKYE6l8RnxQcJrhWUzM+sfWbbvvgxUf992ZDbHEeZBZbR7yVpeGMYz6iNDyy8FWYO0n2qcUm2+Huu4rVXEW/oJpN7Tq2P37egD3hi+dy5LPijTwI+roNpGrI/z90Cp+bXnY2VQbsIWUEdEhuNQisUquchNG9eFK+iJ4AlWIgX2NN68lmbZfOi/x5ymGtKHb7qYpjtnZuSdlXDpkyt55RyoRqhAg9FykI4Tu5aT5GIKfz0kKpCyhAckWgfwLjb78KVQFre8E54zmkAB+r5vhecv/cihhPiSayuh88UGGipfQ09sUT9QyHwTcnbTeGKWk9IOn1aN3dVQbrDRIQn5zYQ8eR/1XmQRF4Ep7DvxKRDFKkWEsMvI56ALMiMA5F1aTGnW+O9/AsQbGq9MS6MH2PKHHnhYqN0+peIYY6R1xEsHIl8w7hB2uNuB/zNIF1zRgE6Z3JViPeU+4nnh6EO05d/CfAVC7LC3GqezDZIXPqAZkPubVTIRWvGDBNx3g6Eorxh2IbtR3xgEpkYE0Z6Q4MAcBWhJP1SSgnmHO17EJZy2Om/gG6XmUTwSAua4k6w0Zfh9CWih0SjLlkk+LgoncxuPAjurCedYuZghRqo0oRe90nHIJDekyC2czbFsGZryFMxYdjjWYHkjsfAEy+dV7CIEpM1XWRUEbS87GHdoIwsojNP0+auHZshEb709/1b35Qg0Rm0j6FeQTJ4ZFuj72TnrmN5BtL0182CVCZAIH3z2bM2tbCuQp2eEc+4qin7XcLavoySZyISaAi1mSSmn5pk8YaRo0+Y9RXF6Q7DMAzLGu6RNpOCGIfp694hvNE7uLWL1kk2R2Muj4Noc8qefsJEPqekERZIfPQMYc9rn9lr1hsB9x3CzwdVTErpCoeqCZydBrT50HLnORXdhmV16a0/nX4dn2lTjIvHs+GUSJiSbA6XI+ByHuqum9tYxnE9ydJ7gDQFOqVX8eNuLp0njJURztjTuLs2J3G13lveErY8Q7a9qIpdPUj0veW27SNcEjgtuVxvZAi9gjGL7NPxWhWdaR1zcE6h535TCLeBuCfwsTAqFaH7kD47ZqCPUt9GnofyLnPkysoALUzlAORXi+RmtgyOo9swWhbw2TaNU6gFeFa71e4tOv6zoWMj+uj8uDLeEUlx+pQuyaKfM8kSjgJH8gvCE5w1PeeA3Q8qLnxr0PrR9kQVcQcR9a6hMsZbeLwcu8nZAROpBYCIhiJCXm14LJod2RiijA8RDSGXwDTmbsVPhu69JVVbB+6wgBH1k6sUz2gCDgWubgU6jVjFEtuhFx0wK/hmmCCht7NyB6N4iq6UUp1ZDxEHgwSi31eBxVZAkzjZWH9mA+bPtgPCsFekY5PO2bxaneuK2m7H4BnzJoa2owUpAKgWTSsdqXzqPqFmMGcn51wI1lpFoPbX90jL9ET1QacQNyoaSGzyh18V+NwoUKuyA/tI81E/wlw2/noqd5rb4NYFPM2qxcM73nq2deJ0FdgDtTOuSxDokoQpK7dd6eeDaZ481jO7vDDuP6YzWeIQn9yQGCeuE5cukPHJJz0hxdPhxlZx8pvdNCcW8wtel8lqUYee01xcKv+DwhjFEF59TumrS5lhAKHhkis03obgnGQjdCi04KHNRdqEY9oChVGrOZKOIqTO15uprk2BihO5KyN9NvLbRDIe9297lcI0bwlGMb1bJawSEHTKGpYKwQT2l5BTRwpCR4pUuNXQVgmp3JZnpkWl1ksjRdyeavSGiJ9tRxWILj/y3beEp6NdS+1mqo+ZwlrtelT2OSg3kE1flbQW/5U/QT4t8fHs2MlgAWmglChJtUwgMgaOw8Yv2fvpsQEkiAADrCas2c19lnke7bOoiOrkl+COUM885WGxu2C/wds6mKUJWuIjBb+FLvDYKx4msp4MT/36HRvfTj+pyMzvdjG20SY9bICHy+uDX5pMeoyIJv5rSxsLxqi+V2f00LiineInKX63QGewX5Gpysv1gg6SMRJpm0C8oVwalNcQtIdOsH5ZNYjQbyv7DH02OgPHnaP9ykOVVdEadFSPJPmQObO2bDSm/3Rqk0zIOjv0RhkFAAXJ0fZmfQrSCtPGLE2diW/wAVw12QdgYz3zTwkfVKIE4fyMzhG13aw+ApQL7avsW4UmpYvL+2d+tdagHrCbdVpVDYZhGMZEdo6JyKBlzPQeEg7VGpUFbu57sq9WIKF1U5vNW1d4CNuiBRteopiv5Dt03gFwFzXA7YRjlnNuFtqFH73QjEkWahVnoaoklFpz4UbeHl1/rzypoOw5gV2yIzhzZ4XqAw/Ee1QgkulwJdeXDUcUF0r0jAdsXaLfm8ivXEIEWMn3+1hXKLxfddUiZ4z7LRQ5OCCHeZI9KsTDv2PAKHTElpU50fTMF8hDLzCTDYmpeGD4k35wGjzEdPvGCbGCbzl4dAwrBqxqjb3kV6vRB02+AHcKeswLp3RTlTRibQyK10EVt9TovuzuxFNaLGh210OyLJwPmv10dVdojziXvrgfBckKu54jWboiF/9Nvl0U7TMOaLH7sIgUtxCXQpTDcYihygj4ZKrXb6LwMdQmB02tssdUqVE5c68tjEuAxS5VJHPd0ie7Mn6i94LlwOQAE360e7hHwItmjeoZUQNbtrlyMnAnPRXmkTlt39kGhbwmph+vU9zfkTtcjdGlC7xiUtF8dKmIhBB6AVYH0ImXqaiBkl351KHKAuGd8GqsJZL4aZe9QnZTTCrTA864hTA0gysYB8wWcGM73D55gVL2yskHUv+GUsEZcADcchhzZYaGy48BRIvc+tXDWpcKzBC4R3mxKjVDYaOJsPxMx+ltCTcxVXmPHZvUwjdzI7rKPeCMMSY3Kwkt6rohD+DFzmksALM4fr8po2cA/6nzZQUiH++DJSwT+VANbuHRvOlHUrsNWfmdDvR+Z6Fr12c7QcJFVu3Pxdr0S+suqOLHCBIrO8agzjIZ0J4EYp/cq5J4meEFIPQrY5tgQ+mf2iMCxYqxe0uB48zQ8t06XW9hX9cmCIFIIsIYEhbodTZWV/8yqzIAhPzBRUnoM2nXUCfDaBXqJv1LeOmOSSRb4T1PSNpjjw4NWc3tIN62FlPcCiv3Y3cZoJYH4iOYR/Un1N2evcPISEZthGPNAb8vfAiNgygpJFiZ2TTa/HBQ7KldqqhZoVxB/2JHdo2Xz5PclE6b+X0Izhw4M26AW4LvOXA4p+FKqlmTP/PENG5nkKr1R3Ra+CZ8M9q9fKugvcdENbYGNmsiDqzQkJNRIjwhOgUbaaQnDdVWZvCR2jtl7WFvhTmIlSW33QNf2Sh7TkOa3gyD0OVB/y6dkqobfz940lzZIy+8i5wfGxqf30rWFCQBIbGVzMd62G68g21TfHXc8mvcabvHa9VOqrKuy3uVdX+1Iyeg8d2QQ1Lkd/3A3SBSJIyjwfEBdC2cFdD4n/Pb2u2GqQVH4nvTnYLyYPwdpFMnZkfP1F+iobx5m0d/vTxpdjw0vWEU6YfUdDB51h4bEeyd+hcvFUM+Qd1JUA3AFHe5VtEOnqbaAQzDMIwUzrfe6R8zbeQGwMJYxclCfZSJoahI72o+YDCO3prui91AKXXkN3dEse0jx5cORt6JJtmt/EoabMKW7SOVjqWQJwwtXkSVcoBZxnZBVx75rvesllPIUIo5mHKKNAsZOEc3SAQPJU/CAeeTSdqvXn7vyvWo1e0cLx3GKFdNA+w/rYmozpL0cLae3WGU6sv81tGftybVuqPESWzxerRLSW6nCML4vYwGwP79qMVAK0mw/A1DyyRo0IQM2OWGZWypm50yvHqEms3g1MyHqwq709uLAZ+zY7nOSgZ2ewaUYAtSKVgji74vgmRcgab+llkKavqan8ZxNK8HHYcGWmXj4URUz8Qmmv5cmdIobXRJxu2HJgFP9NpuCXJNEkJiGBI0c028F7FspemlzZBtUqGvxPNzyk0j4yvfgXqFnEpJWR3/oPqLdG/xzTIricai9ymdtrer8iAEvkAg5Zf0q7NwgObLwRaHiZD3Ap5VsxSefbLDvNiaLmcrFTGouyEZIXwGLCYLyZxpsRT7A1wy7HwtdBtXd14WbAyUaE15320qyw8U14Euby53KuIJdriCq48L6p+ixG7fGJ7GnvOwKsUE/xobufEXs30RaZAnVuxunnUTRod95NtY4MFQwMbJ6pEC6/S9zW/zzTFHUYTZK4Cbc2rCE9lKjNKdrpuO2p6YN8hznM/4lMcwZ4FAqEOp+oE5J5BJM0otvc+EryvNQliF6VWk171AKDkxnA9Nv7wRv/FcAsz4hP3kp2fbBj7XfoNXS2TuQpJAJri1YIi9gYR30ESUXouElNAqHdsnFk3Hj9W/2sX0DVceg2HBUhvTQdxkkYhodMC1AYjHlS6pW+hxJ0gzCG7qs4b7U+xw4ELQkfb6ZuZMYIKrc5QYqaOU/OcQvVx3Ch0lP7YFyqRjVXybdmS6nTp5HAk0JwLaQ0Pl2CjzZ6UEv9laXUmqyBLCisL+lYMzbB2wc6skuAOmoNnYyOH0no3DxGsDbqRdrLdaUEtHZW7s4klCEgO+16gRt6LIXrhZTpZKFYSpDn5BCDKWiiKugwMkryaL7MIUkGA0Xff63tOv10SRQxSLYgc5C772HqXcNg9ExRx4SrUosLFLXyc85HIKvbpKmMWyl4sI831L77N1gyZNCOLJfoymSSyyXOUGe20kFm86qkZ14vGQ8gSpmV4h8clgOwj9PYDVRDHMicEo9nI+3y21nH8ReexCTidl5GzvUsmD3Pq/Jp4Wjy7X77U+fq72WJO5y5+lupEYcJsKPB1ClTmQhU+vwP2s9U2n9lBfpMcOdKIOL5/90eZYlplCfO/eChrM5cWuwz0V+1R5PRwvgLIRBJimISpdmZien7xbvOwFST5OlJOIaMMwDMOi7I+LyAZvdXVQg3yR88JJC2mHemDsVz+qUjXq+fNSjHkuLs1v4GqoRlpPO2ZCfO4EjvijcbI5WZwD5VQoz5cdsp1pqNvCi6AT7kGupHMP6ggtceuGYD10G9oS39c8JjEdMqJnwEjHq2Udv+oJmPC1kyc9kHqx7cgyOQaFLQRrPuFdBZEME0Eelql8i/7hcVPUWdOVoQYRwSzQzuiAz6wnnF1LcvMivzI7bntkjLM9MCiuYzku4Aa1N1qgIy9PDe110YYhPrN0Gx10EjdpT9uDw90WFAogxnTC7iywNUbD5pESO6aXeHP6DudeVY6+a6VMMuFxn0goNzKddB9GUEFymPJ82bX3FP13wy6zFbkkdk6GnC5Q34A3EnofGGgjf5liM/YAjc209JdgTWwLZg1CchJE66k45Go9JfDDZsjp0ux7JJEmpj6fUJPC+NMjjRWUM1lINk0PPXVgv2I5HuYsCXu9BsoFXNZOXUlLJSdUom+c6cFkQOjvB9CukQbIl4QNcYaytZAPU5oeaViqhhIha5ddCBO++0qoD98+6SDzmvXuhdTfmWKthEwcBgmOoMfGj3nRcWtWI6YhszZGh40FSn1UdtsxXWB5lec4KUQ6WPXYu80tPD0d9TfJwwyTFeBgK1JMyWFyiXLU8aa76fGDYn13zCav6Ji/mWv6TjLveEXdwINM+PNSjFKXvenaQ/c3ldBc3ax5TGJsF+Ji98lzeKPkkC0Dn1mPE9m4LThD2hxuqCAOaYyGzbVTVUfN2APkyZxme3Ijf5k0SM9NoslYlo0Uzt87HuYsXtmTz+Dp5OUy+xrAOMi8ZikNaz97wlEEsO4LxEWyAhyEe/ozjuDNXpZS8uu+T57DtVFQ9aD8nKkso0KkAE8nLz1dQTy6MTV6u7n2NMzG1OhBEKqJrxML5DeprkNfvPqLlrIcxz9E+vX6OzXvMua01gRT35nHY5R4RHJQwSuNEl1hIKodKF79BRw7pGvcBUWrWhzIIjxnDa5k7Px+woGkhB7RxCWn5/Ds/JGAylRGWz8z6jk8W8E52Uo8GGKjipxigciIhT/mtNZcGla1Lnl418XrSW6putev9wjnmFmbv/t87hz9JAmi9YAq/c6Z+vIAmxpZzKvJsehXZ7g6nvch162kl3KJNUp05SCPrE/aJOTXCiwHf3aakiCA2pVF4g7SRpQbtLgwe1eZhQlduMUqZmA51u/ZylmdZCdhc9SLRtn38aukrwuKVg0p0wwV+JpDEhTh72fLvyugCXI+Qvk4aNDQY5DIEZpqf9p1aFpAWr7BedBCZATEJ+KvLKQB9qvwG3okqsADSQn9cFtvb8uMPThtovZRd/S7r3leIdqoiWvpgzgSgxiGYRjJPV7HV5BkUXY9tPlU+omsDKO0EEbRVkk+JEUv1add07vPfqrABS7dZUNIcR1nQBlnhgN+SvB6QsgT+7O9Q/GC0E6Vl8VomfeTimaHjrhgOEDPKglktYPhJ+JbNeMkvE2nh1slnTmB81rN48rwuSzjmj5y9hc4Plj/wsWa10hQNc2Y37hfSVVSdGItEfklLGkibNvBKbtqPj97c3WMQ+TaBleagfPOqGcpZ57O/OHNCKzRxv3mnkLVWaWwJLR2+/z3jpB8XPATyjKUIcFzffuPJpT32TNVVkV4YKrGZKv6tg+rVRBCzeaq8r+P/LU/Cp3B7GqVe5BFCYUTtFsoSh5tthq7evA5/0NkBHaKd3XCrpFMfj+ZtbOXhqQwpFCwxQhMJb8hn9miFaP8Ps2t2HgYLohjI2gvYNL0EhD9b9/IOPT6RXrCToFAeJUifq0rGG+dl/pWofq1wKnDwHH5xaTHsXTieAszoR5XUQAI85tYwtjYomekKt8+SA/1TnqOYHTNtZVm+FjrnBdaEN8OTCpdy9tpM6D8Vupptf1HJxo0YBkA2TR/za/G60miHtNP9Thvh0QjJO4TcLu5S+Ny0X80TvfJ4OlFybknxLLKZganf9uq5ynIr4riqpBYgPzJM37bAPhAuZuOnSH+GR/B0A22IMC6uSx5vbfoYptWjpZ/8ZNwdJM2aFweE5/LXR/W1iQM2+7tHvyEpjYFQ09DGBS8eA6wssqjs7nMs4hcso1aIDBUWXqVSKvDwW/+5K3ivk6yZJm4kRFkooTaZrk5p6IH23LzIPVIzdKHFHbDFGg+JQyrNc1P9DsC7wZVgwNaLmkjiF4LhEIAtCvuI0iah3tTUUReFH3WFUaJ762339qx7lWF+oXQHYAI5TkgSlqHT2iftzUVyIvoL6FkmhQSdMyu/3D3Wp/NeVpVnLUUaNTLF/e9p9EVxOHbNqwDXMD4YrbooqZczcBcqzmf2Fu+BXX2GQ4+kMVaqLQh+difiBZQ+2C562Y0D1dDxpuTWR5zrQNnaUGXPPHgbJzFr4Exz9zxxPP2zFSeq1xkXmn1k+0co4+DJLOkRzbcAHy5xuKXcPbxlqtHb2XgEAcO1kiOi238XpTxpqdF4sdvVbm1UYRaYcCwN+X/xAxG1G4qcDygWxULqDiwVyCc71hPw+7H/gI9QTTh50UXrNQX0LnsxqeQiAMU8MppoZ250Ln5HPoLRxFCny/KM8ib0wyhtLg2EuzjVuK4AlCeFg2De053smwqqOu3xe+QgrSPjVbJOU/kZwS3JdF8Vjsgl2nNjwKHXAUKkxmuaNbFuuXwISEjEEar8x0zMhbrcYfI0VBNsFQsOcO4jHgXV6spZQF/vOWrNvwxDMMwjMxlfcG3pnsxQwZETqFu5iN9lL6zCkwdTWiLS2AJsITxtED2DxK3ZWKEFdhy7o6nTJ8MsKHNHaRx8Cvb7jMtKFPixijLhIrKLd7I3pDrz3VIF9xOb7ejlJJmeQwLffx9rI1ONuUeXjYJsmkMbkBFrROAPdOLe2dVbUngAoT5Qd5YCXXAv1sdFozgwA63If+yeUJqmYN6+NWzHdVJ/bQS4QadZfg2y503eyfZHOENSUAw1Dg8frqfActPcORSle5x4KK1J4qZ/MmA2sH555mdHzzdwpWkYmWp0gkD9QZbpTuLRu6V/M1UIiSL3ayvHgsFzX2/8jAuuugy/Vd1e8xlfcG+kVWjxYun+6wXBj6iwxcU5SjaGurEVKzCcHqK9TIXlUzr50DTvNNEtUF3SjwxpeZiTkv4VeM133xL3w0fN0c8eYwLLUJhwIlfwMqvGRlJubSipHguRqhwWW1gZqTd9dP2uRuAiXft6cSvTyiO61kHqeTRGJJ0DLnOmDSZnlnrijCEi1vqwUtb3irCVQBWCfhaMjPrvrihw4AkaBZKr7ol7pM/OxQy/p0KTeezTgF9rsDYkWxyXpVsDnYZcui/mciGxqPOIRpR9pihcDP6gMEZQb9tF8xSoqW690yG0aXghHg0AU2n6YVIkebJR4jug+8WWn3w8USL0QicOoK2pnsxmF2+J6C9P4DsCyJO8f77xVYNINE9XlLx4fJishU1eX1/4Rxjk5pWhRfc0JK86IA+EBWiQMC1m8u+ZeFHg66FUfbPhY4aGJGvAuFA2uolE7gfichLDqSk4Nxtsc/xj7FGhTo0vgY5wfby1x0Ll2ZGdrRjXSNstE9jgh5AcClHLI745OrLGLk/lT8PAPvNVVFHFEpadUejsFLqhQndI4wcBmJma3qBAUl3COwhX1Oy3cr47mJgPwhIn+xUBmlExfjGnFf9ApXKM20mZUlg1Wcso59hhbZ9sxCmdQ8rL2NxmjvRaz0V1Z8xku8x6EItGy5TQr8kPR0KZKB3t5h9OIcMiByVPUG58jRIuQZgWKQ0m+z1i1PR20dP36jVbxojqm+jxuaZOH/4gwl4VgLz1E56eKvqiLKulRv5qU237dYJopo+Z3gwJfcN2sYLaHfIpDUqenSQjiLYIHiJprydBTf4OnSKmvA7wMg7PzXlL4MSz/+Jnhhkad++RWIteVg0yIqdXLNxHDES68QaxL5KJL5fJ1Zr1CoM15kTj7drqKRm9du43WQAx73KYVhuEWpDUgNws7D4K08RJgVmW3evVXkW7g/SHUmqKsTRKuDEdLPLdRN5vh9PCuQRDHayc3oaRiT8qcvVhkpMtj7dZPiQPu9tEtqpQwWmWGA0ktm9NhDpMAzDcNgtlePWzEr3avAOEHaylrFJk4/RhXtMfyX86/A3dxYShRQfW++7Ygf4kWQm2KOb7L/s7EvMpFEDbntBTB0prGpCCx7gXpz5dbndbl8UvZlPquWVgOGiaflfgd/qGp01ypyC24Q24aEAViSQimU/oJmX2bKdnZcU7VGqoVYXXrvttrM+8dAspIKPpWAgjnYsaYpJkTLacH6wHh1HrBzbpXZRf29OA7B9eEWNrwGJy3HaUU1KwGTIsjEBUQIzm9kBTVRGHBSiTnm0S6tCiAa9CEReMFAGOmRWFD5Bg0nMFHT8BerP76rHthfVbwUXfafdd+Acj6UE27OHrVUIu8pFsaEVogEjXigHPVOXuvZFObez7adtDAC8PXXLHmbTikRTmPoksxVKaxMWBU+I/n7uc1ViFhMMJt+6/pB8CbX790D2MltvZpQtaxQrkneNL7j3ZMcUBAI5OaVOPeaZ+R7W5ZXm2YSMpnkPSSUFr3R9U16P1I8zjFI7NHfugGA1zR8T/Vj8tLLT4FFgx01+ZCGQK7EBbzey4eLmZgITVQAEuOo1KSTXpYzB9JYzyZUPM8uPp9+qz4EVAk6MIZkiDoe7TXAFhNkyUAhIxpZ419AfLkLoB//aQMOt0KYZ+uoSNSP0TjYiE6AiVlEZQH8AFYdawqWGKQwxJ7kbmKB+sc5rZDghFhyPUIjLgUw8IDClcKSQSvTLWihRP15fcZSz+kVZyJQMBlHGj0QfwpEiIl4gVoxSm5f65VditoegnP425TnMYJkjgkrOitCw6gQI+8PyKXP+61bMHddmv+emGemhrGxf9ShW2TvSAx2ZyQZxWusqitvX1voRj1MgGoGgp2rVzN5BpMsSSKyygxK8Rvd8f5rJCuYNJvxyYYQ8hfSnvZW+NqrlSeDMVs4cT9J1mxtHvrOMvh+ZRTQEyvsk5JjRl2PVmrbY0moArbDE5am6LVXFQQpo23VisdjBdRduLDs7eacUTuSFFWTd3m9SK6gpDwn/VS4qGYea3v1pdV2x+smpSTa5RSVjjwrO1Ec+5lQ9rqOj66RGZYrGdJT7Pp0ZeICzRDinuCRPSjF4twTvyv6+PMgJQf3l+bhqqelhFzgyU6hP6Xz1Fai8ZeEqEOam6OYn16+VNC87eBtIxQuzWMWymyFO89hOfK8aU7AWQ3ttj6ZJWpjHW9fI9N+2AiYx2/BMs5ZE8uoIU+Uy/V5jv+BxKOEJbJdc0KjXqXDhWqpE8D9kjje0sI595pbwYesjwWdtBhrOCacqQBY+0fv2dfMlncStAJBZ5nQJrHTV969gUFlFfo68n8bMML34yA8PqxxaATz7vCIPXREw54OoctfjezaXhngDJ9J9bEcTm7a0lK/QCPBpB6AeG3A3onCVW7AcSwABttpCKkqQv05FZvcD+LxTNHpBS9GuEutqyX3isMhYZKxxOgz86MeFM/7O/Wnk47eGJ6ffGiuvs/9vzdferP9vPftcWC8AGhp8tp/tO7Wl6Oltu4+rGDAe++63yvf5+FVsh5vfGuNte/i18T3t+QlzHyT8b2s7u7dK+p7yxgy5lCcTRckRuh4EHehuQzPqWVosxuwUC/IAYR4gMBdapA9gm4DgfHzwDfC9syYg+Lbrn7EADyj4DGiYgdOV0vuIzDm14ZPCCY+ETNbP/4f2EIkZRhIYPzP+Brm6yd669N6U8VaWlcJsEOvcUJPKk4sqnmgql+Z83Y+DTVnoRxjjZRaVfx5I3a3Wqvz1pY5HI3aVyjdX9+rRulx5VcYrLSsn/g7mdWtrqfz3oYqXaCoPfvF1R442RUbUEUI8b1E5WJC6rcaqfLup4/GIQ6UxcXXvdtblxlkZr7KsnPnPoK0r1VJ58a6KZzWVvTu+7o8nm7LyMsIsXm5R+fSH1N3bWpUL/9Tx/y6ZFEni1O1gYMiSUiacyg4dmK9gVX4VlnW/Dup4y8FUlvoRp8pSkOJlXN2Hq6ry4cG63HGaujutMt49X859WVbOPdvEo0FfVzuN0Fdq31J5k1rUvXlUxftNyrVXTeXaxipeyded+lRXTv21KRtmxKJuYz1CEe+HK18+LCpf9tbxElL36E1ZefTLqhxbWdYdO6rjzbbj7pssJ9rFv6upVFENX8l4BCsXO1LpVOoi6sOpMrAt1dzTW9KX5GwnYFnKsyxtasvvKKno0m4oquUrmRvByuiWVDprdQm2h9MVDMqWaua+ZMmyVda7/qx3/l+96tzUd2s2irubcbJ4+ZQer/rtpp61EQg7GV7e+o/1y+MMfqz5rcbskOrxhfabrBo0dWa09Hw/l7Ou1A9zht77CUzPDMX6wv73fxsML3lwztpcyN5rXKMF2u0+wed7pMrec581sMLCh+PmNo4zmzLLcrAeF4JQqJ0ujE79cA2pwrmRP1hks5ze56Oaxa4JN9zbV68j1CI+oB2kiueZ/E57J3OKGzE2w21buyJ0BwmlgyQ9H5FVQuoFBmM5bqeAsixOWRBEexCFBVHvx47B/xfEhBQTdPzAjjH8C6ORwwyfqZFEmdlDBTHzgUmss3jKRJflF58Ys/GsE1gAAKAIhBFXNd0AuIRoGaTRzBrLuC2w5wc6pwXYqTcWhtBhFhcId6UJVe6AeSZ03QEfyCm63V0BxKkczLpOHCHlACrXFKgIV0dGYpNMxAJXbhcJ2P4gDQc3yIAzdtCEyxygTKLcb1K0QRCpGLtA5jrciQ0mUaIhCY/c+Rr+hvfPruV5W9oBNXRQN+4ov2FdnQjWHwFiD3Ae4A0ItTPgKgAzgGYs2dbTaFTcBy+TfdXDrpQ7GKdelR5VrzNwtWn85Jovs49b+Q4MxrnAfpfBjPB0BFPyEFrqFq2v4jyzlp+JzKxu9gbRfuXmcErnDM4kHodAy987Ktn06nnjv+N+B4/1eelux7rMrfKc1P4/ArJk10fj29qNSKlPz4miH9/Nk6cRrbfK1UzMIBvnLZaXO63Mkr/Wx70KtD5ujYxmdWa0yIXW1UVpcn7oOvof9N50S7oOtdd5zYdONbbWh6zJ9ZwzbKfJN3VH5o6W0+tL+fbfsnfZOs/mFwWqgjC3Bjgsayh8H0kxI4PF0SkET0UF0rnp3LTSGSPgTx/U4LJBDd47mUEzLymPgv2UE+97c2wPWWj96Y+ZeWmjRmUPqvWvDjP349Xyx0U3arTtnZr5W69mVj1yye5myGxcSl7aZ0kVJ3nuu0l8NevUvcomV1GT8/6xM/k4R6h33b48AcimnFD93dcEIvQ35VQ2z0bW57+sV/fcDHmUzfMsKzykUvjqlymykS/m6AIW3nH41dhrQD6yMhh3A4QA5MCY7hg0AqZ9DsyAfQuwfIOJLZSm/QJ7tzbA/gHQNJQa+k9pAgyJkN2klRU9UX0mGJIgzzOULM5L7OMF4Ez7D7HZYl+CiXlhItcz1eeWzRZ8H8xon2D0r59q3Bdg2pTsokMvgCTpnPRXFXllZqmYLp/U3MN405zkiJllz8xzwcytnxzjoxuAbJ8q/TuCCisv5CALEbpK1LDaGXJ2ynmnUhpqWP1ql/0m9EVA/tDVRLX5oNqUp5x7upqo2erOLJ+yWVZ0Ncm30n3PXsrvKedZ1sfL0urNlLtTzrm86mVTTmVTlqec63tlWb/a6PMm61PLq0JWrB4VOT7lfMmKbGTFf/sYoES5Fg0SclsjU8mWrWhLpa2ItKUq96IlgdYjVqKxlctATStRYumUW9EQS7sSAiWgopEyXwnbVFbKnWjogrY1MpRot5erQK2tdEIp0IkGX9J+Q7lkx6XojMpa8tKVWQVRCMoCCdug3JfKCW/Wboa1pLZ7t1AswltuyWMHAaeTnLLYhBt4cb0qKhsW0K4K/xXRy9vAzHa5A1uAu3FY2svHCdX5AVmiGe0fOvy9J53azDBiUUuE+s8G7jxDVcLSG3U1sIZ1jPXt1ddBY1uG7/77D47RbDCgRgfYpUtlSYlQ/8jkrPw/0hSXl/4W/7dX9vkQXWDac3tUDTsToVF2w/Pq+0McfXh1B/72A4OcdvF35duT1a/9trbx27CvCAvAEdDXIOhH27P32pTyDK8an38/LjMUNPhr6b9aaP6ze2IblyMW/cml9oeex/cHBrgdY39ILz/Xm7BFcsomytXAieLZv4RvX+q4SoRetz54NxDQn0dfJCBxk496d71WMX0uxk05pvRou9ivXmOUShxE+DmJb8/Fd1p3nrlW4UxIUduplCn5dgorqwyLyF+YDEsmuL42LWe3sIK+E6wbJzfdgkPM5uTv9vFcEtB2+1DVImhg3yb7eCITLfcWVpguysW29+FbmZadWhiIypTvyEJOIk6sm0xf1x0Wp3QkTxwUM6YTLMi2pEmk3g/CH7Ac2jenSjlGG+F7eZZ2SLBkLOeYhaYDyZgkmuBrBobBLTjPcDndCXwJ6D/Jit6Zap5tzlsG8xJP5xThwJzbVDNjmN6FnjxDTGi2hI4x7EAdTtjVGTYTNys9P+KLXJlJuuzyKBRhkNO8hpJGJ9g0dbTbvPYSrqXh3Ex8nRvykbMrPFn3wj1xCgCkIsNEztVvFKXm5iz4npB8SbTuooLr/XjkRPwCLsQIx5XKXkxgVD+PD9twTsqI4LcAx2nk725hshl66PZsYn05a66UD8rpuxMKO0X3BFaY56InAUdn+JxZN6/3jX/jQ5VcyfMy1TT1v/CFecY7Zs6rqhTGQtiLpwDu5sPtEeJmviU5wLmvXiFZyk64YrTp+D6clQ/9TDzeq3gsFxbC707oQJ0ja+08qSQq60InWi6N9OHgy1lYnoZgXvNykrOpDuZ0lJ9Z6VD36BluUL4NsbUWViZIdYrtHPQbHNFa863QEcA9nCjdpGlWMhWWcHTnzx+anTWdzBebjYcO3SzV0tzWwphiThlA/RY3wonFp3fCXJnfbqzkbhPYIueKS6qJk5WePwhm+is5yerXTwVyayphVvr+sqlfQXfy1K6znYRiPhvKBXSqq3w5eYtFS63yoX2iHdU5Jzg7CNqT+Mh5alj5DCuGZB1xnSShWct5/1DXYR6rjUcx/lSBlZwq4YaGdNkJDLMQNBgL3nHoLlmm9tFDJt7YV8mV++Fa54SWkVwdh7qh4NxiIEFHSrAEbtQzcIdwZDijLIYeNUkj9YLWwe88SbSX/WpnGdyC2OU9LS42wzyiMif4DidEZ3IKOFWaBl7ZEQcttml/7gqry7DTmRiKujbqXmtrSz+M73ujQFmyU6p4JhDGpyfklV1mlcmliFRZfM8J0FT4S7RyGLyRr0JlOLvQzTHqoOyi1bQJzLqsJAN0bApFUM4QoJBqVVrUvaMrCHM93RhozsMiL9qZOQ1ry7W078NePxJDQ5sXJjgNubPHgiXPVNKfrwTqV7ahTMpIsK2rBpXuQ7niQk/Seao/Nci4by9nuZU9f8W0xOVytW/ceuwqLA71G30Rqnn5bjjshaNion6E3L0y2rPC8SWGJ3HQsY30kidv3q8AU1ElCgntgsqlHFkDmUaqBBY88W1udC4Ck77tmSJSFzC3SYFYYLx71a6wiEh5WvM7o9Jr23F3SEPjTOtk8bf4ZSbhNo/wvcy7ENZvHbbKY+FJIxj45S8ImpcaYr9cKdfj7kJiclr1H+CruY+SW0C/VAe7hP6g4pdYSHkWfoS03vxpV+BbK3Ygnq+XfjpmM0t1fb6vhxH5sc6Iad0qN3HDvwR1uNXdchNc4rfJkyibO/d/tf4v1wIQ7Qt7tMX+Z1rkyZ/mMED/Ndtf78H5fS1OtrHg8fOskv+rnk6XeifJ+JsMh891ca7Hf10v3D3Bfz9fP/LOMvxA1+6bF1++l/8/rsaPx57TnLy/2Ukez+3qOUXgZPq79hJW0bgfX9+27rI2q1f32Xz+/dlHPx3D5ZzSey+pDptd+r+BYf1PAiTfUrL/H/Wf6HF/euv9P7/pS+M4kBCCHh75/vMvALj/4yDPCHjV9Ay4wJ36eQVnLE+I5kT2GTitAhUwlQkpgQjo0RgoejLY8ygssJD/wPEAzUtXC7C9vud/wFPPf9VxFCj7fGKSnKj3626XzY94xJLdWY/gtHilwheXHTRY5ESnYlxIZdn0cm1Xfnk6+ek6ZJ8aC4I41OTb5g6Y64R54kOGUCP5jl4XfKuP7HEounUhbKwE0d804hAD6We+8JH0txUEl4gg/fj3HOYMXS0PojwLsrYpaQh/uJB/CIbD4MovsuEkRUgWxsPQK8XyLIc8LQFgzd16SFs0BYEcnwTCHdDAGgKHHH/7e0lc01quUHzJpjJMaCujbVVHtS1bYuJaCA0s96hTWMjnIpMaBklbPJTsI25nWir0WrFhAgy3fF1BrHtQdH8MErsTT4HBEdLDe7hnfU780uSG0z7UMURD54gHL1gKxr6R/eg/hFfHVbzhzLfqYOBW0PzxifpG3gNyDgu6+k/jOUCsHD/y9xHb/PlUPpfUL8o7/P1m/ssn9vwK+6TPZ4rRTZp7+cBEvqORkxCaBWvk8syEwaORH6pMmL9DGF/8mtWaGnO+JuNdvkdXrIhxK8IzkBKiQqcw41T9R4noLnIqvh/oBa330TppC+zB0UI6j7RYrq919tLjCSuP2/Ndw4QX1ZNTgJGRFcUwn+mT4U3/i39Qcfrb1KqiJABjxY8Lp5K4b8/B0oblh8XAbzTB7osrkT8wkTbMkPz+Iw8ziudGGOWH3BPNhMhBXN03B73F7Omn4Wx7uT3tOPa0brTPIZ7+fKS8A+DKD64gyAdk9wyMA3me96UuoEDKa3PBDLLENw7bKg47tQ9b/m2b9QwRU33vAY8geHL8SEdhKT0d6b9UGOsqOAeKIF4LQo/Yf8o1/vqSsg5NqtKEKQ8YEAPpn4BDUYCal+fhyqTLVB/N/q7QMVX4NQj+JZlE1cvJ/7QRIhEijhkPVoyhF4WiJgSX2NfVXGkMh7loMIfG9mPMvUWrELAC5LaYeOC7g9F67P2SvKFPeJAJ1SBeMIu7Vng1O0sFDK56XTPsaoR1V0b2biVGZxHnU7RWoBdEi7qIdxa63F/QwubpNKYEDXuN+mg7S81UDcXnKK+Guli9L+96F/uHVhOtV6DuN08n01jWyy8WGsDd5PkyuXi1QHTcg8FgL/YYWxffWSKh6b37RwvuxMZPHChsfvCCk/m47w2ZnxcODKSGQKvjjZ8Wf9IxheB04CB63mzsxbvAawr2SBUDRgxUwC9NJmAgd75ba0AxdjcroRXThwWiIjqdhq499alH5/RtGMPWxF1m8KwzXUl9z33CRgx7qAt5k4E2wQrYIlpasZPCTU3jRLwXj5smIqXo5QRmJq82wd4idYJzk5+g06GsPwSmyCORJ5c9v/gHpE1wvfnRjlD6Ool07VprmReELy/XXpX/Qq/aiP0VQ8H0PzeL4ijHOhSfqtdnslamm9j7WCb15IILFSST8AyEFdiutZPjKcUFT/T8SfQlysrACi294eeS1DXk7X9HqGGp2ynI5gO5enL7Ox6Zn49E0pM2w6H7ZKAj91Gn6gihHPIF/VuI75Vyo9dvfA2iRqoTD+ClyXWAGoJcd0rvOW3i+rKEC8rIBAqBhBZvTBQmM7YwwH2+TnL4rRlDR6dKWfQIIuCuzWhZMmoI+otWFDPfmdlryMrFvJwy1ua0OjshJSWigYCQzhvGcx1dBdeQqRyWaQacOGfwEZbRpwWv2JpgX5iX678zivg6vXAtkyRnsG5LNLau++UgL+asiiEwoD0/0x1XnwI/sNX3CsQHaY2UzgIxwPjo93Bb1EaIlpiQ1X4f543tAst1aLu2a6gmmIrV/1yMU2tpA5+hjQy8Fq+XP7aTe7s28o12xvII0hHu3J5MfZLpW5BTNu4OWqdMoatkcMoNzBG7InqTf1gjRJr8uNhqj1mZQf7ZqWSQA+PR8rjZ9W9PX9m9ZiNonR+YIwk/vF6ybXcL1lS0DDUQdA+zIc2i3JECxZWm0e+Iz5GLoeVDc9xt28g4LDj7OAJsONpJvCt5bdy6bwPSFPQJjiTiXoXNB0UknAqXLLPnbA1xx+UV8YnjpIKoQLcX7+jJ1OO9f/vdM7j1kf/lFMUFOrqpUjC3P9JlAhbJ3wa3ivvaRuCbiAh+wr8lsobVL42RY30D6Ha9vN69gXlYymsoweXO3s8oJBPdV46Y+1U6IYczc/9thKGHLteWEMmwhGcYPi0SGaj8PNPazYm6Wo/CKfdJTTEtvco6KfpAR0i6Uy274mSZGHi0v6ai9iCcKqg/lFnqWTznrgZhdMzJo41K8gt89wI2nakBT1cBNiLJBQSOqc2USM7BC2rTOZGXsWCmpj6e548vNSrl1vBXy7K/BFdySSLfl4fj3pBbxV00U3Vp3BQvE5e66ATSOTWHSbW5YnBZXwvurpW9pOQ+3x2KhtI5ddwbm91Fg7kxxva4H4jdfDTXz7uiJ8LGB6lOldbKy2HvdzUecKUxiMHlxnSZwt9KQchg8THRwIVNYdjsRMCZI/AWjOfT3BF7CJllIPXvRULFlWqyqPelxsCFGCHc3DuPr8l5RNm8Kuv9P8crxSfqqFQKXEop/3xif3f/voZAazel3weruz8HM8Fa+gCrQXH7hahodfj3w76s8cYpyCTVXtTf9FPXRk12AsT2Yq7Zr/DqQfYYTX2EIuCI03R8fIFikWxaASQ00ZCKSoRYUMzccY3HdL0cSLy9+GbArQR1+af3v0+RN8DcsdVU8t317jqRFNzP+/GIh0O2BG3NZPDUTeo6E/GNStN8WrrBrYEcnmQrWv+GBiFrkizAa6Bf8PrABSDxXiAs7m74LyACCfLesCnN1P8y4np5WoaclMS/pGZGAKa32HXi3vJ4/c+ECWNG0wp22vpolg7hwSVTcQNWZqGY9kBhQaZQeJTpPFZBlQ2Ayx7Pc6sQhGphpJbxpqcgi98wfyIJM5IazhM1jXQIkUxMHWOyE9h63zpfPb88NYCSBANivG2dKNIbMndavbaS2B7mE3UEN2y0IgZqyqKACXowKxJMpi1D0rfjLQm7OcnnPLvCuyirmd7WjVIlv1s+fU0oTm16exXZK8Y3nGApvfkKlcUW0/QQPRjUG7NOQLRknidFlK6wcK+kT/z2IhtDaZvT3bMEoubFHnBg0ZvL+0ia7r2SGV8AhPURRVT8HdkLqEykFlR9CLU9Z8j80M1fXHAaUKTw3jQ+X1vTcbp3MEeWeua8Z6kStsSR6ebmJkQMjt96r6oscR28N8FAz2xTEQalTkB8Y5Md7FWyfERc8StPo5V4KJ7b3PEKiI2dY/i4ITz1pNlsb12rX0ZDf6cu7chLDl8kAlPQw5GfMvzZXsXIx4G1dPc0yDwaXoGp+rq0fRvCuJAM9GVpWVff6HQ/Cwpw+K6L5OB1OdGxz7b6DVVH99i6Jr4j1Wc9A1g5b6DvL9/8l5UIVDVENMkYNZa1Thh1PcVeSQSa3l4LHd96TWU1ulWQx1ZDTQPih0MLJNs9BXN+QuxnyUHfTK9SD3MSH7zF/ZLpSEXcLhHNrjmwOe5i1mmPdo0nnCvWeBowsERFQfGOXzK/Ar5fVF+AB/jGYgvnP2AlaXzp1WfCSOXPzzCo4F8pKUaguV62Tj0xh91Be2Cbz0VCA+RLxnIco3s2bmHDmkUtrvK3fS1f8DZqgmAmlyl8M5w2UHi5TlccM1HYFumH+dTCTMwm0dnlU9Zh3DkMzKOB96/0IT/R/PRlKUrOd1fZLzpr+ebDe/MCGeH7VXwUOczk+vtWFDWIBpIVOaV8q+aX/fUwM30JpNO+RUjOgQIt2Oqlznzd+X47SLq4raU0HzPEJ9vJmxM95+utQ955N4TFtDCLSTK5fR1x21gNi0XRutgVrQBw6q9Z2HHDy4lBXLF0cOLKADeu3TLQGffLazmAcgdvLgzqmJ1qw4mLZJMjAAWeTJbRcUP+pCCVySSaOqisAKeNSuEAI9jhU2Cl9bn3B5D0zIz3S4dPxJbBDELfyGKKk58MWxwqqvUox5WglJQ/+KLO4HysNekMb5WeaJW6K98Ae8m8c483xnGbUUmSvjeugqJ745IrNxnBJDuFVq5i5eQUBCI31n68OhQ4cZ3sYLTcvUPjgK2X6graMiNux+Nkx+1fvsXbClBxd3/90BOHc4Z/BTvaisBeYhplOQEk75kSQl1UJMpyW5cqU7WbsO9ILw/VbTLJEaXXzMHujeaTE0zYsJlsAGmOqWeZkNuNmqPpj3JW2tuwHNFOM5EOQqd7KiUx6GyKClXDxCnRElPGxWHsyPU4GJG42X8Ydrho0Zhnhupr69IFXZUXo+4pQ/XFLX63jxwMSpA1Zd05LqhbP5t1FMBeyG0vBN6b0+TJ0Rpu80g14kfLeXA73ys7lTLZO7JT3aUcjWI2RpVum6K/a/SikXgH0AlA2ztmrLKkjBKV88MOqF3doSlQQqJ24hPg3HU1TtjJPjF3t6kpM5Kt1HDQK++zBcxuYp+wJUJT5tAj3w0nqXorcqfGlbxWxo1zZh0fBjglEMLAT8y5ve+pLtb5EMOLaR+evHllzmnkVpmFM4KVcd15N98xe+FMjIrnCCmKMVmSt19GDEUqXTiCwlY7Q6lypBIl5yf6TFLj+w0oMtTEQKMnu8wb2ZM209OELWwkvUg6rCdakLdmUuIsB6WZxNtsKbrma1Oyt3MLwIg7gz0AV0o+ZzS5oV25M8a53Gk8yt8i2bXfwZnrpAc/4XYS8ysv0YhbyAp3U+JMRqQMP097Zwx3gVaWfGBl0RSmfBLZCSqmPxuPasnrP10FJBiRhyJfLD/Kn/UYsZ4VjUNbGYdQNg5u/ymAi+tF4OVmlUMMMuO7+0Ra86tz16/KGo+QDi6FubmbkPlTslgItHRLpLcf+MmAltx5ApJaQNl1UikOYmN+ECbibOIf9+46M99dVPwKKNfU+IruDgrFaPIbu7OYLeEwYOC6e/3vn5U7OMx6T4gSED12DL93CpBh2wc3IOM/4ISG5u1h26Z5t1TBCADhIq6UOb1EtQXv7K4h0/V5DhV7wsd59fh6zarbEBV4CumN/eqERd3cnNOl4vBh8SnNex6/gbys7r6D4oNnXZcfTjr7KYeovjlMCyWcqaEQvaaL6ybh2+PFPH9On7aq5CMpz9QIgbq9yiE6syTAU+Vmw5sJmidx3zgCx6cMvMe6mroLueQlAezHVSDs3D7MjS71z4VpvOIXjhzzzJ0KLdfrXzZd3xZzPO64lZgU3wbWzklF9Ijt04/FpLbBR2XJEpGeO9ALA+NDtLvvjyvYwftIRP3YWHwcX9TZPnlNV+6dXbR1vOd4wwNNrufImFQJlvJ5KuFfKaFk1a8liKFrg4+tgbb2m92eWJbimAJ9i3NtJL5Znn5iMRSAK1PrLX4oWl4MWF/aoLPvFVHMSgXWJ6tcKcEjuHNY7qvXy6w56srM4fksd/mZoyfhSp1FLXRTIuTdxUM8jsAtKpm3KtHXLZhBlDa+u3nQcDMaeWZ41zA+uLXHKZWVgOL55LQkUJSjNSW9+ZT3Z411EucxQ3wfwoah732a8NACsWe79SS1rlY8vP56Yh+1t0lJr1YEj3f/IQM7ZcMS6L237JBJhHMFiVBNzvZybqlggVsHiVGPw/ATVP+OLA789sTAAOkU4CcNwoFgPvGMcfgnzZqKO48OLI4trCGqhpMnKVvp+PWKtaUxuNHoSasxk8HqNh4/Q2rc5CuQtvHUEO9tIzdzFCN5ggpSmYnXFkmwSylVElmUnPrnurAUqRQ0OBMpudP/dbTyFeURTs4um3hcIkB5ya5P0ss0uouS+b2liFgP8zJGFenNoUDbKHSUfq4nmc5ktt5AjjjgE26bDM7+ckFTJaTj7jMVq9PvHuJJskfdMypyVzLOdOBJ4P+/gwnyD1gRuwnz3PExdy0SyhKmbv9fukHzs5DbVbC8FEG98nzS+4W9TrAkf0rPEFCQNKfksFTrsxmqTDel6VuGIs5abmaJOHxQ0MMT5T8MbErY2QzqdgT+nfhCW2KG92PhbiCfDKxueddYwTK2iIedlfKCClN/P8oAt2GVbA8xHMp9Hn4RgNx1cStwew+iUHFktwastQEoK18RQVbXYaAW66pa2VHxBK2ADXRw8YjbgcZqK7tlrBuu2T03vwKt0gbxiBuAvGoDuyVTtcJbE0oynjYwBuJongngLffmHZuZpqZWwck1Yd14A8huqRQv14RvZM0YXMBdi3ma+1r2ySuTsPj1OAN30cTTslhwLZrD6zPM6+MXwB6N2bAITZonZRl7F9fEhvz6kv2N7Q1vhyQQOy36uCFkIamKuebf1cCtpCXVaeS2va/JQYKky3VjL2GqawjtIQ+iD5Fb9aGJBFeaO9RZHbHHXZIrB+z0HUhBa7gBTbQOGTy4jlEwR+ydGPozr1AcuVqxjK3xwOq01DqyidlUNWruvawRA0oaCpxcegrnXS0VsFdFThr70GyUcTeSuK39CnGYziSRaOSD9IeFF+UnUk+C02iSJWEgY6P1lzKANSOpqOufnoh1SfV6T4ANMJ+BBMkggBXQZFaT5IqioDbwcRMLjsO0rXRXkViNzmQ3s7mkYH7AQbQw66l9RVI7gmri5A1fIO6/JuImW9E+l7HwMzxt7aJqeSF/kxULBOv/HIzuMqNnDxoU8yXy2++A5NRuJLRUCzAx4kyrXJ4iZco0fVKVPp+nNLyygG06NcL08akYN/l9UILOVYgHPJD3ZVkLHvsOpxe+79dZR5HEbpl5KSZPa9wU4Soq79H8o1ZpIpQ6iuhj8iNpvK86PNKAhLgT3P8qIGlGMAjMzNcb9kl4KxYlyJSX1Up5lpN1Xbj/cRmnxc21WtPyYl4Yrf9Xo4/PF1G2ZJru4ri/0bft+KkTAd/w3d3In9HYOEiVWPQCJxdVbH3KJdD1WQbOCi+hJ5H/K/KZcqm0Me8U2jdVJt5BpcR7C0eCK5Mjzwiy63yz7miSk1F81Aw74/Yrmd0Pq6qmdVbiFvl35/apGfZWSikc4aDckzNcS3CtPg/A9dQKer8cc/tWKsfa8cDpMZRYbnXo1g17Mdz9czz5dP0l38YTNpgVyTSobZk1kVa5MDzlOydto74LkYId/q/Xq9SNcLz9A9279xpv4MFssitGv+e72+MiBSq4esl3Hu2TskcK2WnnxkrfbuYTcVhWE2rbf6GDW+avz2gtUZ+73z2TPJ8jQpRNNR+729dp8NGIJOW3z7W+oT1TPbn0dXmaabOHRDcU+SlLodsUkiCW8JDLsHBN0WFyd+NvuUU25sf2PeTqZy3AytrIjA6TdV9jKI623r83L1WuxOLBAbC+hbzFGO+Penq/+lt3iUuE984T8OXbapGNNhcAL0rLvsIHl58rA7k6U4o1r1DKZ+f+QCLjc74/8mZH1+RCVOGrkOFS83JfT55LMvqgRvOCXW/aByHcFjSIByrMXSLdMi4Wt+C+J00x7M2W4dkNdcMeolg0XtYYTWPvW63SlIccEGUlamVd02T9C3SM2lj3NFmDE62K2t96EZuYdKN13PDVa0slG0UUOykr1uS0G1I8yH6Lycif2EDwMlHsSHzhW/URwfkg1/hMCXsxBB+e+xVb1srn2qaVkfVi3swxE4YAJHBBgf48+kePCrU+1cr9YjQjHV2LdgysM2uFLDAXNoZBSVbiIilttxMuP9m9V/10ErjHRccjPmPkYuBdDycoy4aBzQKd32v/YRTuLR2hOdLnLde/GstOj1dtbICl8EdFlpfa3VM1va5hX28gTkJNh/KwTyCmem5JEBaep7hzMl848ZLwYWkTkU1G51nz5iz4jWZlJzBOiJDb8BwvVQyMLoPBnzlkMroWy8EqMVmGkoCGMkwH2MacHgJyYHHSBsJQ2A4IimZ0VofV94gvgK50vKf8STHvnsQbkceKaLyl5HiFm1HEH6/OpUcOkCEwy6c0pfQG1m0JBObvWtHmsZdVehT/bhSntnlTb/r+CTRd3pEXgxRcUKgIfDKkbyEklZsJafaneu3K3Gvg/k71Bh+0wP7U6KfBgi19CgneJzq4WWK/G+sQJtDqeHIT3/MdwWEaWzz57p7kw1At/1M+FhcNswHltcFVzsxENrPwHiuMSZAINykxXe+O2DK4DVIAlBVdNvCJW0+ybTXm+fF9T7Ix/sQL1+odVUxuCcpk2khi/ijo2HwqXG8kbHgWj2hoWCkJX3cBKgmBvxQgVse2YZko6bZ1OG65ui59WLfY8mVkPaATzJtTMhlPJpSenojzHoHkWJnQkD8RQWd9RhVHe17Y5m8jvxcFb65TChzDYOoQhjj0+Eby1xdLGzLFIjQlIWfIkphfgyRMjvKK5sKG58CMmc0X2EQL1D9Pl1MciGx+appmaZVsoUcm+EXx3wfE4Lp6OELSTLFovB+Xo6riiKnfYwDxQ2o9Ft9xRppcB6P9fsKqP6oI24iE/dWht1yfuw+RLqeT/xlF9FNgnP57I91OxRo1jFVPFul7vT3lHAKVWBJeQYouO1Vht6G6N0I4Z8PhRi87IMRSZQ5VDacaU2DX6ScCvNc6kqSWaM+C1x3VaSvKF1SFRPWCD0mQ9jrziSKF7qJ97WpcKwWpCeB0XvLzkAgqLHGGRxGAEb4b46tIVSOT9+gg5ZO2sBp2vFTfiZdumtSUyBAtBshPA9pAQ1rJEx2xOQ0NYQLL/ZtSjsETicGunxlZ6jJbamdfAoXWe068W9zUSHG8YjKeYo/K6FlkxyMUlqr5VGR6mOu3vJeN6hZDHa8aty01adeSrnezQGQ6KBujkNNW5FbQ2JRdrtqsyG0/JK5+CczM9SnsPaNr2qtKlNOFWSDXReuMj0B2St1CLoOm7OETPLS7VBfkpufPZoWFLKeKNUJm+U6kE900K9jT/oyUh5Cn7v7brnHmWfT0MKrOV4BJINqDIg0mc2iixCuV7bzrF314oH1nuHM7Y/4s5vTGw90d8wbiLuUIE7tqUU1NaYeudHliWh1BIEqQi+5QvIRKnJI+mPkxrT/VCh3Y2W8kaNceVcNUY5WlBTBMVI7zg2cx/qqORRlvkGJ4kxsvKYAGB93HdtN9J1ccfuQxnu5nQKtrzSdxVBG1kb5z/IGczKlkXIs31nzEqZKnIpA4Xae7eRsKn8R8fjK8f4wonWqlEhLpAlYNG2cf1THC7CKbyJTUs8nQtt5NxCB0PIluypq5I6qhreL0ricaCz9bJ1rKyvZISEr0TLAlin1MRU249Bbq1amlhtRxT1JKqeNZxf5/TCtrVroH9+uSTRKUIe36wHv7khbsqSdJtdZSaGrfJysGC/6BKbfEmuQLIWK8Nm7JnD4gIdhUG6GcTO2qsYzOkghHKIsXGWXnGRKdb2Wy7Q24769MfWZn4DiogAOGNM43J1/MAR5UDuE/YJR6tFzJUIW4LNhKe0u1cqjgnXswgY4U9DpeRoKGNg0aM2dSTJvbkF4AhWTB1U/SupqkhNkIZDln6VeF9NbO1751Ye95qBPP/zAfverjP06Q9Qsf4YtnW+8Qd8bngzvIA2rdjGYEXVHzG2cIE7OLqRTheriAT2xLUhaejiUKEdHf1ATXxqJVSzzAJC8Q7G5cxCQsxHJcjUZNHNaicp9O4L2KXUkWbvUhfJ1yWifaG9IR/wrNVjIMe2IgtbuzrQvhcJZaV+s0WB5SuSEYd04r1NlWLfaqSV4g2EQvwOs5oemK1+qCLUNjS12sYWyPhNwjsY9+B/WAZ5cko//9zobK+C5vmwDoGdCvhNLZvqGJVBaw8NB8n0YC7pZZRH1IAudzFB5O7Mr3RqcCclV4X9/JwfAIeYLIbB1fbSgqFc8Erur49Fx2S40r+Zi0Yq/KeQlNLaoWqNnCjMgkAmjx2wrZho5TBIJVD7SSsb+7HxUdJQoQj6Eqa6TgM2/8GgKn4849jm7fcxeABKQ+FkHM43EeBanZZ88/JGkTb3laFUbTL3ZyFxdUY8S/2De3LSckIm1BF+p3ia2db6eU97PHDwmapdQVpgGcdhKFtGCvVKr5bMrC0SbWcstmsDW0KNsKpHUqbAy4+x1G5RZina+U8/SS15IYBPoZpBES2mC1Bo5DHNM3jGxofhUXY1DOZfmhomqEniJRPXzFYfC3ONLaCX1u6z9h25tQmMUhjkJoI+NADqLgQ3qEIgi7rh+MLZ5Dh4Mp5pBYUXdAp5HLMrxWmU9VUHqA6LOG588tReA/Un9a1lPZskveLm8xGS93QOLee9IpKo2VRv7zSqKVM9oy5SAqPZzikaAHWeN4homTL0r7WcrK7HBECV0yX47ijgx/FSVRA6pZyhulbRuOYonaBGrTNj3aZ2Rf/yIJn/jYyFlJgih2Q6FP/5xKS8BHD/WhUWMb480HVstoyYNB9YycaMMbPx1NoHkW4svVMZF/je9IQVkNeu5qnUT9uWSgKaZdKuu299EBu8x7+qWXwhAPlbJO9bRPSQIS65Tch2P6PREH70Gm4HF+PCJwL2NGzzLTF8YSAUTbPhyD9PzzysEoY9MIZQgZfOOrwtUKz82cBJDQS0QJNN03NCBzqzuqHbYywfAdrxJJ9VvuAU7wjBVxvV8OnFFc4Ik+KEeEy20UW9UH6lCIlA1Skyy0COy7rsUPogo/aiTN1npCkciPAeT0od2l3r+iYk+xRPXsCkiduA0PKFVMSjZffT7LMkY00Yqqd2P11hnhfHav9pgEGhQWMK9ksBJQsoTX0Bs29SbqXchsklJTVND9MyfJr/+LsjbQL7z4Fxc23WbE0aqKszWlxvxQp6USynVg81hTashugXbzToIJs671bwxqV/VX8AirklZsWLEJMHAsle8HtSKYd3cHyUzN9s1K7s2Tborp+1IYjG0y+0KEW6hWQuKrQafmWDitrap7b+y365BjLv04jJgEJlQR93g9wM5aF5YyWnnPjHBQW6jKgo6BCV8MM6yzH/6MiAKOs2G2oBouJftNs/3AWYuERJ0qChqjILiNgYUaYAh9YnZgKGmCGr4edfT5bxwoleI6WpziFd+XiPWKbBK4/ek/1G+Lvor0A9NU+GoQ9I+lDyumYBCRndg2LZmkMybjkdj2vlAf0zPeN7xeMiqjqpTgEjT4u+zkb4OvQ1ES/0KxVhTqxnXF+8jjH6rYu6HHWprWKleOSymeZ4lomQyimourXBbFOzR6fok8m0/CjftvgoroHcrbS/Hb3Ny7VYd+fDMmTngbUGt2Jo4Qir+CdPT2GYKYh1HvTL96l6iaZom1ZdUySfjaflkmbc1C/rxNuERQnOwaklH1ImKrRoLGMPyM1T3jg4nhpTQa2cRRQcBsGwN5HkBjoHJJOa3d4nQtwklfyt8BK9E3DcPxulfie1JXkGHzzeHvnJcaD7JBySgDcVISHS2G0GOPySDMzd8QdTghHqFiRr7EHQKw9VPpHFOMnGr2ph5PUyR0TiA9JhvjM0Tf0ENq0wA2OS/emUNZ6ZjOiutFL51+Z146172oUXIRzxG3oOZTw9AVKccCinGdH1oxoE5ZEdaNm4oAHnxyIZTUFx2SnY9NTupePLR+S/z/U1JA+JuamS14fOTN9+dd1MKgieamCn6tYnsmuUtdVX4hn4+/+Z7NvSdqHb+5zVlRBtymhUMWppDs0SthPB+yetYTXUO+Xyy7pV50pCT4P1MKNNdVZvOjsPWFrzgLqsFmiu1FnLqFUWEjx4K967Uk8HcRTm9GzSyPVFjVbb4Kf8DyXLZt8BUjhd+rOtKOU6YDyVcJXMj85poZRSx++orqQYgK8Ws4tf3ROXcQPzCIU4Kh+86sQLvi3m3ydTrRR0vQ0NkQF/MSi6pv88Aj1CM8d6Emei720GIjnLQrakQFDuIHVEsuXqdgnEEnLuj/s8Lh3nPc5INqkLgvTAXMIOmqeqTUZKZhz/gH6hJYwcOPs7sYvXpRx78ZBBu+XDwhxO6stV/He+uZB6OVHkpKecBAKwVvyglaic5LsQXzku21MdoMWTuYtJGkyVtijN154RkfKfUKfy+QvxWWN3vN5FndQTCdV+5SaShUYP2SfDxl8MoCJPECu45MYoH6FR2Q5gzuGzoOIbrujGKNDygkW+/EH7fspWSspFH/yLu/uwPAMzMHe0yIlL+moZtsUAlvdKGDsncJ7ynA1ApoywvI9Y9D816cVRHc/4YjnuRE6Nu4s/yKdMGbCDck0QSdkums/eDrA6+C6n2T63ZKFolQgH2AmBTvFw+3xmRRaPD30AGV26JS+gcNIWn+wkNNHmXeyJ2HgOWLn6uC7x7dGaC6cCb9APXbgjAeqZpIA63DmvipOYqjwsDLJnYkUmJdXPZ5CPJhpZdRQdkjMZvKLEtAHchIzlKW+KMZgcouKJDVhVSzu9V6X0W9hrggZmJNfoppYFPY+cb8FAvvgQunpIvn/dIqJ7ywuEIOjXBpW8mDerJ84nFxGgW4lLCwjR+EiNjTS4Od4/+g5GSIyoAarPktNHU+fDklBz3m8XOnkAuS3AraF8gwIWvMCWw0j8Tr62OC+jQe9PgrOD5tdMRgl78TM+ZhPapX+n0ce3FLmNePSnpqnDOvzewsr4IY9mb8wFdmnpWyuP7ATLoUOm+Dl2jNfpy9QhltZYMGJbHlTLJBL6vfeOOExqXFBev/WEFfIj5XcG5kcH/WFPe/mKYHgsbmoTOQIK720zhesjmkwy7xBYKUWeKYjC09aTNEc7iSHPeR8uVHqXDC55rht2qs5GeeVvNyetpiuZSfUdpKsvmUX+ex5LIzdBF+E8cEf1rGM/LpNPsfOqEB62W500+MiMkWotZIM+h3/Ca6Y4wMBMfTJzy2VjKiTBbri6TXaeYD/mlXTkXNiXbluuBOWZTetKrlGC+Jvxv4HHlxSLk8axp8zHf4APYJ1JzruF/KCS1OkLi7VAXdq1/zsDwLx5DrDeUcdLkuZD0cfm2uwaMvNT+kxxNCj03Pks8QtagpqvqMeiNKn7zNQrBi8Mm1FdTLZudWhdQXc8cN6Nz71kl9NCsdiDif0ZIYjTqpa/Uif/FWHkOaF17yTKRFDN7cuVowtkRi19atlVWCnTWsDlzU9Tox81s7v5yPZcir/q4MrbfQrhW1x3pddaILyGEz81hCmn0Mhan54hCSIKEIAkvCPsyRK47VEbjsS/IfHaAw3xwaIrQ4SQ2PyD89OwYoP26cwyqsyMuRBOoEfgQ+OoHkb/fDf4Wns7NDBwLITqBvS9k4v+SHCmKY8IPXWgzPZSgOlHcmAzPbYnOjHKT494IEFHjU3xnu5/2riAFehnJGgMmDSseDRCKy/QEt3BIFwUhg4+JtnJ4+OGsUiBphb7Qb9fHdFsvDd3OZ8ydPSye9VXZ08PweRjhH/PxLU/tjXT5XP8C1Om79UsOYPeubsWTfr5nCyBciLPMA4Rr0Vhw5Yk7zGuic31jCcGvKI1B2xZYLaFIxXOIlPj8JYK27FFZf6iQ0QdJe9mIYWYIoY9rTaPXgaNHKTuDJzlQTV7YuwEbeYPfYoyRoX6dKEUwMNogaONKALlzEXlHBzvgs8QEzA1mOp9LGp0KwSILRkXNf1KQm+13SFNlD3kSSd579A3hyqx69L6G5VTCsvAhQ2oPLqURt2VIxgDCjRfHm0PHeOqr7WFoy2dOtP9mUjpI5qx7wqZGQrmf6gpwEqdxXrpmfr/e7Gj/dA/IBQm/V6jxBQNMZiQi1HCU60mA7Ti0u7dLOm00r47OQ83RFSSwL4XKk7XHuk9GnenIoQlW9fag6cLtfKgUPuMH/vpo4qot/i0w/861KXDvDphJbqp9iSlQLyuTREryAojrChOhvWSOt/gwP18o/LSbgYRtk86b4DTsW4y2ReZsQxOMlR8fEPHW6d5yfzNuNxmyfXr7lTCWMxRS69Q1svajiAzgYhtMjd9Vu27sxO7teWRK9Xp9GvNwqx8uD+HV6Zvz1Vwk3sSf7nUvxgAWJrw6LYW0XNkKXU5nbWoSdhe4TL4+a8szyF2ttdDt48siMUFczF0FWf1/HB1m0YtUuwbHuut+jucgP/ho3gHzBIMpy0P4mygCO9P/m9BjoN8zV3R9PuxviqufVj3Ao72/x29dZmrVcpE8Z3/cTyetH+v9/rjes8U+2vz0kWGYfTRdJzsXxQaQzZyRZh39dR6NVW+1EQ2JslpTH3NLnKLJ29VheyhhAVJ9iZXhxEpSSusbxRA/r6/edytZ44TSnNecsVET6jlZtdLb+LIunzVynk4tU8Jf3e1I4AohjAXQmEXXWMtfm8iltaROpXiL7njMQQ6FrHWO/deuBIVtd9kQjHl+oRuBQQhcpVZXMzZr3b7TKCKtk5VORUhlTKa1W2tABUUJP9F2tJ3hgmRKNkgNyiqhw5XF6YITLPJ5alFctokzcJL87q/xfp8smf51PtHiNu000i0bo16rSQ883r2pWw4o/zfFZUDxpVfHBG1RxrSyR3g0sMgenQXfMwIBR01tT2mGta7fPGcbvhXqVqbVdT73Peyftw2k1Cn9/sKRnA5c6d3+bNA1d82XW955Er9t6GLFFsSW6LlveumP/OR4MKJSdyMY+0+EpWS8maBV59KR3zOcCu7R42TPzcYdmrvRwnvFY0icwfFwfT1aT0g/TT/iUXOUegkx+SugJOSwxcvgOqZ9iNxrlQ2HilsyMKh/4hrCkJrWMz5g/miIVb0RsWTWEXMr7+ckWuTL76VrK9jV49LkZOI/T67FgeYoqDoxkJ68Q34fDmhtT3ZNV8Lo1KS6lFREn+ke6f6M7hasYh2FBf3+Vde0JZ0DcsKOhLp1fMevKc7hocMcSOe19L27RqzvIkBGMW7vxLnJe+WF1zs+Rm0tv5b0W9pJN0Xu5P2kMAuXe4m/I+jmk9vQjCtKPVuADLv05zCRp6+aBCWFjg2khg3NT44vd+8uOw214DUir9bcH7Qannz6t/bv74KsH1WezfM/Cfh3z2EGmPOT2sDEPo1ejw5AzQ8cN6CyTHBRd5q7AVxhi7jVR9XXmT7yAY3GQoPysLsjHK2mBiTjFIxliK2hlE0M3bTDUUdt9zPGO7VxDVLkw1lkKOm7N9lfG8RIvrxiJN6MuDLuHYnOn8/U8hZ5nvpfz6yOrBbP8/RnNG8wfICuuDzyqt9vXgvtcuJTe40QCTxta/VWLBeb31VhJbloFegH/uT5EZWwjPFOcZdxj6zZW7cHQ1jYFxLYcO0NM/OWkB1ZfbRevLz6cEWG7op5GZvP9E3//KjWKH+rrPr7WbeT1j5pqx/Znu90d2lNeuSAnhBlTUtgM3o2gcaXLv6PRWOSan/ej53lc6/+BQKKkLQvYDSMMCajzgH6FFkVwil1rK2GrvT0BSYu/6RM7+/eG9oimbvd93ERc8cW8ysOySHv507yJc96vaH4FLpe3BcLgpyxu7suCOHKTpRtTB3uMFzXjaKxV9reP3eM7+meYmd3epXoRnPaAKOGSTFdcbTq7evMo9rO+dA8/B610G++2q73BpdMTChsXTjxBTa/cMCi7RrZ8GXUq37M+kiBNc2ekXolJ/7SH1/O/Ia3e6NzJYtSJ3Hfx94CH0f3++7yftUZsxYvr7leyn1S1rtiNDxeu/1dUHtPOGe5APazxexHh5nhKYXgNL+nSKKnW0rEhVOyejJLz3in/et06Jm7921r6QZHXxTX5QZzXlbX6g0Rrpv551nay+3583IbX/daFKZuroCE7oXKFnpplz6NKg7icmfeN9iyNRW+U6gMtbdtzaPc/uQBqbWx5LZo/hpahwLHFE1KIz+4fcsu5y5KAXkwowyk+RCf1ExLVe5Mryrr0LXXT+q5PVsW5fqpQdevyZu8daO7NdapEaoP3DgTBvbjlBvrEnyR15DGz/B6xNwQPUdlY976Ugu0X5b3yW7NrHa1Y1WwV2S2+KbZ5rkgzXMBhe6LvU/fp/emoX/m6rf0Gu4HuNf3ybb5Qf0tc3yKBSMw5a+/c1Ry5+QPOdsd5bwRJugv07iuH824NmP8KedIvKQSBKNGp3BSCJLDChQJ9IXf75PBk9/C3yxatrs3C0n+OyAoX2nyfdTC+ctpPk5Bd7q26tXuvmGaGxCuIx1Y8qxYqNuqTu/SvE8v2XqnR3PiwgB9spS4k9/LefkoE7XnwP0TRV8wltibJinFrUkT+tnJ/XHb2W5WwOYm9vb+bpkb0Td3WvfqAtRNde9Tv4N7rKVcuupf16goYOgtNvrTsNKjLY/zDQaS8+4zsipf2+z3x6vVbGmSe9WuXWwCtlqbXbHLd9figtF5VaGG6MnZ+e9ozOHnQu1PltA3xEkNY8m1NemeyS0esmI79Xpn6rGZjI+NP2m7F1wH2Xcp8w9tc1rBL6q9atv2rukvXAIB2lHjvHfyrGO9ofYHxfWgPIl3NEj2K5Z3hjZmA365e8Atr9+VdbE/lOSpzfvO0g5SBT6Iqo/I+XRfX6bCVhGdajTWw7deH3TjYWZQ39AQPNMvyPjVwzufHKFi4f4SGinNGPF+O835R0zTMspRZlj4O8slexTLdtF+7KDaVG63HWaYPLrGdSw89MvKJFJlSkDqspRB+SNZXX3lIJ0vrQWS2yZddyzf5MaoEkuVmDI+3IPVmLN0pJelmHA0jsnQzfp+XQrGBwGlLus1EjsMsSZ2lIh+ikbWZsp9XrbbRnUuRCJIg0sGqmqcNCKJe0mSHhqRe028Vl5I7v2QoHhORdtXU2NIYuywtRxxxsnbJWkROhWa7BMC8wfH1amyO6LkuEf1pzzsN75I/Hi+lrTwQHvn0yI4AqAyZ3ArJ4PLLFWg3nUFGlqMo2Qqa0HjRW4p9Ol1SSTWdhGXO4Onm1DpKUu8/c2qLEfXyP15x3cuUTl3DZLiWLyCX+YZCf3irU/QNIclpZTomx+5SxayEO8xwrXaRnG6oWdprpz6SjUstHa/E24f1RDpo7GIhlSRp5+jNb6S8lFFqHGyhXDSNpigNjZPqV9Vc2o1mxLNxIWyhNW42CE6SSHppR401fVC9LB2cqYQetcyS6yHn1+6PRz621lpqMb7o/Dwo0ra2urjSkSCStOzSGz9NOC6bC631XisUcV3psOXWXNecIgu7XufeqpGTn7Bt9P1V342wDHZxjSe947W/SjTYuTapidHf+9hfflbHJ+hAew2t+DG8RKYZnt2kIKlRO3R8H4lYdOr+9VleVPuC+dbPE5Bjr+2CtYVEnOxh8+3qVinHY0sLi411U9R7/HqlLvH499/99cfwKzyuJD+63i2D6N3v4mTiQFT0TVSiEpVIjUoNodr0xdKLAgUhB4riT9WUJY0ofCKOaSfrdUCzysQlxcyyOmLCuotZhKOxgunCqVAyHJ7PhXt7ZCGS8fqDsHM5+np074dBLPQAXiI3rlnwcIfW6XpXqfzjoFGi4rnfdtz8Hg5tNvOo2ST9sBqszEVDQGqD137UNEuFnwknMGjTBYFHrn/77WM5av2CW94llGmtl+lLDyt7ZqXu934y+dJu9XPBVl4cTP1pLrjACPnS29YXfq+kA+s7wj9flw4KFSEG/Piu8s7n/ip23j6XOIJ3xnjiV/sN8aY63qTzNInfYU3U8p3UqhK1SP1fkgFjuX/Q68blyBDkJaHp/Qi8jZg3ApgX5KuGtIBryo5zWnDpeukn801R63KU9xNK7t8u+Mvo/ZCv+LgjyGWJTbUs31KHBE6D9w+BhBCUuYx13jfhRzRZJRprAC3Rbe7bGhxt1IGaolOEtxtsUoGcqHpVMD82y2CUGqYjqlspIEnT8JKdg60JkFNjfsPLNgoxSNN07nxs5+p2nIGW5pY3WDnc1h3IaW7kw9+zuyGPYZjkiteuHW6Qgpzm4iD1OlyiNU6M/0e67HRZICfp9NehbPdLNoYpWgDb07ZeKmucqN4dusGMvQ15bi0SM/nDo4mlgVGajjcxsq2Xjk9NzfPOc7RBDWka4fDffGHjAWipLj9ahgUuwRimaeSD7fOLlmiINELeVz/PVbYka5yYuleseM68nzYW6YRuIHG7ulTGMDWzf9hFER+1tdagpWkZ8s/d4dKELVLPN8Rx6cCkiob3XbmtltanJp6qt7n3TcUC5CQXPvTm8ENI2/klxC0d+vrR/cfADNGbZnsOs2HyCKZCzBxMTN79j2XzlO9MJp92Lr2yOYh5GiK//ZYyHbiDrYIROcYndOLbrDfAAZxTKYC0nO/SwVBzU+sQX/DNiXYERB4XY3LJJHjsGAQhVsv5dQTSaJ5UDgpVarkgzL00whACupZ+iHzPS1dJhXz/g6AklxWJ8vIOufHL49ZllQr8pG+OE6Py/6xPKRAmkrR3lWsTtxLjCpOWW9nsSa8nX/N4jLyC4/kHTWW4LZcoQFIONkodUkl4SUef42tm50Hj++clySorMIk++C2zH+e49y47JSr04L5f4WEYQnq605ruuU6Qw5PBq+6WHOEB7T3edYTv8YERv8dPhoN/BFQuSeTbeW7Avyag9ZpAYx5fAaBx05Gh0EURaNVl2Nw47CIbaPG9R8AklfbOFnUor/GHR8j9/ZodGRrewI30sRlaocmSUMrNhM0uhhpdaxG/BIeoNIEepIcMTqXipwGRSrGzu55UVFzfuP/RYMLGKddNrGJoGtXXxPMfsNBjeYluG5/pq39GUSfC12+44UsyzMcQ3ngxI4saNRhS16HhGxkW8LSWeCV7h5v8byfLdpZDzBzPkAdde8xxGKD7t7jAce7YLHMnkA8FPz4rvz+4mJ90Wa0Iw//3/V+CVzReiOgfrT9XDHQB0kH43cjVfvGPJXpXFuyyUoG5Q1amBr5R6VlZYOtKM/OHRVCFu1yQ4Q7XJ7FUxVqFiRfLurH01ysWuis/d5qAIUkWvQtfQjoIX0RfXJ20kHIB+IyzVd7mUtfo3vW0DQv4HprUI1DBm4Qp5MMNAjgNV6BtwMmXNHrZRaANVPjeNWkkDLhumET68gEdSripDte2qHZ853GtLtc8LHpEbGOWgW/aXIMwBVutHW9YA2CqXLPPWcXZD3uoOoImcSROKmnCHskOeibBG2ZBqpxXwK2gqBYj0VGtXdj2nfd4VI7GYAzev+xc+/wJjaOanGy3lptlQyFQVswpBvojl5wbxgydh3e2MwSFW3W3r3q119K6PIvGs0TApe5GWOG94gTPhIsY1JbvAGE/6K1Qebb7veQedWLv3+0Db16GvDNgCpOxSPXBW0umKuL4tO3y8FJZjOf6l6DNJmx/Rije16oztcdUB2B1e5FNMoqI9oUilx0v4ZAHk/atTFYlAz7nFuwQNn0LDD0rc0xkVO0fDT/lUJnwV5jNuJHBST6cBzXB3Yy5+eAeO55XDr4zg/RZM1lWs8vttGHtIdraSWLN6jajDs8DBt8+kft/WVL/Y/DAWVloGGH5KvjZUua92oPAfqY45aRDQyalfACZ9kPcuPWNs7RByeQuRwq6/RFt6Yn8/Dvcvh6QQC3MkWJ1lDf+mcyz9y1+/roQ55MBhpjuxk8A8nopZhIdHAIUX0ClxTturFQZ+FAhRM3W7+7Jpq8XgTmMbmEVknn0giRud9I0NxO274cBSdzxaqDNewWtHRlm29+OMjQ/cs1/NNb5LzguG9p4KY2icvIymwK6ZQJJ6KOEgjYWdR9kp2/IyiU3dWcskGpXTZj+6rm4OYVCRIk4PJKmxmJHeplCp2zg/1CkStgOIJjtdmQrdoz3nkIHw0WvHjxCmIqSJOIbERILMY1LxJcACrZQMTM8NvV2V0aksp56iYg052AguuZrFFCIqc3lViwpLR+BECSO/XUWoBT2xXVOQklE9Hr+zq+HIOn/amCsk2mg883x6KmCiA4zCM9vSE4UU4wicxbKB+drfDANfb9AvE2LsrLMRfKuq8aYFih2YxpxNQ8YFkr6mGIPGaK6LQBHhK+NvI6NCdPDggWiSEtscY/zz20aBHCB4XtdUU8Gs1uw1HjxjvrEONR9k8NERvww73L4AmsUJsGTXQ4XRZ+0AKVVnN1ujQksClPH3eDjSlI3NlkdWMNCg6oQmwSyisKVeUxMjGbjGwIfEqGT6W9E1gA0wIQ1Xlr6Oviqp7v+pAEVfdfoj5lm0eTIdy3RBxglxHHy45YHxR7keUjNciJg9MRlhC403EMpNFiTb1BI5wAwhgAoiysJJcBSs5wItD0x/MQMqrG3fRxnQpAu3kbe+HvNQrC8nSKFMKCcG+zrLbA2JsdTQIVPk92l1koPXWFQuGo0EM8OMbo6ovT7cGopwxKIReRsC6MADNIAod2agx4xz71LqIPIryuYg1V3ddTopAUOTqSFAxyIEnp8d4OgRm3VPLDNxzNg6bJrK7obIh9WTBSHwlpSWH4/LUbgyuSjZrlVnZAMTCe0MR/FxhqbItGW7lfs6WBBJCWcJPwUy/zKv3GvnCF6lACyfHQwED3leDyOKU1QGJ7VrPeX2/E4kzv/XEVgitQ8m6qnRlwPrqAgKhNw8oQQqOHNCRSWxGuSp231KIeql5TBspUY4ldIf44JP/D70lQ0fIYL0plpTFDhqG7rQD5NODbIw3QCXEklLo6LP3NkkqkJubx9eNTGpmg3QidWNsCsrAHkRaEt0E4MsAW6Agze0ZHQiQn5a2UQLAKjU+ECyRzeZXJc+MYv5AXB7qyDJV1qXO1TeKbNK3gImYWc++6ZbsbsJDsfgUOtja7SRBD/gINwsLRcaiZKNL8WNxoVeNy2OpLRgJN/RCTZSD73aS0T3EXofkkptZY83jil+jNBytR0KlpO7JdRh5TsZKa/6PipAW71kGnwv2f7ntJIiohEGuBf6WSzlndKZZ9cgyBMCywltWc9oeGqCSy3I3kvX7Q6K5i4GDbqwc26XZJ1bfCKvsNoOOAz4+uy/6vSmIwIDhG+bdrfdAsf7cY4KPiR1rjVNXXBjMy76M7x5Z27tblTyNuKRMHl5rWcrvkNOULz6O/czAFe/pvaP154EhqsMKJP/i1264pmqJXQP8iQDnfK7nzr9OHFS3uRkyhV1DIbGFYc3DdmTFtG1BwA/byOu9LW9S6j8fOfnPprcFa3K/OPy+nX4yXhoJUfCvfM1k73vip70sk8oBvVT8mVz3Q4d08PXMix/mWz+f/X7+pGFNOrO/w1ex0T6T4QVLAhw6qyBBIc9Z4TCM25x6+9goGWPyRn8LXtUHljUREQhkEiyzzv78iT6YgAWPmhBNIq/90QOzuk0iF8fkyxx9Sdf/3s79sn+u6q6QVg5I762agaaAPx/M6sCEzOTASsHHc7+m2l3B9l0XNpLhg2q29zVl79mLzOUHv/tcf8aFXW+sGVg8M8NJcqhbrlfX0n669xS67tk9+eZaeLIgnpFD/L7nwi3hmHYOzwKnbkzlsG64JRxUjQ63UpIjNucGOqU468iFd7f954V9DbygfgiVoPWKImtUkAP9QCirIo9delGiJLcYeKQNt82l+yC6NLaYypa0XJHEillKcZdmRtreaN7M7amvQmwFhZea+clh+Z08vTO+BCxY4hphpvFQBN4on75k+7nDqXn/IqBFedHrd8YWhpEICeWqo9L1MdnLC1FXAtyJCnihpkJEXTM1p8y/aUY1OteHcLI76wE5eCIetI7qL/sKqiyGB+RfqdFVbIjGdaAM/URNTAC2bTWTxlnVF1NlbGP9C6fyOPM0aSOJdiJljs6l0BGjLgx97EHoHvFniijjIQfBXUFJwK/ncgQaZl6+aLzfTvRnic0r8+k5ESRb4TaV38VDXZaPNItJ510oZPyyC5ow8djyhiEWW/2Ecyl50wPK6SsEUjLlRSFxqgOVwZhvaTAHjSgiCP8afYAverBcVdB9KTjCmS8mm/6mnghQEDyjMpHovraS98suY76RcIlAiX4iheFa6+O7umhu1VbiHEy3AZpoNRjMnpit/O7fjWq1mp8tCSZStcN6x2ho1enXHY8c5SYTqXOxOnIbIKJEPRy1f4XD8bPj9cPLTplduNrEdOePWWm4F9Gdd7QFFzoBP2KKRwng/bbxLCW3jjhHHOBr1OkRCrn14dLRtFOztUs/assb8Z2V915MzoAR9cDes3Z/BmRX6Jf1CISwHgmhgSbIPRPtvDrmylNJ9tAZw8NzCkBpvuB3n/U2XirPAhgJJMKexmcQ3ocDGTtsThHNFK8J37IskGgXpiDUUbKJNXgcC3IMp5uqkJwICyFhAuabL/U5BxFW3g+L2JtPQeSA+NQMIeto7g8WllHyLkV9tMglPCZ2hGPJ1Tar0/yJSdti5RS0n1GrTEKYALCZd90tCHF4Zb5BdAtaQasU70AKE5V9gX1Ev1OMO5JZ3y4tQe62ym+knFHmdAy9bWf2uYzujERYkpK3gWV5GcOVr0ZkRODGR/gldxxCFz55JsVKmj59gswCXRqGwKsF3NUKuBH6LhoAOMEGCTq1Lo4k15y8dvO8Wum7hrk0lpzNYJnE4IMt4KqxqLpXXMJOEcVea2DOC3eZNOM+FoZ/8fP0cefOj1SZwlY5UwlvKaO9lTd8q+2/2RKwcCV77QxebIn4o9J1PVgP+icNummz8RYA/GrlB9zff2zij1ufqMdG4voaF+pDv+KOtg9s0L33q7pB2UGtdCWsvmVECLEp1Q7Cr4JSSradTplM1JjYJroH5KIVE46DsFTwQvYWCgvy6MEUXa3z3LC4XyzTOsuauI5EA+XYy9SCOfbavG9VSgfmTSnfQcjORsAHkbz20aAxV0qOd4mcbnMvrjzPkcDGiGhdWM3oGV8MM5DAMVrDaIwmV72smbIYhgzcs3EoYMcU3ADErTvrZ1MEXRtYRb8j7ZKs4KbZtIW1BcK4luUZFUCSF2/hAT5oG2dRPFzjJcHMQnNcSMRerxyYiBFFO5im6jKvSKUBOgR6J/S1RoGZqCQjSC9Fi3QwOJRjC6AqqcODsyUxIQivx5iXKMVovKIwyKLg/h4x3O8nWo46Rez3WnkWblsmvAK5L8Wc1p1+gKN1g54i5leBiZxRuE0ZODwsHFJQ40n/I5wRZl0BrPStr+gBbMbCTT/PdS1LzUoMYayK9V5myVxlzR2NW3BSXwPYwGjyK3h/uAVJjlyazcAIZE6duI1/YGGBRAa6tAVd5SlSA8JECjLB4hBOljqOs8lHyLkNgY8XAhM/sbv0jPkPgS7K7SRhu44kFX3xMTiBU99rhz1BRtoCm4QtUA0nl2h7CsDwK5jAdf5y7tpi7R1G4Vv0uFTl11rQ124Xg90NGL02/Bcgzp9Js+3RXw6m3Z+VNpEcEHPXmGtg8LntjVdhDOM4+WneHLqwVy7VpoSPFUFecF3seBPy0Bf0GdpATHgpWP8u3DHNhQDo0rWF/zvnBG4NhxTG0E/VXkYIVREmGupH9dWKaqXfmLhPWUQeTrcjrOsYTMk6VuqfwUulAC7T6fBxeTzBBNo0FqQIPYchl7znyyOrVqM9N12HfGUROHenVMfHkGysEUburgRCKvnX7bVT8vDpfEe/ojf60RH+zYTnS8ohFFTan4TDTKAJW+UV5fA1ZTG6OteIJDuwytkbgL0HiAu5OPig34yn9mR3NpOcSf8Agl2hC8GLgMc3Hc3MpgeYDp3iln5p+lN/n1pi4crBWbPxs6HnRRuqzd22ZT4VE/4iJtnSXWMwiM7dBbvdpjMlmqDJWKmzS+OodZ780yojLw2pF0jPx35hJhaUZM3sz4I4AnS6XGgx31a+fYqi5pvEBNTkqqFtjdxhIh5JBTY8OBYhUbR8M0rWxHBJaZTcWKnmXsGELzZAVx905E6E13jlxxMZNB2BHb1TD7DqOJn8Mxcn/UOVtTsjVXjUhOwTtzRN+r0UjvnE8EQxEZ856UER/tj4n6Pe//yAXZ99OOPDtacPpAHRLJ77DVPvHaR3ZZzdsMjtEwm0hGQcVWkuHvRYYkjuF70N2PiqSK3VooA26zH6DtXg3O4dEnt2rMOwwXp1RIjG54Q8AtyDZ1QSuPVJTdh/KIEtY0sXhLMZTrTUV2OtIcJTis0TLbTLU0Kq8pJ/HgLmSN6kzlev+WSWAwoY8kg4QCHf+SYvhhgD4Onjnu+vYm82k/QPepgDdhUz+ni1V4xjcr/X6a3/gMYQdduQ6VigdA+snviQY5zzXQ9W3sBINuXSdIMwHGSFhA/3w/RqQkaFLDNEg7rhKwqJr3y+yor/ddGQf5tbrYgajQrxH+8DrNPzRRyiFNfENbbpO/pBHZ5MZYyMfEzDsTtdNZrHdomVZK86QN/biWdYa4lmvM18TS4ZzCSJVU688NpMONFOJY5n0KdPlmNrvU3xcO4ehGMycc5fuxcbxp8ayYuKrJEr+L9U4z+9tffM+dnSyXyb53rjTiHtz7zYGuOZVZi3CAH1uXfIrNcK3gT87DR3bO6rPmjwYp17ZeHy5dvnBRSW65wVadbH1wHh3QtT4zZ9ulLR8996Uh+OH1ufAsbevL/SPl+HA9dMJbh/fWfeiwLdNUgx29denHLY2O77OHNe0vWy/3Vn7owObtH9O1ZSOX2+ukN12ds/Kq1PU2f9hWvxdMPuWtoCQnW8atdOb1ZoAg6VZpSk+HbLVsRbTzuX9Wuv1lq6NljH5JN0Wfkvh7odSftV3QeX0x0L2pmwMyqQSLBc0Y2DWEQrpBzo4huWRFZhd+JtT/1/hKrH4D4XUI6mj47+LNDLry7qjLSJsrweUTsUwXUVQxzYA/ds/xq2Yww9I3HjFhFSGFtRp8jTpFFmLKzwLBTiZ9zBrN8WUKtuwlas+RdrmLhzzc9HCONZqtfIX9K0ClAJubXa9On20+piveU89+tHM94sDPisMdZcS3q6AzMPsotkUKEYkgG5en69L2YWEKKtmRfv7muVVY+PYfdjPPCjWBRjZxAgdiQPRBqTGGWo1IfcFzCnIhi5oxtww6prAnBwGpIJQggjFL1x/nzEa2qBD2+0HliqbV49c41HhS6toUv38Jc5Drx8oDAfp+PKiU+EnaK9LBG4R2x+JROZe3hxIyZGdnnSeSoyMLYcc35D5vsS5ZJJs7c+f51y80jdhbEEodOdBwFTXs6J3zj0dPEZu4ZYQ4Jap4UCaMQZFowP+mMXxGxHpi5w486aGQwxFICx3IgBGiak2kmzM4EKZTDkaOnpseir+KUFE7B/zLqSQUxYkxUT8QIQ0id277kxfzepyYrqbjF1NvDFjg7K1voYIq2P3ial5gVOcJlrJGfcNnSGNcZkUTU1gpsU0oms0q6SUOaVEInNLSnywCkO1C3RB5Ds+qrD6bJoqnf5hsdyLGQUKBPnsGPeKBgtWj6+CyYleuq1hkDFHcWVU8yv3uBHeEYUswaOJ6ytPvuRZwA0sRYMYyhT7dKI0hfE7NlCCdrsCPiHAKhiMRvep7lU5lF8fhqgCDdlVOcz26ooxMP9OZng3VJDr0p7WUPBckFPBdrdRuJL0+T9PTWax9Sj+0xltaZ9hLXvh1o1NeR37O0qvNDI7/tbupdHc0xqZTbEHu+lYNxhvC0DGZp5Tm4SR0txVTUhfsqzOuzO7yGSGAolRSzbhr4GLH5mjt6jaedhdBc0UMe9K+N6mOpARHa9Gmcor+ZnEhdRe6+PKR0NBsIb37XvTY8suBxS8foIGPksbhCzQai6/yd83STwMfBFHY22LjKsKalACGb3IbQFti8dbEHRN0fB3Rzgv9wmC4aPrvP+eWinUDSiizjPE0BnDd9ZSR9Ikrxih3AtOQol6fzVHToCSE+YDWynblfvDc4RGa1Sd0Wg5BcNJjp83nyn7y4QcOf9K2g0NCYDSXQUWT+S/Aix+VDYJsu5YLWPF9RfQzAx05TSiEPJ7SQML14TegfTVrndJJGvTtTIUZMusL8+KIySHHpA3l1BfiisIhPBj9BNlA4hP36PtENNzK/j9iODT07KvYpIGO6rVt3JeJC9+EEZVAvN3Cy4i/mKWlXcBlMAO/sDtSsIRHjNQOUom2lFJ96oH79z7/SQ2egTpahvees7TXAh89L68V1vDVJAI/OPMN+ed4+UOMOx0LxoasyoY2RhJ2b4CTl1Kwihrx/X3pCMQ0kQ3nms2VGLakiUu6PkYvdgo63jr/HT92Xng4mziR+aVAR48clOZrCXOn5HtSzfKf7U/dt1dc8rNDrlpjabADs64+v+cOMjhs3wKtjYbAYZf1bUyRZhfGUzjss7KmTdBvoGRSm/Q/hTOvNNdyHbFoN/J4skN6aoGVYbbSfmNmbeZbeMuY7d3BcLphm3+jZAb8Na18Z7Ot8H5TmNLWCOOpBR7+bh76MjdxpZ1AsINN9K5t2TWiP5ivugi0hcnVB6X58tD2cg/QQ2G7HRISWguMnfvJKWImwbjuMf88K1d/OZs2D8+YXNeVQ9vIsfgnzQXaVVcti53aFwuxiUO/8vLRhqPr6mV/Obr9yoE05oYBFEzvP1aFz8rwfGitP6mO7AwoJQmjnlt8YRoFvwCuQvaocwDONnJpY/i4fvQsyvPvsyR/zpuOYcDeV+AOfRYYiNXCHcjCSyGsPL3ZHP+4AtVdqorDdPaYYCGeOjghebDQ1Hcpk1DlHFSwBqlKBxXMoApVqHJtqSrB+IPasrbWJnl9FRqkghvobTjiPlZvh44gVZAOW458M7l8B6dz/4eNyP/448XJAXiMkhRWCH5RDFD3BGs9cIa/hWUZl5An9ixwobyUdQ2MNhZ5ZAjbHBfKukrraJXXcdlcYg+4noR2XBsq9cf5fPvvILu8sYrL9LoiGLTR4b55Gk/3TcJjwZQJop/guXzT2mi3HfE3w2KbP4JPu5DNmrhcu8/TrJeAF/Ok97OFWrLFxsHLoOPKFjWEfP50sKBFXsZSRdiVJEOloAJr9K9rUni7sV9tDAFgIaLX+f7pLEvinTsm8yCWM9r4z0+n8KJeADHQug60//j2GvC3SMCKrdjXjg3qhzrZwkBAx9uDwmkPe5QP5zR+h0Yw2LEvKvF0Udg4AMTmyBRh7IAWG5jzptkYsIFO7FhjxEIsf468nlZSHmz7v1fZrcvIE7WbvjIw8LYW+rj9xyGdNFGyfrBbXUaKcGj2YzfxmEJbZy3z/75vQmy23gGLjhYlQo1jqstdI0fkZtfp49hbOLRTgyv1feOQ6l8mPwWdS3nkwcI5WDf/Jhx00VXk1CK8CDlep4vuzaPebgdJNb2hEJ5fp/S26aGHOjQRnwe+pNzDQS5tUh4zzSttJNb5Z0jDUVfeglRnG1Bn8bGJ/pAFidDgvjQ1ruz0l3Omg8r0oqHnrIzOsZJ20vYoLa1V1rAJlYe26aZS9INTrJH+3+60QOuUoYfrA0iE9x7n3nCstyULLgsdqbTkf9gYfaPFGYHUI9gjDzz6nYXe9c5o/wKzAW/AqXMk0glz6NC51b61vhKS1BUSapotCGZU6IOD1AMuRSLck3FOrqHPbYmabr6EY/2Hl0P6hsa1ePWOWjYKDzltoswaMuPBlFlDGRHn+iydzidfAuJTPQZVKdChB2SFeftSmRobhyovRJ8+20b4Jluev1R/EcHv99hIOsBRrPuDsklTFn2Sj3firzIqp3RTtauZF7o6l24p6CeVKlMmVLz6fsknZSORMDnLTrTPvvQuIQ95JZGsxaFwh4NdLvoDYzVPdPwTJSYI8UoEtP0CLhJGYcpa3QTvjxnOGKSMxZAkh6sSFBGaIdT3azbOHFLgB6xKVL2NDWTJLHlj0AKmF1nyNNkznp0QIIEnBSx1Qo2fSDLBPtirvxB7ITmWvm/4vQZWo9oErIbpRXGlfphlUm79++cbZh25SGM71tbO7URO3zoyKp/xkN1ILYJ3ceK4MwH1/lbM/RBygQOT5sQIIEGo92uhsDPDagNeKWmmL5uo4i2zhO6lDYGm8lLmYOOqnmQZnlIu6bBxWVS/zeTVfkguET55pR75XaZURC9XVVHfsQNmr8jDQwxe+NSdL4CRjASRr7VAMFwt3IsoOz9gttSXE+lS7o8xGOR2MaH6RIGSCEM5kJveG4J3uyhvnLlmjGVCrrt35tzcc52Fjk75rRIo9y4lmAnglp8LvAcH1zt69sZ1mREzs1PveWf9iL46yz90PTnNK3Ycw6E0+AUlhINMhKaVRVsqMG114aA64FkCMLAXRFcRHwybPcwdFj1A8/r1GeyZMW4r2SgeIQdCxbAqmluzRwXB4ZwXaiD481IB1cweDhfVsatKDmNMg1EndvRDtPy+eAyGjxeRKsoGzfM1ORA8h2mflbRC369cGa0QCvvBdFXROAewGRe6KUp62P0Mf4AgFEogXfLYhNfThQtAna5JQ1+ORZ8IRMi9TkuDX6NpCGCRsHpJbY0vmUr+0QyJmaTIl58F5flGsSNlgPBzx/L+yxew+gfpUeSlSIFHmmrBT+r4EDlMVA+gi4e6fkiNLL0L58cds3u9ynCBNUdMbi8kZ4kkdz3m0IsR8yvuUuJW4yKvU7V/A3LcUkdvnJXNhkBcr7KZ7zdf3e+Yjarv7XNn/+WnYQ8inO26oKD0zJB8nPnEm47p/leNFGrhVKtNeVo8qu28rjRW0Kh6u6u+avIvpu5EY3L/S7DDwLyBuv3ASMjQtgwJLm/9rETqkwkECyx8Xwf8hXUc43VEKklVeYrc+nCrN3YvI+Wod39AUGbZpWGkOzM7X5hXWUjhyU6zbCPfkbV8MbyN1XdfYTIa2dyq/nEOCW++kc7/Ca/XGzrbQ4tAmFVRaT36wUBPzoryCYn2x6v/jCsuSLypt7bVsHTSL1JtTS21iIv+SkR5LBmzhuhjp94PYm5omzycyLudcrzBITRJd9Kgu14Sz/DqIM9jnl9PJMIau+j45fLUVsa4RwYF9c1Dtu58xcSZ8/3XPiV9yzVFBXK+jeUXU8l3PJ1IJCr76Itrzt289M2xqIcJzEpAofzbxyeOvnd/2b/gg9gjwfZgve4kMD2pPnFYpyxQUPWwS1k5UkLsCkaNuY6zsoFF2ZifIgYWurITD4cR/Lr6DqoczWeHPvxypTeKMZVlaM6Hi5PuOz4WJWkp0Nd5X7Wx1rqIlFWYXvXCm194F1cGQtfs6BYUsIDKuyGjsgUcwWoNcCaUj4BPsNzFHKskK8FW2oBqy4Qshxpqw5TlkuuAfyyrLkd/4wg/peYCF+BEmWcf6izZZcVCay+hcNFW47a3yhkW2s4jRcIseanHQfAB6UH30rNpy9n1f0e876mcmywlpY3aePHY0Od0Xzj0K865nLjdhotPLfvBmzDwin6EhZx5IYykMRC/0qZHzOgRj4TnC4lZPVfoCliDvqgju7fcNS7Pb2buO+48tnqyKTDmoJYufQa5t8qkLt8x1mkNks4fSLx8n8fa1mDrqXZ9ax3v9cLYhB0Ec3wcnEVBBX1nGciDmCtL4zqIywE3odSeqsbrVblbe6ZnTJyswzhla9nDdZ6Df+SqNnwvxI80hInTlBwHHj4p+g67VpzmPDxOn682LyFgnDLPTBcrs7o5tD9YH/+zUKDUBszefHeiofy456ONayf3ZM16LY7KtG9b/PX981Lu006GmHFHVVVqvxsPiiTL5FYgGiw7mSLU1n4WsUtXG6ydH7Pz9eubTYjakn5Mx6SJBmenjwqVGx3DFoVtryP0fVWUqM2RlCTovAQYhT3MWT8PqSJJiI2Ov7yIRpelSAzh0hGNl2fpYmRcUXR3VDFYp6IzA1GF+SkFPxFX7EGdgQlZDWRUUfQtkI2PrFsHKpwhQnpyJL6OdYtxGz91os2HOjeimVzCftAyCYRKRbdrh3HhmOvY0rNUsYWV/f9THo1RpMVI9Cl624Q+NdJuzJoeVllaqKbKBelhHn+blunsW06bqWiwuUvF9rrxHXa7n4r1kh72d99C6wcdLZH/c42IL7fr+Mv5nPi16mvqsa9Eg7ej+9QCCTIs8sDNa209ulSgOVIf/0MEN/PfcSmmXI8ve4rVZdr772sIuuiC8UDDRlam1xspzjxMRJ++3GFvsYq8vNpJ1w6+aKN0wrXqdpRF9YdnJLIdy/aYzvmkGcGz+qYmzJJntha4GGiNpIditEP1NSVrrSnTSuYNTQP+ZlMK2ITOQ4gv7VE7+VEyvPK5IAJohhZbL9NkKuEnX1xwKC3oPmb3ik3cIJcGoP4JKtCA1yL4l19R6JhBoPMJCjA8dUIc1ezFaI7xAvzMGxw5u1zuIzf3p6w1GXKbNJZ8qcpBalcB2KAGGJu7aGGqXf+VRyNecj/n42Df7phagJNqizUDoZvrlAPfzo43Z7nCiTC9dEGcgnmnQbCp3zrtv/+k9I371HKCrqt5ibe9WePT2kQrmYwtj7Iu/g4y3SzvJ3a5lCIxo1m2X7NQD8yRdxTfnAtwks/sDNiiYPzb2Mz8j8Tl2/mH8+lKSEane9y/KiYSfSuw3fGJPT4PSMytoxEFwd/y+jlHxkYq9xjx7nJnoamnIhPIQhMyGyp0VwEisIccwuMxfnVII8mRo1491dGV7Alx2WGC8rozQg432a0RhHb5XqjBVzi47AE+czzGwA0/r8RwmeY564HO1CeYtfmn9jCMVo/VHmcmuPN/pOVTquJQw1y0HrnaOGWQ2/3+MUun3o0CXmd1SiKkPVoJvuqdI12T+j1bFDdK5iuUj9xTYlrseOLlVuXbfdzTTScZ/GmQliQIx0HATrxAjGmYEYH1IfBkxyVMMjESSdZrR0g3FbETF18JWcc0IDS5MfnryxXvwa1v1YugO3gXezhOSTSWfGXeGY0lh4mEM/n1DXW26xe0/9NGNIJQUmyQAwt3y/hjdPM9pBnznj/LQZaVq9Gyn7bzJq6xMPvgWQMOGn2AO7qRO789MKiSWVTj7ildDzsC6OrSufmI0ZQBGlqdPR8ihT75OUl0K9xU1F1SJtMDErHPFUzwq03weqCra/+lKp/1TsZf1su+CkSgvB5WndX0DUJkKS+wptGyAw0xT5NRwh8OmbQ8Qcl7K19lPVrS9S3WisO3sNbOl0W0Tf90GkbXzFqwUZYspMcNYxFg7nMAP6pp7s0WDTzQcJxyVFKlYtJTHFtgAAhEj2xhnwl15xPql5sHOBQ/sLimbB5ueSJaBf9QPXRL1zhWqBq/GyjtibnvU2RjHqHTqOMcAe0IsqpZAkH3gfApUiZTp4gSrwyKQ3cEWc7qtaDUL6FaVSCuV8xjixW94ii1EDxaHu6jaApakfIC8MXitqVT2yBU8VmJBSVZMJoRraEuxAmjj+eiKxRZVdCn2KzGjXUb81GhFwquEuVhfp8lXEMzML30GFfQpd9NO42GbKcNLpP/ZnEuWoKsoFXkLzS4UiI00yp7iC9JadWRDV4AUjDtmA/aayVcOh2aIQkfme5OWLy59OOOT+dzLp0v8/Nwma2V77K+4aQx0WnaQVLR5+onpYkdbulCJENt8w3X9jBJnRDKC8kQkbik992kI1EPuscKOlw+HvcnG9zLxYiUiFekDVEDKiCp8BhTkSeFNJ6sNSJ+IesJOfspo/2DCnitr06DDANYoiQfDI0Ug03XBfj4678fLzEOVuMDRTTGqXwHYw8JKbYUgaQWesjwLU7KBRynWUR7igKaLaOIGwxAM8qghVDsCxzc/RGkB+Cz9mB7oeodmMJvBVfjNLublaFJrAjCM7Y6yOseX91eOc6qdQME87bfGGa1lfDPS/ewMNvGWkF316QI+ksKWNHVAp3G1BySrQtyldKGvpk36zVB9KBAR9/k2McQxtPOhv0GjP6mAe59s8l5SGU8HSGp8AAKjNQRJl+geGGEOmwj8cfH9C2xt314Of1pOLd+g8OQvxAjA5JP8gfBaJd9m8OyhC7jaUT5qRHFp6b69F3c1CULqZdnrj1gW79Un0Vp2n7zwQvTX+9zv0Q/wnkkg2YBQW0Sw3hzbk0SsthzgJRe1RECEAIOua3Pg9H7JrPAlkVf5W04vqJW9P7YtVtleIUsNN2jvRhFK6b4JUJDNwUmtxTI63iZJ5I9lOcKRpLh2M1Gm3FpLAYMh10IESHZhopzKegg9OFZ8Ht9RA2ypIytBZ3pYQLPpr9LHadIsENSx84f7I/6jgQnhSSiB+mvRZu7eW7107AhhycLnC0mXwSUKwEbf961+t6AVtGDsm3Rx3j7vNhatNT1zq2nd71z85FeV5DdstanpVv9OnmHWzqznwj2lbIfE/Z5ptoNj8bRSdqyL4iDyyErp5GySxOyeMHPdSJfJj3vWmB3Uv6rKLjb5Nb5dkvgtWjTP75QAGgZzqPlcv8665/M9Jfn8YlHIhOfzjAj5z7n0sM8boCCRZcy3qg5V4u7U32mP5fdT+bys/l7hNjI5Cd5I6ufpHm5fTiXT7kHhoNh7sPqEsN/qfMC0aop9bdDl8lvoD9SGM2UEEyJmrxhp34cIAU5kWiSgdl13ogE4zT4Pe7TJ4cUiaaztUSaG7ag9LpsOOXQt9x7qhUlR9ZS76ZWfGxeB71Q5ezQxxT0iCaoX7d4+mz5KZIG/cMeZASrR4SNPc08NvzLt4qwwic+rZhrvnlK5y2EpPLzhm7yA/N1oE6elI2u9/jbtBF8iO8aXViD5Qp5IHuoTw6ckXti6h9rShM1tt6IVpaAHhSHqqWL9Qnvmz+dTwj1Qp2zowH5leS8waF3hB54lyDh4APoqiw/Bnq75k8Q4pzHh19/2H/9+QOfLe+Yntz0Yz4Xdtrrf8yv/amhLm/hLmWKk0B/IZvrNVmdCLiYI5KubMiRrG0VHqQsxNW0IKDeSMrwcRdk0Bbfn9vav9+WBjRnmHVcF/TBv/q3VgdMrC9DnZ9LdGQPkbKJ73G8AwLYkF+ZMC4N1CGFqTnHFo9bHzQnU0etAfK/VI9AuZMyORvA/05Lc79+sWZy2FSlNachTbVynGk1vD7s3hJsJf1lSMhRbcrIN23Ol3F3PuGkV2RQv9j1OWDPGS+rdQg3WDvgmWJrUDhUvmCJ0JsK9KoLlonmEraESwjnPDdi6rNPp/S2g+l0G3uuF06ZawsScdryKDNu+xotIKlBppRgmfsiVCEeVGgzEg1E2NMSyKGikhH7hs8lQHmdJauvm5YhM/V58kKBHMJB0Fqc5CtoFesK3Y521K2WV05s2DAKPyEgkSD0OpiPX+P01hG6UHYz4RX0btdeUj03R6yIRiDTc5DDFaUgvSsdaBAwxhFfPNjxqNrbPuteYypiLDbapnbUfNGTue6xFXdZRukePqlr+EapOo16wzgDVAwqP0Cf1AQ57BhjkGzQWtdeXNWTRyk1glN2O7p7Uy8q1CBm1lBXhMlScuqFWXx/afYbHNyDgtD8BsI/fjPl/57VNqlKko5NiWoUScmDOFg7WRt3zk6uN55Tkm2jM9vgGRGSROtH0uoxEm4COZQdKmvAn+qiuAum2JGnfnhoXTS8h5zYlOk9ZaxXx6vA8fEZSch0LIYDajGHuf5tOKGkUgM724AJgCkCfjFRWAanXOQXs5Ub4I01PEGsU4Q30NdTa30WJBhvScsvDd8IDlPh4kyNqjH+P6ehwMlMkVchxRpo/YvhRpmzKmbmglwtGUUB5fNkc2KP3ZoQGmAD141Zg5JoVmtqtlHGN8zIbstT8SVAY+s4fshmP7unrBa8aPdui++Rga6QXU0HPWaeCEl1ytIvmlzIR+q5wdzb6rIIfRuP1+oG5REBHJbZ155bjYATNlPDw33hSv0d0zXbaOsSvjxFtS97KiiLZlCDnMJ7TSyLy8h17IMOdIuyqbXpJmQECC6a8sC7zeqcQ3KpUZx+PQj/4GqCF+jPQ00VOhhGm+s67ZgEXzNO8DDHTI04Aa1yBZsrG8zHGOZunurIDgPih+0ROUFduiupwbUPmzlJDrPnzs99vPMBRa+g1quSJ4xzU4IRmXmVnf/uRYWz7okKAMSvejmn3bEsfM7vygLVkotZXZYmtnkj9u1VM1Jf7PgXC2j+yB35VnJDPCSc7EI7Ru/gSgHUgps4KIEztcLV8FTOcokuPrHludGVgCrPeDtHq5Fg46IFu+R9kKtoI7gySnRI2AwURh0s2m2E6U4sJ4dwRSgqfgNsS88kVXZC6yiPYrcR594+G2pSMTMsSmFxo8Pq/k23+7ghSPBWSjT2zZf47ew86pmkRNHR7IvJDGjOhuvA4ogVMCWad64wTZcfZ1i0lbbXeXJ4NZ0jmmA1h20U5TEXpc9t54Dbm+6rOtHSH+4XLfEkiebQrSFCj6QujWqdbEKGoHm7gfG5ujTmjdwLA6ffmlbaHOHoqJSJsB62hLQsEflO5Q7JAYRL9cDvcgg4N2iW6rWQ/5TxCAgMCO7dIYc+jXhQPIFyFTfrMyVPWx4fu/LA59MO6ePBsz+uXnKr2WPhnT5/Bu0UlE3WGPkyqWJewRWXwVC82nTZn6+J4yDBfLIjyW2TuLc+/nKjZr5cULqIIx5Scfq3u8KC4Q8ByhHdqe3OOpP3/EdfiDILnWckCbqwYERJHMmR9+xHHvgLPAwHCdr/M01tBwIcI99dMG0mYmIydeCUTYttcNtjlVElL3dCFYo8sgWqsgk3BR4AeiwAuyEojpEpVkYHkq8BlZmYq2aORwxE5cJ+AMJCQMAwDiyf0gWggFl5MDqCrIxCnqqtjdj0fPpZwfJZsZCpAWqVXYBbNdFAYYcDNRzo99b7qbeG8dVVW6yFJ640wTZsui+RqUgNud7m427eDn13U12Z7V8+IXLsrBjBCe7QIXudP4qGJy4FUt3Dy02xbceVJc4i+YcmLiBi1Nm2e18K5QYW2B6xTiLWJjnBZ7EjQzMoD6uoNV1jqk2M3J2FTGnJL2OFSOYm6zE0WJmW4G1D3GN0TzJzU6PRaacV6mHupMWKrrUm8KURBrJUNWVRTwvUoAwwjjpsRkMTycF5XmdK7YpILCaKBeskeTV087O0sYaLqCEcQrEAPLCCKj8KIeDVlAo2ppo64r5h0LAbHaAHFmCXiwboYJNcbFsbE83Zb+QxJZ6nbdMtz3r6AEIVzlnIdJu9gmzKRTF9kDqjB6Kom8iJH8WU2x/srnELOuGYkajBe+g+h+A3xkMAC7IUHgU5k9DIrwsCRmTVB42WYy4kEmC42eRmrBkVOdFITF3OIsqtdQR4I8uTdHK9lxv2mmmQZYHzLNtprVnpaEbffI4wDLo1RTd7kMAjtJor0FrOH1dSvrDpmT8MilbFzL24hHXS0NAktooCBrnQdszj5yrMrdoZc0hBnTZ7NcGEoj0wC8kXxC6EdzFNXUvoHY5W8/1rVYv7ZsIbRnz0m3q66r8eQDq5avfdf0LHKX5UOhqJ+6cR9HLfYT95vG1k3JAxJ+b3ZAxYHj1gAVNLGBxJz2MGKvg/q1icU46gYl3g+PidXDIoOEq3DyKJ6rFLcu9xr7X5kK6YaHqEtmeUen0BHmLkxduSa6fGmeZlldqapb2AwncladG0vhZJe5rnbflDRyFknU3K0JqAcxWqD745Y1vCrjRZeKPbJgQTdz/NEsXYgOWwx8/Eo2rpEGtS46KTS5xEbKg/BMdWq1ENOuMOO8G1EfxkEm2bFnTU2uGfJ0XaR7KCTKH4gbUcHRbc+psmdNwTV/MzGONYeGRz6EqFuVyaYmw8Z8nM0j8+0rd51WqcQ5tRgiqD5c8LfHV6QKvV6tb2wqvCoFWNK2qCQwm0iwUtOWoyo/WaoMhzT3nAKY9N4ZPJSVEDAnEENVkiKGBOTjYHwwZ9SKQMwynCRdghLFzBuCnBLyqotPQCHucxVkl0CV+MGcuzb+g5HKqHCQ3scOgai6Uds1yD1n0CAmP2DPN3UqlQAyVvkBfZvMBx1JgxEks1NUeJCXmD78gEto3dw6k83IlS+OwknKarjqkDr7N+90JiVGqnB8lYdIdYivT/CaCElo9hRO4oXAPzQL0K2Mc85AnSKXx3tYUFfU03wIr2MjgncKwfM97ZpmlKYihLmh0vKFqK4m1V3kccThxzLcs+1HA9m6EYbx1gMdA+nNZxE+MHKX024MK5/tdMvHCKNBL1G49qK5wuuhr7LNJeIo7mz5dAFZdH10JV+QoXTeoHKumigyo0xqYuzgjkhtseSHUFxSB2PKtv2JxEdNpm4+BpIE27Brqx+rlagxh8QHPkP83b/caE4xAANaRBNzRH30tvgIay19rz4gUERk4N8KPJ+IBCz9jUiVvbO+S3yIoxFenQJKRds/EAiHrmGxHGj51+YrmHShO3Aoc+/zzjpegu2CqMAdvtKRd6agU9nLbnX+fWmf+iLQQgROZOcz1esCMfULoC6E/O0sThQUPDePASsth0J/XAHNmxMcEIN03vyBi9R6hh0yju4cb1AyaAezd7/GlQ2kb1o6aeXL+sVyWIJ4CIY5/CQBZCEYHUobmMG8fw7d4MUqTkB/pr5iMEh48P1gyIp4u2wO0DR/2+OXkIPDvtla54LnxiM/GmwkQSfaCZLtmCVPnl4aAvcQlE9v8dzWPZjEt32tUejfvwou71/0NwoWfd232ZQTfJVzk8mKhCUQhbfEuDLjEMuM4Qoc8JjpEyPr5K0MUQyc3cwxi2GayndjyZcFNiQoVqY6M2DToDDY2VW/yQ78S9Iig2HMDeoHieXRYSCDv5+Y5SphPpjYSGFxTFel5OgEMuw0v9oQcEAo93R30MM06+Tn6SSK1fAgqzEUVvP6h9CsoMfleJwY/7BWRKQnfIFLqCmuax2wxyw149G43MnjrlsVNWhws4ZCmYXXAzU9vtaQ3AFoonN9mvT4NXkGSJmUToHTKG0saZsjlaD5xGwTUMA73cU5Lz1zlCUK5NRml/s5Gqk0fS+ZUvt/C4XV+3U9c3coYnZmvy3KwKVtgNAr2+NIcslj4nHNqev9FwwieEAfP/tuSU8BFMHAyojECapjvz5DmKjcPekGy0RHtxrxZL0TZx1mF076ujF/4KJCfqibmPWZYkooOUnhomde5P4qHoVFKL2SKNcGns+iBQ8KQ8Oo73BEkRGvHDlUCw1YT6LB6Px0wRkl1TgIrYCNmgU6/W5mxX4x+8NbBVNpFzla41W3fxddaCvVZgIJymdNns5Z4dgbKgYCRQaxEICrJUHsuhwpFIJ/wh1o/1KrAe7p3WUivLMi6eXXCghxmHBVD7V7VXIHzOBH9YZGRLjA2eQRskHBWW96PJaSQa4HYLSKl0rmVsW2HErKyPwv6wMoJaAvEAakLD4ym2FvWdCvWkS/l7cKMgCkFJSAdZjpmqy4/P0DU3Ozml79FMk9HxqRgPAtN3sk9/RQveWvYzFc3yTQK14dtbMLPFN3XNf34r1GjHhyfDwE3efF27JmwPOpDp7I5+JCUR1OV9U7raMAaHGpUAYQ2aG4YMkc20gkUN3vwzmQ1l7hHnVZHJvjrfSFHvveOrnHUrOY2uVFVlCQX9hN+hQCerkxuzArDolfef2s5OoKgXd+02VHacOlEGRHCpUuYvSGs4wWk6QMfuMuVJXOWhj4lBH7Gh3o6T44mYuNpIZaSrX2lTv++GDXWTXDuT5+LBAzs7TKhhkurBKjWGRFTmB+MsoWMusgzswCsoHHpkEIpbwpGRtEa2YW8IopCt4WqhyPqhCgEZcBvMBfkvala7wHOF6thYoUPwiBB+5O5RHc5QKfFGNuduYg+hg1VCsmQJ4pb20mY2PzV/Zq/Ic70JwZjb0fSPwQdJw34jKt7IX+Ejyh8xYFPX5xIGVURWG1GMVxtqig4xPinIXy6P8pN2w5jUtYe6kMOKGoPW241H6ERMh80AvccHY5EsLWd3CBuHQ2cX1ZoF6q6K1atHsZx+8HFl+iikPnDDfaboB+9tGI7TAlMW5RmupA0036Odkl6J5gwxQN05NtrjGUkMJOV6ylwmIROxI6AETGQYvM84lYybGl19sxCbuT60Gop2D244+dezsnnfLY4Mz02H2AgNJIPDrKInunY6BQMLkf4F8QxXVyn0miNgxm8h3eShJuzeOCIDQQRAhlWbjEHFmCzwKxCUIG/UtufJoqYQrnlPxpxinIe+0E/DUzszTBg4vYMp8IVCDdMIa+KGbSFsTzUyg7kXQrqedveF2r8vZoaVRLblagTTuWIjPIrd5/lrTwtj1CgcMFxB1IS0o9hO/RtEKkrY2ke+gn24BbNR+k7nmfcjj936gII9wn8Qfq4En+CXfPA/jriONvnLrebk54M+H0QMMFMIdEnFihESfpU3JogA6RwiOplImLOjHf8w3tBUyHWkd3l7afrZleyLTd7i959cqALUwn8qm509POXyDjfrS5y9MlL6q9lyXxiyU9IbTSk+v9wvFCCUwn/whxYL5qZl9bkFIv2tGDQFxguJ7xFh9IoQmw8DtbHNaI/Wuc2pdnwCzHPwUQK3Z7Fii/Qh+WhWoN2Iho/kx2UjE1dJQn8cwlK6iu8snRfZI6lAfKhTsbwt9m0ZDWY5HnaF3xl1LHV2Wy3qFRMGFBvJrAt6a9v3UMEHt3io4SyYOh5XzhySeQqbTjFKunvmqvMDEa9Cj/PiCcudMHyWacwTHK0Pw4zVNLWm6JrDzUNr82u+uYqP09gVy6yZp3jt2GDnN6AKIwl3OQx7V4ZcH7qTaBbHqm7T3nN9nn8C1Jba+KWwkh8vaHnAo/rPZC0pt08Yj7LWTjQySMkHcnjGu+3Id6kuDtfHsFkkdddkW7IQKksH40dx0iU0YikCGxW3q23kBqwVjWZ6B83d9JZaLFdwE0seIivZXW/iDoB46qpJvpBA+BMxEuFjDAFBI18SPU0v+h0meGc6S202XGPb6TDsOO71XBjKbp5kc11UiT4cqsYMP8Uc0CMCzOdchXCjgp6LsFbp3rGF96wGY2u/5kmnmi+OJj9NYD4Th+UVRDDqo3vIQ8pr5q33wymXqdjn6mlE7IYQLmgTFLLOZRHOoHgCB8VTjHKZvbW+6TzO0Ghm650UWsRp6ZO9vH8Qv2TN4M8BXs8La4dhEzD2AC1cnvJrr/5VX/p9f4MJA7KRkj37Xxf2n1C0Ii7m0fbQqXfbrbPiRpIvQdjoBhFPQO74vSro17t+4O3wLY1Wr6qkwKrrSrMbCruvdXEExYfcNrGdV7xMuvyU9dm7hkEcKoK63kgbW/BkLaxY171NSniykS87h3BMmzYBJpA7Sw1zT2rbLrz3P0gYMj2svEGtzYdhY4E+sVDcd7TxTXcuRixkwEZXTjnOavrojX2hDVpQXJdkoRul9UWpA+pwjAxfi2sve9uEcWBIyelgNTEAJ1P6UJ+i+3VEcV0FY6xnRe3rWy20GNKyNqMtkn+LMBl8Q1Ukh6cGvSqTHS1VoZg8J3NP4gZDmiYJ1zhEzmNOjQ4ceH9xn+do0Ehi87LlorUcFSyNgeNxhsHGy4qgMiFEJG5b5qhBOBXDxkRhsmD6/bqlvo4dUYJeDSXF0E9kJASXDcVFJRhXrcmjKKMGzc59lSsdJgFq0nOX9ZT+OyX7fiDuXfBHNaYxL7HBH631stT896VBnqUgOioSP3xFElnXcsMT59lPLOGDzBF7kVKduZdQYGwo+kaz3uQLiqjHQqr46dKPFYIfh/MbKu6riOMHyuc/x+CPjHG+x6SNZGf2NWlUkSfyhhG+78p+XZ8RFV5+7RYRws4SnIBCTcMTyUJ+ZVOJUPHdSlRi5glt7bSSOw86sSJM8mHUtK84uTMW/nM9wvejWF7CQkIvkx7v1rMt9r5MaN5tf07niFXn03j99s0sxqNuO6Qraqhx+oskOqo3989T0m9AekTudoMHX3Ib9RZ+X/SrEB5jbdERmKY/Ph2wXLxHCZ4PsdgKPUB/ZznZrN1fLuHqyvcK1Tu+SpnfsHfzMd/I/Bdl2zaUJ3MLuIDiLp0gRe/5Eb90JSwQYKzm6cWfqC+UQMsV4tVOokbsHvxUslpj0uM6008v96oYJ6aMj+0+QSDgvQOJhSnQvdNM9TN5g/JNuveg0mTpTGJthYzcrbAxa/SP61dJEpuGgLl+VHHj6p8C596hSHEr2uqd662op/pVJcCujCujLFUfYMiz40GE4n1m3OJcefe2NWZijUQvB0SI+1njX1dzO61Yq0wX/yRsSOO2aFMsAzI6/4r6G5MCPk+bk7sb+MTAy8oviPKfJwEMmuGJA5JASVfKnm2RdLYFnD4pdVcY6aok9/7XFf7D/l0KVClF1DSl7nw+pkARBadTJhw52TGYwXfqu0cxZ0sSwWSXiw6ddqL0M+aO5mMwVEYXsDGeLa0cFF50d8gIZED00wDEsyXF+e19vr4fn/o6cCv8DMjZcZc1oYm2f3ruJjEcBXGda2xps4eL8/H9s9jbbSbS8v7r0BvVpXgaLyK0C1BEmNXunOhE492u6rOrVMEjlOgqF6SaVDrG9I5XAdM5yStxwTX6uusN8RagZKpgJth7SbfQx7D8IzLJtZAJUEFBeYTLkTls9q5FNrCIjACczlQoboxJfLFgcxPdUOdvlUAD9TXumfTbc0uowOjv3c6h/JP07cO04NCdKW7bPN3sefbuM9Dm4ETiTm++3+3cLQXTxjtScyw0DlcECU1Ko2pnWFhiTUpFSP2DK0p8iibM9tG8skOue+2xzAxpD442FsUA/rAprybFt/1y/d/m42ft4sO2+TA4hxxlfg2ZS1nIxTxYztFEY6oR9RBW8bYBHe7wMGIhPquZ/RhDnZ4gBZArAN3zl9bVi/vvq3vxorS+5Zq0G1dGfmhaGt47B+qB/ouk2U8FhwTqyvfZNiOS9fMNy6wlmnYABqVoYFcQ35Pu/BI9lKDnHJ+3ndM3/7vsn6InrFabCo1rD+qmalEnVOxE7KSju6lJy1ljNIj882wn2zokEin2++zPiVjzgL3ue6XbQeQAIbdfcJYbwTelYkMkfIqNFRWtr8XhaBA5bBYulkIK/xW8ftLhMBatywV7nvanyQOmocjeT5UFIND5VkN+P/dLz4TqXmmgzNBtz5MaPuKuAGYNIW/wc5QcetOAn2DjdLvdi87gdjUOibUB6x9wVA5nGC+uaX4P/MCo0p2ihs7bV5bhGK1yyas+RP04GssqrG0mGQVy15FUTX3tRnCCPQdwjOr7jI7IBxq2BoGhsO5niHlCPrJCIIR/twF8oa8gYV4Y6BmCwgFrTCdJDicKxYZhn4+WHl5Ff4JvBzZ6lIvweek+u6cwopm+e4vMQVUf1SLl062JxNgppiL0ChzPIzhHrVnjKWzB+TNg6NmG3DqDZgNsAlAAbA3SFcP2YAXYaIKKPGaXNL9O9btC8o5W+BczuPKTeCoFS3Ujdw5s/nAk039PYIkSJwe3yWtzpI7EvqaIc97z57r12feGwPBc53t9dYKH1SEw5KImNi7x3u97iwoDv7jZ6IaEwlriwuk2pHCMKg27jEil52CoSW2FNAJ3yC00dd1DgtulczkNJszW3deDhhLmKBhShMOGcFDgmWANBBp0hBZeoNodoU1K+MIClKKB2M5sqhlOQgpDY+ZSStDRxGgkwagD9rBaoO0qLcLVPzUo32PO6TH5Y2sBX18eWJP4vKtoABg3HYDKxh6CZpuEDoLywBESAT+kuVLCFfTCijnz4H1bz3e+vn/u1xbkKa7PGDC04eKXqCWoIHrpBSOQdOwEax/03k1G/XFJueghbmTEX5SIw0huvhmv/f3tu5vY1Re9mQ4rnh/nn9Pnc3luW6ELPy3mh/6kce1tDm1PVuXbSY5U72dr1OhdgiZKlb4sxbqag6PVbZdBlLSmBJZAeUe1RBsQ8c5hweGQoX5UTUPPmIpRC86zt1yjpU4vs9NtFesM1MGworxY1dN3URucnstr3GhHkIdNrlxOKmWBZxm+NtXwKV9u//7h57RowlefGlxVUq3HESvYKLZTJx/dFNWsMysrZ0Iya8kRRo5RUncdDE4ysTKPO1yqrlxMWOpx30sqATwSAth9HyGpEsTXzUpd+bGbaYUloaMOrKahlcUJJeSF/1kxdMEy5kKO+2xrecx9JYsX+KqRLmxzw2NViZfqrtGpdUc4d/RbEi/rIb3ovEqGqrRKDf3KnsgS2YMfIiJUJhzC7epDrfKsP3cVmA7yBJc3mBxl5cL3mmK/t8MSisYw/tbjti8yAr+xkLUqqWXG1TnXJ5VtFj1t+Xcizavd6+IRUGKIekVJxPsCSjw8FULecTgEEtRLojULS6aLgSMEoge7qy/4YU/cSyE0DZruMrOoBIjdjgDYAQSNozpUe6rbR5uupflLTnB9lUMz/fcj5JeckIi8UsL9VzkuNTXnWZvAqyZqFMzLIfjmtMTM1k2U6MIQOqSDLivQhA6HB7xtulbl6O7PUs5dnpTReMzCP+r3ItQ7C5j9Vv/5v7dBY/19ofi/1tvL8kF+AfSv5z04RT+BgdXSFLaxh9g5qyznbPfpNc8G6las9tPZdWp19dv8bRWP2PphEcnva3JYBM7IXTnPSh08ZmWhkLn+lMb22e/9rUwV5tbiEc2kqWy8BKP1tZ6z4eG2jB4mExr7BB/U6gdln01P1ey1w0E8wQufWgxYjx4sjhMlFi4+NBvp14B4WAnMAydXSu19zvEwhxfpLYfHKkBIp2D4Hey7MzvLxXFvDc+MpFfmbBCshIWOEfcQt/amCojATg+zd51i2YapWAJrgPEJb9/Qee10k8S76HRlWzOifw5nIoNPT23csM1mlu/aKfffYkvnwMhM+XqHSljh1ezE0Gh006QxVlH0s5cUL+k7RjdV/rnZ7VpWnTRldfK0X4bowBEp9SYscFe+HdVr8frXIONvnQ6ZOo6WNumtQjfRsZh3LQNTr2TP1NZpR5UC1Ay9hh4fnDjy8bmPbmqr2jSmpNRONgIkDByyi98gCswJfU1nK1yPDYHklOrGEnIQMKkJWUk2dV//naf3lZr3/ALGUfP0ncbG+JgCJzbqjLF6nAiSLrNlf9pmWa2raZOyWW2njevG/WV53bQ2rg79Wc1w6mMw1FK3JEZ62ZzdfjQJt4CBEN/K+eq40bhuLobHkPRLvANhdeFGWfq0wnZ/TsI1MWXqP2LOX0Nh1QnqG/tNhmm7lg0nGGo3C/O4EKD5gIk2G8hExTY569Vy9VbV7oY0rJ/vi7zceVbyyKR3sbKWyB48+CcyRxJvYKvtTqUTLckmMedAyLCGjYZNsAEs+IsH4BddI3wQ2pnILQDCM7C/+yNwMFLvoYDKckyMH5ZuPvr6MPiLRa3f9wwHF2zhVzvcMlClna98l3xhqU7Nzt7XESnQ01W2qP5ZCCtsuvYT1H6JXZSTK9n3g1gF788cxFu1TPPE1mNirwIf7ZLs/sMy3taTURajiYTGTk7dPF1gjoZ6C3GXYhje0ImzdD1jK4iwqVl2Xbc0ezs4jFuctVyprWbojnkx32uOEmF3P6pinjFyhmZNPdt0la+57KBQZy2D5w9BWVD+Q3qJw7M8WSlbk5MPy7lp7HnFW8nZ7KmUjWl72hsuwi2W0ZJDIz5EYplg+yOKLtujPfD36g9WuzNg/KrRttgkx6AV09F1X/G3feVwgSDzfkDWJPR4+mmOLVqlO/pxTtMMbQXWE1P4ulWSCs2pxZ6xEH9jku6fTtbdSMgHWeTIGYM1yG+ofze5zR5Ze2Vd3/KBpZ9f/xsWbrdjM774mGgSozAjvi3k0+F1R3ght2ly6rQQT7l0GlQE4jtxv6UnY64D54JWJf4qGzYRc6d13GWZp5NDBMK/g41PblxNsvkaeCFnDtwl9yJ1VEnnoHTua2xuiT6K+l0dDjXbCoFfwrKu0FTL+7y5kcZNFCU06SJ+qNSoNOwvn9Oime/u8vly3WfHdZ8cd999/0ps9v4Njn8M1DhC3KWa9yqdL/PNb2T3SagpfEjYJieeoXln98nx6WbzysjDNYZ4l7tOyl1n3V1HHJeMlanZNov2tcDVhr2sVNeqQMP+cJVh52HjrXRwQiOziqLaEtn0ZrE1bIxjas2gJqSgOUUtaU3Vx4nPTxM3Dca5MxAJX4B3lz6+d5vavTevPN1eJm4OXxbmATR+Je18YbfsOcL2mryg0adQG838Ykg8+qauIDWNGMZkcv60cb/GQToDqGc6fbHUccBGwb3vL3uMIej+N3vamfoxI/KZVNyNqspV8aCw03S07unfsRcLLd7vNd4AdQh/5bZnrEMhQ3tCk372Q6B2EQ8CYD2aG6KXDpNDgLS9VFE38FI9CJvu23Y2hjSvn8+yclZ/wbw8kffT4CHwJudD5RFZje/t8yEVI9u8lKQEw0mx0IG1wfzH0fXjv9DC6/80wa6+9B/syPaSUP2wHaF7FLlq08TkOqKP/v/PsOFuYMLAx3WYymO77//JH3GCpcSSyerDHzO2P+4vwajb5oJlAXSjMWkgsNE4f0ZF82rQBDU5FDyPeF5BImVSMywh6os3L5qW9ZL2XF8NL7ftekGXsDJIwwxDFpbHnvEbVC97HuihOlEb2PEz5vZwryRIbOe70S7Twadub/B8usp91atfV8JXYArlU88maxOZqE24AEMnIB+704L9JkvPM4m7fEpvOTKeM+0aPaiRLYocRA/lZ+FOvl/XrHi7ul3pMjveKv9eroMi4H9ID57y3gFt9dkqX8qrU2ZdjNB8sb2KLxUEUTX6CeKHLHRWIjaIqZJa/uFJBZOcwYkFzwqJpzVOadGjh6c6A63Ikx0t0ajHMdxsVYfxge+atOwbPGzwIHNdz7HHAI2N+szTPMXeZ4UPRg6OJFfCQaswRmS+rDLVncrjPFIsR4Bee5NLkvQZHg7GxptkhtG4bnHElxd0ROekUk0YlQs5BRspwwwEbuEs7UWzyKhhibzkmhzn3QZWXn6WG4JWS6906hEBMCiEndQQuhu/SPI0yiUmCncwKcSjeP3jEiMF75j4uAaVGCsaZD5FDtlN8C/3lA1bqUT3yRkN5ap8SuuhD0q4rhkDA8FGoFRT60hoj7ueSLuxeQsdSz7i8/SK2fRFC9kPETwYtQk5o+q7s4VRkajXYAhLV5fwTgnjbw6aWyypjbPG8L9mgP3qwXRcO9mGqdPfd6XROzhZrJ3luy0G55V/pOgj60xUiKh7VE1MOnCGjgF6TSu8XFuCg+v55cRjFHLrnpgJ7m3dYAaxWQ/zOS1vKjmDrvPwZC/r7cD+fiKsDpBFcG6YJhA4jWJbVRnWCHKrOfOoR9eFWcWmEJLEBVAmpMDo0TYwATbQtR5WsVky44r7c9gPesDYCkJ9sgNvHpvN4DTewb5Uai/zZyFyJobxNyUELXl6zJotlu5jq7+4oXlRQyufnyGVGaKWp8yk9sSP8n0JJyjNnjBJ0blbf9iFl+2q717JafLg3XAqwsqUv0XP/K5ScWp5Kwv7QhY1TrwEoi3v9kaL9iIplnNhUpRgGYmthVtjbj40OG0aOzA+Gv16uhzhaM7XjC0rlJLRBFEoXTgHJNF3E9mVW6xZklLuImxjtBazNMlZuXQWXF05iF1DXM2v6k/fn5NNSDavd3qwn/L2qM/p6hZfLGMjLC+otCcwqoS8haPrc8xLtfidpV0GM0mmYvkSUFrKoGPvLoRz75HRR+WzBk5bZ+8rgi+TEebdT8vc1bzu1qoQ9C4SENpmnnXupKy3i2Vyv5nKzK3kgVxnZmGR0+H7uEhAdKkpF6HAqnyj95NV8HegF+kqGWnhq6I7IbU1vbyLOwigR2aJkvMvyw6Z8HJCtInPMqeeLFEc7LrTQRrULLjVS0Wb+ymgOfNQL+aGC1Gvo5prQ88gWW4lkvM/rQ11SQP5ekV5sHHZ1yGvaaWHjptQAf1R7qT3SpqyqUpXQ+OljVMWwKSeK15IWtQiVAmDgpBrtuyMcq10MciF4OsxR6IIv2bHYt24s9FfBGUU/JrgEG4OxeBpPtkigAH5SAn0CcZ65ztSESwKWYLRmS/yvOFKlE7hkidMyt3aE1BfdldvHAlT3jXD2Ts2f/KsKE3myfUqPS40LdkEKc9pAqhuN93zN/2ptpn47TPXbTFYdsLtshVGEAKm9fDzdkV7JFGEbId6F41Vt5W358bQDXN85KLvSGIxZa9PE0FbCuK42KQhF7ZpM/h9EHisMZyeMrg+oUwSrPZ1KHgwC2DuQ3Uj1PsAzNZFnnoyPpRRX+t9eQB2VgMKpCi1wMuJZSEpob2O7I6WfWKFoeGRzyM6DEoKuV5pk9yS6hQ3ALd0aZo2bU94cC9JJHOotZwygR35ymHgTh8IGuAwVvOcZ4SZE3RFMupCfgNIlssrEq8uQdtr447/XoijUTZDIf9RV66ietYgPtV+GeeJUMLZA2AL8Kw8zX4ISc/tWPEmbpIncTRYwowiyTX6b/Iw5EJe/ZIi0eH9psUzMi0mU9hvnnuvrInTv7TYApnkCQdYvzrps9FpptBTCuvgq8AypfjfDQTq+QEYcoSO9ygRkQBjTGLbNd9j+u01cXc8CsxrA7cJrCFexm8Gbx8H4/hdaqa5y2wLSpd6klmJUo9tLNNLqDOlwZtJZuPNOzFpG2iXOIguWQRWqHAvhG5AfUcN5kjq1ATtgFpwuYzqKdjV+qzVdm2shG3BJjwCCKad8AFfz2zQwjLh+VewVq5USc6MVM6IQE96pLSaPjp6ywGJE9/WIQQe+Sa8OYtSOXsimuGogoHAL5m8z8N+5ceaXRPOSag2ozSpZZ8Q7C1ZohPF7LV6ZsBv0xR35Ij9VM8YGLn9aAZGDy3FkVcH5rt9sVQkjGhGLA60u/Zm9GCDitJN3Fw95C0dR+tEkr1IXHpWYJYuBHldISaAIjCHSOpcOwkM13/aMgrTQuu710g+/z0IdKB9roK6kEcBqNxKAejuOBHK4o/i46v75I+286HALzT8B1YbobPjyH3IftoMv5NBf+X2OTtbcy109QdxEUn+QC4rvmqS1miQHiPuB+CJG7CGk3RKm+TRkwzCZpyzxTxKeQV4cFrkZnIZ6299bkJw0JmM7+bOy3UarhchQcuLh86hv0I8llN7v5V/rIDlK6dXvQgpgoEVmMlGCSKA/BrqyBJzbMrcfCc8bW7Q7DMVmFmJOuEbO4tqjsWkNnCwl9bB/b/jQ+m36BWVYMvCW0ZY23aXg49nJpEFxBdQGB2x3kfusK/b8pZeutoZHlJD17/cjdbtpewQ13lXF+r8w6vCk5Ufesuvxdd7Y39AnwbX/x82CZ62PBxXDuvIeHkpGKXxeTX8kHB7P+XCKIHXA8PmFikziEayllbaGbusHuW7Uj+SUTJzGbg73Y1rOPIFBHXN3TKR3vMpSe/f4qByOUiHiu2v3eX2K6HDw6CubIjTWcegd4jrnB0JBmFs5DsuhaECGGZuPmzehBN/k9aWEumRri9OTq4eKdMblHfMHl+zYm/rnfsaMfrCn6cPQaww6pRZqaczh0nlc47knl275CSZqa4AC36tfNi5UgfElE71j9zBe8nSNxc/30BjHOno2TBy2yLRBTGU67MbgrakI9Nc6UzZiulkcUplQ+r2u0UorF/+uFYcg1JqQQxL+SCZolVnEo7OXLA3KJl4IpJiX1Up01UMergU33jZ+AFDD6B4rA6ks7zKN9rypZrur/Irg9su4HU0X1350bx78XZPSDdj7yOEdmBc4P3RDIQdCXuyzIMGdO0U6JCh4x29+M3qqyk9WiacrAYMbJL5hLZ2btP7t24bkgwEa90QYJYl4vJUWudD0DaH5Wte5gn2ZqDctkApy7rlu3KE/ubBTIZXAZeFEn8koEeByEv3LvtlJvYlnloP6/rQABjtRBaN8TIwjOnLuAdS9C86mlynYodmcMGS/5GJkURj/Pph8+jnexT4/cl6UhovvUXGj/3x3ACa3iuRzt5ymw88MI9u9l+vlJhVFfTpRypm3XmJhUjcvgOzzXga1tbwVXdqxJXUYpbFAC1PG9/6z7Y6RwyAPE0k/M4AMfsEbQMK3X7tf+ZQkIOq8rzqaIqKOQyPDM9TczvNiYfiXWrUNvpG5Mcf6c322pP23FF44WZXSVU2jAp0Kbck2qiQH9wqybuUFC1jxf+he2bfl2AL2qrVvsJJHdToDC6Saai+zP2RQ/Py1u1Oy83eJyioYh0Y6LiA09BZwuh3j29iNcA1xFohYJw1hHWedq3ZNT/8bNX6vHPOiIB2F/gSvVQETPnezLFtP0eRxtbx9Jrp+8cFyEEEu57oPrd3kyXPHhngcS9IUKgXbcWnl54murpkuJkaKZTPFRh1Sz58UpQ+AraYHYveL8W9926eUre+/Jy31WcGZ7WE7bpn4bM9B3ZoV2KeEHt9UVWuvCz3pL2VTw+GXej1LrhCL/oTePGZnIFK7x940pZJCiSkdnZrO0FOHUAmmMUWiySUSwEbgTcKiOwnH4ybfmHxa8iOBK6qa3IdSUQPxvkwnkw3nkeRlC9Kbf0SzIHqKnT2xiLfRT6L/M69BnZ2VwVm4lutq4ZP1dK4tzm/APPXoJwdtuz1OrCwVyJlV7gRb/NmpPxHoljNMbVjqCzjvHqDGS/Cn0LUA2dVOzaKkhL5N9B33eyEtqV8vM/4K2vO5PkomHVGiZyAJLVWtf7gyR0lTEUb7XYqrE4R+iohZP8aAdsyIcoji3KoyTCrSMC9J38COmsg6bSw6SET83/Dnnui0yzuCN/B3WYDqWlSKrizhX4D+Qr6eBACSDC+9jeSJH8lu9FS7Whenx77XO37/bhG7NSh7YWCF6GL/bY+PtZfgS/4g00JFECSKtxyHfz5rScHByxCbMGhHbgjKFEkb4WiBZiMMb1ag6xCh1ROtI+FXfEdQ5QwhFlJ7YXchQ5e255+AdCdX6Z4mdo634tKYctuwPxfYKOIOkw1VYLviiDEd1Nxq9FXaycVTUNQYqcVLaDBVLtLJ11iEoHBjXQgniJWbaW2EmQOmuAGz+Lh9GJtkvLuySutYvgTfF8n21KxGXBZ+Z8w2dF57U7lwYnO2y8p0/ubz7N5t8i40+pLkTcuCmJsYDttm7ApH9aiuuROXWhAKEFaHmnYsm6wFfF+OAorWWogQUjPNd4NUupFDlNU0RvBKs65sjRWdc/NlEOMUErJG0k7OPmQGeUteXSEpbvtaZSshPSyYtqBR/O1Z40+sGzYy7fp4mz85gsNwPtxKNvCJCL4jOogD4IsV/v18MHLr1LdDVtb3tCxlbO+Hvj/XMS2ZZgb6/2rSjWmKK7KjuhmLySis39bXRxOIO7FIJr3rw1o51yKnyxNa6s0vFYcWLq/tnF2toHZKstvjStZm+mSBqr79E8oVy8p/fl1dr/0I0qNS2Qxl4Wrsz3kn7sG3efTqMgrH1k/eR9xPbxRdsq+ttToqjv6AId9ayUT11pZG/mffaIKRE1PqKKcOqrwKgBCkhdIRQDMFXgj3ixthUHDCTXX79vF6kS01o/xX50X2LJ8eCbjv6ft0WtmOBo6t/GUfAMSwddSOKKVGLfDlTXT/alpOvQlE+xb1q2shensnKoQtMADeKnsRC3P8tT5+VP11VNnTgzE2u4e7Xjzm8nQA4YJUxT1KLoVN/fs1YE0WBKiRsvkc2GvX+eEfIshEaf1bylEEAZ4yUPrZeyP/jH7DYWPOlZGSXWPyNqPOFzzGDu49/iTh/gztKdyMpT3d+FQdksd4pllymZD7m0+46Um8u1ePZOUoNW3lkM8zlEXoQaZZb96PASHyMWPwptfZmIvxtDdVn/q8Yb4P1AXwgnOSHK1nIL/uWxoszqpHldYxl/At51yvwz19qrLWb0A+k7Xsf43C+HLdXwdP0hG9t90iwrAjHwIBOn/NQU4fENesBfkPm23D0ZSi1JgVzRVJS4IXU7CbmNMZ76Opjw9rLVmr9NUzdUueLfbujg2D2rUhy0m0n+JZcizEQl4vzz913oXZptfV1d/d+7f09dvQ54E/ru24Ybcd4S8EDy6mH5g4KHrlznZkOjysITr1gTzqFUx39mii55B7SGbZSQcek3vFITgdLxxeZy05Gf7y+66nXbWnkvo10o9V0475v8ExadLC5kCqaFXZh3zVKL/c3DIYetBJt96o/jJfUkWdQ/379oFCTaVXYpUtpQm7NowwF7rO/3dirzUQAby46V3grC8OrMendmblCSTHvBEVz+mmIfcnpQz/xs34tJc7r9kQ0FVf8aUKntaOn1mHEtjkab1gMx4PVHz3csHDQ5vI/wvAWDMRDNJmvJFy4zaN71mGAj+8fuTFKLVltZgL8cUpBu8+xbHdjFpqt5X+n4ty2BOCKpwacTxn2dUj0P7r9zgF9aNlZEUIRHlEAsVh/wogxFLPxSla6PExbWHv5n2e+Gp0htwXjpX3Fl4vK4bSs51VRMP4WBjnbC/5Ph7KvIjaYnMGpGrWXHhnOvFOCkQ+/ey48IGQOQwaTPiaSFzfr3HGNtLn6xy/pf9tz3eRXThk1hDvHjYLtqhqBmxgui7xxHpziaid+gU1pDrX1FPzgtlnoR7sf5Q1WsZ3IcLQoDnpdO6BoEkviSRv7pw5g0Z6plfdcD5KIu5FRntfZkI735ZO0OmV4d5Q6wPiPjUU/KEFJ9HLgZDIAsndIy8f10DXkqK5ybamcsZcgsOvtWwcp1xc2giSWHCeKmhSLfTG0fvjC5vPOr/h6b3ArV/CaopOIpRIMFGIQnkisJxDEv9ljki8LYuM8AcMQDCLEDb3uRGtJxVBQBQIAWAJACpwuMAJbvyOG+Had5sBoZgjyfVsjRxyeGgN062jlfVALOanXTJjv3DYvC0xjnykTpQh7+kyAx+EkoFZSWny+xfSTwKbRojLes1UWbmE5M/zg8aUIkPwZ5GFMS9fRlpiEfeB59Zw/5s+ne2lxdiGTxeD7uN8KtrT/sds1uZ+cslretixK0DFeEr3VPeU3dz0FtAej5jzcVttz2rPjStHjBWwzgvh3vkJbIG5SCSBl3IQn6tfyedrN5P0yK4b4tBgyDVF9XchdHwghKuBzUCbxUb53yBN9QvGhS5wx3qEtuhsM9yYcpAKhGlMA3GbL+V5rK9y2RHOebyaEXNbynsOz+4yYstdXveKYU6slho9eUihnBgVIfcFJjNXc/CZiz8Z4oMURUDecie5q3mWLKsG5PJmf0dLVOWU+5n1L6+ybtgKQAb0+ZUpGFMf/Uyqnt2xLt39R/i4utrP5wiacI4MP8VriCD9irNKNhX/Yw55YaK0UIIDqjwHbMPLXWBv5IOrSY0CPf3XSUd7KgrB3GBaFOajRkPSJBJdbAY5a227MRfz+Lre4Djwy+P6erDM9jMgmK11XRIcB1EQx0KoHCEj+JHGOM7GlJpnzSqWJagzM4bJWjfk2CjyEIJBb23Yn/SEnWUwtAb+oUqs6MF/1TG7y24us6mzghYOnHiZoxKhjyIsy+iwS4FhnSLP5WrI2ieKPvva8HffUEYJQoAohLAkD5wm+10tlPPuYkjhJ/FAL6hBUCwT/EMj/glH/sVpyYY1GvPNv+RE3m2cVBznf4+bxGYd4DtbLa/fD11g58Q7tsid2m7DlFLUkmm5667/YbpBwqq/V3UP2605E2yz8j6XMqKluTz1y6cWhEt64ER0+s7d4wbsOzDRu2vJ+Rzy4zHNJPX+uQBY2Vn4iFh2g7fQdGXkdX7uJHcEBuynHTQrKfdzwkPKTS0e2dVvR+dUoXt5QxHfmHkafOD/cQtQjX30vU2S7F9jVx+wLigOoBpAt9fUI5SACDLkHEEo/QuPPBuWCAN+c5wzGW/sN1cjejEk5VocurCi7jpSO7tLBkVku6FZ01jA5in0nt73L2eF9cFLjF/jAHc9EwPJFfk1uXdNB1okRN0zGVJZZ3s6tWjWl2VNzl1ot2Ns7hDIWxTewLDd0cbelOv6+5VPtifbasExDqBGBXXYzwNBvl0pEh/sEPU2JT9oNbvZ4/VZWehyQAUYN5Eq6Io6H222H+Sr5us9GLfoihMiW4Z1Q46m7kOTTVYhqNDqrg12ijaGEQ9Y5Iaw1Umph7zrGqXTIP8KTKyo36RSz4TpVlLsxjnROaiIjF5xv3jlv/EYs2rxxOCwWUP/WZajh8/1Q183FGWK3dhOvCTGtWtOOsixyDOjKnMmUwQ4nw3JQ7me9dinIpblSbMeOnKHSiLlEC/EOwyRDl2cA2Wa+vBxoeZG/ll9klfZYliVtsafpsn1jZdto1MoQwvghpoq26oohLVLZ30WMtzS1q7NBujHUT7PlhS2oBBe1NNW6ptlwzILZsASI+nrFS0H1+1L0k7fJNf5EIlLeIYms0TplF2HjncPoZTUYGBL6m7a8Wd/d+qCMvSKlU2q9Ga8cHplQK2zGU90ULzVdt1/2maTqvpghXvEL5QqtpV9+xlmvKzVsNnOes53CB0dXGK9bfPN6hNNTiU01Oya3TSna5zCxju0kmj7YX+sbLbV3bl5DIZlrstKWwSPuThkFFpjm1vdEEPwN9OvILCUPAULpH6w/42veTWaZdV5SIBnZc9GD6NnyNuZrHoHPL4XAvrwNDSQ12McWYkjttc5SEaOAoM0jEEEbs5YKLkfxwlN16fqY14dS5X6BwMfM4ATcJ8v6EnrDp3R79SfUXFMjkvmbyj6Z8A5dKR1VTrMU7bPrbEfFg+vupoYbaUtfH3XHB1/P6C31uQAQtcGtjMMWBlYD4M8FWHMNXCwxo8+08I5AoTPh425tFTlt0P4B6FdAKGEIqvG1CI6qE5eL50ijjuTaJWPSj1+l5Pzp1YY1II1P+BB4jCPm9S3LAEnORSb3IscYJIfGdq99za3LLbLWkFQUbf4YNshsgRWmQ4nFahC6XZYxUEcV+B2Vz7NCUxcSVAmwHh+gIeiq0IujPQPzG1guC9mEjovUxcV4/5IuSp67u30RYbrJQd82De0TLJRrxD5PWltd9EV8vK7dKegBCwPueHcNULndcdl+X8QGIRIRmnzYIzncG6RukWqZNZ9hDtco1L2VWKAsv79fuBv40oyHKkazZYB0badMnqi1WScFGPr/neO+26H2U+8KUGKdR2/bZFm+/WAfd/uoZXybqWADLksCWT8V6aZF+btOu5ri96PXDkbwnGdfoAd9OB1NsAstPAkSeSrdnS1nJrHzr2gPV75B2SC/ndNvpGpLY0onzh+roHdsnA0XSXnkd8ojmBZd1IX7m20u1C4KTSuukuHZ/yHfrXEt0vBRJJfb9bLvsF1w/d9jyMrGQbOzZpf+eNvavGRVd+V3GUb7vdyGEyzxJZurzR3hwleT/y+5dXhzmooHWthZjoYUfCR6Mao4lzDXJe6jrTpfUxjAzlMeHK3oGHFZLQKF/TpOCM5qUTmPLqoMPKNhevPZlX858eDKunO4+peBdxY6LzpVJ0RhqYncz5N/U2FBd9mdHQ1wAQs+HOnSq1zFtpuhDiV07eJSyPICa/VTv+IzY5vnKgTB2uVzbQ2lfdlL8GzYf5ZFi1EzSeD3aki9+fUetHhci0XdvezoIO0HZOb57jtZBh3+HZpm/OxOsOsxxP1N/mQ5sXZ9z/VaeT/xekQLoixdd/Jb2NPL85Icv7Nw6pms9xDzfrd8/4v/Z6tc0yBjuTTPQ7R8tdavv7NUNLdbZ+H+6XJuYcdPznoReRHmqxVfoT1nrpYDjcKc5UwVIdPt+oNyn6OfqGPXEjMPgmRWeRYSQ4U+p3e1cPkscinvhegpTPAhF4mf2HSK4fHclIbydYViiGbk0pXoyVFN5uyJui8EbMrd5LqGE68Qogl/duAEJeRoQE0AUAVh+1c3Pbmm4ANux81//0c46uyy6bAxcgixhQXtQHACGsMy9wkhYjrkbzr99wEQjZwiKcRd9cPil/VfDU6lQ4syVDfUOHGNViMNgivQaRGX7VR2hJeaTcD51MCvcfXABDrrmgqlEWGlz0mKIR3mK5c3hrze9a/t7kj2niJtZ6omaHPaWo54uMCxaoo7y5utebIB0WVH2Vv1s0+olCJG0QkBj6qBquYh3vR0LlfHS9SnsQL+VY8/vvqTXD60WHgT+HkduWF2opyQr0HXPUqOaAON9AdssJCmdx/oLJWqx2cgiyhNlQNZCKglbafNRtj9VWmONYtCfDoFsZ7avvhFD/n9J+fqv3ZoZRxzfDimXErqkQ6eDwe6+d0af+dB+8D5/PhzSG8NQZlmd1aFzXxkDS/M1c+ewXhw1+79zy7uks9pilWOqeMn4CYvlbg8sXFxHu491eeOJbP1YhhT/HNIJzBxzssrE/I04JN1igeW5IhE/w47pY9JoHIkq1KQ9tdqko06ca8waM6lS7ihCnKYVde/5AQIy71AWFVS16QRoP62QmtYqD7+BROg1/46TzHOm0BiYLGn5ReE08aBFgCG3xkWas79AOFmNhTMgovAXJYFiqkDMDgLs9IwX8T66g3y9KJya5r1jzL+ZTLqc82rRgdkm/4mEz3uilB0i3B4UXgnfs2KROMloe+J19ZgKTWoKpPb8C7B3+N9Pj5Xq0WwsZLx0VhQrm8Lzo+t/o2teqtHTR2eKRP7RhBqBBgdIR7bVPyxGTFcYCwJyaiYY1LmlHVcxjRKwgcgp2UaqTZDQc027EiEs9lWSRQM+OykufNs0pL0UZzWNS6iWWVEVvZWyz9hOGzwB86/9ANby+I0X84dLoDAUIbqqePU3R7znGnkF4rlPj/lovoIzKcSWZtuuaVOiRIchGz+2j0Mmh87PCa1eEMi0FkTcoGO+BZkpjQpV0hti4+x2m7LCy5aG+BHW3OjBMjc9yRmJddk0GfdTWO0oJ0ELGVCDijMfYmcjTjE2XU8LA6Mo4DvFTkk4TojYEdsHtMiXIlmul033kvLKfDDLFiNrzWZdIizPuJE6oLQZOeLeJwiQbn3OiB1jVFcMjeUl+KD7jYL8chyN2GW01Pe+Gfp91/9g4emOoFC7uvKs1pHW+31TmvsPD/IL9t1W7x6c0dmPT3KvBq3bgQqKvhc0GvYYnM5HcT1Tt5RroxdNGF8/zpJYX2/2O3Wu7ZCnzZYdmi6MM+hQpEabIRskQJ7tjTcaFakGkWK2p8xySyq7qjt6mBio+b9yBwSS9SbOAy9axEY9xQsYjHdYnXKTEJyqP6yN4iNcoFJWZTnedN7w8rDm+qzrdlZDXhz2ma/RKV43QLZzFLdX9cz0G91DWvc5INeSJ18gFhHHfq+po1I5IunbF67z862HhyiyCOn3cgiAcLjluaPybHwfGt24MKKLoTbE7Nf5o41RcrY/OlLj9Fkm4345Pvf24/+pwxWY0ibV6GIqGiC2IRJ3et/TmbKSH+oHWXWcL7bJ8hT0BWWTD1DIhSTFSNW//W0xjPGF4qvD04BlCYridnADeo3kA4PfP+V3lrZCFgFMYfYIc5r4qJGxCCsHLMgp5oWK47qivudUXpFGzUUuvsBQIhyJeKUswQNcVxA8rxmxYQ4UpWDKhdUSJUSZf7dqLuWxxkwkO8hQneaENUvfk7/OS5NSwn4Di40uTtfZ5yQzsByauJWFAdPTnQFN16TU7Aw7m/1DuraX422UGub2urXPBFe18Gc+G2DAhR1tkAgIEHquKwHR6IH21SKs4+rdP3t/7LH92nAhDYH4bYhldGTdMm+6mhdMOyQLuvxSu5yUWqnFCM3Ebo7H1lDcqsQP8JLRrYMG83qySKHwnvZDbOj7UmDkkv/cB7wFwoz4JytUFxGOpKqEb0jy7cD12ZLqVn8oNXug+X3kgfRNstmfgt+YhjAt10OrN2YdWILoqw2XMgeArHL6EMUioqhHwzx0Cl3H7S64jpIjPcSLzMFwWVpD01je5RqfjeFkYNzBqYW/zqgd+lkdY2lea1cHmtTZ5UUMGb75MQ1gK0460Ibi2Juuid/ts215Ebj+sJ45O2AezG8cMAjlxBCj5A6wEKXkLRwHgsdcrc2Dm2YV8U+T1L8fmxfnOW2u3u4Tczmq3vDqNVp3xNRWQGCJDHIMGi1tpI2OrUuxs4zTE7ZYD4Z0XFX6MtO24rhy8dJPAgOOyi0EwWcuP3VOtd2DyZsn2r66rBwCC2Q9/Ob5uH0mkjbz5n5CXOXXkRHRm6GSWEoMLNcCLsYFrvftFaW4Vob+e9rY+qzbFzqhCSFRgCJ50awW+RRJpSh/Gm07IbSEuDoAHgDDqU4Yo7tlIkAAwHNQ2JJrjGoc1bJ6YhgxCwLoCGxfoDYJxdKm0/LJCdh9HiLwK5aBtgcWpWg5JQrmUGy54xmVvSDAER2aglTx15WLK1afjrMLUupS7IdojwKiQdMPiPGzghSpO+mharQ7CmtLJEFydHDZcMCSG32iVdQXBbSSkDwhwwX5igIHgMAUkEQAKclip90rztwwPuJIwY2iSdF4Esvr3FLyQxJR7IGEdk/LOG6g3j54gFQaPauaVw0f2VYuG3yq4/Fr9bSPkMDmMhJYP8/FloiQORL+9rpTTL/A7ztNx4v0kzi2aHD2qAFOILm8wiRg/4bh7V95HMFQUIdYdnrmxKDzRne8UYRBUskiHKbEjWKUtIpK7QfU7sK20x1VMYQoNUOCqr5T1mXYuM/AUWUD3kBtwaZ0DlBZXiUg9TgUPUBWFwvk97jW1kNU88TNNUakBkU6OujFppF1udP2Namqz3c8rdHXBHvZXBt4Nzvr1rK5T6cjYC44ZXLUuYKpchsEQgDDV0LsJ8wBbWAEf/nRnFLjvTaq2R3LjdU98Z2XIJxe9tHr+Yu7KnpSNf2VDOPnhudltmJPR0SKIY2HP5dA2R9GMZCf8BwYo+7B7KwL+jZGozMN1zGEzQbPVknWW4DgqqAPkYNUkq7CoYXhg4H2G4x2v+5f7uyAgYpATo12ZDaCJWlrXBQd9r6txIommrN9bkGkEyFXsqzmD+VFwua8kkOIUOwbm2sE3w0j//9oU+8okyCsVY5mOuUuag6BVIakfDtuzhj5kYYc3pUOT7LACw8YJk7YfG/S9cfKb3W233v4xn7kaqPVUPNqk+dor1Xu6cOrhAZbRKiKxM59rcZHlWhCaRh5fnWF4DgS6NYd/aPPQrtFGf1E0JLGW4c3RjWHjuIxqXNabTRgcQj+fPCxd3bQeosTaowWtsHqDy1Ab9hCH/Th5fkSjuPteWcbXCBgTKRZ5plkuqTYyhKi1GR0F5KpYCGUDSlJRlA8ZeduIWepwWub61vW6dJFTYkCddsXpBJhQ+xNOONdCx3Ke1igcnV6wsCfQn/POuKyzcAY5/KMSNuqlCPsNJH+aP6DvthU+GXXk34Qzq7M+pujm8abxJvGG8Vmu4IMtHgpP5qvGUo0qUsLaovQiIahBSEr/OS0wvpFVnqU0CHOwkOV4CmdLsVF5k7LxaPN4C/Emo41G5o/p+fRhmd6t8PiwUsToB5F+PI2nSUzMFe6d6rFThtpiYHzdow2CxAJBPOKlrHCde+mUMq0OxVyo97jSxzaJaRkgVEVQEjE1CCoVQLSMGFKfL+HKqFlF3rLXxkI125gKT+0pXyHbcrVIHbzwQza/fEMfnTIakcEsW6D1RoNZ32p3eJfabVTULm1sc98ePK5yVfvmerXJpS7lyWalN6D+KTrep0zTcA47uQ3awLXxGDDjibmn7TQlUrEwD7G655aYPlvA8hvopfIHsOCHtFQ+2Ux/aEEcRYANsJLpia5lkFVoT19eIQHMiRblknz3dEYZOc5M6wTIxGze2A4ycsWFcQ44RtYndgIPQ6Zwtg11TdXrQxBzeHZNm2MjpWi1EowgMA0KP8Tr9vZa0qN+5MnXbuvy4qwLdRExQDUceqpcd23I0NaLmi6RK/BLLnFNlUBPI2UW3M5VuoLwt20fsYDrsw1m4pZdVC/ipbxTlF/hAj08YMeF2yw2eaTXQ5IO8Rhs8LdqnGIOlDiF2uWrBMGCLJTLA5IfyWepa5DfIrkD/dhnCtVRNxdc7j6x+gDtEqS+olFjjc4SN/zIY0fwCUACpSWkgrJI3KRYsNYSCh+AaD5CSgM7Ey21lFhrREJSJ2P1X3CQxD4Xxfh5Lp6ySEP8l/bLWtZyvBYntwvM/rEfHeo2/H7ttpjG9qly+SMhh3trGW+rj+Pd8a2MTV23ooFAXr9BvviS/OJJWuHyuMfDU3uN0yMLNgfSo+WhMK5KqRkNlwiP2BpvMoOHf3p+7+/ySPsmprYylnR7baAQejwY4W5tzK99dd+XnWzTuUTkfNssUlLTC+E0lYo9B02m9ZHl/1yGzXuUOEjAENSj1neAQ6U7NnAzyvrjfAcUMxNajaiWxcuHhoNsPrjZXsC1eHm/vEc48X19eDk1Tzp1U4WW5nzTYl1SHWWqmCUAjigZBAZ37nnXKfhudONRQ4tpltHbgV1nnWrU6rOobboAUy/yH3dfk9oyx+GxBhswEPjSm4WNgccLOV4fnAqjZFbXP5OzTqImTh7QfiGAWHvbfoXJwND04wXQxIem/nN7n1///MdoJ0+xT9OFvaGh+lqhuAixpd0qGGWHNWmIuLjEs47jxbBaj6RbUrMD66rymlVSwLFmbv8eyOVdvw/gbp5QHD4+1lve9mreS0EPIwSjARP+e+F9FC48sIfDF7aHyd5pnVy2Rq/ow0feMEigEq7EC7ReW7zBknNSJrwzJ+XiQhqGkXLgrrX0Ejkx3g16sOHqFe9Ru/SQHohpMcxRQzaYLeEMjQnNKFA2HB7ZLmhNG6pxpFkszm94hsUVlFTi/ZyDB4HHsOYwYExpz1D3LakS6MojJ0+0DxKCu6naEMo5N6kwwbIe2Kv2nMHh4tPHmKcKpLDIDePBDw0x+tyVWqkyZuFSpjb7XUjjvjsryrKzlewl9zKCEjS9JMS9MG078fp+6x3+ZIYX0qzVKS1FUw/XuWmDkr7wdYcE7nUmCiao/hxV8MbVU23JnhcgerZkuC/WBnnseECzbZpFUEZeTSDpIQPF6s4Ws1mRLeUt59U8NAPRxDOHshUtwvFLpXrIH53QcdLl9IZiG8GHcqpIYHUTrGHMk1w5DL1hnLmNv818EDZjlAMmwl2Ux6zOoKJKZmk2AB7MoOfDPsxGPy7mY4eYKvdhG0NKl39ZgXV4E3Q6rjKtufWe8FEKb5eDvTPZ1Id82NqwtWFtWB6qeCyAWg53hLyS28PGFXjHBOCZ1ZMNPyS5vjOjT8vmFJlRcEJqdEconnxAZ7GzMi7LO89jtnjI7Zw9EPGd2vAhQdOzcPIS1gkdNvEkX/nP+oNwH4ZXYB1utx4DK30itLGcl7aN401jSd9y9E84TL5UTd1uYnhyJcWxwe0aT+ZgQ6cIUnXHnLJp50bMLP6gM+SwGjEpI03RgoddlawLutxMr3s2+S0yJtVpverxYffJA1sCPX+Nk77whtx+BIiJenweQO9SP947zSJy6oH1w686iemFRudeaomQVWE/+QEvTPNErQX8aIW9dAkLhD9d54gPxKlMtFCjhYdYd3a3uqa8GN5x7nS8I7rXDw1uDJWblveXe1voiBEv+Q/dctK8b3kcbEaHigmdf8xZBxWm0r+trU6/kp5cQXotadyhvVlGgR5Mo+m3/s6/cvzb3roMxIU9h30sFd8bSFrp9FD2vKQV+L8hjfhOTJUXPUb/x3Rr9K3FptBbUMlebm48xSmRFpMhhPmGkVF8cuVUuIjJnxJXa1ounFi40T7RMOczN1pX+SMc7Oc+2tJHYTxi4s0QhbDMug7Rk9COX9OadF89oQP5xO1zejgM9nru6oZYKMQn5TSbh3y8I8KVEpMBBaQRotTk+gnRJ/6oPqojCeFs+Pe5LzBCeNxrT4re7R9UsVaI9etNh3pcVjNMnn5peAD3/k0TdF7zpDG8SKIV1YHpgVh2JP9dA83MntXqDzwzI+v13PKZ5STaY/jLEBqfzhTENzkj4bgDFMJAYbGNb8b9683Pp7LvAurNcZnYwZPf90a+nU7JdWUyE4+ys+7oUhaIMbZvPU1o94CaHDgZ8B3K4+NKfM1r/ePUhP7HrHcC0o2gWI4CVW+RWgv7CqIZ/9X1lmAPXI+Y+XtanZQ9/YnC7qAGxqi+QazGaH3BKVB7iCi6P2sGBfHBtyKI7m4fmOCOJ1bPebG250slOEaXVYUmmkS8qL4qz18G78fW/38Iqdd92DSWNioJz4vLl8hmIrR+itRWzmQP+5rk3ASeI9XfTEC22qejk7EQU8SJGPk4+blgvtg7WB6x1yh4CNF0+XEXL+NlLFLqcHJinFlqmjd0OzrSnFjdb5nm2+tDjuQwTKBQ9l8=","base64")).toString()),CR)});var q6=w((mR,Y6)=>{(function(r,e){typeof mR=="object"?Y6.exports=e():typeof define=="function"&&define.amd?define(e):r.treeify=e()})(mR,function(){function r(n,s){var o=s?"\u2514":"\u251C";return n?o+="\u2500 ":o+="\u2500\u2500\u2510",o}function e(n,s){var o=[];for(var a in n)!n.hasOwnProperty(a)||s&&typeof n[a]=="function"||o.push(a);return o}function t(n,s,o,a,l,c,u){var g="",f=0,h,p,m=a.slice(0);if(m.push([s,o])&&a.length>0&&(a.forEach(function(b,v){v>0&&(g+=(b[1]?" ":"\u2502")+" "),!p&&b[0]===s&&(p=!0)}),g+=r(n,o)+n,l&&(typeof s!="object"||s instanceof Date)&&(g+=": "+s),p&&(g+=" (circular ref.)"),u(g)),!p&&typeof s=="object"){var y=e(s,c);y.forEach(function(b){h=++f===y.length,t(b,s[b],h,m,l,c,u)})}}var i={};return i.asLines=function(n,s,o,a){var l=typeof o!="function"?o:!1;t(".",n,!1,[],s,l,a||o)},i.asTree=function(n,s,o){var a="";return t(".",n,!1,[],s,o,function(l){a+=l+` +`}),a},i})});var VB=w((aAt,t7)=>{var wNe=Hs(),BNe=yd(),bNe=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,QNe=/^\w*$/;function SNe(r,e){if(wNe(r))return!1;var t=typeof r;return t=="number"||t=="symbol"||t=="boolean"||r==null||BNe(r)?!0:QNe.test(r)||!bNe.test(r)||e!=null&&r in Object(e)}t7.exports=SNe});var XB=w((AAt,r7)=>{var vNe=zc(),kNe=Fn(),xNe="[object AsyncFunction]",PNe="[object Function]",DNe="[object GeneratorFunction]",RNe="[object Proxy]";function FNe(r){if(!kNe(r))return!1;var e=vNe(r);return e==PNe||e==DNe||e==xNe||e==RNe}r7.exports=FNe});var n7=w((lAt,i7)=>{var NNe=Ts(),LNe=NNe["__core-js_shared__"];i7.exports=LNe});var a7=w((cAt,s7)=>{var SR=n7(),o7=function(){var r=/[^.]+$/.exec(SR&&SR.keys&&SR.keys.IE_PROTO||"");return r?"Symbol(src)_1."+r:""}();function TNe(r){return!!o7&&o7 in r}s7.exports=TNe});var vR=w((uAt,A7)=>{var ONe=Function.prototype,MNe=ONe.toString;function UNe(r){if(r!=null){try{return MNe.call(r)}catch(e){}try{return r+""}catch(e){}}return""}A7.exports=UNe});var c7=w((gAt,l7)=>{var KNe=XB(),HNe=a7(),jNe=Fn(),GNe=vR(),YNe=/[\\^$.*+?()[\]{}|]/g,qNe=/^\[object .+?Constructor\]$/,JNe=Function.prototype,WNe=Object.prototype,zNe=JNe.toString,_Ne=WNe.hasOwnProperty,VNe=RegExp("^"+zNe.call(_Ne).replace(YNe,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$");function XNe(r){if(!jNe(r)||HNe(r))return!1;var e=KNe(r)?VNe:qNe;return e.test(GNe(r))}l7.exports=XNe});var g7=w((fAt,u7)=>{function ZNe(r,e){return r==null?void 0:r[e]}u7.exports=ZNe});var Fl=w((hAt,f7)=>{var $Ne=c7(),eLe=g7();function tLe(r,e){var t=eLe(r,e);return $Ne(t)?t:void 0}f7.exports=tLe});var uC=w((pAt,h7)=>{var rLe=Fl(),iLe=rLe(Object,"create");h7.exports=iLe});var C7=w((dAt,p7)=>{var d7=uC();function nLe(){this.__data__=d7?d7(null):{},this.size=0}p7.exports=nLe});var E7=w((CAt,m7)=>{function sLe(r){var e=this.has(r)&&delete this.__data__[r];return this.size-=e?1:0,e}m7.exports=sLe});var y7=w((mAt,I7)=>{var oLe=uC(),aLe="__lodash_hash_undefined__",ALe=Object.prototype,lLe=ALe.hasOwnProperty;function cLe(r){var e=this.__data__;if(oLe){var t=e[r];return t===aLe?void 0:t}return lLe.call(e,r)?e[r]:void 0}I7.exports=cLe});var B7=w((EAt,w7)=>{var uLe=uC(),gLe=Object.prototype,fLe=gLe.hasOwnProperty;function hLe(r){var e=this.__data__;return uLe?e[r]!==void 0:fLe.call(e,r)}w7.exports=hLe});var Q7=w((IAt,b7)=>{var pLe=uC(),dLe="__lodash_hash_undefined__";function CLe(r,e){var t=this.__data__;return this.size+=this.has(r)?0:1,t[r]=pLe&&e===void 0?dLe:e,this}b7.exports=CLe});var v7=w((yAt,S7)=>{var mLe=C7(),ELe=E7(),ILe=y7(),yLe=B7(),wLe=Q7();function Pf(r){var e=-1,t=r==null?0:r.length;for(this.clear();++e{function BLe(){this.__data__=[],this.size=0}k7.exports=BLe});var Df=w((BAt,P7)=>{function bLe(r,e){return r===e||r!==r&&e!==e}P7.exports=bLe});var gC=w((bAt,D7)=>{var QLe=Df();function SLe(r,e){for(var t=r.length;t--;)if(QLe(r[t][0],e))return t;return-1}D7.exports=SLe});var F7=w((QAt,R7)=>{var vLe=gC(),kLe=Array.prototype,xLe=kLe.splice;function PLe(r){var e=this.__data__,t=vLe(e,r);if(t<0)return!1;var i=e.length-1;return t==i?e.pop():xLe.call(e,t,1),--this.size,!0}R7.exports=PLe});var L7=w((SAt,N7)=>{var DLe=gC();function RLe(r){var e=this.__data__,t=DLe(e,r);return t<0?void 0:e[t][1]}N7.exports=RLe});var O7=w((vAt,T7)=>{var FLe=gC();function NLe(r){return FLe(this.__data__,r)>-1}T7.exports=NLe});var U7=w((kAt,M7)=>{var LLe=gC();function TLe(r,e){var t=this.__data__,i=LLe(t,r);return i<0?(++this.size,t.push([r,e])):t[i][1]=e,this}M7.exports=TLe});var fC=w((xAt,K7)=>{var OLe=x7(),MLe=F7(),ULe=L7(),KLe=O7(),HLe=U7();function Rf(r){var e=-1,t=r==null?0:r.length;for(this.clear();++e{var jLe=Fl(),GLe=Ts(),YLe=jLe(GLe,"Map");H7.exports=YLe});var Y7=w((DAt,j7)=>{var G7=v7(),qLe=fC(),JLe=ZB();function WLe(){this.size=0,this.__data__={hash:new G7,map:new(JLe||qLe),string:new G7}}j7.exports=WLe});var J7=w((RAt,q7)=>{function zLe(r){var e=typeof r;return e=="string"||e=="number"||e=="symbol"||e=="boolean"?r!=="__proto__":r===null}q7.exports=zLe});var hC=w((FAt,W7)=>{var _Le=J7();function VLe(r,e){var t=r.__data__;return _Le(e)?t[typeof e=="string"?"string":"hash"]:t.map}W7.exports=VLe});var _7=w((NAt,z7)=>{var XLe=hC();function ZLe(r){var e=XLe(this,r).delete(r);return this.size-=e?1:0,e}z7.exports=ZLe});var X7=w((LAt,V7)=>{var $Le=hC();function eTe(r){return $Le(this,r).get(r)}V7.exports=eTe});var $7=w((TAt,Z7)=>{var tTe=hC();function rTe(r){return tTe(this,r).has(r)}Z7.exports=rTe});var tX=w((OAt,eX)=>{var iTe=hC();function nTe(r,e){var t=iTe(this,r),i=t.size;return t.set(r,e),this.size+=t.size==i?0:1,this}eX.exports=nTe});var $B=w((MAt,rX)=>{var sTe=Y7(),oTe=_7(),aTe=X7(),ATe=$7(),lTe=tX();function Ff(r){var e=-1,t=r==null?0:r.length;for(this.clear();++e{var nX=$B(),cTe="Expected a function";function kR(r,e){if(typeof r!="function"||e!=null&&typeof e!="function")throw new TypeError(cTe);var t=function(){var i=arguments,n=e?e.apply(this,i):i[0],s=t.cache;if(s.has(n))return s.get(n);var o=r.apply(this,i);return t.cache=s.set(n,o)||s,o};return t.cache=new(kR.Cache||nX),t}kR.Cache=nX;iX.exports=kR});var aX=w((KAt,oX)=>{var uTe=sX(),gTe=500;function fTe(r){var e=uTe(r,function(i){return t.size===gTe&&t.clear(),i}),t=e.cache;return e}oX.exports=fTe});var lX=w((HAt,AX)=>{var hTe=aX(),pTe=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,dTe=/\\(\\)?/g,CTe=hTe(function(r){var e=[];return r.charCodeAt(0)===46&&e.push(""),r.replace(pTe,function(t,i,n,s){e.push(n?s.replace(dTe,"$1"):i||t)}),e});AX.exports=CTe});var Nf=w((jAt,cX)=>{var mTe=Hs(),ETe=VB(),ITe=lX(),yTe=cf();function wTe(r,e){return mTe(r)?r:ETe(r,e)?[r]:ITe(yTe(r))}cX.exports=wTe});var fu=w((GAt,uX)=>{var BTe=yd(),bTe=1/0;function QTe(r){if(typeof r=="string"||BTe(r))return r;var e=r+"";return e=="0"&&1/r==-bTe?"-0":e}uX.exports=QTe});var pC=w((YAt,gX)=>{var STe=Nf(),vTe=fu();function kTe(r,e){e=STe(e,r);for(var t=0,i=e.length;r!=null&&t{var xTe=Fl(),PTe=function(){try{var r=xTe(Object,"defineProperty");return r({},"",{}),r}catch(e){}}();fX.exports=PTe});var Lf=w((JAt,hX)=>{var pX=xR();function DTe(r,e,t){e=="__proto__"&&pX?pX(r,e,{configurable:!0,enumerable:!0,value:t,writable:!0}):r[e]=t}hX.exports=DTe});var e0=w((WAt,dX)=>{var RTe=Lf(),FTe=Df(),NTe=Object.prototype,LTe=NTe.hasOwnProperty;function TTe(r,e,t){var i=r[e];(!(LTe.call(r,e)&&FTe(i,t))||t===void 0&&!(e in r))&&RTe(r,e,t)}dX.exports=TTe});var dC=w((zAt,CX)=>{var OTe=9007199254740991,MTe=/^(?:0|[1-9]\d*)$/;function UTe(r,e){var t=typeof r;return e=e==null?OTe:e,!!e&&(t=="number"||t!="symbol"&&MTe.test(r))&&r>-1&&r%1==0&&r{var KTe=e0(),HTe=Nf(),jTe=dC(),EX=Fn(),GTe=fu();function YTe(r,e,t,i){if(!EX(r))return r;e=HTe(e,r);for(var n=-1,s=e.length,o=s-1,a=r;a!=null&&++n{var qTe=pC(),JTe=PR(),WTe=Nf();function zTe(r,e,t){for(var i=-1,n=e.length,s={};++i{function _Te(r,e){return r!=null&&e in Object(r)}wX.exports=_Te});var QX=w((ZAt,bX)=>{var VTe=zc(),XTe=ra(),ZTe="[object Arguments]";function $Te(r){return XTe(r)&&VTe(r)==ZTe}bX.exports=$Te});var CC=w(($At,SX)=>{var vX=QX(),eOe=ra(),kX=Object.prototype,tOe=kX.hasOwnProperty,rOe=kX.propertyIsEnumerable,iOe=vX(function(){return arguments}())?vX:function(r){return eOe(r)&&tOe.call(r,"callee")&&!rOe.call(r,"callee")};SX.exports=iOe});var t0=w((elt,xX)=>{var nOe=9007199254740991;function sOe(r){return typeof r=="number"&&r>-1&&r%1==0&&r<=nOe}xX.exports=sOe});var DR=w((tlt,PX)=>{var oOe=Nf(),aOe=CC(),AOe=Hs(),lOe=dC(),cOe=t0(),uOe=fu();function gOe(r,e,t){e=oOe(e,r);for(var i=-1,n=e.length,s=!1;++i{var fOe=BX(),hOe=DR();function pOe(r,e){return r!=null&&hOe(r,e,fOe)}DX.exports=pOe});var FX=w((ilt,RX)=>{var dOe=yX(),COe=RR();function mOe(r,e){return dOe(r,e,function(t,i){return COe(r,i)})}RX.exports=mOe});var r0=w((nlt,NX)=>{function EOe(r,e){for(var t=-1,i=e.length,n=r.length;++t{var TX=Wc(),IOe=CC(),yOe=Hs(),OX=TX?TX.isConcatSpreadable:void 0;function wOe(r){return yOe(r)||IOe(r)||!!(OX&&r&&r[OX])}LX.exports=wOe});var HX=w((olt,UX)=>{var BOe=r0(),bOe=MX();function KX(r,e,t,i,n){var s=-1,o=r.length;for(t||(t=bOe),n||(n=[]);++s0&&t(a)?e>1?KX(a,e-1,t,i,n):BOe(n,a):i||(n[n.length]=a)}return n}UX.exports=KX});var GX=w((alt,jX)=>{var QOe=HX();function SOe(r){var e=r==null?0:r.length;return e?QOe(r,1):[]}jX.exports=SOe});var qX=w((Alt,YX)=>{function vOe(r,e,t){switch(t.length){case 0:return r.call(e);case 1:return r.call(e,t[0]);case 2:return r.call(e,t[0],t[1]);case 3:return r.call(e,t[0],t[1],t[2])}return r.apply(e,t)}YX.exports=vOe});var FR=w((llt,JX)=>{var kOe=qX(),WX=Math.max;function xOe(r,e,t){return e=WX(e===void 0?r.length-1:e,0),function(){for(var i=arguments,n=-1,s=WX(i.length-e,0),o=Array(s);++n{function POe(r){return function(){return r}}zX.exports=POe});var i0=w((ult,VX)=>{function DOe(r){return r}VX.exports=DOe});var $X=w((glt,XX)=>{var ROe=_X(),ZX=xR(),FOe=i0(),NOe=ZX?function(r,e){return ZX(r,"toString",{configurable:!0,enumerable:!1,value:ROe(e),writable:!0})}:FOe;XX.exports=NOe});var tZ=w((flt,eZ)=>{var LOe=800,TOe=16,OOe=Date.now;function MOe(r){var e=0,t=0;return function(){var i=OOe(),n=TOe-(i-t);if(t=i,n>0){if(++e>=LOe)return arguments[0]}else e=0;return r.apply(void 0,arguments)}}eZ.exports=MOe});var NR=w((hlt,rZ)=>{var UOe=$X(),KOe=tZ(),HOe=KOe(UOe);rZ.exports=HOe});var nZ=w((plt,iZ)=>{var jOe=GX(),GOe=FR(),YOe=NR();function qOe(r){return YOe(GOe(r,void 0,jOe),r+"")}iZ.exports=qOe});var oZ=w((dlt,sZ)=>{var JOe=FX(),WOe=nZ(),zOe=WOe(function(r,e){return r==null?{}:JOe(r,e)});sZ.exports=zOe});var mZ=w((fut,pZ)=>{"use strict";var YR;try{YR=Map}catch(r){}var qR;try{qR=Set}catch(r){}function dZ(r,e,t){if(!r||typeof r!="object"||typeof r=="function")return r;if(r.nodeType&&"cloneNode"in r)return r.cloneNode(!0);if(r instanceof Date)return new Date(r.getTime());if(r instanceof RegExp)return new RegExp(r);if(Array.isArray(r))return r.map(CZ);if(YR&&r instanceof YR)return new Map(Array.from(r.entries()));if(qR&&r instanceof qR)return new Set(Array.from(r.values()));if(r instanceof Object){e.push(r);var i=Object.create(r);t.push(i);for(var n in r){var s=e.findIndex(function(o){return o===r[n]});i[n]=s>-1?t[s]:dZ(r[n],e,t)}return i}return r}function CZ(r){return dZ(r,[],[])}pZ.exports=CZ});var yC=w(JR=>{"use strict";Object.defineProperty(JR,"__esModule",{value:!0});JR.default=iMe;var nMe=Object.prototype.toString,sMe=Error.prototype.toString,oMe=RegExp.prototype.toString,aMe=typeof Symbol!="undefined"?Symbol.prototype.toString:()=>"",AMe=/^Symbol\((.*)\)(.*)$/;function lMe(r){return r!=+r?"NaN":r===0&&1/r<0?"-0":""+r}function EZ(r,e=!1){if(r==null||r===!0||r===!1)return""+r;let t=typeof r;if(t==="number")return lMe(r);if(t==="string")return e?`"${r}"`:r;if(t==="function")return"[Function "+(r.name||"anonymous")+"]";if(t==="symbol")return aMe.call(r).replace(AMe,"Symbol($1)");let i=nMe.call(r).slice(8,-1);return i==="Date"?isNaN(r.getTime())?""+r:r.toISOString(r):i==="Error"||r instanceof Error?"["+sMe.call(r)+"]":i==="RegExp"?oMe.call(r):null}function iMe(r,e){let t=EZ(r,e);return t!==null?t:JSON.stringify(r,function(i,n){let s=EZ(this[i],e);return s!==null?s:n},2)}});var mA=w(bi=>{"use strict";Object.defineProperty(bi,"__esModule",{value:!0});bi.default=bi.array=bi.object=bi.boolean=bi.date=bi.number=bi.string=bi.mixed=void 0;var IZ=cMe(yC());function cMe(r){return r&&r.__esModule?r:{default:r}}var yZ={default:"${path} is invalid",required:"${path} is a required field",oneOf:"${path} must be one of the following values: ${values}",notOneOf:"${path} must not be one of the following values: ${values}",notType:({path:r,type:e,value:t,originalValue:i})=>{let n=i!=null&&i!==t,s=`${r} must be a \`${e}\` type, but the final value was: \`${(0,IZ.default)(t,!0)}\``+(n?` (cast from the value \`${(0,IZ.default)(i,!0)}\`).`:".");return t===null&&(s+='\n If "null" is intended as an empty value be sure to mark the schema as `.nullable()`'),s},defined:"${path} must be defined"};bi.mixed=yZ;var wZ={length:"${path} must be exactly ${length} characters",min:"${path} must be at least ${min} characters",max:"${path} must be at most ${max} characters",matches:'${path} must match the following: "${regex}"',email:"${path} must be a valid email",url:"${path} must be a valid URL",uuid:"${path} must be a valid UUID",trim:"${path} must be a trimmed string",lowercase:"${path} must be a lowercase string",uppercase:"${path} must be a upper case string"};bi.string=wZ;var BZ={min:"${path} must be greater than or equal to ${min}",max:"${path} must be less than or equal to ${max}",lessThan:"${path} must be less than ${less}",moreThan:"${path} must be greater than ${more}",positive:"${path} must be a positive number",negative:"${path} must be a negative number",integer:"${path} must be an integer"};bi.number=BZ;var bZ={min:"${path} field must be later than ${min}",max:"${path} field must be at earlier than ${max}"};bi.date=bZ;var QZ={isValue:"${path} field must be ${value}"};bi.boolean=QZ;var SZ={noUnknown:"${path} field has unspecified keys: ${unknown}"};bi.object=SZ;var vZ={min:"${path} field must have at least ${min} items",max:"${path} field must have less than or equal to ${max} items",length:"${path} must be have ${length} items"};bi.array=vZ;var uMe=Object.assign(Object.create(null),{mixed:yZ,string:wZ,number:BZ,date:bZ,object:SZ,array:vZ,boolean:QZ});bi.default=uMe});var xZ=w((dut,kZ)=>{var gMe=Object.prototype,fMe=gMe.hasOwnProperty;function hMe(r,e){return r!=null&&fMe.call(r,e)}kZ.exports=hMe});var wC=w((Cut,PZ)=>{var pMe=xZ(),dMe=DR();function CMe(r,e){return r!=null&&dMe(r,e,pMe)}PZ.exports=CMe});var Of=w(a0=>{"use strict";Object.defineProperty(a0,"__esModule",{value:!0});a0.default=void 0;var mMe=r=>r&&r.__isYupSchema__;a0.default=mMe});var FZ=w(A0=>{"use strict";Object.defineProperty(A0,"__esModule",{value:!0});A0.default=void 0;var EMe=DZ(wC()),IMe=DZ(Of());function DZ(r){return r&&r.__esModule?r:{default:r}}var RZ=class{constructor(e,t){if(this.refs=e,this.refs=e,typeof t=="function"){this.fn=t;return}if(!(0,EMe.default)(t,"is"))throw new TypeError("`is:` is required for `when()` conditions");if(!t.then&&!t.otherwise)throw new TypeError("either `then:` or `otherwise:` is required for `when()` conditions");let{is:i,then:n,otherwise:s}=t,o=typeof i=="function"?i:(...a)=>a.every(l=>l===i);this.fn=function(...a){let l=a.pop(),c=a.pop(),u=o(...a)?n:s;if(!!u)return typeof u=="function"?u(c):c.concat(u.resolve(l))}}resolve(e,t){let i=this.refs.map(s=>s.getValue(t==null?void 0:t.value,t==null?void 0:t.parent,t==null?void 0:t.context)),n=this.fn.apply(e,i.concat(e,t));if(n===void 0||n===e)return e;if(!(0,IMe.default)(n))throw new TypeError("conditions must return a schema object");return n.resolve(t)}},yMe=RZ;A0.default=yMe});var zR=w(WR=>{"use strict";Object.defineProperty(WR,"__esModule",{value:!0});WR.default=wMe;function wMe(r){return r==null?[]:[].concat(r)}});var hu=w(l0=>{"use strict";Object.defineProperty(l0,"__esModule",{value:!0});l0.default=void 0;var BMe=NZ(yC()),bMe=NZ(zR());function NZ(r){return r&&r.__esModule?r:{default:r}}function _R(){return _R=Object.assign||function(r){for(var e=1;e(0,BMe.default)(t[s])):typeof e=="function"?e(t):e}static isError(e){return e&&e.name==="ValidationError"}constructor(e,t,i,n){super();this.name="ValidationError",this.value=t,this.path=i,this.type=n,this.errors=[],this.inner=[],(0,bMe.default)(e).forEach(s=>{BC.isError(s)?(this.errors.push(...s.errors),this.inner=this.inner.concat(s.inner.length?s.inner:s)):this.errors.push(s)}),this.message=this.errors.length>1?`${this.errors.length} errors occurred`:this.errors[0],Error.captureStackTrace&&Error.captureStackTrace(this,BC)}};l0.default=BC});var c0=w(VR=>{"use strict";Object.defineProperty(VR,"__esModule",{value:!0});VR.default=SMe;var XR=vMe(hu());function vMe(r){return r&&r.__esModule?r:{default:r}}var kMe=r=>{let e=!1;return(...t)=>{e||(e=!0,r(...t))}};function SMe(r,e){let{endEarly:t,tests:i,args:n,value:s,errors:o,sort:a,path:l}=r,c=kMe(e),u=i.length,g=[];if(o=o||[],!u)return o.length?c(new XR.default(o,s,l)):c(null,s);for(let f=0;f{function xMe(r){return function(e,t,i){for(var n=-1,s=Object(e),o=i(e),a=o.length;a--;){var l=o[r?a:++n];if(t(s[l],l,s)===!1)break}return e}}LZ.exports=xMe});var ZR=w((but,OZ)=>{var PMe=TZ(),DMe=PMe();OZ.exports=DMe});var UZ=w((Qut,MZ)=>{function RMe(r,e){for(var t=-1,i=Array(r);++t{function FMe(){return!1}KZ.exports=FMe});var QC=w((bC,Mf)=>{var NMe=Ts(),LMe=HZ(),jZ=typeof bC=="object"&&bC&&!bC.nodeType&&bC,GZ=jZ&&typeof Mf=="object"&&Mf&&!Mf.nodeType&&Mf,TMe=GZ&&GZ.exports===jZ,YZ=TMe?NMe.Buffer:void 0,OMe=YZ?YZ.isBuffer:void 0,MMe=OMe||LMe;Mf.exports=MMe});var JZ=w((vut,qZ)=>{var UMe=zc(),KMe=t0(),HMe=ra(),jMe="[object Arguments]",GMe="[object Array]",YMe="[object Boolean]",qMe="[object Date]",JMe="[object Error]",WMe="[object Function]",zMe="[object Map]",_Me="[object Number]",VMe="[object Object]",XMe="[object RegExp]",ZMe="[object Set]",$Me="[object String]",e1e="[object WeakMap]",t1e="[object ArrayBuffer]",r1e="[object DataView]",i1e="[object Float32Array]",n1e="[object Float64Array]",s1e="[object Int8Array]",o1e="[object Int16Array]",a1e="[object Int32Array]",A1e="[object Uint8Array]",l1e="[object Uint8ClampedArray]",c1e="[object Uint16Array]",u1e="[object Uint32Array]",wr={};wr[i1e]=wr[n1e]=wr[s1e]=wr[o1e]=wr[a1e]=wr[A1e]=wr[l1e]=wr[c1e]=wr[u1e]=!0;wr[jMe]=wr[GMe]=wr[t1e]=wr[YMe]=wr[r1e]=wr[qMe]=wr[JMe]=wr[WMe]=wr[zMe]=wr[_Me]=wr[VMe]=wr[XMe]=wr[ZMe]=wr[$Me]=wr[e1e]=!1;function g1e(r){return HMe(r)&&KMe(r.length)&&!!wr[UMe(r)]}qZ.exports=g1e});var u0=w((kut,WZ)=>{function f1e(r){return function(e){return r(e)}}WZ.exports=f1e});var g0=w((SC,Uf)=>{var h1e=ix(),zZ=typeof SC=="object"&&SC&&!SC.nodeType&&SC,vC=zZ&&typeof Uf=="object"&&Uf&&!Uf.nodeType&&Uf,p1e=vC&&vC.exports===zZ,$R=p1e&&h1e.process,d1e=function(){try{var r=vC&&vC.require&&vC.require("util").types;return r||$R&&$R.binding&&$R.binding("util")}catch(e){}}();Uf.exports=d1e});var f0=w((xut,_Z)=>{var C1e=JZ(),m1e=u0(),VZ=g0(),XZ=VZ&&VZ.isTypedArray,E1e=XZ?m1e(XZ):C1e;_Z.exports=E1e});var eF=w((Put,ZZ)=>{var I1e=UZ(),y1e=CC(),w1e=Hs(),B1e=QC(),b1e=dC(),Q1e=f0(),S1e=Object.prototype,v1e=S1e.hasOwnProperty;function k1e(r,e){var t=w1e(r),i=!t&&y1e(r),n=!t&&!i&&B1e(r),s=!t&&!i&&!n&&Q1e(r),o=t||i||n||s,a=o?I1e(r.length,String):[],l=a.length;for(var c in r)(e||v1e.call(r,c))&&!(o&&(c=="length"||n&&(c=="offset"||c=="parent")||s&&(c=="buffer"||c=="byteLength"||c=="byteOffset")||b1e(c,l)))&&a.push(c);return a}ZZ.exports=k1e});var h0=w((Dut,$Z)=>{var x1e=Object.prototype;function P1e(r){var e=r&&r.constructor,t=typeof e=="function"&&e.prototype||x1e;return r===t}$Z.exports=P1e});var tF=w((Rut,e$)=>{function D1e(r,e){return function(t){return r(e(t))}}e$.exports=D1e});var r$=w((Fut,t$)=>{var R1e=tF(),F1e=R1e(Object.keys,Object);t$.exports=F1e});var n$=w((Nut,i$)=>{var N1e=h0(),L1e=r$(),T1e=Object.prototype,O1e=T1e.hasOwnProperty;function M1e(r){if(!N1e(r))return L1e(r);var e=[];for(var t in Object(r))O1e.call(r,t)&&t!="constructor"&&e.push(t);return e}i$.exports=M1e});var kC=w((Lut,s$)=>{var U1e=XB(),K1e=t0();function H1e(r){return r!=null&&K1e(r.length)&&!U1e(r)}s$.exports=H1e});var Kf=w((Tut,o$)=>{var j1e=eF(),G1e=n$(),Y1e=kC();function q1e(r){return Y1e(r)?j1e(r):G1e(r)}o$.exports=q1e});var rF=w((Out,a$)=>{var J1e=ZR(),W1e=Kf();function z1e(r,e){return r&&J1e(r,e,W1e)}a$.exports=z1e});var l$=w((Mut,A$)=>{var _1e=fC();function V1e(){this.__data__=new _1e,this.size=0}A$.exports=V1e});var u$=w((Uut,c$)=>{function X1e(r){var e=this.__data__,t=e.delete(r);return this.size=e.size,t}c$.exports=X1e});var f$=w((Kut,g$)=>{function Z1e(r){return this.__data__.get(r)}g$.exports=Z1e});var p$=w((Hut,h$)=>{function $1e(r){return this.__data__.has(r)}h$.exports=$1e});var C$=w((jut,d$)=>{var eUe=fC(),tUe=ZB(),rUe=$B(),iUe=200;function nUe(r,e){var t=this.__data__;if(t instanceof eUe){var i=t.__data__;if(!tUe||i.length{var sUe=fC(),oUe=l$(),aUe=u$(),AUe=f$(),lUe=p$(),cUe=C$();function Hf(r){var e=this.__data__=new sUe(r);this.size=e.size}Hf.prototype.clear=oUe;Hf.prototype.delete=aUe;Hf.prototype.get=AUe;Hf.prototype.has=lUe;Hf.prototype.set=cUe;m$.exports=Hf});var I$=w((Yut,E$)=>{var uUe="__lodash_hash_undefined__";function gUe(r){return this.__data__.set(r,uUe),this}E$.exports=gUe});var w$=w((qut,y$)=>{function fUe(r){return this.__data__.has(r)}y$.exports=fUe});var b$=w((Jut,B$)=>{var hUe=$B(),pUe=I$(),dUe=w$();function p0(r){var e=-1,t=r==null?0:r.length;for(this.__data__=new hUe;++e{function CUe(r,e){for(var t=-1,i=r==null?0:r.length;++t{function mUe(r,e){return r.has(e)}v$.exports=mUe});var iF=w((_ut,x$)=>{var EUe=b$(),IUe=S$(),yUe=k$(),wUe=1,BUe=2;function bUe(r,e,t,i,n,s){var o=t&wUe,a=r.length,l=e.length;if(a!=l&&!(o&&l>a))return!1;var c=s.get(r),u=s.get(e);if(c&&u)return c==e&&u==r;var g=-1,f=!0,h=t&BUe?new EUe:void 0;for(s.set(r,e),s.set(e,r);++g{var QUe=Ts(),SUe=QUe.Uint8Array;P$.exports=SUe});var R$=w((Xut,D$)=>{function vUe(r){var e=-1,t=Array(r.size);return r.forEach(function(i,n){t[++e]=[n,i]}),t}D$.exports=vUe});var N$=w((Zut,F$)=>{function kUe(r){var e=-1,t=Array(r.size);return r.forEach(function(i){t[++e]=i}),t}F$.exports=kUe});var U$=w(($ut,L$)=>{var T$=Wc(),O$=nF(),xUe=Df(),PUe=iF(),DUe=R$(),RUe=N$(),FUe=1,NUe=2,LUe="[object Boolean]",TUe="[object Date]",OUe="[object Error]",MUe="[object Map]",UUe="[object Number]",KUe="[object RegExp]",HUe="[object Set]",jUe="[object String]",GUe="[object Symbol]",YUe="[object ArrayBuffer]",qUe="[object DataView]",M$=T$?T$.prototype:void 0,sF=M$?M$.valueOf:void 0;function JUe(r,e,t,i,n,s,o){switch(t){case qUe:if(r.byteLength!=e.byteLength||r.byteOffset!=e.byteOffset)return!1;r=r.buffer,e=e.buffer;case YUe:return!(r.byteLength!=e.byteLength||!s(new O$(r),new O$(e)));case LUe:case TUe:case UUe:return xUe(+r,+e);case OUe:return r.name==e.name&&r.message==e.message;case KUe:case jUe:return r==e+"";case MUe:var a=DUe;case HUe:var l=i&FUe;if(a||(a=RUe),r.size!=e.size&&!l)return!1;var c=o.get(r);if(c)return c==e;i|=NUe,o.set(r,e);var u=PUe(a(r),a(e),i,n,s,o);return o.delete(r),u;case GUe:if(sF)return sF.call(r)==sF.call(e)}return!1}L$.exports=JUe});var oF=w((egt,K$)=>{var WUe=r0(),zUe=Hs();function _Ue(r,e,t){var i=e(r);return zUe(r)?i:WUe(i,t(r))}K$.exports=_Ue});var j$=w((tgt,H$)=>{function VUe(r,e){for(var t=-1,i=r==null?0:r.length,n=0,s=[];++t{function XUe(){return[]}G$.exports=XUe});var d0=w((igt,Y$)=>{var ZUe=j$(),$Ue=aF(),eKe=Object.prototype,tKe=eKe.propertyIsEnumerable,q$=Object.getOwnPropertySymbols,rKe=q$?function(r){return r==null?[]:(r=Object(r),ZUe(q$(r),function(e){return tKe.call(r,e)}))}:$Ue;Y$.exports=rKe});var AF=w((ngt,J$)=>{var iKe=oF(),nKe=d0(),sKe=Kf();function oKe(r){return iKe(r,sKe,nKe)}J$.exports=oKe});var _$=w((sgt,W$)=>{var z$=AF(),aKe=1,AKe=Object.prototype,lKe=AKe.hasOwnProperty;function cKe(r,e,t,i,n,s){var o=t&aKe,a=z$(r),l=a.length,c=z$(e),u=c.length;if(l!=u&&!o)return!1;for(var g=l;g--;){var f=a[g];if(!(o?f in e:lKe.call(e,f)))return!1}var h=s.get(r),p=s.get(e);if(h&&p)return h==e&&p==r;var m=!0;s.set(r,e),s.set(e,r);for(var y=o;++g{var uKe=Fl(),gKe=Ts(),fKe=uKe(gKe,"DataView");V$.exports=fKe});var $$=w((agt,Z$)=>{var hKe=Fl(),pKe=Ts(),dKe=hKe(pKe,"Promise");Z$.exports=dKe});var tee=w((Agt,eee)=>{var CKe=Fl(),mKe=Ts(),EKe=CKe(mKe,"Set");eee.exports=EKe});var iee=w((lgt,ree)=>{var IKe=Fl(),yKe=Ts(),wKe=IKe(yKe,"WeakMap");ree.exports=wKe});var PC=w((cgt,nee)=>{var lF=X$(),cF=ZB(),uF=$$(),gF=tee(),fF=iee(),see=zc(),jf=vR(),oee="[object Map]",BKe="[object Object]",aee="[object Promise]",Aee="[object Set]",lee="[object WeakMap]",cee="[object DataView]",bKe=jf(lF),QKe=jf(cF),SKe=jf(uF),vKe=jf(gF),kKe=jf(fF),pu=see;(lF&&pu(new lF(new ArrayBuffer(1)))!=cee||cF&&pu(new cF)!=oee||uF&&pu(uF.resolve())!=aee||gF&&pu(new gF)!=Aee||fF&&pu(new fF)!=lee)&&(pu=function(r){var e=see(r),t=e==BKe?r.constructor:void 0,i=t?jf(t):"";if(i)switch(i){case bKe:return cee;case QKe:return oee;case SKe:return aee;case vKe:return Aee;case kKe:return lee}return e});nee.exports=pu});var mee=w((ugt,uee)=>{var hF=xC(),xKe=iF(),PKe=U$(),DKe=_$(),gee=PC(),fee=Hs(),hee=QC(),RKe=f0(),FKe=1,pee="[object Arguments]",dee="[object Array]",C0="[object Object]",NKe=Object.prototype,Cee=NKe.hasOwnProperty;function LKe(r,e,t,i,n,s){var o=fee(r),a=fee(e),l=o?dee:gee(r),c=a?dee:gee(e);l=l==pee?C0:l,c=c==pee?C0:c;var u=l==C0,g=c==C0,f=l==c;if(f&&hee(r)){if(!hee(e))return!1;o=!0,u=!1}if(f&&!u)return s||(s=new hF),o||RKe(r)?xKe(r,e,t,i,n,s):PKe(r,e,l,t,i,n,s);if(!(t&FKe)){var h=u&&Cee.call(r,"__wrapped__"),p=g&&Cee.call(e,"__wrapped__");if(h||p){var m=h?r.value():r,y=p?e.value():e;return s||(s=new hF),n(m,y,t,i,s)}}return f?(s||(s=new hF),DKe(r,e,t,i,n,s)):!1}uee.exports=LKe});var pF=w((ggt,Eee)=>{var TKe=mee(),Iee=ra();function yee(r,e,t,i,n){return r===e?!0:r==null||e==null||!Iee(r)&&!Iee(e)?r!==r&&e!==e:TKe(r,e,t,i,yee,n)}Eee.exports=yee});var Bee=w((fgt,wee)=>{var OKe=xC(),MKe=pF(),UKe=1,KKe=2;function HKe(r,e,t,i){var n=t.length,s=n,o=!i;if(r==null)return!s;for(r=Object(r);n--;){var a=t[n];if(o&&a[2]?a[1]!==r[a[0]]:!(a[0]in r))return!1}for(;++n{var jKe=Fn();function GKe(r){return r===r&&!jKe(r)}bee.exports=GKe});var See=w((pgt,Qee)=>{var YKe=dF(),qKe=Kf();function JKe(r){for(var e=qKe(r),t=e.length;t--;){var i=e[t],n=r[i];e[t]=[i,n,YKe(n)]}return e}Qee.exports=JKe});var CF=w((dgt,vee)=>{function WKe(r,e){return function(t){return t==null?!1:t[r]===e&&(e!==void 0||r in Object(t))}}vee.exports=WKe});var xee=w((Cgt,kee)=>{var zKe=Bee(),_Ke=See(),VKe=CF();function XKe(r){var e=_Ke(r);return e.length==1&&e[0][2]?VKe(e[0][0],e[0][1]):function(t){return t===r||zKe(t,r,e)}}kee.exports=XKe});var m0=w((mgt,Pee)=>{var ZKe=pC();function $Ke(r,e,t){var i=r==null?void 0:ZKe(r,e);return i===void 0?t:i}Pee.exports=$Ke});var Ree=w((Egt,Dee)=>{var e2e=pF(),t2e=m0(),r2e=RR(),i2e=VB(),n2e=dF(),s2e=CF(),o2e=fu(),a2e=1,A2e=2;function l2e(r,e){return i2e(r)&&n2e(e)?s2e(o2e(r),e):function(t){var i=t2e(t,r);return i===void 0&&i===e?r2e(t,r):e2e(e,i,a2e|A2e)}}Dee.exports=l2e});var Nee=w((Igt,Fee)=>{function c2e(r){return function(e){return e==null?void 0:e[r]}}Fee.exports=c2e});var Tee=w((ygt,Lee)=>{var u2e=pC();function g2e(r){return function(e){return u2e(e,r)}}Lee.exports=g2e});var Mee=w((wgt,Oee)=>{var f2e=Nee(),h2e=Tee(),p2e=VB(),d2e=fu();function C2e(r){return p2e(r)?f2e(d2e(r)):h2e(r)}Oee.exports=C2e});var mF=w((Bgt,Uee)=>{var m2e=xee(),E2e=Ree(),I2e=i0(),y2e=Hs(),w2e=Mee();function B2e(r){return typeof r=="function"?r:r==null?I2e:typeof r=="object"?y2e(r)?E2e(r[0],r[1]):m2e(r):w2e(r)}Uee.exports=B2e});var EF=w((bgt,Kee)=>{var b2e=Lf(),Q2e=rF(),S2e=mF();function v2e(r,e){var t={};return e=S2e(e,3),Q2e(r,function(i,n,s){b2e(t,n,e(i,n,s))}),t}Kee.exports=v2e});var DC=w((Qgt,Hee)=>{"use strict";function du(r){this._maxSize=r,this.clear()}du.prototype.clear=function(){this._size=0,this._values=Object.create(null)};du.prototype.get=function(r){return this._values[r]};du.prototype.set=function(r,e){return this._size>=this._maxSize&&this.clear(),r in this._values||this._size++,this._values[r]=e};var k2e=/[^.^\]^[]+|(?=\[\]|\.\.)/g,jee=/^\d+$/,x2e=/^\d/,P2e=/[~`!#$%\^&*+=\-\[\]\\';,/{}|\\":<>\?]/g,D2e=/^\s*(['"]?)(.*?)(\1)\s*$/,IF=512,Gee=new du(IF),Yee=new du(IF),qee=new du(IF);Hee.exports={Cache:du,split:wF,normalizePath:yF,setter:function(r){var e=yF(r);return Yee.get(r)||Yee.set(r,function(i,n){for(var s=0,o=e.length,a=i;s{"use strict";Object.defineProperty(RC,"__esModule",{value:!0});RC.create=T2e;RC.default=void 0;var O2e=DC(),E0={context:"$",value:"."};function T2e(r,e){return new I0(r,e)}var I0=class{constructor(e,t={}){if(typeof e!="string")throw new TypeError("ref must be a string, got: "+e);if(this.key=e.trim(),e==="")throw new TypeError("ref must be a non-empty string");this.isContext=this.key[0]===E0.context,this.isValue=this.key[0]===E0.value,this.isSibling=!this.isContext&&!this.isValue;let i=this.isContext?E0.context:this.isValue?E0.value:"";this.path=this.key.slice(i.length),this.getter=this.path&&(0,O2e.getter)(this.path,!0),this.map=t.map}getValue(e,t,i){let n=this.isContext?i:this.isValue?e:t;return this.getter&&(n=this.getter(n||{})),this.map&&(n=this.map(n)),n}cast(e,t){return this.getValue(e,t==null?void 0:t.parent,t==null?void 0:t.context)}resolve(){return this}describe(){return{type:"ref",key:this.key}}toString(){return`Ref(${this.key})`}static isRef(e){return e&&e.__isYupRef}};RC.default=I0;I0.prototype.__isYupRef=!0});var Jee=w(bF=>{"use strict";Object.defineProperty(bF,"__esModule",{value:!0});bF.default=M2e;var U2e=QF(EF()),y0=QF(hu()),K2e=QF(Cu());function QF(r){return r&&r.__esModule?r:{default:r}}function w0(){return w0=Object.assign||function(r){for(var e=1;e=0)&&(t[n]=r[n]);return t}function M2e(r){function e(t,i){let{value:n,path:s="",label:o,options:a,originalValue:l,sync:c}=t,u=H2e(t,["value","path","label","options","originalValue","sync"]),{name:g,test:f,params:h,message:p}=r,{parent:m,context:y}=a;function b(q){return K2e.default.isRef(q)?q.getValue(n,m,y):q}function v(q={}){let $=(0,U2e.default)(w0({value:n,originalValue:l,label:o,path:q.path||s},h,q.params),b),z=new y0.default(y0.default.formatError(q.message||p,$),n,$.path,q.type||g);return z.params=$,z}let k=w0({path:s,parent:m,type:g,createError:v,resolve:b,options:a,originalValue:l},u);if(!c){try{Promise.resolve(f.call(k,n,k)).then(q=>{y0.default.isError(q)?i(q):q?i(null,q):i(v())})}catch(q){i(q)}return}let T;try{var Y;if(T=f.call(k,n,k),typeof((Y=T)==null?void 0:Y.then)=="function")throw new Error(`Validation test of type: "${k.type}" returned a Promise during a synchronous validate. This test will finish after the validate call has returned`)}catch(q){i(q);return}y0.default.isError(T)?i(T):T?i(null,T):i(v())}return e.OPTIONS=r,e}});var SF=w(FC=>{"use strict";Object.defineProperty(FC,"__esModule",{value:!0});FC.getIn=Wee;FC.default=void 0;var j2e=DC(),G2e=r=>r.substr(0,r.length-1).substr(1);function Wee(r,e,t,i=t){let n,s,o;return e?((0,j2e.forEach)(e,(a,l,c)=>{let u=l?G2e(a):a;if(r=r.resolve({context:i,parent:n,value:t}),r.innerType){let g=c?parseInt(u,10):0;if(t&&g>=t.length)throw new Error(`Yup.reach cannot resolve an array item at index: ${a}, in the path: ${e}. because there is no value at that index. `);n=t,t=t&&t[g],r=r.innerType}if(!c){if(!r.fields||!r.fields[u])throw new Error(`The schema does not contain the path: ${e}. (failed at: ${o} which is a type: "${r._type}")`);n=t,t=t&&t[u],r=r.fields[u]}s=u,o=l?"["+a+"]":"."+a}),{schema:r,parent:n,parentPath:s}):{parent:n,parentPath:e,schema:r}}var Y2e=(r,e,t,i)=>Wee(r,e,t,i).schema,q2e=Y2e;FC.default=q2e});var _ee=w(B0=>{"use strict";Object.defineProperty(B0,"__esModule",{value:!0});B0.default=void 0;var zee=J2e(Cu());function J2e(r){return r&&r.__esModule?r:{default:r}}var b0=class{constructor(){this.list=new Set,this.refs=new Map}get size(){return this.list.size+this.refs.size}describe(){let e=[];for(let t of this.list)e.push(t);for(let[,t]of this.refs)e.push(t.describe());return e}toArray(){return Array.from(this.list).concat(Array.from(this.refs.values()))}add(e){zee.default.isRef(e)?this.refs.set(e.key,e):this.list.add(e)}delete(e){zee.default.isRef(e)?this.refs.delete(e.key):this.list.delete(e)}has(e,t){if(this.list.has(e))return!0;let i,n=this.refs.values();for(;i=n.next(),!i.done;)if(t(i.value)===e)return!0;return!1}clone(){let e=new b0;return e.list=new Set(this.list),e.refs=new Map(this.refs),e}merge(e,t){let i=this.clone();return e.list.forEach(n=>i.add(n)),e.refs.forEach(n=>i.add(n)),t.list.forEach(n=>i.delete(n)),t.refs.forEach(n=>i.delete(n)),i}};B0.default=b0});var IA=w(Q0=>{"use strict";Object.defineProperty(Q0,"__esModule",{value:!0});Q0.default=void 0;var Vee=EA(mZ()),Gf=mA(),W2e=EA(FZ()),Xee=EA(c0()),S0=EA(Jee()),Zee=EA(yC()),z2e=EA(Cu()),_2e=SF(),V2e=EA(zR()),$ee=EA(hu()),ete=EA(_ee());function EA(r){return r&&r.__esModule?r:{default:r}}function zs(){return zs=Object.assign||function(r){for(var e=1;e{this.typeError(Gf.mixed.notType)}),this.type=(e==null?void 0:e.type)||"mixed",this.spec=zs({strip:!1,strict:!1,abortEarly:!0,recursive:!0,nullable:!1,presence:"optional"},e==null?void 0:e.spec)}get _type(){return this.type}_typeCheck(e){return!0}clone(e){if(this._mutate)return e&&Object.assign(this.spec,e),this;let t=Object.create(Object.getPrototypeOf(this));return t.type=this.type,t._typeError=this._typeError,t._whitelistError=this._whitelistError,t._blacklistError=this._blacklistError,t._whitelist=this._whitelist.clone(),t._blacklist=this._blacklist.clone(),t.exclusiveTests=zs({},this.exclusiveTests),t.deps=[...this.deps],t.conditions=[...this.conditions],t.tests=[...this.tests],t.transforms=[...this.transforms],t.spec=(0,Vee.default)(zs({},this.spec,e)),t}label(e){var t=this.clone();return t.spec.label=e,t}meta(...e){if(e.length===0)return this.spec.meta;let t=this.clone();return t.spec.meta=Object.assign(t.spec.meta||{},e[0]),t}withMutation(e){let t=this._mutate;this._mutate=!0;let i=e(this);return this._mutate=t,i}concat(e){if(!e||e===this)return this;if(e.type!==this.type&&this.type!=="mixed")throw new TypeError(`You cannot \`concat()\` schema's of different types: ${this.type} and ${e.type}`);let t=this,i=e.clone(),n=zs({},t.spec,i.spec);return i.spec=n,i._typeError||(i._typeError=t._typeError),i._whitelistError||(i._whitelistError=t._whitelistError),i._blacklistError||(i._blacklistError=t._blacklistError),i._whitelist=t._whitelist.merge(e._whitelist,e._blacklist),i._blacklist=t._blacklist.merge(e._blacklist,e._whitelist),i.tests=t.tests,i.exclusiveTests=t.exclusiveTests,i.withMutation(s=>{e.tests.forEach(o=>{s.test(o.OPTIONS)})}),i}isType(e){return this.spec.nullable&&e===null?!0:this._typeCheck(e)}resolve(e){let t=this;if(t.conditions.length){let i=t.conditions;t=t.clone(),t.conditions=[],t=i.reduce((n,s)=>s.resolve(n,e),t),t=t.resolve(e)}return t}cast(e,t={}){let i=this.resolve(zs({value:e},t)),n=i._cast(e,t);if(e!==void 0&&t.assert!==!1&&i.isType(n)!==!0){let s=(0,Zee.default)(e),o=(0,Zee.default)(n);throw new TypeError(`The value of ${t.path||"field"} could not be cast to a value that satisfies the schema type: "${i._type}". + +attempted value: ${s} +`+(o!==s?`result of cast: ${o}`:""))}return n}_cast(e,t){let i=e===void 0?e:this.transforms.reduce((n,s)=>s.call(this,n,e,this),e);return i===void 0&&(i=this.getDefault()),i}_validate(e,t={},i){let{sync:n,path:s,from:o=[],originalValue:a=e,strict:l=this.spec.strict,abortEarly:c=this.spec.abortEarly}=t,u=e;l||(u=this._cast(u,zs({assert:!1},t)));let g={value:u,path:s,options:t,originalValue:a,schema:this,label:this.spec.label,sync:n,from:o},f=[];this._typeError&&f.push(this._typeError),this._whitelistError&&f.push(this._whitelistError),this._blacklistError&&f.push(this._blacklistError),(0,Xee.default)({args:g,value:u,path:s,sync:n,tests:f,endEarly:c},h=>{if(h)return void i(h,u);(0,Xee.default)({tests:this.tests,args:g,path:s,sync:n,value:u,endEarly:c},i)})}validate(e,t,i){let n=this.resolve(zs({},t,{value:e}));return typeof i=="function"?n._validate(e,t,i):new Promise((s,o)=>n._validate(e,t,(a,l)=>{a?o(a):s(l)}))}validateSync(e,t){let i=this.resolve(zs({},t,{value:e})),n;return i._validate(e,zs({},t,{sync:!0}),(s,o)=>{if(s)throw s;n=o}),n}isValid(e,t){return this.validate(e,t).then(()=>!0,i=>{if($ee.default.isError(i))return!1;throw i})}isValidSync(e,t){try{return this.validateSync(e,t),!0}catch(i){if($ee.default.isError(i))return!1;throw i}}_getDefault(){let e=this.spec.default;return e==null?e:typeof e=="function"?e.call(this):(0,Vee.default)(e)}getDefault(e){return this.resolve(e||{})._getDefault()}default(e){return arguments.length===0?this._getDefault():this.clone({default:e})}strict(e=!0){var t=this.clone();return t.spec.strict=e,t}_isPresent(e){return e!=null}defined(e=Gf.mixed.defined){return this.test({message:e,name:"defined",exclusive:!0,test(t){return t!==void 0}})}required(e=Gf.mixed.required){return this.clone({presence:"required"}).withMutation(t=>t.test({message:e,name:"required",exclusive:!0,test(i){return this.schema._isPresent(i)}}))}notRequired(){var e=this.clone({presence:"optional"});return e.tests=e.tests.filter(t=>t.OPTIONS.name!=="required"),e}nullable(e=!0){var t=this.clone({nullable:e!==!1});return t}transform(e){var t=this.clone();return t.transforms.push(e),t}test(...e){let t;if(e.length===1?typeof e[0]=="function"?t={test:e[0]}:t=e[0]:e.length===2?t={name:e[0],test:e[1]}:t={name:e[0],message:e[1],test:e[2]},t.message===void 0&&(t.message=Gf.mixed.default),typeof t.test!="function")throw new TypeError("`test` is a required parameters");let i=this.clone(),n=(0,S0.default)(t),s=t.exclusive||t.name&&i.exclusiveTests[t.name]===!0;if(t.exclusive&&!t.name)throw new TypeError("Exclusive tests must provide a unique `name` identifying the test");return t.name&&(i.exclusiveTests[t.name]=!!t.exclusive),i.tests=i.tests.filter(o=>!(o.OPTIONS.name===t.name&&(s||o.OPTIONS.test===n.OPTIONS.test))),i.tests.push(n),i}when(e,t){!Array.isArray(e)&&typeof e!="string"&&(t=e,e=".");let i=this.clone(),n=(0,V2e.default)(e).map(s=>new z2e.default(s));return n.forEach(s=>{s.isSibling&&i.deps.push(s.key)}),i.conditions.push(new W2e.default(n,t)),i}typeError(e){var t=this.clone();return t._typeError=(0,S0.default)({message:e,name:"typeError",test(i){return i!==void 0&&!this.schema.isType(i)?this.createError({params:{type:this.schema._type}}):!0}}),t}oneOf(e,t=Gf.mixed.oneOf){var i=this.clone();return e.forEach(n=>{i._whitelist.add(n),i._blacklist.delete(n)}),i._whitelistError=(0,S0.default)({message:t,name:"oneOf",test(n){if(n===void 0)return!0;let s=this.schema._whitelist;return s.has(n,this.resolve)?!0:this.createError({params:{values:s.toArray().join(", ")}})}}),i}notOneOf(e,t=Gf.mixed.notOneOf){var i=this.clone();return e.forEach(n=>{i._blacklist.add(n),i._whitelist.delete(n)}),i._blacklistError=(0,S0.default)({message:t,name:"notOneOf",test(n){let s=this.schema._blacklist;return s.has(n,this.resolve)?this.createError({params:{values:s.toArray().join(", ")}}):!0}}),i}strip(e=!0){let t=this.clone();return t.spec.strip=e,t}describe(){let e=this.clone(),{label:t,meta:i}=e.spec;return{meta:i,label:t,type:e.type,oneOf:e._whitelist.describe(),notOneOf:e._blacklist.describe(),tests:e.tests.map(s=>({name:s.OPTIONS.name,params:s.OPTIONS.params})).filter((s,o,a)=>a.findIndex(l=>l.name===s.name)===o)}}};Q0.default=ga;ga.prototype.__isYupSchema__=!0;for(let r of["validate","validateSync"])ga.prototype[`${r}At`]=function(e,t,i={}){let{parent:n,parentPath:s,schema:o}=(0,_2e.getIn)(this,e,t,i.context);return o[r](n&&n[s],zs({},i,{parent:n,path:e}))};for(let r of["equals","is"])ga.prototype[r]=ga.prototype.oneOf;for(let r of["not","nope"])ga.prototype[r]=ga.prototype.notOneOf;ga.prototype.optional=ga.prototype.notRequired});var rte=w(NC=>{"use strict";Object.defineProperty(NC,"__esModule",{value:!0});NC.create=tte;NC.default=void 0;var Z2e=X2e(IA());function X2e(r){return r&&r.__esModule?r:{default:r}}var vF=Z2e.default,$2e=vF;NC.default=$2e;function tte(){return new vF}tte.prototype=vF.prototype});var Yf=w(v0=>{"use strict";Object.defineProperty(v0,"__esModule",{value:!0});v0.default=void 0;var eHe=r=>r==null;v0.default=eHe});var ate=w(LC=>{"use strict";Object.defineProperty(LC,"__esModule",{value:!0});LC.create=ite;LC.default=void 0;var tHe=nte(IA()),ste=mA(),ote=nte(Yf());function nte(r){return r&&r.__esModule?r:{default:r}}function ite(){return new k0}var k0=class extends tHe.default{constructor(){super({type:"boolean"});this.withMutation(()=>{this.transform(function(e){if(!this.isType(e)){if(/^(true|1)$/i.test(String(e)))return!0;if(/^(false|0)$/i.test(String(e)))return!1}return e})})}_typeCheck(e){return e instanceof Boolean&&(e=e.valueOf()),typeof e=="boolean"}isTrue(e=ste.boolean.isValue){return this.test({message:e,name:"is-value",exclusive:!0,params:{value:"true"},test(t){return(0,ote.default)(t)||t===!0}})}isFalse(e=ste.boolean.isValue){return this.test({message:e,name:"is-value",exclusive:!0,params:{value:"false"},test(t){return(0,ote.default)(t)||t===!1}})}};LC.default=k0;ite.prototype=k0.prototype});var cte=w(TC=>{"use strict";Object.defineProperty(TC,"__esModule",{value:!0});TC.create=Ate;TC.default=void 0;var fa=mA(),yA=lte(Yf()),rHe=lte(IA());function lte(r){return r&&r.__esModule?r:{default:r}}var iHe=/^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i,nHe=/^((https?|ftp):)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i,sHe=/^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i,oHe=r=>(0,yA.default)(r)||r===r.trim(),aHe={}.toString();function Ate(){return new x0}var x0=class extends rHe.default{constructor(){super({type:"string"});this.withMutation(()=>{this.transform(function(e){if(this.isType(e)||Array.isArray(e))return e;let t=e!=null&&e.toString?e.toString():e;return t===aHe?e:t})})}_typeCheck(e){return e instanceof String&&(e=e.valueOf()),typeof e=="string"}_isPresent(e){return super._isPresent(e)&&!!e.length}length(e,t=fa.string.length){return this.test({message:t,name:"length",exclusive:!0,params:{length:e},test(i){return(0,yA.default)(i)||i.length===this.resolve(e)}})}min(e,t=fa.string.min){return this.test({message:t,name:"min",exclusive:!0,params:{min:e},test(i){return(0,yA.default)(i)||i.length>=this.resolve(e)}})}max(e,t=fa.string.max){return this.test({name:"max",exclusive:!0,message:t,params:{max:e},test(i){return(0,yA.default)(i)||i.length<=this.resolve(e)}})}matches(e,t){let i=!1,n,s;return t&&(typeof t=="object"?{excludeEmptyString:i=!1,message:n,name:s}=t:n=t),this.test({name:s||"matches",message:n||fa.string.matches,params:{regex:e},test:o=>(0,yA.default)(o)||o===""&&i||o.search(e)!==-1})}email(e=fa.string.email){return this.matches(iHe,{name:"email",message:e,excludeEmptyString:!0})}url(e=fa.string.url){return this.matches(nHe,{name:"url",message:e,excludeEmptyString:!0})}uuid(e=fa.string.uuid){return this.matches(sHe,{name:"uuid",message:e,excludeEmptyString:!1})}ensure(){return this.default("").transform(e=>e===null?"":e)}trim(e=fa.string.trim){return this.transform(t=>t!=null?t.trim():t).test({message:e,name:"trim",test:oHe})}lowercase(e=fa.string.lowercase){return this.transform(t=>(0,yA.default)(t)?t:t.toLowerCase()).test({message:e,name:"string_case",exclusive:!0,test:t=>(0,yA.default)(t)||t===t.toLowerCase()})}uppercase(e=fa.string.uppercase){return this.transform(t=>(0,yA.default)(t)?t:t.toUpperCase()).test({message:e,name:"string_case",exclusive:!0,test:t=>(0,yA.default)(t)||t===t.toUpperCase()})}};TC.default=x0;Ate.prototype=x0.prototype});var fte=w(OC=>{"use strict";Object.defineProperty(OC,"__esModule",{value:!0});OC.create=ute;OC.default=void 0;var mu=mA(),Eu=gte(Yf()),AHe=gte(IA());function gte(r){return r&&r.__esModule?r:{default:r}}var lHe=r=>r!=+r;function ute(){return new P0}var P0=class extends AHe.default{constructor(){super({type:"number"});this.withMutation(()=>{this.transform(function(e){let t=e;if(typeof t=="string"){if(t=t.replace(/\s/g,""),t==="")return NaN;t=+t}return this.isType(t)?t:parseFloat(t)})})}_typeCheck(e){return e instanceof Number&&(e=e.valueOf()),typeof e=="number"&&!lHe(e)}min(e,t=mu.number.min){return this.test({message:t,name:"min",exclusive:!0,params:{min:e},test(i){return(0,Eu.default)(i)||i>=this.resolve(e)}})}max(e,t=mu.number.max){return this.test({message:t,name:"max",exclusive:!0,params:{max:e},test(i){return(0,Eu.default)(i)||i<=this.resolve(e)}})}lessThan(e,t=mu.number.lessThan){return this.test({message:t,name:"max",exclusive:!0,params:{less:e},test(i){return(0,Eu.default)(i)||ithis.resolve(e)}})}positive(e=mu.number.positive){return this.moreThan(0,e)}negative(e=mu.number.negative){return this.lessThan(0,e)}integer(e=mu.number.integer){return this.test({name:"integer",message:e,test:t=>(0,Eu.default)(t)||Number.isInteger(t)})}truncate(){return this.transform(e=>(0,Eu.default)(e)?e:e|0)}round(e){var t,i=["ceil","floor","round","trunc"];if(e=((t=e)==null?void 0:t.toLowerCase())||"round",e==="trunc")return this.truncate();if(i.indexOf(e.toLowerCase())===-1)throw new TypeError("Only valid options for round() are: "+i.join(", "));return this.transform(n=>(0,Eu.default)(n)?n:Math[e](n))}};OC.default=P0;ute.prototype=P0.prototype});var hte=w(kF=>{"use strict";Object.defineProperty(kF,"__esModule",{value:!0});kF.default=cHe;var uHe=/^(\d{4}|[+\-]\d{6})(?:-?(\d{2})(?:-?(\d{2}))?)?(?:[ T]?(\d{2}):?(\d{2})(?::?(\d{2})(?:[,\.](\d{1,}))?)?(?:(Z)|([+\-])(\d{2})(?::?(\d{2}))?)?)?$/;function cHe(r){var e=[1,4,5,6,7,10,11],t=0,i,n;if(n=uHe.exec(r)){for(var s=0,o;o=e[s];++s)n[o]=+n[o]||0;n[2]=(+n[2]||1)-1,n[3]=+n[3]||1,n[7]=n[7]?String(n[7]).substr(0,3):0,(n[8]===void 0||n[8]==="")&&(n[9]===void 0||n[9]==="")?i=+new Date(n[1],n[2],n[3],n[4],n[5],n[6],n[7]):(n[8]!=="Z"&&n[9]!==void 0&&(t=n[10]*60+n[11],n[9]==="+"&&(t=0-t)),i=Date.UTC(n[1],n[2],n[3],n[4],n[5]+t,n[6],n[7]))}else i=Date.parse?Date.parse(r):NaN;return i}});var Cte=w(MC=>{"use strict";Object.defineProperty(MC,"__esModule",{value:!0});MC.create=xF;MC.default=void 0;var gHe=D0(hte()),pte=mA(),dte=D0(Yf()),fHe=D0(Cu()),hHe=D0(IA());function D0(r){return r&&r.__esModule?r:{default:r}}var PF=new Date(""),pHe=r=>Object.prototype.toString.call(r)==="[object Date]";function xF(){return new UC}var UC=class extends hHe.default{constructor(){super({type:"date"});this.withMutation(()=>{this.transform(function(e){return this.isType(e)?e:(e=(0,gHe.default)(e),isNaN(e)?PF:new Date(e))})})}_typeCheck(e){return pHe(e)&&!isNaN(e.getTime())}prepareParam(e,t){let i;if(fHe.default.isRef(e))i=e;else{let n=this.cast(e);if(!this._typeCheck(n))throw new TypeError(`\`${t}\` must be a Date or a value that can be \`cast()\` to a Date`);i=n}return i}min(e,t=pte.date.min){let i=this.prepareParam(e,"min");return this.test({message:t,name:"min",exclusive:!0,params:{min:e},test(n){return(0,dte.default)(n)||n>=this.resolve(i)}})}max(e,t=pte.date.max){var i=this.prepareParam(e,"max");return this.test({message:t,name:"max",exclusive:!0,params:{max:e},test(n){return(0,dte.default)(n)||n<=this.resolve(i)}})}};MC.default=UC;UC.INVALID_DATE=PF;xF.prototype=UC.prototype;xF.INVALID_DATE=PF});var Ete=w((Mgt,mte)=>{function dHe(r,e,t,i){var n=-1,s=r==null?0:r.length;for(i&&s&&(t=r[++n]);++n{function CHe(r){return function(e){return r==null?void 0:r[e]}}Ite.exports=CHe});var Bte=w((Kgt,wte)=>{var mHe=yte(),EHe={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},IHe=mHe(EHe);wte.exports=IHe});var Qte=w((Hgt,bte)=>{var yHe=Bte(),wHe=cf(),BHe=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,bHe="\\u0300-\\u036f",QHe="\\ufe20-\\ufe2f",SHe="\\u20d0-\\u20ff",vHe=bHe+QHe+SHe,kHe="["+vHe+"]",xHe=RegExp(kHe,"g");function PHe(r){return r=wHe(r),r&&r.replace(BHe,yHe).replace(xHe,"")}bte.exports=PHe});var vte=w((jgt,Ste)=>{var DHe=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g;function RHe(r){return r.match(DHe)||[]}Ste.exports=RHe});var xte=w((Ggt,kte)=>{var FHe=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/;function NHe(r){return FHe.test(r)}kte.exports=NHe});var zte=w((Ygt,Pte)=>{var Dte="\\ud800-\\udfff",LHe="\\u0300-\\u036f",THe="\\ufe20-\\ufe2f",OHe="\\u20d0-\\u20ff",MHe=LHe+THe+OHe,Rte="\\u2700-\\u27bf",Fte="a-z\\xdf-\\xf6\\xf8-\\xff",UHe="\\xac\\xb1\\xd7\\xf7",KHe="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",HHe="\\u2000-\\u206f",jHe=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Nte="A-Z\\xc0-\\xd6\\xd8-\\xde",GHe="\\ufe0e\\ufe0f",Lte=UHe+KHe+HHe+jHe,Tte="['\u2019]",Ote="["+Lte+"]",YHe="["+MHe+"]",Mte="\\d+",qHe="["+Rte+"]",Ute="["+Fte+"]",Kte="[^"+Dte+Lte+Mte+Rte+Fte+Nte+"]",JHe="\\ud83c[\\udffb-\\udfff]",WHe="(?:"+YHe+"|"+JHe+")",zHe="[^"+Dte+"]",Hte="(?:\\ud83c[\\udde6-\\uddff]){2}",jte="[\\ud800-\\udbff][\\udc00-\\udfff]",qf="["+Nte+"]",_He="\\u200d",Gte="(?:"+Ute+"|"+Kte+")",VHe="(?:"+qf+"|"+Kte+")",Yte="(?:"+Tte+"(?:d|ll|m|re|s|t|ve))?",qte="(?:"+Tte+"(?:D|LL|M|RE|S|T|VE))?",Jte=WHe+"?",Wte="["+GHe+"]?",XHe="(?:"+_He+"(?:"+[zHe,Hte,jte].join("|")+")"+Wte+Jte+")*",ZHe="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",$He="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",eje=Wte+Jte+XHe,tje="(?:"+[qHe,Hte,jte].join("|")+")"+eje,rje=RegExp([qf+"?"+Ute+"+"+Yte+"(?="+[Ote,qf,"$"].join("|")+")",VHe+"+"+qte+"(?="+[Ote,qf+Gte,"$"].join("|")+")",qf+"?"+Gte+"+"+Yte,qf+"+"+qte,$He,ZHe,Mte,tje].join("|"),"g");function ije(r){return r.match(rje)||[]}Pte.exports=ije});var Vte=w((qgt,_te)=>{var nje=vte(),sje=xte(),oje=cf(),aje=zte();function Aje(r,e,t){return r=oje(r),e=t?void 0:e,e===void 0?sje(r)?aje(r):nje(r):r.match(e)||[]}_te.exports=Aje});var DF=w((Jgt,Xte)=>{var lje=Ete(),cje=Qte(),uje=Vte(),gje="['\u2019]",fje=RegExp(gje,"g");function hje(r){return function(e){return lje(uje(cje(e).replace(fje,"")),r,"")}}Xte.exports=hje});var $te=w((Wgt,Zte)=>{var pje=DF(),dje=pje(function(r,e,t){return r+(t?"_":"")+e.toLowerCase()});Zte.exports=dje});var tre=w((zgt,ere)=>{var Cje=$w(),mje=DF(),Eje=mje(function(r,e,t){return e=e.toLowerCase(),r+(t?Cje(e):e)});ere.exports=Eje});var ire=w((_gt,rre)=>{var Ije=Lf(),yje=rF(),wje=mF();function Bje(r,e){var t={};return e=wje(e,3),yje(r,function(i,n,s){Ije(t,e(i,n,s),i)}),t}rre.exports=Bje});var sre=w((Vgt,RF)=>{RF.exports=function(r){return nre(bje(r),r)};RF.exports.array=nre;function nre(r,e){var t=r.length,i=new Array(t),n={},s=t,o=Qje(e),a=Sje(r);for(e.forEach(function(c){if(!a.has(c[0])||!a.has(c[1]))throw new Error("Unknown node. There is an unknown node in the supplied edges.")});s--;)n[s]||l(r[s],s,new Set);return i;function l(c,u,g){if(g.has(c)){var f;try{f=", node was:"+JSON.stringify(c)}catch(m){f=""}throw new Error("Cyclic dependency"+f)}if(!a.has(c))throw new Error("Found unknown node. Make sure to provided all involved nodes. Unknown node: "+JSON.stringify(c));if(!n[u]){n[u]=!0;var h=o.get(c)||new Set;if(h=Array.from(h),u=h.length){g.add(c);do{var p=h[--u];l(p,a.get(p),g)}while(u);g.delete(c)}i[--t]=c}}}function bje(r){for(var e=new Set,t=0,i=r.length;t{"use strict";Object.defineProperty(FF,"__esModule",{value:!0});FF.default=vje;var kje=R0(wC()),xje=R0(sre()),Pje=DC(),Dje=R0(Cu()),Rje=R0(Of());function R0(r){return r&&r.__esModule?r:{default:r}}function vje(r,e=[]){let t=[],i=[];function n(s,o){var a=(0,Pje.split)(s)[0];~i.indexOf(a)||i.push(a),~e.indexOf(`${o}-${a}`)||t.push([o,a])}for(let s in r)if((0,kje.default)(r,s)){let o=r[s];~i.indexOf(s)||i.push(s),Dje.default.isRef(o)&&o.isSibling?n(o.path,s):(0,Rje.default)(o)&&"deps"in o&&o.deps.forEach(a=>n(a,s))}return xje.default.array(i,t).reverse()}});var Are=w(NF=>{"use strict";Object.defineProperty(NF,"__esModule",{value:!0});NF.default=Fje;function are(r,e){let t=Infinity;return r.some((i,n)=>{var s;if(((s=e.path)==null?void 0:s.indexOf(i))!==-1)return t=n,!0}),t}function Fje(r){return(e,t)=>are(r,e)-are(r,t)}});var pre=w(KC=>{"use strict";Object.defineProperty(KC,"__esModule",{value:!0});KC.create=lre;KC.default=void 0;var cre=ha(wC()),ure=ha($te()),Nje=ha(tre()),Lje=ha(ire()),Tje=ha(EF()),Oje=DC(),gre=mA(),Mje=ha(ore()),fre=ha(Are()),Uje=ha(c0()),Kje=ha(hu()),LF=ha(IA());function ha(r){return r&&r.__esModule?r:{default:r}}function Jf(){return Jf=Object.assign||function(r){for(var e=1;eObject.prototype.toString.call(r)==="[object Object]";function Hje(r,e){let t=Object.keys(r.fields);return Object.keys(e).filter(i=>t.indexOf(i)===-1)}var jje=(0,fre.default)([]),F0=class extends LF.default{constructor(e){super({type:"object"});this.fields=Object.create(null),this._sortErrors=jje,this._nodes=[],this._excludedEdges=[],this.withMutation(()=>{this.transform(function(i){if(typeof i=="string")try{i=JSON.parse(i)}catch(n){i=null}return this.isType(i)?i:null}),e&&this.shape(e)})}_typeCheck(e){return hre(e)||typeof e=="function"}_cast(e,t={}){var i;let n=super._cast(e,t);if(n===void 0)return this.getDefault();if(!this._typeCheck(n))return n;let s=this.fields,o=(i=t.stripUnknown)!=null?i:this.spec.noUnknown,a=this._nodes.concat(Object.keys(n).filter(g=>this._nodes.indexOf(g)===-1)),l={},c=Jf({},t,{parent:l,__validating:t.__validating||!1}),u=!1;for(let g of a){let f=s[g],h=(0,cre.default)(n,g);if(f){let p,m=n[g];c.path=(t.path?`${t.path}.`:"")+g,f=f.resolve({value:m,context:t.context,parent:l});let y="spec"in f?f.spec:void 0,b=y==null?void 0:y.strict;if(y==null?void 0:y.strip){u=u||g in n;continue}p=!t.__validating||!b?f.cast(n[g],c):n[g],p!==void 0&&(l[g]=p)}else h&&!o&&(l[g]=n[g]);l[g]!==n[g]&&(u=!0)}return u?l:n}_validate(e,t={},i){let n=[],{sync:s,from:o=[],originalValue:a=e,abortEarly:l=this.spec.abortEarly,recursive:c=this.spec.recursive}=t;o=[{schema:this,value:a},...o],t.__validating=!0,t.originalValue=a,t.from=o,super._validate(e,t,(u,g)=>{if(u){if(!Kje.default.isError(u)||l)return void i(u,g);n.push(u)}if(!c||!hre(g)){i(n[0]||null,g);return}a=a||g;let f=this._nodes.map(h=>(p,m)=>{let y=h.indexOf(".")===-1?(t.path?`${t.path}.`:"")+h:`${t.path||""}["${h}"]`,b=this.fields[h];if(b&&"validate"in b){b.validate(g[h],Jf({},t,{path:y,from:o,strict:!0,parent:g,originalValue:a[h]}),m);return}m(null)});(0,Uje.default)({sync:s,tests:f,value:g,errors:n,endEarly:l,sort:this._sortErrors,path:t.path},i)})}clone(e){let t=super.clone(e);return t.fields=Jf({},this.fields),t._nodes=this._nodes,t._excludedEdges=this._excludedEdges,t._sortErrors=this._sortErrors,t}concat(e){let t=super.concat(e),i=t.fields;for(let[n,s]of Object.entries(this.fields)){let o=i[n];o===void 0?i[n]=s:o instanceof LF.default&&s instanceof LF.default&&(i[n]=s.concat(o))}return t.withMutation(()=>t.shape(i))}getDefaultFromShape(){let e={};return this._nodes.forEach(t=>{let i=this.fields[t];e[t]="default"in i?i.getDefault():void 0}),e}_getDefault(){if("default"in this.spec)return super._getDefault();if(!!this._nodes.length)return this.getDefaultFromShape()}shape(e,t=[]){let i=this.clone(),n=Object.assign(i.fields,e);if(i.fields=n,i._sortErrors=(0,fre.default)(Object.keys(n)),t.length){Array.isArray(t[0])||(t=[t]);let s=t.map(([o,a])=>`${o}-${a}`);i._excludedEdges=i._excludedEdges.concat(s)}return i._nodes=(0,Mje.default)(n,i._excludedEdges),i}pick(e){let t={};for(let i of e)this.fields[i]&&(t[i]=this.fields[i]);return this.clone().withMutation(i=>(i.fields={},i.shape(t)))}omit(e){let t=this.clone(),i=t.fields;t.fields={};for(let n of e)delete i[n];return t.withMutation(()=>t.shape(i))}from(e,t,i){let n=(0,Oje.getter)(e,!0);return this.transform(s=>{if(s==null)return s;let o=s;return(0,cre.default)(s,e)&&(o=Jf({},s),i||delete o[e],o[t]=n(s)),o})}noUnknown(e=!0,t=gre.object.noUnknown){typeof e=="string"&&(t=e,e=!0);let i=this.test({name:"noUnknown",exclusive:!0,message:t,test(n){if(n==null)return!0;let s=Hje(this.schema,n);return!e||s.length===0||this.createError({params:{unknown:s.join(", ")}})}});return i.spec.noUnknown=e,i}unknown(e=!0,t=gre.object.noUnknown){return this.noUnknown(!e,t)}transformKeys(e){return this.transform(t=>t&&(0,Lje.default)(t,(i,n)=>e(n)))}camelCase(){return this.transformKeys(Nje.default)}snakeCase(){return this.transformKeys(ure.default)}constantCase(){return this.transformKeys(e=>(0,ure.default)(e).toUpperCase())}describe(){let e=super.describe();return e.fields=(0,Tje.default)(this.fields,t=>t.describe()),e}};KC.default=F0;function lre(r){return new F0(r)}lre.prototype=F0.prototype});var Cre=w(HC=>{"use strict";Object.defineProperty(HC,"__esModule",{value:!0});HC.create=dre;HC.default=void 0;var TF=Wf(Yf()),Gje=Wf(Of()),Yje=Wf(yC()),OF=mA(),qje=Wf(c0()),Jje=Wf(hu()),Wje=Wf(IA());function Wf(r){return r&&r.__esModule?r:{default:r}}function N0(){return N0=Object.assign||function(r){for(var e=1;e{this.transform(function(t){if(typeof t=="string")try{t=JSON.parse(t)}catch(i){t=null}return this.isType(t)?t:null})})}_typeCheck(e){return Array.isArray(e)}get _subType(){return this.innerType}_cast(e,t){let i=super._cast(e,t);if(!this._typeCheck(i)||!this.innerType)return i;let n=!1,s=i.map((o,a)=>{let l=this.innerType.cast(o,N0({},t,{path:`${t.path||""}[${a}]`}));return l!==o&&(n=!0),l});return n?s:i}_validate(e,t={},i){var n,s;let o=[],a=t.sync,l=t.path,c=this.innerType,u=(n=t.abortEarly)!=null?n:this.spec.abortEarly,g=(s=t.recursive)!=null?s:this.spec.recursive,f=t.originalValue!=null?t.originalValue:e;super._validate(e,t,(h,p)=>{if(h){if(!Jje.default.isError(h)||u)return void i(h,p);o.push(h)}if(!g||!c||!this._typeCheck(p)){i(o[0]||null,p);return}f=f||p;let m=new Array(p.length);for(let y=0;yc.validate(b,k,Y)}(0,qje.default)({sync:a,path:l,value:p,errors:o,endEarly:u,tests:m},i)})}clone(e){let t=super.clone(e);return t.innerType=this.innerType,t}concat(e){let t=super.concat(e);return t.innerType=this.innerType,e.innerType&&(t.innerType=t.innerType?t.innerType.concat(e.innerType):e.innerType),t}of(e){let t=this.clone();if(!(0,Gje.default)(e))throw new TypeError("`array.of()` sub-schema must be a valid yup schema not: "+(0,Yje.default)(e));return t.innerType=e,t}length(e,t=OF.array.length){return this.test({message:t,name:"length",exclusive:!0,params:{length:e},test(i){return(0,TF.default)(i)||i.length===this.resolve(e)}})}min(e,t){return t=t||OF.array.min,this.test({message:t,name:"min",exclusive:!0,params:{min:e},test(i){return(0,TF.default)(i)||i.length>=this.resolve(e)}})}max(e,t){return t=t||OF.array.max,this.test({message:t,name:"max",exclusive:!0,params:{max:e},test(i){return(0,TF.default)(i)||i.length<=this.resolve(e)}})}ensure(){return this.default(()=>[]).transform((e,t)=>this._typeCheck(e)?e:t==null?[]:[].concat(t))}compact(e){let t=e?(i,n,s)=>!e(i,n,s):i=>!!i;return this.transform(i=>i!=null?i.filter(t):i)}describe(){let e=super.describe();return this.innerType&&(e.innerType=this.innerType.describe()),e}nullable(e=!0){return super.nullable(e)}defined(){return super.defined()}required(e){return super.required(e)}};HC.default=L0;dre.prototype=L0.prototype});var mre=w(jC=>{"use strict";Object.defineProperty(jC,"__esModule",{value:!0});jC.create=zje;jC.default=void 0;var Vje=_je(Of());function _je(r){return r&&r.__esModule?r:{default:r}}function zje(r){return new MF(r)}var MF=class{constructor(e){this.type="lazy",this.__isYupSchema__=!0,this._resolve=(t,i={})=>{let n=this.builder(t,i);if(!(0,Vje.default)(n))throw new TypeError("lazy() functions must return a valid schema");return n.resolve(i)},this.builder=e}resolve(e){return this._resolve(e.value,e)}cast(e,t){return this._resolve(e,t).cast(e,t)}validate(e,t,i){return this._resolve(e,t).validate(e,t,i)}validateSync(e,t){return this._resolve(e,t).validateSync(e,t)}validateAt(e,t,i){return this._resolve(t,i).validateAt(e,t,i)}validateSyncAt(e,t,i){return this._resolve(t,i).validateSyncAt(e,t,i)}describe(){return null}isValid(e,t){return this._resolve(e,t).isValid(e,t)}isValidSync(e,t){return this._resolve(e,t).isValidSync(e,t)}},Xje=MF;jC.default=Xje});var Ere=w(UF=>{"use strict";Object.defineProperty(UF,"__esModule",{value:!0});UF.default=Zje;var eGe=$je(mA());function $je(r){return r&&r.__esModule?r:{default:r}}function Zje(r){Object.keys(r).forEach(e=>{Object.keys(r[e]).forEach(t=>{eGe.default[e][t]=r[e][t]})})}});var HF=w(Br=>{"use strict";Object.defineProperty(Br,"__esModule",{value:!0});Br.addMethod=tGe;Object.defineProperty(Br,"MixedSchema",{enumerable:!0,get:function(){return Ire.default}});Object.defineProperty(Br,"mixed",{enumerable:!0,get:function(){return Ire.create}});Object.defineProperty(Br,"BooleanSchema",{enumerable:!0,get:function(){return KF.default}});Object.defineProperty(Br,"bool",{enumerable:!0,get:function(){return KF.create}});Object.defineProperty(Br,"boolean",{enumerable:!0,get:function(){return KF.create}});Object.defineProperty(Br,"StringSchema",{enumerable:!0,get:function(){return yre.default}});Object.defineProperty(Br,"string",{enumerable:!0,get:function(){return yre.create}});Object.defineProperty(Br,"NumberSchema",{enumerable:!0,get:function(){return wre.default}});Object.defineProperty(Br,"number",{enumerable:!0,get:function(){return wre.create}});Object.defineProperty(Br,"DateSchema",{enumerable:!0,get:function(){return Bre.default}});Object.defineProperty(Br,"date",{enumerable:!0,get:function(){return Bre.create}});Object.defineProperty(Br,"ObjectSchema",{enumerable:!0,get:function(){return bre.default}});Object.defineProperty(Br,"object",{enumerable:!0,get:function(){return bre.create}});Object.defineProperty(Br,"ArraySchema",{enumerable:!0,get:function(){return Qre.default}});Object.defineProperty(Br,"array",{enumerable:!0,get:function(){return Qre.create}});Object.defineProperty(Br,"ref",{enumerable:!0,get:function(){return rGe.create}});Object.defineProperty(Br,"lazy",{enumerable:!0,get:function(){return iGe.create}});Object.defineProperty(Br,"ValidationError",{enumerable:!0,get:function(){return nGe.default}});Object.defineProperty(Br,"reach",{enumerable:!0,get:function(){return sGe.default}});Object.defineProperty(Br,"isSchema",{enumerable:!0,get:function(){return Sre.default}});Object.defineProperty(Br,"setLocale",{enumerable:!0,get:function(){return oGe.default}});Object.defineProperty(Br,"BaseSchema",{enumerable:!0,get:function(){return aGe.default}});var Ire=Iu(rte()),KF=Iu(ate()),yre=Iu(cte()),wre=Iu(fte()),Bre=Iu(Cte()),bre=Iu(pre()),Qre=Iu(Cre()),rGe=Cu(),iGe=mre(),nGe=GC(hu()),sGe=GC(SF()),Sre=GC(Of()),oGe=GC(Ere()),aGe=GC(IA());function GC(r){return r&&r.__esModule?r:{default:r}}function vre(){if(typeof WeakMap!="function")return null;var r=new WeakMap;return vre=function(){return r},r}function Iu(r){if(r&&r.__esModule)return r;if(r===null||typeof r!="object"&&typeof r!="function")return{default:r};var e=vre();if(e&&e.has(r))return e.get(r);var t={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var n in r)if(Object.prototype.hasOwnProperty.call(r,n)){var s=i?Object.getOwnPropertyDescriptor(r,n):null;s&&(s.get||s.set)?Object.defineProperty(t,n,s):t[n]=r[n]}return t.default=r,e&&e.set(r,t),t}function tGe(r,e,t){if(!r||!(0,Sre.default)(r.prototype))throw new TypeError("You must provide a yup schema constructor function");if(typeof e!="string")throw new TypeError("A Method name must be provided");if(typeof t!="function")throw new TypeError("Method function must be provided");r.prototype[e]=t}});var Rre=w((dft,qC)=>{"use strict";var cGe=process.env.TERM_PROGRAM==="Hyper",uGe=process.platform==="win32",xre=process.platform==="linux",jF={ballotDisabled:"\u2612",ballotOff:"\u2610",ballotOn:"\u2611",bullet:"\u2022",bulletWhite:"\u25E6",fullBlock:"\u2588",heart:"\u2764",identicalTo:"\u2261",line:"\u2500",mark:"\u203B",middot:"\xB7",minus:"\uFF0D",multiplication:"\xD7",obelus:"\xF7",pencilDownRight:"\u270E",pencilRight:"\u270F",pencilUpRight:"\u2710",percent:"%",pilcrow2:"\u2761",pilcrow:"\xB6",plusMinus:"\xB1",section:"\xA7",starsOff:"\u2606",starsOn:"\u2605",upDownArrow:"\u2195"},Pre=Object.assign({},jF,{check:"\u221A",cross:"\xD7",ellipsisLarge:"...",ellipsis:"...",info:"i",question:"?",questionSmall:"?",pointer:">",pointerSmall:"\xBB",radioOff:"( )",radioOn:"(*)",warning:"\u203C"}),Dre=Object.assign({},jF,{ballotCross:"\u2718",check:"\u2714",cross:"\u2716",ellipsisLarge:"\u22EF",ellipsis:"\u2026",info:"\u2139",question:"?",questionFull:"\uFF1F",questionSmall:"\uFE56",pointer:xre?"\u25B8":"\u276F",pointerSmall:xre?"\u2023":"\u203A",radioOff:"\u25EF",radioOn:"\u25C9",warning:"\u26A0"});qC.exports=uGe&&!cGe?Pre:Dre;Reflect.defineProperty(qC.exports,"common",{enumerable:!1,value:jF});Reflect.defineProperty(qC.exports,"windows",{enumerable:!1,value:Pre});Reflect.defineProperty(qC.exports,"other",{enumerable:!1,value:Dre})});var yo=w((Cft,GF)=>{"use strict";var gGe=r=>r!==null&&typeof r=="object"&&!Array.isArray(r),fGe=/[\u001b\u009b][[\]#;?()]*(?:(?:(?:[^\W_]*;?[^\W_]*)\u0007)|(?:(?:[0-9]{1,4}(;[0-9]{0,4})*)?[~0-9=<>cf-nqrtyA-PRZ]))/g,Fre=()=>{let r={enabled:!0,visible:!0,styles:{},keys:{}};"FORCE_COLOR"in process.env&&(r.enabled=process.env.FORCE_COLOR!=="0");let e=s=>{let o=s.open=`[${s.codes[0]}m`,a=s.close=`[${s.codes[1]}m`,l=s.regex=new RegExp(`\\u001b\\[${s.codes[1]}m`,"g");return s.wrap=(c,u)=>{c.includes(a)&&(c=c.replace(l,a+o));let g=o+c+a;return u?g.replace(/\r*\n/g,`${a}$&${o}`):g},s},t=(s,o,a)=>typeof s=="function"?s(o):s.wrap(o,a),i=(s,o)=>{if(s===""||s==null)return"";if(r.enabled===!1)return s;if(r.visible===!1)return"";let a=""+s,l=a.includes(` +`),c=o.length;for(c>0&&o.includes("unstyle")&&(o=[...new Set(["unstyle",...o])].reverse());c-- >0;)a=t(r.styles[o[c]],a,l);return a},n=(s,o,a)=>{r.styles[s]=e({name:s,codes:o}),(r.keys[a]||(r.keys[a]=[])).push(s),Reflect.defineProperty(r,s,{configurable:!0,enumerable:!0,set(c){r.alias(s,c)},get(){let c=u=>i(u,c.stack);return Reflect.setPrototypeOf(c,r),c.stack=this.stack?this.stack.concat(s):[s],c}})};return n("reset",[0,0],"modifier"),n("bold",[1,22],"modifier"),n("dim",[2,22],"modifier"),n("italic",[3,23],"modifier"),n("underline",[4,24],"modifier"),n("inverse",[7,27],"modifier"),n("hidden",[8,28],"modifier"),n("strikethrough",[9,29],"modifier"),n("black",[30,39],"color"),n("red",[31,39],"color"),n("green",[32,39],"color"),n("yellow",[33,39],"color"),n("blue",[34,39],"color"),n("magenta",[35,39],"color"),n("cyan",[36,39],"color"),n("white",[37,39],"color"),n("gray",[90,39],"color"),n("grey",[90,39],"color"),n("bgBlack",[40,49],"bg"),n("bgRed",[41,49],"bg"),n("bgGreen",[42,49],"bg"),n("bgYellow",[43,49],"bg"),n("bgBlue",[44,49],"bg"),n("bgMagenta",[45,49],"bg"),n("bgCyan",[46,49],"bg"),n("bgWhite",[47,49],"bg"),n("blackBright",[90,39],"bright"),n("redBright",[91,39],"bright"),n("greenBright",[92,39],"bright"),n("yellowBright",[93,39],"bright"),n("blueBright",[94,39],"bright"),n("magentaBright",[95,39],"bright"),n("cyanBright",[96,39],"bright"),n("whiteBright",[97,39],"bright"),n("bgBlackBright",[100,49],"bgBright"),n("bgRedBright",[101,49],"bgBright"),n("bgGreenBright",[102,49],"bgBright"),n("bgYellowBright",[103,49],"bgBright"),n("bgBlueBright",[104,49],"bgBright"),n("bgMagentaBright",[105,49],"bgBright"),n("bgCyanBright",[106,49],"bgBright"),n("bgWhiteBright",[107,49],"bgBright"),r.ansiRegex=fGe,r.hasColor=r.hasAnsi=s=>(r.ansiRegex.lastIndex=0,typeof s=="string"&&s!==""&&r.ansiRegex.test(s)),r.alias=(s,o)=>{let a=typeof o=="string"?r[o]:o;if(typeof a!="function")throw new TypeError("Expected alias to be the name of an existing color (string) or a function");a.stack||(Reflect.defineProperty(a,"name",{value:s}),r.styles[s]=a,a.stack=[s]),Reflect.defineProperty(r,s,{configurable:!0,enumerable:!0,set(l){r.alias(s,l)},get(){let l=c=>i(c,l.stack);return Reflect.setPrototypeOf(l,r),l.stack=this.stack?this.stack.concat(a.stack):a.stack,l}})},r.theme=s=>{if(!gGe(s))throw new TypeError("Expected theme to be an object");for(let o of Object.keys(s))r.alias(o,s[o]);return r},r.alias("unstyle",s=>typeof s=="string"&&s!==""?(r.ansiRegex.lastIndex=0,s.replace(r.ansiRegex,"")):""),r.alias("noop",s=>s),r.none=r.clear=r.noop,r.stripColor=r.unstyle,r.symbols=Rre(),r.define=n,r};GF.exports=Fre();GF.exports.create=Fre});var Xi=w(Lt=>{"use strict";var hGe=Object.prototype.toString,_s=yo(),Nre=!1,YF=[],Lre={yellow:"blue",cyan:"red",green:"magenta",black:"white",blue:"yellow",red:"cyan",magenta:"green",white:"black"};Lt.longest=(r,e)=>r.reduce((t,i)=>Math.max(t,e?i[e].length:i.length),0);Lt.hasColor=r=>!!r&&_s.hasColor(r);var O0=Lt.isObject=r=>r!==null&&typeof r=="object"&&!Array.isArray(r);Lt.nativeType=r=>hGe.call(r).slice(8,-1).toLowerCase().replace(/\s/g,"");Lt.isAsyncFn=r=>Lt.nativeType(r)==="asyncfunction";Lt.isPrimitive=r=>r!=null&&typeof r!="object"&&typeof r!="function";Lt.resolve=(r,e,...t)=>typeof e=="function"?e.call(r,...t):e;Lt.scrollDown=(r=[])=>[...r.slice(1),r[0]];Lt.scrollUp=(r=[])=>[r.pop(),...r];Lt.reorder=(r=[])=>{let e=r.slice();return e.sort((t,i)=>t.index>i.index?1:t.index{let i=r.length,n=t===i?0:t<0?i-1:t,s=r[e];r[e]=r[n],r[n]=s};Lt.width=(r,e=80)=>{let t=r&&r.columns?r.columns:e;return r&&typeof r.getWindowSize=="function"&&(t=r.getWindowSize()[0]),process.platform==="win32"?t-1:t};Lt.height=(r,e=20)=>{let t=r&&r.rows?r.rows:e;return r&&typeof r.getWindowSize=="function"&&(t=r.getWindowSize()[1]),t};Lt.wordWrap=(r,e={})=>{if(!r)return r;typeof e=="number"&&(e={width:e});let{indent:t="",newline:i=` +`+t,width:n=80}=e;n-=((i+t).match(/[^\S\n]/g)||[]).length;let o=`.{1,${n}}([\\s\\u200B]+|$)|[^\\s\\u200B]+?([\\s\\u200B]+|$)`,a=r.trim(),l=new RegExp(o,"g"),c=a.match(l)||[];return c=c.map(u=>u.replace(/\n$/,"")),e.padEnd&&(c=c.map(u=>u.padEnd(n," "))),e.padStart&&(c=c.map(u=>u.padStart(n," "))),t+c.join(i)};Lt.unmute=r=>{let e=r.stack.find(i=>_s.keys.color.includes(i));return e?_s[e]:r.stack.find(i=>i.slice(2)==="bg")?_s[e.slice(2)]:i=>i};Lt.pascal=r=>r?r[0].toUpperCase()+r.slice(1):"";Lt.inverse=r=>{if(!r||!r.stack)return r;let e=r.stack.find(i=>_s.keys.color.includes(i));if(e){let i=_s["bg"+Lt.pascal(e)];return i?i.black:r}let t=r.stack.find(i=>i.slice(0,2)==="bg");return t?_s[t.slice(2).toLowerCase()]||r:_s.none};Lt.complement=r=>{if(!r||!r.stack)return r;let e=r.stack.find(i=>_s.keys.color.includes(i)),t=r.stack.find(i=>i.slice(0,2)==="bg");if(e&&!t)return _s[Lre[e]||e];if(t){let i=t.slice(2).toLowerCase(),n=Lre[i];return n&&_s["bg"+Lt.pascal(n)]||r}return _s.none};Lt.meridiem=r=>{let e=r.getHours(),t=r.getMinutes(),i=e>=12?"pm":"am";e=e%12;let n=e===0?12:e,s=t<10?"0"+t:t;return n+":"+s+" "+i};Lt.set=(r={},e="",t)=>e.split(".").reduce((i,n,s,o)=>{let a=o.length-1>s?i[n]||{}:t;return!Lt.isObject(a)&&s{let i=r[e]==null?e.split(".").reduce((n,s)=>n&&n[s],r):r[e];return i==null?t:i};Lt.mixin=(r,e)=>{if(!O0(r))return e;if(!O0(e))return r;for(let t of Object.keys(e)){let i=Object.getOwnPropertyDescriptor(e,t);if(i.hasOwnProperty("value"))if(r.hasOwnProperty(t)&&O0(i.value)){let n=Object.getOwnPropertyDescriptor(r,t);O0(n.value)?r[t]=Lt.merge({},r[t],e[t]):Reflect.defineProperty(r,t,i)}else Reflect.defineProperty(r,t,i);else Reflect.defineProperty(r,t,i)}return r};Lt.merge=(...r)=>{let e={};for(let t of r)Lt.mixin(e,t);return e};Lt.mixinEmitter=(r,e)=>{let t=e.constructor.prototype;for(let i of Object.keys(t)){let n=t[i];typeof n=="function"?Lt.define(r,i,n.bind(e)):Lt.define(r,i,n)}};Lt.onExit=r=>{let e=(t,i)=>{Nre||(Nre=!0,YF.forEach(n=>n()),t===!0&&process.exit(128+i))};YF.length===0&&(process.once("SIGTERM",e.bind(null,!0,15)),process.once("SIGINT",e.bind(null,!0,2)),process.once("exit",e)),YF.push(r)};Lt.define=(r,e,t)=>{Reflect.defineProperty(r,e,{value:t})};Lt.defineExport=(r,e,t)=>{let i;Reflect.defineProperty(r,e,{enumerable:!0,configurable:!0,set(n){i=n},get(){return i?i():t()}})}});var Tre=w(_f=>{"use strict";_f.ctrl={a:"first",b:"backward",c:"cancel",d:"deleteForward",e:"last",f:"forward",g:"reset",i:"tab",k:"cutForward",l:"reset",n:"newItem",m:"cancel",j:"submit",p:"search",r:"remove",s:"save",u:"undo",w:"cutLeft",x:"toggleCursor",v:"paste"};_f.shift={up:"shiftUp",down:"shiftDown",left:"shiftLeft",right:"shiftRight",tab:"prev"};_f.fn={up:"pageUp",down:"pageDown",left:"pageLeft",right:"pageRight",delete:"deleteForward"};_f.option={b:"backward",f:"forward",d:"cutRight",left:"cutLeft",up:"altUp",down:"altDown"};_f.keys={pageup:"pageUp",pagedown:"pageDown",home:"home",end:"end",cancel:"cancel",delete:"deleteForward",backspace:"delete",down:"down",enter:"submit",escape:"cancel",left:"left",space:"space",number:"number",return:"submit",right:"right",tab:"next",up:"up"}});var Ure=w((Ift,Ore)=>{"use strict";var Mre=require("readline"),pGe=Tre(),dGe=/^(?:\x1b)([a-zA-Z0-9])$/,CGe=/^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/,mGe={OP:"f1",OQ:"f2",OR:"f3",OS:"f4","[11~":"f1","[12~":"f2","[13~":"f3","[14~":"f4","[[A":"f1","[[B":"f2","[[C":"f3","[[D":"f4","[[E":"f5","[15~":"f5","[17~":"f6","[18~":"f7","[19~":"f8","[20~":"f9","[21~":"f10","[23~":"f11","[24~":"f12","[A":"up","[B":"down","[C":"right","[D":"left","[E":"clear","[F":"end","[H":"home",OA:"up",OB:"down",OC:"right",OD:"left",OE:"clear",OF:"end",OH:"home","[1~":"home","[2~":"insert","[3~":"delete","[4~":"end","[5~":"pageup","[6~":"pagedown","[[5~":"pageup","[[6~":"pagedown","[7~":"home","[8~":"end","[a":"up","[b":"down","[c":"right","[d":"left","[e":"clear","[2$":"insert","[3$":"delete","[5$":"pageup","[6$":"pagedown","[7$":"home","[8$":"end",Oa:"up",Ob:"down",Oc:"right",Od:"left",Oe:"clear","[2^":"insert","[3^":"delete","[5^":"pageup","[6^":"pagedown","[7^":"home","[8^":"end","[Z":"tab"};function EGe(r){return["[a","[b","[c","[d","[e","[2$","[3$","[5$","[6$","[7$","[8$","[Z"].includes(r)}function IGe(r){return["Oa","Ob","Oc","Od","Oe","[2^","[3^","[5^","[6^","[7^","[8^"].includes(r)}var M0=(r="",e={})=>{let t,i=N({name:e.name,ctrl:!1,meta:!1,shift:!1,option:!1,sequence:r,raw:r},e);if(Buffer.isBuffer(r)?r[0]>127&&r[1]===void 0?(r[0]-=128,r=""+String(r)):r=String(r):r!==void 0&&typeof r!="string"?r=String(r):r||(r=i.sequence||""),i.sequence=i.sequence||r||i.name,r==="\r")i.raw=void 0,i.name="return";else if(r===` +`)i.name="enter";else if(r===" ")i.name="tab";else if(r==="\b"||r==="\x7F"||r==="\x7F"||r==="\b")i.name="backspace",i.meta=r.charAt(0)==="";else if(r===""||r==="")i.name="escape",i.meta=r.length===2;else if(r===" "||r===" ")i.name="space",i.meta=r.length===2;else if(r<="")i.name=String.fromCharCode(r.charCodeAt(0)+"a".charCodeAt(0)-1),i.ctrl=!0;else if(r.length===1&&r>="0"&&r<="9")i.name="number";else if(r.length===1&&r>="a"&&r<="z")i.name=r;else if(r.length===1&&r>="A"&&r<="Z")i.name=r.toLowerCase(),i.shift=!0;else if(t=dGe.exec(r))i.meta=!0,i.shift=/^[A-Z]$/.test(t[1]);else if(t=CGe.exec(r)){let n=[...r];n[0]===""&&n[1]===""&&(i.option=!0);let s=[t[1],t[2],t[4],t[6]].filter(Boolean).join(""),o=(t[3]||t[5]||1)-1;i.ctrl=!!(o&4),i.meta=!!(o&10),i.shift=!!(o&1),i.code=s,i.name=mGe[s],i.shift=EGe(s)||i.shift,i.ctrl=IGe(s)||i.ctrl}return i};M0.listen=(r={},e)=>{let{stdin:t}=r;if(!t||t!==process.stdin&&!t.isTTY)throw new Error("Invalid stream passed");let i=Mre.createInterface({terminal:!0,input:t});Mre.emitKeypressEvents(t,i);let n=(a,l)=>e(a,M0(a,l),i),s=t.isRaw;return t.isTTY&&t.setRawMode(!0),t.on("keypress",n),i.resume(),()=>{t.isTTY&&t.setRawMode(s),t.removeListener("keypress",n),i.pause(),i.close()}};M0.action=(r,e,t)=>{let i=N(N({},pGe),t);return e.ctrl?(e.action=i.ctrl[e.name],e):e.option&&i.option?(e.action=i.option[e.name],e):e.shift?(e.action=i.shift[e.name],e):(e.action=i.keys[e.name],e)};Ore.exports=M0});var Hre=w((yft,Kre)=>{"use strict";Kre.exports=r=>{r.timers=r.timers||{};let e=r.options.timers;if(!!e)for(let t of Object.keys(e)){let i=e[t];typeof i=="number"&&(i={interval:i}),yGe(r,t,i)}};function yGe(r,e,t={}){let i=r.timers[e]={name:e,start:Date.now(),ms:0,tick:0},n=t.interval||120;i.frames=t.frames||[],i.loading=!0;let s=setInterval(()=>{i.ms=Date.now()-i.start,i.tick++,r.render()},n);return i.stop=()=>{i.loading=!1,clearInterval(s)},Reflect.defineProperty(i,"interval",{value:s}),r.once("close",()=>i.stop()),i.stop}});var Yre=w((wft,jre)=>{"use strict";var{define:wGe,width:BGe}=Xi(),Gre=class{constructor(e){let t=e.options;wGe(this,"_prompt",e),this.type=e.type,this.name=e.name,this.message="",this.header="",this.footer="",this.error="",this.hint="",this.input="",this.cursor=0,this.index=0,this.lines=0,this.tick=0,this.prompt="",this.buffer="",this.width=BGe(t.stdout||process.stdout),Object.assign(this,t),this.name=this.name||this.message,this.message=this.message||this.name,this.symbols=e.symbols,this.styles=e.styles,this.required=new Set,this.cancelled=!1,this.submitted=!1}clone(){let e=N({},this);return e.status=this.status,e.buffer=Buffer.from(e.buffer),delete e.clone,e}set color(e){this._color=e}get color(){let e=this.prompt.styles;if(this.cancelled)return e.cancelled;if(this.submitted)return e.submitted;let t=this._color||e[this.status];return typeof t=="function"?t:e.pending}set loading(e){this._loading=e}get loading(){return typeof this._loading=="boolean"?this._loading:this.loadingChoices?"choices":!1}get status(){return this.cancelled?"cancelled":this.submitted?"submitted":"pending"}};jre.exports=Gre});var Jre=w((Bft,qre)=>{"use strict";var qF=Xi(),Ni=yo(),JF={default:Ni.noop,noop:Ni.noop,set inverse(r){this._inverse=r},get inverse(){return this._inverse||qF.inverse(this.primary)},set complement(r){this._complement=r},get complement(){return this._complement||qF.complement(this.primary)},primary:Ni.cyan,success:Ni.green,danger:Ni.magenta,strong:Ni.bold,warning:Ni.yellow,muted:Ni.dim,disabled:Ni.gray,dark:Ni.dim.gray,underline:Ni.underline,set info(r){this._info=r},get info(){return this._info||this.primary},set em(r){this._em=r},get em(){return this._em||this.primary.underline},set heading(r){this._heading=r},get heading(){return this._heading||this.muted.underline},set pending(r){this._pending=r},get pending(){return this._pending||this.primary},set submitted(r){this._submitted=r},get submitted(){return this._submitted||this.success},set cancelled(r){this._cancelled=r},get cancelled(){return this._cancelled||this.danger},set typing(r){this._typing=r},get typing(){return this._typing||this.dim},set placeholder(r){this._placeholder=r},get placeholder(){return this._placeholder||this.primary.dim},set highlight(r){this._highlight=r},get highlight(){return this._highlight||this.inverse}};JF.merge=(r={})=>{r.styles&&typeof r.styles.enabled=="boolean"&&(Ni.enabled=r.styles.enabled),r.styles&&typeof r.styles.visible=="boolean"&&(Ni.visible=r.styles.visible);let e=qF.merge({},JF,r.styles);delete e.merge;for(let t of Object.keys(Ni))e.hasOwnProperty(t)||Reflect.defineProperty(e,t,{get:()=>Ni[t]});for(let t of Object.keys(Ni.styles))e.hasOwnProperty(t)||Reflect.defineProperty(e,t,{get:()=>Ni[t]});return e};qre.exports=JF});var zre=w((bft,Wre)=>{"use strict";var WF=process.platform==="win32",wA=yo(),bGe=Xi(),zF=te(N({},wA.symbols),{upDownDoubleArrow:"\u21D5",upDownDoubleArrow2:"\u2B0D",upDownArrow:"\u2195",asterisk:"*",asterism:"\u2042",bulletWhite:"\u25E6",electricArrow:"\u2301",ellipsisLarge:"\u22EF",ellipsisSmall:"\u2026",fullBlock:"\u2588",identicalTo:"\u2261",indicator:wA.symbols.check,leftAngle:"\u2039",mark:"\u203B",minus:"\u2212",multiplication:"\xD7",obelus:"\xF7",percent:"%",pilcrow:"\xB6",pilcrow2:"\u2761",pencilUpRight:"\u2710",pencilDownRight:"\u270E",pencilRight:"\u270F",plus:"+",plusMinus:"\xB1",pointRight:"\u261E",rightAngle:"\u203A",section:"\xA7",hexagon:{off:"\u2B21",on:"\u2B22",disabled:"\u2B22"},ballot:{on:"\u2611",off:"\u2610",disabled:"\u2612"},stars:{on:"\u2605",off:"\u2606",disabled:"\u2606"},folder:{on:"\u25BC",off:"\u25B6",disabled:"\u25B6"},prefix:{pending:wA.symbols.question,submitted:wA.symbols.check,cancelled:wA.symbols.cross},separator:{pending:wA.symbols.pointerSmall,submitted:wA.symbols.middot,cancelled:wA.symbols.middot},radio:{off:WF?"( )":"\u25EF",on:WF?"(*)":"\u25C9",disabled:WF?"(|)":"\u24BE"},numbers:["\u24EA","\u2460","\u2461","\u2462","\u2463","\u2464","\u2465","\u2466","\u2467","\u2468","\u2469","\u246A","\u246B","\u246C","\u246D","\u246E","\u246F","\u2470","\u2471","\u2472","\u2473","\u3251","\u3252","\u3253","\u3254","\u3255","\u3256","\u3257","\u3258","\u3259","\u325A","\u325B","\u325C","\u325D","\u325E","\u325F","\u32B1","\u32B2","\u32B3","\u32B4","\u32B5","\u32B6","\u32B7","\u32B8","\u32B9","\u32BA","\u32BB","\u32BC","\u32BD","\u32BE","\u32BF"]});zF.merge=r=>{let e=bGe.merge({},wA.symbols,zF,r.symbols);return delete e.merge,e};Wre.exports=zF});var Vre=w((Qft,_re)=>{"use strict";var QGe=Jre(),SGe=zre(),vGe=Xi();_re.exports=r=>{r.options=vGe.merge({},r.options.theme,r.options),r.symbols=SGe.merge(r.options),r.styles=QGe.merge(r.options)}});var tie=w((Xre,Zre)=>{"use strict";var $re=process.env.TERM_PROGRAM==="Apple_Terminal",kGe=yo(),_F=Xi(),wo=Zre.exports=Xre,Lr="[",eie="\x07",VF=!1,Nl=wo.code={bell:eie,beep:eie,beginning:`${Lr}G`,down:`${Lr}J`,esc:Lr,getPosition:`${Lr}6n`,hide:`${Lr}?25l`,line:`${Lr}2K`,lineEnd:`${Lr}K`,lineStart:`${Lr}1K`,restorePosition:Lr+($re?"8":"u"),savePosition:Lr+($re?"7":"s"),screen:`${Lr}2J`,show:`${Lr}?25h`,up:`${Lr}1J`},yu=wo.cursor={get hidden(){return VF},hide(){return VF=!0,Nl.hide},show(){return VF=!1,Nl.show},forward:(r=1)=>`${Lr}${r}C`,backward:(r=1)=>`${Lr}${r}D`,nextLine:(r=1)=>`${Lr}E`.repeat(r),prevLine:(r=1)=>`${Lr}F`.repeat(r),up:(r=1)=>r?`${Lr}${r}A`:"",down:(r=1)=>r?`${Lr}${r}B`:"",right:(r=1)=>r?`${Lr}${r}C`:"",left:(r=1)=>r?`${Lr}${r}D`:"",to(r,e){return e?`${Lr}${e+1};${r+1}H`:`${Lr}${r+1}G`},move(r=0,e=0){let t="";return t+=r<0?yu.left(-r):r>0?yu.right(r):"",t+=e<0?yu.up(-e):e>0?yu.down(e):"",t},restore(r={}){let{after:e,cursor:t,initial:i,input:n,prompt:s,size:o,value:a}=r;if(i=_F.isPrimitive(i)?String(i):"",n=_F.isPrimitive(n)?String(n):"",a=_F.isPrimitive(a)?String(a):"",o){let l=wo.cursor.up(o)+wo.cursor.to(s.length),c=n.length-t;return c>0&&(l+=wo.cursor.left(c)),l}if(a||e){let l=!n&&!!i?-i.length:-n.length+t;return e&&(l-=e.length),n===""&&i&&!s.includes(i)&&(l+=i.length),wo.cursor.move(l)}}},XF=wo.erase={screen:Nl.screen,up:Nl.up,down:Nl.down,line:Nl.line,lineEnd:Nl.lineEnd,lineStart:Nl.lineStart,lines(r){let e="";for(let t=0;t{if(!e)return XF.line+yu.to(0);let t=s=>[...kGe.unstyle(s)].length,i=r.split(/\r?\n/),n=0;for(let s of i)n+=1+Math.floor(Math.max(t(s)-1,0)/e);return(XF.line+yu.prevLine()).repeat(n-1)+XF.line+yu.to(0)}});var Vf=w((Sft,rie)=>{"use strict";var xGe=require("events"),iie=yo(),ZF=Ure(),PGe=Hre(),DGe=Yre(),RGe=Vre(),On=Xi(),wu=tie(),U0=class extends xGe{constructor(e={}){super();this.name=e.name,this.type=e.type,this.options=e,RGe(this),PGe(this),this.state=new DGe(this),this.initial=[e.initial,e.default].find(t=>t!=null),this.stdout=e.stdout||process.stdout,this.stdin=e.stdin||process.stdin,this.scale=e.scale||1,this.term=this.options.term||process.env.TERM_PROGRAM,this.margin=NGe(this.options.margin),this.setMaxListeners(0),FGe(this)}async keypress(e,t={}){this.keypressed=!0;let i=ZF.action(e,ZF(e,t),this.options.actions);this.state.keypress=i,this.emit("keypress",e,i),this.emit("state",this.state.clone());let n=this.options[i.action]||this[i.action]||this.dispatch;if(typeof n=="function")return await n.call(this,e,i);this.alert()}alert(){delete this.state.alert,this.options.show===!1?this.emit("alert"):this.stdout.write(wu.code.beep)}cursorHide(){this.stdout.write(wu.cursor.hide()),On.onExit(()=>this.cursorShow())}cursorShow(){this.stdout.write(wu.cursor.show())}write(e){!e||(this.stdout&&this.state.show!==!1&&this.stdout.write(e),this.state.buffer+=e)}clear(e=0){let t=this.state.buffer;this.state.buffer="",!(!t&&!e||this.options.show===!1)&&this.stdout.write(wu.cursor.down(e)+wu.clear(t,this.width))}restore(){if(this.state.closed||this.options.show===!1)return;let{prompt:e,after:t,rest:i}=this.sections(),{cursor:n,initial:s="",input:o="",value:a=""}=this,l=this.state.size=i.length,c={after:t,cursor:n,initial:s,input:o,prompt:e,size:l,value:a},u=wu.cursor.restore(c);u&&this.stdout.write(u)}sections(){let{buffer:e,input:t,prompt:i}=this.state;i=iie.unstyle(i);let n=iie.unstyle(e),s=n.indexOf(i),o=n.slice(0,s),l=n.slice(s).split(` +`),c=l[0],u=l[l.length-1],f=(i+(t?" "+t:"")).length,h=fe.call(this,this.value),this.result=()=>i.call(this,this.value),typeof t.initial=="function"&&(this.initial=await t.initial.call(this,this)),typeof t.onRun=="function"&&await t.onRun.call(this,this),typeof t.onSubmit=="function"){let n=t.onSubmit.bind(this),s=this.submit.bind(this);delete this.options.onSubmit,this.submit=async()=>(await n(this.name,this.value,this),s())}await this.start(),await this.render()}render(){throw new Error("expected prompt to have a custom render method")}run(){return new Promise(async(e,t)=>{if(this.once("submit",e),this.once("cancel",t),await this.skip())return this.render=()=>{},this.submit();await this.initialize(),this.emit("run")})}async element(e,t,i){let{options:n,state:s,symbols:o,timers:a}=this,l=a&&a[e];s.timer=l;let c=n[e]||s[e]||o[e],u=t&&t[e]!=null?t[e]:await c;if(u==="")return u;let g=await this.resolve(u,s,t,i);return!g&&t&&t[e]?this.resolve(c,s,t,i):g}async prefix(){let e=await this.element("prefix")||this.symbols,t=this.timers&&this.timers.prefix,i=this.state;return i.timer=t,On.isObject(e)&&(e=e[i.status]||e.pending),On.hasColor(e)?e:(this.styles[i.status]||this.styles.pending)(e)}async message(){let e=await this.element("message");return On.hasColor(e)?e:this.styles.strong(e)}async separator(){let e=await this.element("separator")||this.symbols,t=this.timers&&this.timers.separator,i=this.state;i.timer=t;let n=e[i.status]||e.pending||i.separator,s=await this.resolve(n,i);return On.isObject(s)&&(s=s[i.status]||s.pending),On.hasColor(s)?s:this.styles.muted(s)}async pointer(e,t){let i=await this.element("pointer",e,t);if(typeof i=="string"&&On.hasColor(i))return i;if(i){let n=this.styles,s=this.index===t,o=s?n.primary:c=>c,a=await this.resolve(i[s?"on":"off"]||i,this.state),l=On.hasColor(a)?a:o(a);return s?l:" ".repeat(a.length)}}async indicator(e,t){let i=await this.element("indicator",e,t);if(typeof i=="string"&&On.hasColor(i))return i;if(i){let n=this.styles,s=e.enabled===!0,o=s?n.success:n.dark,a=i[s?"on":"off"]||i;return On.hasColor(a)?a:o(a)}return""}body(){return null}footer(){if(this.state.status==="pending")return this.element("footer")}header(){if(this.state.status==="pending")return this.element("header")}async hint(){if(this.state.status==="pending"&&!this.isValue(this.state.input)){let e=await this.element("hint");return On.hasColor(e)?e:this.styles.muted(e)}}error(e){return this.state.submitted?"":e||this.state.error}format(e){return e}result(e){return e}validate(e){return this.options.required===!0?this.isValue(e):!0}isValue(e){return e!=null&&e!==""}resolve(e,...t){return On.resolve(this,e,...t)}get base(){return U0.prototype}get style(){return this.styles[this.state.status]}get height(){return this.options.rows||On.height(this.stdout,25)}get width(){return this.options.columns||On.width(this.stdout,80)}get size(){return{width:this.width,height:this.height}}set cursor(e){this.state.cursor=e}get cursor(){return this.state.cursor}set input(e){this.state.input=e}get input(){return this.state.input}set value(e){this.state.value=e}get value(){let{input:e,value:t}=this.state,i=[t,e].find(this.isValue.bind(this));return this.isValue(i)?i:this.initial}static get prompt(){return e=>new this(e).run()}};function FGe(r){let e=n=>r[n]===void 0||typeof r[n]=="function",t=["actions","choices","initial","margin","roles","styles","symbols","theme","timers","value"],i=["body","footer","error","header","hint","indicator","message","prefix","separator","skip"];for(let n of Object.keys(r.options)){if(t.includes(n)||/^on[A-Z]/.test(n))continue;let s=r.options[n];typeof s=="function"&&e(n)?i.includes(n)||(r[n]=s.bind(r)):typeof r[n]!="function"&&(r[n]=s)}}function NGe(r){typeof r=="number"&&(r=[r,r,r,r]);let e=[].concat(r||[]),t=n=>n%2==0?` +`:" ",i=[];for(let n=0;n<4;n++){let s=t(n);e[n]?i.push(s.repeat(e[n])):i.push("")}return i}rie.exports=U0});var oie=w((vft,nie)=>{"use strict";var LGe=Xi(),sie={default(r,e){return e},checkbox(r,e){throw new Error("checkbox role is not implemented yet")},editable(r,e){throw new Error("editable role is not implemented yet")},expandable(r,e){throw new Error("expandable role is not implemented yet")},heading(r,e){return e.disabled="",e.indicator=[e.indicator," "].find(t=>t!=null),e.message=e.message||"",e},input(r,e){throw new Error("input role is not implemented yet")},option(r,e){return sie.default(r,e)},radio(r,e){throw new Error("radio role is not implemented yet")},separator(r,e){return e.disabled="",e.indicator=[e.indicator," "].find(t=>t!=null),e.message=e.message||r.symbols.line.repeat(5),e},spacer(r,e){return e}};nie.exports=(r,e={})=>{let t=LGe.merge({},sie,e.roles);return t[r]||t.default}});var JC=w((kft,aie)=>{"use strict";var TGe=yo(),OGe=Vf(),MGe=oie(),K0=Xi(),{reorder:$F,scrollUp:UGe,scrollDown:KGe,isObject:Aie,swap:HGe}=K0,lie=class extends OGe{constructor(e){super(e);this.cursorHide(),this.maxSelected=e.maxSelected||Infinity,this.multiple=e.multiple||!1,this.initial=e.initial||0,this.delay=e.delay||0,this.longest=0,this.num=""}async initialize(){typeof this.options.initial=="function"&&(this.initial=await this.options.initial.call(this)),await this.reset(!0),await super.initialize()}async reset(){let{choices:e,initial:t,autofocus:i,suggest:n}=this.options;if(this.state._choices=[],this.state.choices=[],this.choices=await Promise.all(await this.toChoices(e)),this.choices.forEach(s=>s.enabled=!1),typeof n!="function"&&this.selectable.length===0)throw new Error("At least one choice must be selectable");Aie(t)&&(t=Object.keys(t)),Array.isArray(t)?(i!=null&&(this.index=this.findIndex(i)),t.forEach(s=>this.enable(this.find(s))),await this.render()):(i!=null&&(t=i),typeof t=="string"&&(t=this.findIndex(t)),typeof t=="number"&&t>-1&&(this.index=Math.max(0,Math.min(t,this.choices.length)),this.enable(this.find(this.index)))),this.isDisabled(this.focused)&&await this.down()}async toChoices(e,t){this.state.loadingChoices=!0;let i=[],n=0,s=async(o,a)=>{typeof o=="function"&&(o=await o.call(this)),o instanceof Promise&&(o=await o);for(let l=0;l(this.state.loadingChoices=!1,o))}async toChoice(e,t,i){if(typeof e=="function"&&(e=await e.call(this,this)),e instanceof Promise&&(e=await e),typeof e=="string"&&(e={name:e}),e.normalized)return e;e.normalized=!0;let n=e.value;if(e=MGe(e.role,this.options)(this,e),typeof e.disabled=="string"&&!e.hint&&(e.hint=e.disabled,e.disabled=!0),e.disabled===!0&&e.hint==null&&(e.hint="(disabled)"),e.index!=null)return e;e.name=e.name||e.key||e.title||e.value||e.message,e.message=e.message||e.name||"",e.value=[e.value,e.name].find(this.isValue.bind(this)),e.input="",e.index=t,e.cursor=0,K0.define(e,"parent",i),e.level=i?i.level+1:1,e.indent==null&&(e.indent=i?i.indent+" ":e.indent||""),e.path=i?i.path+"."+e.name:e.name,e.enabled=!!(this.multiple&&!this.isDisabled(e)&&(e.enabled||this.isSelected(e))),this.isDisabled(e)||(this.longest=Math.max(this.longest,TGe.unstyle(e.message).length));let o=N({},e);return e.reset=(a=o.input,l=o.value)=>{for(let c of Object.keys(o))e[c]=o[c];e.input=a,e.value=l},n==null&&typeof e.initial=="function"&&(e.input=await e.initial.call(this,this.state,e,t)),e}async onChoice(e,t){this.emit("choice",e,t,this),typeof e.onChoice=="function"&&await e.onChoice.call(this,this.state,e,t)}async addChoice(e,t,i){let n=await this.toChoice(e,t,i);return this.choices.push(n),this.index=this.choices.length-1,this.limit=this.choices.length,n}async newItem(e,t,i){let n=N({name:"New choice name?",editable:!0,newChoice:!0},e),s=await this.addChoice(n,t,i);return s.updateChoice=()=>{delete s.newChoice,s.name=s.message=s.input,s.input="",s.cursor=0},this.render()}indent(e){return e.indent==null?e.level>1?" ".repeat(e.level-1):"":e.indent}dispatch(e,t){if(this.multiple&&this[t.name])return this[t.name]();this.alert()}focus(e,t){return typeof t!="boolean"&&(t=e.enabled),t&&!e.enabled&&this.selected.length>=this.maxSelected?this.alert():(this.index=e.index,e.enabled=t&&!this.isDisabled(e),e)}space(){return this.multiple?(this.toggle(this.focused),this.render()):this.alert()}a(){if(this.maxSelectedt.enabled);return this.choices.forEach(t=>t.enabled=!e),this.render()}i(){return this.choices.length-this.selected.length>this.maxSelected?this.alert():(this.choices.forEach(e=>e.enabled=!e.enabled),this.render())}g(e=this.focused){return this.choices.some(t=>!!t.parent)?(this.toggle(e.parent&&!e.choices?e.parent:e),this.render()):this.a()}toggle(e,t){if(!e.enabled&&this.selected.length>=this.maxSelected)return this.alert();typeof t!="boolean"&&(t=!e.enabled),e.enabled=t,e.choices&&e.choices.forEach(n=>this.toggle(n,t));let i=e.parent;for(;i;){let n=i.choices.filter(s=>this.isDisabled(s));i.enabled=n.every(s=>s.enabled===!0),i=i.parent}return cie(this,this.choices),this.emit("toggle",e,this),e}enable(e){return this.selected.length>=this.maxSelected?this.alert():(e.enabled=!this.isDisabled(e),e.choices&&e.choices.forEach(this.enable.bind(this)),e)}disable(e){return e.enabled=!1,e.choices&&e.choices.forEach(this.disable.bind(this)),e}number(e){this.num+=e;let t=i=>{let n=Number(i);if(n>this.choices.length-1)return this.alert();let s=this.focused,o=this.choices.find(a=>n===a.index);if(!o.enabled&&this.selected.length>=this.maxSelected)return this.alert();if(this.visible.indexOf(o)===-1){let a=$F(this.choices),l=a.indexOf(o);if(s.index>l){let c=a.slice(l,l+this.limit),u=a.filter(g=>!c.includes(g));this.choices=c.concat(u)}else{let c=l-this.limit+1;this.choices=a.slice(c).concat(a.slice(0,c))}}return this.index=this.choices.indexOf(o),this.toggle(this.focused),this.render()};return clearTimeout(this.numberTimeout),new Promise(i=>{let n=this.choices.length,s=this.num,o=(a=!1,l)=>{clearTimeout(this.numberTimeout),a&&(l=t(s)),this.num="",i(l)};if(s==="0"||s.length===1&&Number(s+"0")>n)return o(!0);if(Number(s)>n)return o(!1,this.alert());this.numberTimeout=setTimeout(()=>o(!0),this.delay)})}home(){return this.choices=$F(this.choices),this.index=0,this.render()}end(){let e=this.choices.length-this.limit,t=$F(this.choices);return this.choices=t.slice(e).concat(t.slice(0,e)),this.index=this.limit-1,this.render()}first(){return this.index=0,this.render()}last(){return this.index=this.visible.length-1,this.render()}prev(){return this.visible.length<=1?this.alert():this.up()}next(){return this.visible.length<=1?this.alert():this.down()}right(){return this.cursor>=this.input.length?this.alert():(this.cursor++,this.render())}left(){return this.cursor<=0?this.alert():(this.cursor--,this.render())}up(){let e=this.choices.length,t=this.visible.length,i=this.index;return this.options.scroll===!1&&i===0?this.alert():e>t&&i===0?this.scrollUp():(this.index=(i-1%e+e)%e,this.isDisabled()?this.up():this.render())}down(){let e=this.choices.length,t=this.visible.length,i=this.index;return this.options.scroll===!1&&i===t-1?this.alert():e>t&&i===t-1?this.scrollDown():(this.index=(i+1)%e,this.isDisabled()?this.down():this.render())}scrollUp(e=0){return this.choices=UGe(this.choices),this.index=e,this.isDisabled()?this.up():this.render()}scrollDown(e=this.visible.length-1){return this.choices=KGe(this.choices),this.index=e,this.isDisabled()?this.down():this.render()}async shiftUp(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index-1),await this.up(),this.sorting=!1;return}return this.scrollUp(this.index)}async shiftDown(){if(this.options.sort===!0){this.sorting=!0,this.swap(this.index+1),await this.down(),this.sorting=!1;return}return this.scrollDown(this.index)}pageUp(){return this.visible.length<=1?this.alert():(this.limit=Math.max(this.limit-1,0),this.index=Math.min(this.limit-1,this.index),this._limit=this.limit,this.isDisabled()?this.up():this.render())}pageDown(){return this.visible.length>=this.choices.length?this.alert():(this.index=Math.max(0,this.index),this.limit=Math.min(this.limit+1,this.choices.length),this._limit=this.limit,this.isDisabled()?this.down():this.render())}swap(e){HGe(this.choices,this.index,e)}isDisabled(e=this.focused){return e&&["disabled","collapsed","hidden","completing","readonly"].some(i=>e[i]===!0)?!0:e&&e.role==="heading"}isEnabled(e=this.focused){if(Array.isArray(e))return e.every(t=>this.isEnabled(t));if(e.choices){let t=e.choices.filter(i=>!this.isDisabled(i));return e.enabled&&t.every(i=>this.isEnabled(i))}return e.enabled&&!this.isDisabled(e)}isChoice(e,t){return e.name===t||e.index===Number(t)}isSelected(e){return Array.isArray(this.initial)?this.initial.some(t=>this.isChoice(e,t)):this.isChoice(e,this.initial)}map(e=[],t="value"){return[].concat(e||[]).reduce((i,n)=>(i[n]=this.find(n,t),i),{})}filter(e,t){let i=(a,l)=>[a.name,l].includes(e),n=typeof e=="function"?e:i,o=(this.options.multiple?this.state._choices:this.choices).filter(n);return t?o.map(a=>a[t]):o}find(e,t){if(Aie(e))return t?e[t]:e;let i=(o,a)=>[o.name,a].includes(e),n=typeof e=="function"?e:i,s=this.choices.find(n);if(s)return t?s[t]:s}findIndex(e){return this.choices.indexOf(this.find(e))}async submit(){let e=this.focused;if(!e)return this.alert();if(e.newChoice)return e.input?(e.updateChoice(),this.render()):this.alert();if(this.choices.some(o=>o.newChoice))return this.alert();let{reorder:t,sort:i}=this.options,n=this.multiple===!0,s=this.selected;return s===void 0?this.alert():(Array.isArray(s)&&t!==!1&&i!==!0&&(s=K0.reorder(s)),this.value=n?s.map(o=>o.name):s.name,super.submit())}set choices(e=[]){this.state._choices=this.state._choices||[],this.state.choices=e;for(let t of e)this.state._choices.some(i=>i.name===t.name)||this.state._choices.push(t);if(!this._initial&&this.options.initial){this._initial=!0;let t=this.initial;if(typeof t=="string"||typeof t=="number"){let i=this.find(t);i&&(this.initial=i.index,this.focus(i,!0))}}}get choices(){return cie(this,this.state.choices||[])}set visible(e){this.state.visible=e}get visible(){return(this.state.visible||this.choices).slice(0,this.limit)}set limit(e){this.state.limit=e}get limit(){let{state:e,options:t,choices:i}=this,n=e.limit||this._limit||t.limit||i.length;return Math.min(n,this.height)}set value(e){super.value=e}get value(){return typeof super.value!="string"&&super.value===this.initial?this.input:super.value}set index(e){this.state.index=e}get index(){return Math.max(0,this.state?this.state.index:0)}get enabled(){return this.filter(this.isEnabled.bind(this))}get focused(){let e=this.choices[this.index];return e&&this.state.submitted&&this.multiple!==!0&&(e.enabled=!0),e}get selectable(){return this.choices.filter(e=>!this.isDisabled(e))}get selected(){return this.multiple?this.enabled:this.focused}};function cie(r,e){if(e instanceof Promise)return e;if(typeof e=="function"){if(K0.isAsyncFn(e))return e;e=e.call(r,r)}for(let t of e){if(Array.isArray(t.choices)){let i=t.choices.filter(n=>!r.isDisabled(n));t.enabled=i.every(n=>n.enabled===!0)}r.isDisabled(t)===!0&&delete t.enabled}return e}aie.exports=lie});var Ll=w((xft,uie)=>{"use strict";var jGe=JC(),eN=Xi(),gie=class extends jGe{constructor(e){super(e);this.emptyError=this.options.emptyError||"No items were selected"}async dispatch(e,t){if(this.multiple)return this[t.name]?await this[t.name](e,t):await super.dispatch(e,t);this.alert()}separator(){if(this.options.separator)return super.separator();let e=this.styles.muted(this.symbols.ellipsis);return this.state.submitted?super.separator():e}pointer(e,t){return!this.multiple||this.options.pointer?super.pointer(e,t):""}indicator(e,t){return this.multiple?super.indicator(e,t):""}choiceMessage(e,t){let i=this.resolve(e.message,this.state,e,t);return e.role==="heading"&&!eN.hasColor(i)&&(i=this.styles.strong(i)),this.resolve(i,this.state,e,t)}choiceSeparator(){return":"}async renderChoice(e,t){await this.onChoice(e,t);let i=this.index===t,n=await this.pointer(e,t),s=await this.indicator(e,t)+(e.pad||""),o=await this.resolve(e.hint,this.state,e,t);o&&!eN.hasColor(o)&&(o=this.styles.muted(o));let a=this.indent(e),l=await this.choiceMessage(e,t),c=()=>[this.margin[3],a+n+s,l,this.margin[1],o].filter(Boolean).join(" ");return e.role==="heading"?c():e.disabled?(eN.hasColor(l)||(l=this.styles.disabled(l)),c()):(i&&(l=this.styles.em(l)),c())}async renderChoices(){if(this.state.loading==="choices")return this.styles.warning("Loading choices");if(this.state.submitted)return"";let e=this.visible.map(async(s,o)=>await this.renderChoice(s,o)),t=await Promise.all(e);t.length||t.push(this.styles.danger("No matching choices"));let i=this.margin[0]+t.join(` +`),n;return this.options.choicesHeader&&(n=await this.resolve(this.options.choicesHeader,this.state)),[n,i].filter(Boolean).join(` +`)}format(){return!this.state.submitted||this.state.cancelled?"":Array.isArray(this.selected)?this.selected.map(e=>this.styles.primary(e.name)).join(", "):this.styles.primary(this.selected.name)}async render(){let{submitted:e,size:t}=this.state,i="",n=await this.header(),s=await this.prefix(),o=await this.separator(),a=await this.message();this.options.promptLine!==!1&&(i=[s,a,o,""].join(" "),this.state.prompt=i);let l=await this.format(),c=await this.error()||await this.hint(),u=await this.renderChoices(),g=await this.footer();l&&(i+=l),c&&!i.includes(c)&&(i+=" "+c),e&&!l&&!u.trim()&&this.multiple&&this.emptyError!=null&&(i+=this.styles.danger(this.emptyError)),this.clear(t),this.write([n,i,u,g].filter(Boolean).join(` +`)),this.write(this.margin[2]),this.restore()}};uie.exports=gie});var pie=w((Pft,fie)=>{"use strict";var GGe=Ll(),YGe=(r,e)=>{let t=r.toLowerCase();return i=>{let s=i.toLowerCase().indexOf(t),o=e(i.slice(s,s+t.length));return s>=0?i.slice(0,s)+o+i.slice(s+t.length):i}},hie=class extends GGe{constructor(e){super(e);this.cursorShow()}moveCursor(e){this.state.cursor+=e}dispatch(e){return this.append(e)}space(e){return this.options.multiple?super.space(e):this.append(e)}append(e){let{cursor:t,input:i}=this.state;return this.input=i.slice(0,t)+e+i.slice(t),this.moveCursor(1),this.complete()}delete(){let{cursor:e,input:t}=this.state;return t?(this.input=t.slice(0,e-1)+t.slice(e),this.moveCursor(-1),this.complete()):this.alert()}deleteForward(){let{cursor:e,input:t}=this.state;return t[e]===void 0?this.alert():(this.input=`${t}`.slice(0,e)+`${t}`.slice(e+1),this.complete())}number(e){return this.append(e)}async complete(){this.completing=!0,this.choices=await this.suggest(this.input,this.state._choices),this.state.limit=void 0,this.index=Math.min(Math.max(this.visible.length-1,0),this.index),await this.render(),this.completing=!1}suggest(e=this.input,t=this.state._choices){if(typeof this.options.suggest=="function")return this.options.suggest.call(this,e,t);let i=e.toLowerCase();return t.filter(n=>n.message.toLowerCase().includes(i))}pointer(){return""}format(){if(!this.focused)return this.input;if(this.options.multiple&&this.state.submitted)return this.selected.map(e=>this.styles.primary(e.message)).join(", ");if(this.state.submitted){let e=this.value=this.input=this.focused.value;return this.styles.primary(e)}return this.input}async render(){if(this.state.status!=="pending")return super.render();let e=this.options.highlight?this.options.highlight.bind(this):this.styles.placeholder,t=YGe(this.input,e),i=this.choices;this.choices=i.map(n=>te(N({},n),{message:t(n.message)})),await super.render(),this.choices=i}submit(){return this.options.multiple&&(this.value=this.selected.map(e=>e.name)),super.submit()}};fie.exports=hie});var rN=w((Dft,die)=>{"use strict";var tN=Xi();die.exports=(r,e={})=>{r.cursorHide();let{input:t="",initial:i="",pos:n,showCursor:s=!0,color:o}=e,a=o||r.styles.placeholder,l=tN.inverse(r.styles.primary),c=m=>l(r.styles.black(m)),u=t,g=" ",f=c(g);if(r.blink&&r.blink.off===!0&&(c=m=>m,f=""),s&&n===0&&i===""&&t==="")return c(g);if(s&&n===0&&(t===i||t===""))return c(i[0])+a(i.slice(1));i=tN.isPrimitive(i)?`${i}`:"",t=tN.isPrimitive(t)?`${t}`:"";let h=i&&i.startsWith(t)&&i!==t,p=h?c(i[t.length]):f;if(n!==t.length&&s===!0&&(u=t.slice(0,n)+c(t[n])+t.slice(n+1),p=""),s===!1&&(p=""),h){let m=r.styles.unstyle(u+p);return u+p+a(i.slice(m.length))}return u+p}});var H0=w((Rft,Cie)=>{"use strict";var qGe=yo(),JGe=Ll(),WGe=rN(),mie=class extends JGe{constructor(e){super(te(N({},e),{multiple:!0}));this.type="form",this.initial=this.options.initial,this.align=[this.options.align,"right"].find(t=>t!=null),this.emptyError="",this.values={}}async reset(e){return await super.reset(),e===!0&&(this._index=this.index),this.index=this._index,this.values={},this.choices.forEach(t=>t.reset&&t.reset()),this.render()}dispatch(e){return!!e&&this.append(e)}append(e){let t=this.focused;if(!t)return this.alert();let{cursor:i,input:n}=t;return t.value=t.input=n.slice(0,i)+e+n.slice(i),t.cursor++,this.render()}delete(){let e=this.focused;if(!e||e.cursor<=0)return this.alert();let{cursor:t,input:i}=e;return e.value=e.input=i.slice(0,t-1)+i.slice(t),e.cursor--,this.render()}deleteForward(){let e=this.focused;if(!e)return this.alert();let{cursor:t,input:i}=e;if(i[t]===void 0)return this.alert();let n=`${i}`.slice(0,t)+`${i}`.slice(t+1);return e.value=e.input=n,this.render()}right(){let e=this.focused;return e?e.cursor>=e.input.length?this.alert():(e.cursor++,this.render()):this.alert()}left(){let e=this.focused;return e?e.cursor<=0?this.alert():(e.cursor--,this.render()):this.alert()}space(e,t){return this.dispatch(e,t)}number(e,t){return this.dispatch(e,t)}next(){let e=this.focused;if(!e)return this.alert();let{initial:t,input:i}=e;return t&&t.startsWith(i)&&i!==t?(e.value=e.input=t,e.cursor=e.value.length,this.render()):super.next()}prev(){let e=this.focused;return e?e.cursor===0?super.prev():(e.value=e.input="",e.cursor=0,this.render()):this.alert()}separator(){return""}format(e){return this.state.submitted?"":super.format(e)}pointer(){return""}indicator(e){return e.input?"\u29BF":"\u2299"}async choiceSeparator(e,t){let i=await this.resolve(e.separator,this.state,e,t)||":";return i?" "+this.styles.disabled(i):""}async renderChoice(e,t){await this.onChoice(e,t);let{state:i,styles:n}=this,{cursor:s,initial:o="",name:a,hint:l,input:c=""}=e,{muted:u,submitted:g,primary:f,danger:h}=n,p=l,m=this.index===t,y=e.validate||(()=>!0),b=await this.choiceSeparator(e,t),v=e.message;this.align==="right"&&(v=v.padStart(this.longest+1," ")),this.align==="left"&&(v=v.padEnd(this.longest+1," "));let k=this.values[a]=c||o,T=c?"success":"dark";await y.call(e,k,this.state)!==!0&&(T="danger");let q=n[T](await this.indicator(e,t))+(e.pad||""),$=this.indent(e),z=()=>[$,q,v+b,c,p].filter(Boolean).join(" ");if(i.submitted)return v=qGe.unstyle(v),c=g(c),p="",z();if(e.format)c=await e.format.call(this,c,e,t);else{let ne=this.styles.muted;c=WGe(this,{input:c,initial:o,pos:s,showCursor:m,color:ne})}return this.isValue(c)||(c=this.styles.muted(this.symbols.ellipsis)),e.result&&(this.values[a]=await e.result.call(this,k,e,t)),m&&(v=f(v)),e.error?c+=(c?" ":"")+h(e.error.trim()):e.hint&&(c+=(c?" ":"")+u(e.hint.trim())),z()}async submit(){return this.value=this.values,super.base.submit.call(this)}};Cie.exports=mie});var iN=w((Fft,Eie)=>{"use strict";var zGe=H0(),_Ge=()=>{throw new Error("expected prompt to have a custom authenticate method")},Iie=(r=_Ge)=>{class e extends zGe{constructor(i){super(i)}async submit(){this.value=await r.call(this,this.values,this.state),super.base.submit.call(this)}static create(i){return Iie(i)}}return e};Eie.exports=Iie()});var Bie=w((Nft,yie)=>{"use strict";var VGe=iN();function XGe(r,e){return r.username===this.options.username&&r.password===this.options.password}var wie=(r=XGe)=>{let e=[{name:"username",message:"username"},{name:"password",message:"password",format(i){return this.options.showPassword?i:(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(i.length))}}];class t extends VGe.create(r){constructor(n){super(te(N({},n),{choices:e}))}static create(n){return wie(n)}}return t};yie.exports=wie()});var j0=w((Lft,bie)=>{"use strict";var ZGe=Vf(),{isPrimitive:$Ge,hasColor:eYe}=Xi(),Qie=class extends ZGe{constructor(e){super(e);this.cursorHide()}async initialize(){let e=await this.resolve(this.initial,this.state);this.input=await this.cast(e),await super.initialize()}dispatch(e){return this.isValue(e)?(this.input=e,this.submit()):this.alert()}format(e){let{styles:t,state:i}=this;return i.submitted?t.success(e):t.primary(e)}cast(e){return this.isTrue(e)}isTrue(e){return/^[ty1]/i.test(e)}isFalse(e){return/^[fn0]/i.test(e)}isValue(e){return $Ge(e)&&(this.isTrue(e)||this.isFalse(e))}async hint(){if(this.state.status==="pending"){let e=await this.element("hint");return eYe(e)?e:this.styles.muted(e)}}async render(){let{input:e,size:t}=this.state,i=await this.prefix(),n=await this.separator(),s=await this.message(),o=this.styles.muted(this.default),a=[i,s,o,n].filter(Boolean).join(" ");this.state.prompt=a;let l=await this.header(),c=this.value=this.cast(e),u=await this.format(c),g=await this.error()||await this.hint(),f=await this.footer();g&&!a.includes(g)&&(u+=" "+g),a+=" "+u,this.clear(t),this.write([l,a,f].filter(Boolean).join(` +`)),this.restore()}set value(e){super.value=e}get value(){return this.cast(super.value)}};bie.exports=Qie});var kie=w((Tft,Sie)=>{"use strict";var tYe=j0(),vie=class extends tYe{constructor(e){super(e);this.default=this.options.default||(this.initial?"(Y/n)":"(y/N)")}};Sie.exports=vie});var Die=w((Oft,xie)=>{"use strict";var rYe=Ll(),iYe=H0(),Xf=iYe.prototype,Pie=class extends rYe{constructor(e){super(te(N({},e),{multiple:!0}));this.align=[this.options.align,"left"].find(t=>t!=null),this.emptyError="",this.values={}}dispatch(e,t){let i=this.focused,n=i.parent||{};return!i.editable&&!n.editable&&(e==="a"||e==="i")?super[e]():Xf.dispatch.call(this,e,t)}append(e,t){return Xf.append.call(this,e,t)}delete(e,t){return Xf.delete.call(this,e,t)}space(e){return this.focused.editable?this.append(e):super.space()}number(e){return this.focused.editable?this.append(e):super.number(e)}next(){return this.focused.editable?Xf.next.call(this):super.next()}prev(){return this.focused.editable?Xf.prev.call(this):super.prev()}async indicator(e,t){let i=e.indicator||"",n=e.editable?i:super.indicator(e,t);return await this.resolve(n,this.state,e,t)||""}indent(e){return e.role==="heading"?"":e.editable?" ":" "}async renderChoice(e,t){return e.indent="",e.editable?Xf.renderChoice.call(this,e,t):super.renderChoice(e,t)}error(){return""}footer(){return this.state.error}async validate(){let e=!0;for(let t of this.choices){if(typeof t.validate!="function"||t.role==="heading")continue;let i=t.parent?this.value[t.parent.name]:this.value;if(t.editable?i=t.value===t.name?t.initial||"":t.value:this.isDisabled(t)||(i=t.enabled===!0),e=await t.validate(i,this.state),e!==!0)break}return e!==!0&&(this.state.error=typeof e=="string"?e:"Invalid Input"),e}submit(){if(this.focused.newChoice===!0)return super.submit();if(this.choices.some(e=>e.newChoice))return this.alert();this.value={};for(let e of this.choices){let t=e.parent?this.value[e.parent.name]:this.value;if(e.role==="heading"){this.value[e.name]={};continue}e.editable?t[e.name]=e.value===e.name?e.initial||"":e.value:this.isDisabled(e)||(t[e.name]=e.enabled===!0)}return this.base.submit.call(this)}};xie.exports=Pie});var Bu=w((Mft,Rie)=>{"use strict";var nYe=Vf(),sYe=rN(),{isPrimitive:oYe}=Xi(),Fie=class extends nYe{constructor(e){super(e);this.initial=oYe(this.initial)?String(this.initial):"",this.initial&&this.cursorHide(),this.state.prevCursor=0,this.state.clipboard=[]}async keypress(e,t={}){let i=this.state.prevKeypress;return this.state.prevKeypress=t,this.options.multiline===!0&&t.name==="return"&&(!i||i.name!=="return")?this.append(` +`,t):super.keypress(e,t)}moveCursor(e){this.cursor+=e}reset(){return this.input=this.value="",this.cursor=0,this.render()}dispatch(e,t){if(!e||t.ctrl||t.code)return this.alert();this.append(e)}append(e){let{cursor:t,input:i}=this.state;this.input=`${i}`.slice(0,t)+e+`${i}`.slice(t),this.moveCursor(String(e).length),this.render()}insert(e){this.append(e)}delete(){let{cursor:e,input:t}=this.state;if(e<=0)return this.alert();this.input=`${t}`.slice(0,e-1)+`${t}`.slice(e),this.moveCursor(-1),this.render()}deleteForward(){let{cursor:e,input:t}=this.state;if(t[e]===void 0)return this.alert();this.input=`${t}`.slice(0,e)+`${t}`.slice(e+1),this.render()}cutForward(){let e=this.cursor;if(this.input.length<=e)return this.alert();this.state.clipboard.push(this.input.slice(e)),this.input=this.input.slice(0,e),this.render()}cutLeft(){let e=this.cursor;if(e===0)return this.alert();let t=this.input.slice(0,e),i=this.input.slice(e),n=t.split(" ");this.state.clipboard.push(n.pop()),this.input=n.join(" "),this.cursor=this.input.length,this.input+=i,this.render()}paste(){if(!this.state.clipboard.length)return this.alert();this.insert(this.state.clipboard.pop()),this.render()}toggleCursor(){this.state.prevCursor?(this.cursor=this.state.prevCursor,this.state.prevCursor=0):(this.state.prevCursor=this.cursor,this.cursor=0),this.render()}first(){this.cursor=0,this.render()}last(){this.cursor=this.input.length-1,this.render()}next(){let e=this.initial!=null?String(this.initial):"";if(!e||!e.startsWith(this.input))return this.alert();this.input=this.initial,this.cursor=this.initial.length,this.render()}prev(){if(!this.input)return this.alert();this.reset()}backward(){return this.left()}forward(){return this.right()}right(){return this.cursor>=this.input.length?this.alert():(this.moveCursor(1),this.render())}left(){return this.cursor<=0?this.alert():(this.moveCursor(-1),this.render())}isValue(e){return!!e}async format(e=this.value){let t=await this.resolve(this.initial,this.state);return this.state.submitted?this.styles.submitted(e||t):sYe(this,{input:e,initial:t,pos:this.cursor})}async render(){let e=this.state.size,t=await this.prefix(),i=await this.separator(),n=await this.message(),s=[t,n,i].filter(Boolean).join(" ");this.state.prompt=s;let o=await this.header(),a=await this.format(),l=await this.error()||await this.hint(),c=await this.footer();l&&!a.includes(l)&&(a+=" "+l),s+=" "+a,this.clear(e),this.write([o,s,c].filter(Boolean).join(` +`)),this.restore()}};Rie.exports=Fie});var Lie=w((Uft,Nie)=>{"use strict";var aYe=r=>r.filter((e,t)=>r.lastIndexOf(e)===t),G0=r=>aYe(r).filter(Boolean);Nie.exports=(r,e={},t="")=>{let{past:i=[],present:n=""}=e,s,o;switch(r){case"prev":case"undo":return s=i.slice(0,i.length-1),o=i[i.length-1]||"",{past:G0([t,...s]),present:o};case"next":case"redo":return s=i.slice(1),o=i[0]||"",{past:G0([...s,t]),present:o};case"save":return{past:G0([...i,t]),present:""};case"remove":return o=G0(i.filter(a=>a!==t)),n="",o.length&&(n=o.pop()),{past:o,present:n};default:throw new Error(`Invalid action: "${r}"`)}}});var nN=w((Kft,Tie)=>{"use strict";var AYe=Bu(),Oie=Lie(),Mie=class extends AYe{constructor(e){super(e);let t=this.options.history;if(t&&t.store){let i=t.values||this.initial;this.autosave=!!t.autosave,this.store=t.store,this.data=this.store.get("values")||{past:[],present:i},this.initial=this.data.present||this.data.past[this.data.past.length-1]}}completion(e){return this.store?(this.data=Oie(e,this.data,this.input),this.data.present?(this.input=this.data.present,this.cursor=this.input.length,this.render()):this.alert()):this.alert()}altUp(){return this.completion("prev")}altDown(){return this.completion("next")}prev(){return this.save(),super.prev()}save(){!this.store||(this.data=Oie("save",this.data,this.input),this.store.set("values",this.data))}submit(){return this.store&&this.autosave===!0&&this.save(),super.submit()}};Tie.exports=Mie});var Hie=w((Hft,Uie)=>{"use strict";var lYe=Bu(),Kie=class extends lYe{format(){return""}};Uie.exports=Kie});var Yie=w((jft,jie)=>{"use strict";var cYe=Bu(),Gie=class extends cYe{constructor(e={}){super(e);this.sep=this.options.separator||/, */,this.initial=e.initial||""}split(e=this.value){return e?String(e).split(this.sep):[]}format(){let e=this.state.submitted?this.styles.primary:t=>t;return this.list.map(e).join(", ")}async submit(e){let t=this.state.error||await this.validate(this.list,this.state);return t!==!0?(this.state.error=t,super.submit()):(this.value=this.list,super.submit())}get list(){return this.split()}};jie.exports=Gie});var Wie=w((Gft,qie)=>{"use strict";var uYe=Ll(),Jie=class extends uYe{constructor(e){super(te(N({},e),{multiple:!0}))}};qie.exports=Jie});var sN=w((Yft,zie)=>{"use strict";var gYe=Bu(),_ie=class extends gYe{constructor(e={}){super(N({style:"number"},e));this.min=this.isValue(e.min)?this.toNumber(e.min):-Infinity,this.max=this.isValue(e.max)?this.toNumber(e.max):Infinity,this.delay=e.delay!=null?e.delay:1e3,this.float=e.float!==!1,this.round=e.round===!0||e.float===!1,this.major=e.major||10,this.minor=e.minor||1,this.initial=e.initial!=null?e.initial:"",this.input=String(this.initial),this.cursor=this.input.length,this.cursorShow()}append(e){return!/[-+.]/.test(e)||e==="."&&this.input.includes(".")?this.alert("invalid number"):super.append(e)}number(e){return super.append(e)}next(){return this.input&&this.input!==this.initial?this.alert():this.isValue(this.initial)?(this.input=this.initial,this.cursor=String(this.initial).length,this.render()):this.alert()}up(e){let t=e||this.minor,i=this.toNumber(this.input);return i>this.max+t?this.alert():(this.input=`${i+t}`,this.render())}down(e){let t=e||this.minor,i=this.toNumber(this.input);return ithis.isValue(t));return this.value=this.toNumber(e||0),super.submit()}};zie.exports=_ie});var Xie=w((qft,Vie)=>{Vie.exports=sN()});var ene=w((Jft,Zie)=>{"use strict";var fYe=Bu(),$ie=class extends fYe{constructor(e){super(e);this.cursorShow()}format(e=this.input){return this.keypressed?(this.state.submitted?this.styles.primary:this.styles.muted)(this.symbols.asterisk.repeat(e.length)):""}};Zie.exports=$ie});var nne=w((Wft,tne)=>{"use strict";var hYe=yo(),pYe=JC(),rne=Xi(),ine=class extends pYe{constructor(e={}){super(e);this.widths=[].concat(e.messageWidth||50),this.align=[].concat(e.align||"left"),this.linebreak=e.linebreak||!1,this.edgeLength=e.edgeLength||3,this.newline=e.newline||` + `;let t=e.startNumber||1;typeof this.scale=="number"&&(this.scaleKey=!1,this.scale=Array(this.scale).fill(0).map((i,n)=>({name:n+t})))}async reset(){return this.tableized=!1,await super.reset(),this.render()}tableize(){if(this.tableized===!0)return;this.tableized=!0;let e=0;for(let t of this.choices){e=Math.max(e,t.message.length),t.scaleIndex=t.initial||2,t.scale=[];for(let i=0;i=this.scale.length-1?this.alert():(e.scaleIndex++,this.render())}left(){let e=this.focused;return e.scaleIndex<=0?this.alert():(e.scaleIndex--,this.render())}indent(){return""}format(){return this.state.submitted?this.choices.map(t=>this.styles.info(t.index)).join(", "):""}pointer(){return""}renderScaleKey(){if(this.scaleKey===!1||this.state.submitted)return"";let e=this.scale.map(i=>` ${i.name} - ${i.message}`);return["",...e].map(i=>this.styles.muted(i)).join(` +`)}renderScaleHeading(e){let t=this.scale.map(l=>l.name);typeof this.options.renderScaleHeading=="function"&&(t=this.options.renderScaleHeading.call(this,e));let i=this.scaleLength-t.join("").length,n=Math.round(i/(t.length-1)),o=t.map(l=>this.styles.strong(l)).join(" ".repeat(n)),a=" ".repeat(this.widths[0]);return this.margin[3]+a+this.margin[1]+o}scaleIndicator(e,t,i){if(typeof this.options.scaleIndicator=="function")return this.options.scaleIndicator.call(this,e,t,i);let n=e.scaleIndex===t.index;return t.disabled?this.styles.hint(this.symbols.radio.disabled):n?this.styles.success(this.symbols.radio.on):this.symbols.radio.off}renderScale(e,t){let i=e.scale.map(s=>this.scaleIndicator(e,s,t)),n=this.term==="Hyper"?"":" ";return i.join(n+this.symbols.line.repeat(this.edgeLength))}async renderChoice(e,t){await this.onChoice(e,t);let i=this.index===t,n=await this.pointer(e,t),s=await e.hint;s&&!rne.hasColor(s)&&(s=this.styles.muted(s));let o=p=>this.margin[3]+p.replace(/\s+$/,"").padEnd(this.widths[0]," "),a=this.newline,l=this.indent(e),c=await this.resolve(e.message,this.state,e,t),u=await this.renderScale(e,t),g=this.margin[1]+this.margin[3];this.scaleLength=hYe.unstyle(u).length,this.widths[0]=Math.min(this.widths[0],this.width-this.scaleLength-g.length);let h=rne.wordWrap(c,{width:this.widths[0],newline:a}).split(` +`).map(p=>o(p)+this.margin[1]);return i&&(u=this.styles.info(u),h=h.map(p=>this.styles.info(p))),h[0]+=u,this.linebreak&&h.push(""),[l+n,h.join(` +`)].filter(Boolean)}async renderChoices(){if(this.state.submitted)return"";this.tableize();let e=this.visible.map(async(n,s)=>await this.renderChoice(n,s)),t=await Promise.all(e),i=await this.renderScaleHeading();return this.margin[0]+[i,...t.map(n=>n.join(" "))].join(` +`)}async render(){let{submitted:e,size:t}=this.state,i=await this.prefix(),n=await this.separator(),s=await this.message(),o="";this.options.promptLine!==!1&&(o=[i,s,n,""].join(" "),this.state.prompt=o);let a=await this.header(),l=await this.format(),c=await this.renderScaleKey(),u=await this.error()||await this.hint(),g=await this.renderChoices(),f=await this.footer(),h=this.emptyError;l&&(o+=l),u&&!o.includes(u)&&(o+=" "+u),e&&!l&&!g.trim()&&this.multiple&&h!=null&&(o+=this.styles.danger(h)),this.clear(t),this.write([a,o,c,g,f].filter(Boolean).join(` +`)),this.state.submitted||this.write(this.margin[2]),this.restore()}submit(){this.value={};for(let e of this.choices)this.value[e.name]=e.scaleIndex;return this.base.submit.call(this)}};tne.exports=ine});var Ane=w((zft,sne)=>{"use strict";var one=yo(),dYe=(r="")=>typeof r=="string"?r.replace(/^['"]|['"]$/g,""):"",ane=class{constructor(e){this.name=e.key,this.field=e.field||{},this.value=dYe(e.initial||this.field.initial||""),this.message=e.message||this.name,this.cursor=0,this.input="",this.lines=[]}},CYe=async(r={},e={},t=i=>i)=>{let i=new Set,n=r.fields||[],s=r.template,o=[],a=[],l=[],c=1;typeof s=="function"&&(s=await s());let u=-1,g=()=>s[++u],f=()=>s[u+1],h=p=>{p.line=c,o.push(p)};for(h({type:"bos",value:""});uT.name===b.key);b.field=n.find(T=>T.name===b.key),k||(k=new ane(b),a.push(k)),k.lines.push(b.line-1);continue}let m=o[o.length-1];m.type==="text"&&m.line===c?m.value+=p:h({type:"text",value:p})}return h({type:"eos",value:""}),{input:s,tabstops:o,unique:i,keys:l,items:a}};sne.exports=async r=>{let e=r.options,t=new Set(e.required===!0?[]:e.required||[]),i=N(N({},e.values),e.initial),{tabstops:n,items:s,keys:o}=await CYe(e,i),a=oN("result",r,e),l=oN("format",r,e),c=oN("validate",r,e,!0),u=r.isValue.bind(r);return async(g={},f=!1)=>{let h=0;g.required=t,g.items=s,g.keys=o,g.output="";let p=async(v,k,T,Y)=>{let q=await c(v,k,T,Y);return q===!1?"Invalid field "+T.name:q};for(let v of n){let k=v.value,T=v.key;if(v.type!=="template"){k&&(g.output+=k);continue}if(v.type==="template"){let Y=s.find(ee=>ee.name===T);e.required===!0&&g.required.add(Y.name);let q=[Y.input,g.values[Y.value],Y.value,k].find(u),z=(Y.field||{}).message||v.inner;if(f){let ee=await p(g.values[T],g,Y,h);if(ee&&typeof ee=="string"||ee===!1){g.invalid.set(T,ee);continue}g.invalid.delete(T);let A=await a(g.values[T],g,Y,h);g.output+=one.unstyle(A);continue}Y.placeholder=!1;let ne=k;k=await l(k,g,Y,h),q!==k?(g.values[T]=q,k=r.styles.typing(q),g.missing.delete(z)):(g.values[T]=void 0,q=`<${z}>`,k=r.styles.primary(q),Y.placeholder=!0,g.required.has(T)&&g.missing.add(z)),g.missing.has(z)&&g.validating&&(k=r.styles.warning(q)),g.invalid.has(T)&&g.validating&&(k=r.styles.danger(q)),h===g.index&&(ne!==k?k=r.styles.underline(k):k=r.styles.heading(one.unstyle(k))),h++}k&&(g.output+=k)}let m=g.output.split(` +`).map(v=>" "+v),y=s.length,b=0;for(let v of s)g.invalid.has(v.name)&&v.lines.forEach(k=>{m[k][0]===" "&&(m[k]=g.styles.danger(g.symbols.bullet)+m[k].slice(1))}),r.isValue(g.values[v.name])&&b++;return g.completed=(b/y*100).toFixed(0),g.output=m.join(` +`),g.output}};function oN(r,e,t,i){return(n,s,o,a)=>typeof o.field[r]=="function"?o.field[r].call(e,n,s,o,a):[i,n].find(l=>e.isValue(l))}});var une=w((_ft,lne)=>{"use strict";var mYe=yo(),EYe=Ane(),IYe=Vf(),cne=class extends IYe{constructor(e){super(e);this.cursorHide(),this.reset(!0)}async initialize(){this.interpolate=await EYe(this),await super.initialize()}async reset(e){this.state.keys=[],this.state.invalid=new Map,this.state.missing=new Set,this.state.completed=0,this.state.values={},e!==!0&&(await this.initialize(),await this.render())}moveCursor(e){let t=this.getItem();this.cursor+=e,t.cursor+=e}dispatch(e,t){if(!t.code&&!t.ctrl&&e!=null&&this.getItem()){this.append(e,t);return}this.alert()}append(e,t){let i=this.getItem(),n=i.input.slice(0,this.cursor),s=i.input.slice(this.cursor);this.input=i.input=`${n}${e}${s}`,this.moveCursor(1),this.render()}delete(){let e=this.getItem();if(this.cursor<=0||!e.input)return this.alert();let t=e.input.slice(this.cursor),i=e.input.slice(0,this.cursor-1);this.input=e.input=`${i}${t}`,this.moveCursor(-1),this.render()}increment(e){return e>=this.state.keys.length-1?0:e+1}decrement(e){return e<=0?this.state.keys.length-1:e-1}first(){this.state.index=0,this.render()}last(){this.state.index=this.state.keys.length-1,this.render()}right(){if(this.cursor>=this.input.length)return this.alert();this.moveCursor(1),this.render()}left(){if(this.cursor<=0)return this.alert();this.moveCursor(-1),this.render()}prev(){this.state.index=this.decrement(this.state.index),this.getItem(),this.render()}next(){this.state.index=this.increment(this.state.index),this.getItem(),this.render()}up(){this.prev()}down(){this.next()}format(e){let t=this.state.completed<100?this.styles.warning:this.styles.success;return this.state.submitted===!0&&this.state.completed!==100&&(t=this.styles.danger),t(`${this.state.completed}% completed`)}async render(){let{index:e,keys:t=[],submitted:i,size:n}=this.state,s=[this.options.newline,` +`].find(v=>v!=null),o=await this.prefix(),a=await this.separator(),l=await this.message(),c=[o,l,a].filter(Boolean).join(" ");this.state.prompt=c;let u=await this.header(),g=await this.error()||"",f=await this.hint()||"",h=i?"":await this.interpolate(this.state),p=this.state.key=t[e]||"",m=await this.format(p),y=await this.footer();m&&(c+=" "+m),f&&!m&&this.state.completed===0&&(c+=" "+f),this.clear(n);let b=[u,c,h,y,g.trim()];this.write(b.filter(Boolean).join(s)),this.restore()}getItem(e){let{items:t,keys:i,index:n}=this.state,s=t.find(o=>o.name===i[n]);return s&&s.input!=null&&(this.input=s.input,this.cursor=s.cursor),s}async submit(){typeof this.interpolate!="function"&&await this.initialize(),await this.interpolate(this.state,!0);let{invalid:e,missing:t,output:i,values:n}=this.state;if(e.size){let a="";for(let[l,c]of e)a+=`Invalid ${l}: ${c} +`;return this.state.error=a,super.submit()}if(t.size)return this.state.error="Required: "+[...t.keys()].join(", "),super.submit();let o=mYe.unstyle(i).split(` +`).map(a=>a.slice(1)).join(` +`);return this.value={values:n,result:o},super.submit()}};lne.exports=cne});var hne=w((Vft,gne)=>{"use strict";var yYe="(Use + to sort)",wYe=Ll(),fne=class extends wYe{constructor(e){super(te(N({},e),{reorder:!1,sort:!0,multiple:!0}));this.state.hint=[this.options.hint,yYe].find(this.isValue.bind(this))}indicator(){return""}async renderChoice(e,t){let i=await super.renderChoice(e,t),n=this.symbols.identicalTo+" ",s=this.index===t&&this.sorting?this.styles.muted(n):" ";return this.options.drag===!1&&(s=""),this.options.numbered===!0?s+`${t+1} - `+i:s+i}get selected(){return this.choices}submit(){return this.value=this.choices.map(e=>e.value),super.submit()}};gne.exports=fne});var Cne=w((Xft,pne)=>{"use strict";var BYe=JC(),dne=class extends BYe{constructor(e={}){super(e);if(this.emptyError=e.emptyError||"No items were selected",this.term=process.env.TERM_PROGRAM,!this.options.header){let t=["","4 - Strongly Agree","3 - Agree","2 - Neutral","1 - Disagree","0 - Strongly Disagree",""];t=t.map(i=>this.styles.muted(i)),this.state.header=t.join(` + `)}}async toChoices(...e){if(this.createdScales)return!1;this.createdScales=!0;let t=await super.toChoices(...e);for(let i of t)i.scale=bYe(5,this.options),i.scaleIdx=2;return t}dispatch(){this.alert()}space(){let e=this.focused,t=e.scale[e.scaleIdx],i=t.selected;return e.scale.forEach(n=>n.selected=!1),t.selected=!i,this.render()}indicator(){return""}pointer(){return""}separator(){return this.styles.muted(this.symbols.ellipsis)}right(){let e=this.focused;return e.scaleIdx>=e.scale.length-1?this.alert():(e.scaleIdx++,this.render())}left(){let e=this.focused;return e.scaleIdx<=0?this.alert():(e.scaleIdx--,this.render())}indent(){return" "}async renderChoice(e,t){await this.onChoice(e,t);let i=this.index===t,n=this.term==="Hyper",s=n?9:8,o=n?"":" ",a=this.symbols.line.repeat(s),l=" ".repeat(s+(n?0:1)),c=k=>(k?this.styles.success("\u25C9"):"\u25EF")+o,u=t+1+".",g=i?this.styles.heading:this.styles.noop,f=await this.resolve(e.message,this.state,e,t),h=this.indent(e),p=h+e.scale.map((k,T)=>c(T===e.scaleIdx)).join(a),m=k=>k===e.scaleIdx?g(k):k,y=h+e.scale.map((k,T)=>m(T)).join(l),b=()=>[u,f].filter(Boolean).join(" "),v=()=>[b(),p,y," "].filter(Boolean).join(` +`);return i&&(p=this.styles.cyan(p),y=this.styles.cyan(y)),v()}async renderChoices(){if(this.state.submitted)return"";let e=this.visible.map(async(i,n)=>await this.renderChoice(i,n)),t=await Promise.all(e);return t.length||t.push(this.styles.danger("No matching choices")),t.join(` +`)}format(){return this.state.submitted?this.choices.map(t=>this.styles.info(t.scaleIdx)).join(", "):""}async render(){let{submitted:e,size:t}=this.state,i=await this.prefix(),n=await this.separator(),s=await this.message(),o=[i,s,n].filter(Boolean).join(" ");this.state.prompt=o;let a=await this.header(),l=await this.format(),c=await this.error()||await this.hint(),u=await this.renderChoices(),g=await this.footer();(l||!c)&&(o+=" "+l),c&&!o.includes(c)&&(o+=" "+c),e&&!l&&!u&&this.multiple&&this.type!=="form"&&(o+=this.styles.danger(this.emptyError)),this.clear(t),this.write([o,a,u,g].filter(Boolean).join(` +`)),this.restore()}submit(){this.value={};for(let e of this.choices)this.value[e.name]=e.scaleIdx;return this.base.submit.call(this)}};function bYe(r,e={}){if(Array.isArray(e.scale))return e.scale.map(i=>N({},i));let t=[];for(let i=1;i{mne.exports=nN()});var wne=w(($ft,Ine)=>{"use strict";var QYe=j0(),yne=class extends QYe{async initialize(){await super.initialize(),this.value=this.initial=!!this.options.initial,this.disabled=this.options.disabled||"no",this.enabled=this.options.enabled||"yes",await this.render()}reset(){this.value=this.initial,this.render()}delete(){this.alert()}toggle(){this.value=!this.value,this.render()}enable(){if(this.value===!0)return this.alert();this.value=!0,this.render()}disable(){if(this.value===!1)return this.alert();this.value=!1,this.render()}up(){this.toggle()}down(){this.toggle()}right(){this.toggle()}left(){this.toggle()}next(){this.toggle()}prev(){this.toggle()}dispatch(e="",t){switch(e.toLowerCase()){case" ":return this.toggle();case"1":case"y":case"t":return this.enable();case"0":case"n":case"f":return this.disable();default:return this.alert()}}format(){let e=i=>this.styles.primary.underline(i);return[this.value?this.disabled:e(this.disabled),this.value?e(this.enabled):this.enabled].join(this.styles.muted(" / "))}async render(){let{size:e}=this.state,t=await this.header(),i=await this.prefix(),n=await this.separator(),s=await this.message(),o=await this.format(),a=await this.error()||await this.hint(),l=await this.footer(),c=[i,s,n,o].join(" ");this.state.prompt=c,a&&!c.includes(a)&&(c+=" "+a),this.clear(e),this.write([t,c,l].filter(Boolean).join(` +`)),this.write(this.margin[2]),this.restore()}};Ine.exports=yne});var Qne=w((eht,Bne)=>{"use strict";var SYe=Ll(),bne=class extends SYe{constructor(e){super(e);if(typeof this.options.correctChoice!="number"||this.options.correctChoice<0)throw new Error("Please specify the index of the correct answer from the list of choices")}async toChoices(e,t){let i=await super.toChoices(e,t);if(i.length<2)throw new Error("Please give at least two choices to the user");if(this.options.correctChoice>i.length)throw new Error("Please specify the index of the correct answer from the list of choices");return i}check(e){return e.index===this.options.correctChoice}async result(e){return{selectedAnswer:e,correctAnswer:this.options.choices[this.options.correctChoice].value,correct:await this.check(this.state)}}};Bne.exports=bne});var vne=w(aN=>{"use strict";var Sne=Xi(),mi=(r,e)=>{Sne.defineExport(aN,r,e),Sne.defineExport(aN,r.toLowerCase(),e)};mi("AutoComplete",()=>pie());mi("BasicAuth",()=>Bie());mi("Confirm",()=>kie());mi("Editable",()=>Die());mi("Form",()=>H0());mi("Input",()=>nN());mi("Invisible",()=>Hie());mi("List",()=>Yie());mi("MultiSelect",()=>Wie());mi("Numeral",()=>Xie());mi("Password",()=>ene());mi("Scale",()=>nne());mi("Select",()=>Ll());mi("Snippet",()=>une());mi("Sort",()=>hne());mi("Survey",()=>Cne());mi("Text",()=>Ene());mi("Toggle",()=>wne());mi("Quiz",()=>Qne())});var xne=w((rht,kne)=>{kne.exports={ArrayPrompt:JC(),AuthPrompt:iN(),BooleanPrompt:j0(),NumberPrompt:sN(),StringPrompt:Bu()}});var zC=w((iht,Pne)=>{"use strict";var Dne=require("assert"),AN=require("events"),Tl=Xi(),pa=class extends AN{constructor(e,t){super();this.options=Tl.merge({},e),this.answers=N({},t)}register(e,t){if(Tl.isObject(e)){for(let n of Object.keys(e))this.register(n,e[n]);return this}Dne.equal(typeof t,"function","expected a function");let i=e.toLowerCase();return t.prototype instanceof this.Prompt?this.prompts[i]=t:this.prompts[i]=t(this.Prompt,this),this}async prompt(e=[]){for(let t of[].concat(e))try{typeof t=="function"&&(t=await t.call(this)),await this.ask(Tl.merge({},this.options,t))}catch(i){return Promise.reject(i)}return this.answers}async ask(e){typeof e=="function"&&(e=await e.call(this));let t=Tl.merge({},this.options,e),{type:i,name:n}=e,{set:s,get:o}=Tl;if(typeof i=="function"&&(i=await i.call(this,e,this.answers)),!i)return this.answers[n];Dne(this.prompts[i],`Prompt "${i}" is not registered`);let a=new this.prompts[i](t),l=o(this.answers,n);a.state.answers=this.answers,a.enquirer=this,n&&a.on("submit",u=>{this.emit("answer",n,u,a),s(this.answers,n,u)});let c=a.emit.bind(a);return a.emit=(...u)=>(this.emit.call(this,...u),c(...u)),this.emit("prompt",a,this),t.autofill&&l!=null?(a.value=a.input=l,t.autofill==="show"&&await a.submit()):l=a.value=await a.run(),l}use(e){return e.call(this,this),this}set Prompt(e){this._Prompt=e}get Prompt(){return this._Prompt||this.constructor.Prompt}get prompts(){return this.constructor.prompts}static set Prompt(e){this._Prompt=e}static get Prompt(){return this._Prompt||Vf()}static get prompts(){return vne()}static get types(){return xne()}static get prompt(){let e=(t,...i)=>{let n=new this(...i),s=n.emit.bind(n);return n.emit=(...o)=>(e.emit(...o),s(...o)),n.prompt(t)};return Tl.mixinEmitter(e,new AN),e}};Tl.mixinEmitter(pa,new AN);var lN=pa.prompts;for(let r of Object.keys(lN)){let e=r.toLowerCase(),t=i=>new lN[r](i).run();pa.prompt[e]=t,pa[e]=t,pa[r]||Reflect.defineProperty(pa,r,{get:()=>lN[r]})}var WC=r=>{Tl.defineExport(pa,r,()=>pa.types[r])};WC("ArrayPrompt");WC("AuthPrompt");WC("BooleanPrompt");WC("NumberPrompt");WC("StringPrompt");Pne.exports=pa});var Yne=w((Jht,Gne)=>{function DYe(r,e){for(var t=-1,i=r==null?0:r.length;++t{var RYe=e0(),FYe=Lf();function NYe(r,e,t,i){var n=!t;t||(t={});for(var s=-1,o=e.length;++s{var LYe=$f(),TYe=Kf();function OYe(r,e){return r&&LYe(e,TYe(e),r)}Jne.exports=OYe});var _ne=w((_ht,zne)=>{function MYe(r){var e=[];if(r!=null)for(var t in Object(r))e.push(t);return e}zne.exports=MYe});var Xne=w((Vht,Vne)=>{var UYe=Fn(),KYe=h0(),HYe=_ne(),jYe=Object.prototype,GYe=jYe.hasOwnProperty;function YYe(r){if(!UYe(r))return HYe(r);var e=KYe(r),t=[];for(var i in r)i=="constructor"&&(e||!GYe.call(r,i))||t.push(i);return t}Vne.exports=YYe});var eh=w((Xht,Zne)=>{var qYe=eF(),JYe=Xne(),WYe=kC();function zYe(r){return WYe(r)?qYe(r,!0):JYe(r)}Zne.exports=zYe});var ese=w((Zht,$ne)=>{var _Ye=$f(),VYe=eh();function XYe(r,e){return r&&_Ye(e,VYe(e),r)}$ne.exports=XYe});var pN=w((tm,th)=>{var ZYe=Ts(),tse=typeof tm=="object"&&tm&&!tm.nodeType&&tm,rse=tse&&typeof th=="object"&&th&&!th.nodeType&&th,$Ye=rse&&rse.exports===tse,ise=$Ye?ZYe.Buffer:void 0,nse=ise?ise.allocUnsafe:void 0;function eqe(r,e){if(e)return r.slice();var t=r.length,i=nse?nse(t):new r.constructor(t);return r.copy(i),i}th.exports=eqe});var dN=w(($ht,sse)=>{function tqe(r,e){var t=-1,i=r.length;for(e||(e=Array(i));++t{var rqe=$f(),iqe=d0();function nqe(r,e){return rqe(r,iqe(r),e)}ose.exports=nqe});var Y0=w((tpt,Ase)=>{var sqe=tF(),oqe=sqe(Object.getPrototypeOf,Object);Ase.exports=oqe});var CN=w((rpt,lse)=>{var aqe=r0(),Aqe=Y0(),lqe=d0(),cqe=aF(),uqe=Object.getOwnPropertySymbols,gqe=uqe?function(r){for(var e=[];r;)aqe(e,lqe(r)),r=Aqe(r);return e}:cqe;lse.exports=gqe});var use=w((ipt,cse)=>{var fqe=$f(),hqe=CN();function pqe(r,e){return fqe(r,hqe(r),e)}cse.exports=pqe});var fse=w((npt,gse)=>{var dqe=oF(),Cqe=CN(),mqe=eh();function Eqe(r){return dqe(r,mqe,Cqe)}gse.exports=Eqe});var pse=w((spt,hse)=>{var Iqe=Object.prototype,yqe=Iqe.hasOwnProperty;function wqe(r){var e=r.length,t=new r.constructor(e);return e&&typeof r[0]=="string"&&yqe.call(r,"index")&&(t.index=r.index,t.input=r.input),t}hse.exports=wqe});var q0=w((opt,dse)=>{var Cse=nF();function Bqe(r){var e=new r.constructor(r.byteLength);return new Cse(e).set(new Cse(r)),e}dse.exports=Bqe});var Ese=w((apt,mse)=>{var bqe=q0();function Qqe(r,e){var t=e?bqe(r.buffer):r.buffer;return new r.constructor(t,r.byteOffset,r.byteLength)}mse.exports=Qqe});var yse=w((Apt,Ise)=>{var Sqe=/\w*$/;function vqe(r){var e=new r.constructor(r.source,Sqe.exec(r));return e.lastIndex=r.lastIndex,e}Ise.exports=vqe});var Sse=w((lpt,wse)=>{var Bse=Wc(),bse=Bse?Bse.prototype:void 0,Qse=bse?bse.valueOf:void 0;function kqe(r){return Qse?Object(Qse.call(r)):{}}wse.exports=kqe});var mN=w((cpt,vse)=>{var xqe=q0();function Pqe(r,e){var t=e?xqe(r.buffer):r.buffer;return new r.constructor(t,r.byteOffset,r.length)}vse.exports=Pqe});var xse=w((upt,kse)=>{var Dqe=q0(),Rqe=Ese(),Fqe=yse(),Nqe=Sse(),Lqe=mN(),Tqe="[object Boolean]",Oqe="[object Date]",Mqe="[object Map]",Uqe="[object Number]",Kqe="[object RegExp]",Hqe="[object Set]",jqe="[object String]",Gqe="[object Symbol]",Yqe="[object ArrayBuffer]",qqe="[object DataView]",Jqe="[object Float32Array]",Wqe="[object Float64Array]",zqe="[object Int8Array]",_qe="[object Int16Array]",Vqe="[object Int32Array]",Xqe="[object Uint8Array]",Zqe="[object Uint8ClampedArray]",$qe="[object Uint16Array]",eJe="[object Uint32Array]";function tJe(r,e,t){var i=r.constructor;switch(e){case Yqe:return Dqe(r);case Tqe:case Oqe:return new i(+r);case qqe:return Rqe(r,t);case Jqe:case Wqe:case zqe:case _qe:case Vqe:case Xqe:case Zqe:case $qe:case eJe:return Lqe(r,t);case Mqe:return new i;case Uqe:case jqe:return new i(r);case Kqe:return Fqe(r);case Hqe:return new i;case Gqe:return Nqe(r)}}kse.exports=tJe});var Rse=w((gpt,Pse)=>{var rJe=Fn(),Dse=Object.create,iJe=function(){function r(){}return function(e){if(!rJe(e))return{};if(Dse)return Dse(e);r.prototype=e;var t=new r;return r.prototype=void 0,t}}();Pse.exports=iJe});var EN=w((fpt,Fse)=>{var nJe=Rse(),sJe=Y0(),oJe=h0();function aJe(r){return typeof r.constructor=="function"&&!oJe(r)?nJe(sJe(r)):{}}Fse.exports=aJe});var Lse=w((hpt,Nse)=>{var AJe=PC(),lJe=ra(),cJe="[object Map]";function uJe(r){return lJe(r)&&AJe(r)==cJe}Nse.exports=uJe});var Use=w((ppt,Tse)=>{var gJe=Lse(),fJe=u0(),Ose=g0(),Mse=Ose&&Ose.isMap,hJe=Mse?fJe(Mse):gJe;Tse.exports=hJe});var Hse=w((dpt,Kse)=>{var pJe=PC(),dJe=ra(),CJe="[object Set]";function mJe(r){return dJe(r)&&pJe(r)==CJe}Kse.exports=mJe});var qse=w((Cpt,jse)=>{var EJe=Hse(),IJe=u0(),Gse=g0(),Yse=Gse&&Gse.isSet,yJe=Yse?IJe(Yse):EJe;jse.exports=yJe});var Vse=w((mpt,Jse)=>{var wJe=xC(),BJe=Yne(),bJe=e0(),QJe=Wne(),SJe=ese(),vJe=pN(),kJe=dN(),xJe=ase(),PJe=use(),DJe=AF(),RJe=fse(),FJe=PC(),NJe=pse(),LJe=xse(),TJe=EN(),OJe=Hs(),MJe=QC(),UJe=Use(),KJe=Fn(),HJe=qse(),jJe=Kf(),GJe=eh(),YJe=1,qJe=2,JJe=4,Wse="[object Arguments]",WJe="[object Array]",zJe="[object Boolean]",_Je="[object Date]",VJe="[object Error]",zse="[object Function]",XJe="[object GeneratorFunction]",ZJe="[object Map]",$Je="[object Number]",_se="[object Object]",eWe="[object RegExp]",tWe="[object Set]",rWe="[object String]",iWe="[object Symbol]",nWe="[object WeakMap]",sWe="[object ArrayBuffer]",oWe="[object DataView]",aWe="[object Float32Array]",AWe="[object Float64Array]",lWe="[object Int8Array]",cWe="[object Int16Array]",uWe="[object Int32Array]",gWe="[object Uint8Array]",fWe="[object Uint8ClampedArray]",hWe="[object Uint16Array]",pWe="[object Uint32Array]",dr={};dr[Wse]=dr[WJe]=dr[sWe]=dr[oWe]=dr[zJe]=dr[_Je]=dr[aWe]=dr[AWe]=dr[lWe]=dr[cWe]=dr[uWe]=dr[ZJe]=dr[$Je]=dr[_se]=dr[eWe]=dr[tWe]=dr[rWe]=dr[iWe]=dr[gWe]=dr[fWe]=dr[hWe]=dr[pWe]=!0;dr[VJe]=dr[zse]=dr[nWe]=!1;function J0(r,e,t,i,n,s){var o,a=e&YJe,l=e&qJe,c=e&JJe;if(t&&(o=n?t(r,i,n,s):t(r)),o!==void 0)return o;if(!KJe(r))return r;var u=OJe(r);if(u){if(o=NJe(r),!a)return kJe(r,o)}else{var g=FJe(r),f=g==zse||g==XJe;if(MJe(r))return vJe(r,a);if(g==_se||g==Wse||f&&!n){if(o=l||f?{}:TJe(r),!a)return l?PJe(r,SJe(o,r)):xJe(r,QJe(o,r))}else{if(!dr[g])return n?r:{};o=LJe(r,g,a)}}s||(s=new wJe);var h=s.get(r);if(h)return h;s.set(r,o),HJe(r)?r.forEach(function(y){o.add(J0(y,e,t,y,r,s))}):UJe(r)&&r.forEach(function(y,b){o.set(b,J0(y,e,t,b,r,s))});var p=c?l?RJe:DJe:l?GJe:jJe,m=u?void 0:p(r);return BJe(m||r,function(y,b){m&&(b=y,y=r[b]),bJe(o,b,J0(y,e,t,b,r,s))}),o}Jse.exports=J0});var IN=w((Ept,Xse)=>{var dWe=Vse(),CWe=1,mWe=4;function EWe(r){return dWe(r,CWe|mWe)}Xse.exports=EWe});var $se=w((Ipt,Zse)=>{var IWe=PR();function yWe(r,e,t){return r==null?r:IWe(r,e,t)}Zse.exports=yWe});var soe=w((Spt,noe)=>{function wWe(r){var e=r==null?0:r.length;return e?r[e-1]:void 0}noe.exports=wWe});var aoe=w((vpt,ooe)=>{var BWe=pC(),bWe=XP();function QWe(r,e){return e.length<2?r:BWe(r,bWe(e,0,-1))}ooe.exports=QWe});var loe=w((kpt,Aoe)=>{var SWe=Nf(),vWe=soe(),kWe=aoe(),xWe=fu();function PWe(r,e){return e=SWe(e,r),r=kWe(r,e),r==null||delete r[xWe(vWe(e))]}Aoe.exports=PWe});var uoe=w((xpt,coe)=>{var DWe=loe();function RWe(r,e){return r==null?!0:DWe(r,e)}coe.exports=RWe});var Ioe=w((sdt,Eoe)=>{Eoe.exports={name:"@yarnpkg/cli",version:"3.2.3",license:"BSD-2-Clause",main:"./sources/index.ts",dependencies:{"@yarnpkg/core":"workspace:^","@yarnpkg/fslib":"workspace:^","@yarnpkg/libzip":"workspace:^","@yarnpkg/parsers":"workspace:^","@yarnpkg/plugin-compat":"workspace:^","@yarnpkg/plugin-dlx":"workspace:^","@yarnpkg/plugin-essentials":"workspace:^","@yarnpkg/plugin-file":"workspace:^","@yarnpkg/plugin-git":"workspace:^","@yarnpkg/plugin-github":"workspace:^","@yarnpkg/plugin-http":"workspace:^","@yarnpkg/plugin-init":"workspace:^","@yarnpkg/plugin-link":"workspace:^","@yarnpkg/plugin-nm":"workspace:^","@yarnpkg/plugin-npm":"workspace:^","@yarnpkg/plugin-npm-cli":"workspace:^","@yarnpkg/plugin-pack":"workspace:^","@yarnpkg/plugin-patch":"workspace:^","@yarnpkg/plugin-pnp":"workspace:^","@yarnpkg/plugin-pnpm":"workspace:^","@yarnpkg/shell":"workspace:^",chalk:"^3.0.0","ci-info":"^3.2.0",clipanion:"3.2.0-rc.4",semver:"^7.1.2",tslib:"^1.13.0",typanion:"^3.3.0",yup:"^0.32.9"},devDependencies:{"@types/semver":"^7.1.0","@types/yup":"^0","@yarnpkg/builder":"workspace:^","@yarnpkg/monorepo":"workspace:^","@yarnpkg/pnpify":"workspace:^",micromatch:"^4.0.2"},peerDependencies:{"@yarnpkg/core":"workspace:^"},scripts:{postpack:"rm -rf lib",prepack:'run build:compile "$(pwd)"',"build:cli+hook":"run build:pnp:hook && builder build bundle","build:cli":"builder build bundle","run:cli":"builder run","update-local":"run build:cli --no-git-hash && rsync -a --delete bundles/ bin/"},publishConfig:{main:"./lib/index.js",types:"./lib/index.d.ts",bin:null},files:["/lib/**/*","!/lib/pluginConfiguration.*","!/lib/cli.*"],"@yarnpkg/builder":{bundles:{standard:["@yarnpkg/plugin-essentials","@yarnpkg/plugin-compat","@yarnpkg/plugin-dlx","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm"]}},repository:{type:"git",url:"ssh://git@github.com/yarnpkg/berry.git",directory:"packages/yarnpkg-cli"},engines:{node:">=12 <14 || 14.2 - 14.9 || >14.10.0"}}});var RN=w((OEt,oae)=>{"use strict";oae.exports=function(e,t){t===!0&&(t=0);var i=e.indexOf("://"),n=e.substring(0,i).split("+").filter(Boolean);return typeof t=="number"?n[t]:n}});var FN=w((MEt,aae)=>{"use strict";var $We=RN();function Aae(r){if(Array.isArray(r))return r.indexOf("ssh")!==-1||r.indexOf("rsync")!==-1;if(typeof r!="string")return!1;var e=$We(r);return r=r.substring(r.indexOf("://")+3),Aae(e)?!0:r.indexOf("@"){"use strict";var e3e=RN(),t3e=FN(),r3e=require("querystring");function i3e(r){r=(r||"").trim();var e={protocols:e3e(r),protocol:null,port:null,resource:"",user:"",pathname:"",hash:"",search:"",href:r,query:Object.create(null)},t=r.indexOf("://"),i=-1,n=null,s=null;r.startsWith(".")&&(r.startsWith("./")&&(r=r.substring(2)),e.pathname=r,e.protocol="file");var o=r.charAt(1);return e.protocol||(e.protocol=e.protocols[0],e.protocol||(t3e(r)?e.protocol="ssh":((o==="/"||o==="~")&&(r=r.substring(2)),e.protocol="file"))),t!==-1&&(r=r.substring(t+3)),s=r.split("/"),e.protocol!=="file"?e.resource=s.shift():e.resource="",n=e.resource.split("@"),n.length===2&&(e.user=n[0],e.resource=n[1]),n=e.resource.split(":"),n.length===2&&(e.resource=n[0],n[1]?(e.port=Number(n[1]),isNaN(e.port)&&(e.port=null,s.unshift(n[1]))):e.port=null),s=s.filter(Boolean),e.protocol==="file"?e.pathname=e.href:e.pathname=e.pathname||(e.protocol!=="file"||e.href[0]==="/"?"/":"")+s.join("/"),n=e.pathname.split("#"),n.length===2&&(e.pathname=n[0],e.hash=n[1]),n=e.pathname.split("?"),n.length===2&&(e.pathname=n[0],e.search=n[1]),e.query=r3e.parse(e.search),e.href=e.href.replace(/\/$/,""),e.pathname=e.pathname.replace(/\/$/,""),e}lae.exports=i3e});var fae=w((KEt,uae)=>{"use strict";var n3e="text/plain",s3e="us-ascii",gae=(r,e)=>e.some(t=>t instanceof RegExp?t.test(r):t===r),o3e=(r,{stripHash:e})=>{let t=/^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(r);if(!t)throw new Error(`Invalid URL: ${r}`);let{type:i,data:n,hash:s}=t.groups,o=i.split(";");s=e?"":s;let a=!1;o[o.length-1]==="base64"&&(o.pop(),a=!0);let l=(o.shift()||"").toLowerCase(),u=[...o.map(g=>{let[f,h=""]=g.split("=").map(p=>p.trim());return f==="charset"&&(h=h.toLowerCase(),h===s3e)?"":`${f}${h?`=${h}`:""}`}).filter(Boolean)];return a&&u.push("base64"),(u.length!==0||l&&l!==n3e)&&u.unshift(l),`data:${u.join(";")},${a?n.trim():n}${s?`#${s}`:""}`},a3e=(r,e)=>{if(e=N({defaultProtocol:"http:",normalizeProtocol:!0,forceHttp:!1,forceHttps:!1,stripAuthentication:!0,stripHash:!1,stripTextFragment:!0,stripWWW:!0,removeQueryParameters:[/^utm_\w+/i],removeTrailingSlash:!0,removeSingleSlash:!0,removeDirectoryIndex:!1,sortQueryParameters:!0},e),r=r.trim(),/^data:/i.test(r))return o3e(r,e);if(/^view-source:/i.test(r))throw new Error("`view-source:` is not supported as it is a non-standard protocol");let t=r.startsWith("//");!t&&/^\.*\//.test(r)||(r=r.replace(/^(?!(?:\w+:)?\/\/)|^\/\//,e.defaultProtocol));let n=new URL(r);if(e.forceHttp&&e.forceHttps)throw new Error("The `forceHttp` and `forceHttps` options cannot be used together");if(e.forceHttp&&n.protocol==="https:"&&(n.protocol="http:"),e.forceHttps&&n.protocol==="http:"&&(n.protocol="https:"),e.stripAuthentication&&(n.username="",n.password=""),e.stripHash?n.hash="":e.stripTextFragment&&(n.hash=n.hash.replace(/#?:~:text.*?$/i,"")),n.pathname&&(n.pathname=n.pathname.replace(/(?0){let o=n.pathname.split("/"),a=o[o.length-1];gae(a,e.removeDirectoryIndex)&&(o=o.slice(0,o.length-1),n.pathname=o.slice(1).join("/")+"/")}if(n.hostname&&(n.hostname=n.hostname.replace(/\.$/,""),e.stripWWW&&/^www\.(?!www\.)(?:[a-z\-\d]{1,63})\.(?:[a-z.\-\d]{2,63})$/.test(n.hostname)&&(n.hostname=n.hostname.replace(/^www\./,""))),Array.isArray(e.removeQueryParameters))for(let o of[...n.searchParams.keys()])gae(o,e.removeQueryParameters)&&n.searchParams.delete(o);e.removeQueryParameters===!0&&(n.search=""),e.sortQueryParameters&&n.searchParams.sort(),e.removeTrailingSlash&&(n.pathname=n.pathname.replace(/\/$/,""));let s=r;return r=n.toString(),!e.removeSingleSlash&&n.pathname==="/"&&!s.endsWith("/")&&n.hash===""&&(r=r.replace(/\/$/,"")),(e.removeTrailingSlash||n.pathname==="/")&&n.hash===""&&e.removeSingleSlash&&(r=r.replace(/\/$/,"")),t&&!e.normalizeProtocol&&(r=r.replace(/^http:\/\//,"//")),e.stripProtocol&&(r=r.replace(/^(?:https?:)?\/\//,"")),r};uae.exports=a3e});var pae=w((HEt,hae)=>{"use strict";var A3e=typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?function(r){return typeof r}:function(r){return r&&typeof Symbol=="function"&&r.constructor===Symbol&&r!==Symbol.prototype?"symbol":typeof r},l3e=cae(),c3e=fae();function u3e(r){var e=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;if(typeof r!="string"||!r.trim())throw new Error("Invalid url.");e&&((typeof e=="undefined"?"undefined":A3e(e))!=="object"&&(e={stripHash:!1}),r=c3e(r,e));var t=l3e(r);return t}hae.exports=u3e});var mae=w((jEt,dae)=>{"use strict";var g3e=pae(),Cae=FN();function f3e(r){var e=g3e(r);e.token="";var t=e.user.split(":");return t.length===2&&(t[1]==="x-oauth-basic"?e.token=t[0]:t[0]==="x-token-auth"&&(e.token=t[1])),Cae(e.protocols)||Cae(r)?e.protocol="ssh":e.protocols.length?e.protocol=e.protocols[0]:e.protocol="file",e.href=e.href.replace(/\/$/,""),e}dae.exports=f3e});var Iae=w((GEt,Eae)=>{"use strict";var h3e=mae();function NN(r){if(typeof r!="string")throw new Error("The url must be a string.");var e=h3e(r),t=e.resource.split("."),i=null;switch(e.toString=function(l){return NN.stringify(this,l)},e.source=t.length>2?t.slice(1-t.length).join("."):e.source=e.resource,e.git_suffix=/\.git$/.test(e.pathname),e.name=decodeURIComponent(e.pathname.replace(/^\//,"").replace(/\.git$/,"")),e.owner=decodeURIComponent(e.user),e.source){case"git.cloudforge.com":e.owner=e.user,e.organization=t[0],e.source="cloudforge.com";break;case"visualstudio.com":if(e.resource==="vs-ssh.visualstudio.com"){i=e.name.split("/"),i.length===4&&(e.organization=i[1],e.owner=i[2],e.name=i[3],e.full_name=i[2]+"/"+i[3]);break}else{i=e.name.split("/"),i.length===2?(e.owner=i[1],e.name=i[1],e.full_name="_git/"+e.name):i.length===3?(e.name=i[2],i[0]==="DefaultCollection"?(e.owner=i[2],e.organization=i[0],e.full_name=e.organization+"/_git/"+e.name):(e.owner=i[0],e.full_name=e.owner+"/_git/"+e.name)):i.length===4&&(e.organization=i[0],e.owner=i[1],e.name=i[3],e.full_name=e.organization+"/"+e.owner+"/_git/"+e.name);break}case"dev.azure.com":case"azure.com":if(e.resource==="ssh.dev.azure.com"){i=e.name.split("/"),i.length===4&&(e.organization=i[1],e.owner=i[2],e.name=i[3]);break}else{i=e.name.split("/"),i.length===5?(e.organization=i[0],e.owner=i[1],e.name=i[4],e.full_name="_git/"+e.name):i.length===3?(e.name=i[2],i[0]==="DefaultCollection"?(e.owner=i[2],e.organization=i[0],e.full_name=e.organization+"/_git/"+e.name):(e.owner=i[0],e.full_name=e.owner+"/_git/"+e.name)):i.length===4&&(e.organization=i[0],e.owner=i[1],e.name=i[3],e.full_name=e.organization+"/"+e.owner+"/_git/"+e.name);break}default:i=e.name.split("/");var n=i.length-1;if(i.length>=2){var s=i.indexOf("blob",2),o=i.indexOf("tree",2),a=i.indexOf("commit",2);n=s>0?s-1:o>0?o-1:a>0?a-1:n,e.owner=i.slice(0,n).join("/"),e.name=i[n],a&&(e.commit=i[n+2])}e.ref="",e.filepathtype="",e.filepath="",i.length>n+2&&["blob","tree"].indexOf(i[n+1])>=0&&(e.filepathtype=i[n+1],e.ref=i[n+2],i.length>n+3&&(e.filepath=i.slice(n+3).join("/"))),e.organization=e.owner;break}return e.full_name||(e.full_name=e.owner,e.name&&(e.full_name&&(e.full_name+="/"),e.full_name+=e.name)),e}NN.stringify=function(r,e){e=e||(r.protocols&&r.protocols.length?r.protocols.join("+"):r.protocol);var t=r.port?":"+r.port:"",i=r.user||"git",n=r.git_suffix?".git":"";switch(e){case"ssh":return t?"ssh://"+i+"@"+r.resource+t+"/"+r.full_name+n:i+"@"+r.resource+":"+r.full_name+n;case"git+ssh":case"ssh+git":case"ftp":case"ftps":return e+"://"+i+"@"+r.resource+t+"/"+r.full_name+n;case"http":case"https":var s=r.token?p3e(r):r.user&&(r.protocols.includes("http")||r.protocols.includes("https"))?r.user+"@":"";return e+"://"+s+r.resource+t+"/"+r.full_name+n;default:return r.href}};function p3e(r){switch(r.source){case"bitbucket.org":return"x-token-auth:"+r.token+"@";default:return r.token+"@"}}Eae.exports=NN});var uL=w((Zwt,jae)=>{var N3e=Lf(),L3e=Df();function T3e(r,e,t){(t!==void 0&&!L3e(r[e],t)||t===void 0&&!(e in r))&&N3e(r,e,t)}jae.exports=T3e});var Yae=w(($wt,Gae)=>{var O3e=kC(),M3e=ra();function U3e(r){return M3e(r)&&O3e(r)}Gae.exports=U3e});var Wae=w((eBt,qae)=>{var K3e=zc(),H3e=Y0(),j3e=ra(),G3e="[object Object]",Y3e=Function.prototype,q3e=Object.prototype,Jae=Y3e.toString,J3e=q3e.hasOwnProperty,W3e=Jae.call(Object);function z3e(r){if(!j3e(r)||K3e(r)!=G3e)return!1;var e=H3e(r);if(e===null)return!0;var t=J3e.call(e,"constructor")&&e.constructor;return typeof t=="function"&&t instanceof t&&Jae.call(t)==W3e}qae.exports=z3e});var gL=w((tBt,zae)=>{function _3e(r,e){if(!(e==="constructor"&&typeof r[e]=="function")&&e!="__proto__")return r[e]}zae.exports=_3e});var Vae=w((rBt,_ae)=>{var V3e=$f(),X3e=eh();function Z3e(r){return V3e(r,X3e(r))}_ae.exports=Z3e});var rAe=w((iBt,Xae)=>{var Zae=uL(),$3e=pN(),e4e=mN(),t4e=dN(),r4e=EN(),$ae=CC(),eAe=Hs(),i4e=Yae(),n4e=QC(),s4e=XB(),o4e=Fn(),a4e=Wae(),A4e=f0(),tAe=gL(),l4e=Vae();function c4e(r,e,t,i,n,s,o){var a=tAe(r,t),l=tAe(e,t),c=o.get(l);if(c){Zae(r,t,c);return}var u=s?s(a,l,t+"",r,e,o):void 0,g=u===void 0;if(g){var f=eAe(l),h=!f&&n4e(l),p=!f&&!h&&A4e(l);u=l,f||h||p?eAe(a)?u=a:i4e(a)?u=t4e(a):h?(g=!1,u=$3e(l,!0)):p?(g=!1,u=e4e(l,!0)):u=[]:a4e(l)||$ae(l)?(u=a,$ae(a)?u=l4e(a):(!o4e(a)||s4e(a))&&(u=r4e(l))):g=!1}g&&(o.set(l,u),n(u,l,i,s,o),o.delete(l)),Zae(r,t,u)}Xae.exports=c4e});var sAe=w((nBt,iAe)=>{var u4e=xC(),g4e=uL(),f4e=ZR(),h4e=rAe(),p4e=Fn(),d4e=eh(),C4e=gL();function nAe(r,e,t,i,n){r!==e&&f4e(e,function(s,o){if(n||(n=new u4e),p4e(s))h4e(r,e,o,t,nAe,i,n);else{var a=i?i(C4e(r,o),s,o+"",r,e,n):void 0;a===void 0&&(a=s),g4e(r,o,a)}},d4e)}iAe.exports=nAe});var aAe=w((sBt,oAe)=>{var m4e=i0(),E4e=FR(),I4e=NR();function y4e(r,e){return I4e(E4e(r,e,m4e),r+"")}oAe.exports=y4e});var lAe=w((oBt,AAe)=>{var w4e=Df(),B4e=kC(),b4e=dC(),Q4e=Fn();function S4e(r,e,t){if(!Q4e(t))return!1;var i=typeof e;return(i=="number"?B4e(t)&&b4e(e,t.length):i=="string"&&e in t)?w4e(t[e],r):!1}AAe.exports=S4e});var uAe=w((aBt,cAe)=>{var v4e=aAe(),k4e=lAe();function x4e(r){return v4e(function(e,t){var i=-1,n=t.length,s=n>1?t[n-1]:void 0,o=n>2?t[2]:void 0;for(s=r.length>3&&typeof s=="function"?(n--,s):void 0,o&&k4e(t[0],t[1],o)&&(s=n<3?void 0:s,n=1),e=Object(e);++i{var P4e=sAe(),D4e=uAe(),R4e=D4e(function(r,e,t){P4e(r,e,t)});gAe.exports=R4e});var xAe=w((f0t,kAe)=>{var SL;kAe.exports=()=>(typeof SL=="undefined"&&(SL=require("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),SL)});var KAe=w((J0t,UAe)=>{var PL;UAe.exports=()=>(typeof PL=="undefined"&&(PL=require("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),PL)});var zAe=w((OL,ML)=>{(function(r){OL&&typeof OL=="object"&&typeof ML!="undefined"?ML.exports=r():typeof define=="function"&&define.amd?define([],r):typeof window!="undefined"?window.isWindows=r():typeof global!="undefined"?global.isWindows=r():typeof self!="undefined"?self.isWindows=r():this.isWindows=r()})(function(){"use strict";return function(){return process&&(process.platform==="win32"||/^(msys|cygwin)$/.test(process.env.OSTYPE))}})});var ZAe=w((Zbt,_Ae)=>{"use strict";UL.ifExists=Ize;var Ah=require("util"),Vs=require("path"),VAe=zAe(),yze=/^#!\s*(?:\/usr\/bin\/env)?\s*([^ \t]+)(.*)$/,wze={createPwshFile:!0,createCmdFile:VAe(),fs:require("fs")},Bze=new Map([[".js","node"],[".cjs","node"],[".mjs","node"],[".cmd","cmd"],[".bat","cmd"],[".ps1","pwsh"],[".sh","sh"]]);function XAe(r){let e=N(N({},wze),r),t=e.fs;return e.fs_={chmod:t.chmod?Ah.promisify(t.chmod):async()=>{},mkdir:Ah.promisify(t.mkdir),readFile:Ah.promisify(t.readFile),stat:Ah.promisify(t.stat),unlink:Ah.promisify(t.unlink),writeFile:Ah.promisify(t.writeFile)},e}async function UL(r,e,t){let i=XAe(t);await i.fs_.stat(r),await bze(r,e,i)}function Ize(r,e,t){return UL(r,e,t).catch(()=>{})}function Qze(r,e){return e.fs_.unlink(r).catch(()=>{})}async function bze(r,e,t){let i=await kze(r,t);return await Sze(e,t),vze(r,e,i,t)}function Sze(r,e){return e.fs_.mkdir(Vs.dirname(r),{recursive:!0})}function vze(r,e,t,i){let n=XAe(i),s=[{generator:Dze,extension:""}];return n.createCmdFile&&s.push({generator:Pze,extension:".cmd"}),n.createPwshFile&&s.push({generator:Rze,extension:".ps1"}),Promise.all(s.map(o=>xze(r,e+o.extension,t,o.generator,n)))}function Fze(r,e){return Qze(r,e)}function Lze(r,e){return Nze(r,e)}async function kze(r,e){let n=(await e.fs_.readFile(r,"utf8")).trim().split(/\r*\n/)[0].match(yze);if(!n){let s=Vs.extname(r).toLowerCase();return{program:Bze.get(s)||null,additionalArgs:""}}return{program:n[1],additionalArgs:n[2]}}async function xze(r,e,t,i,n){let s=n.preserveSymlinks?"--preserve-symlinks":"",o=[t.additionalArgs,s].filter(a=>a).join(" ");return n=Object.assign({},n,{prog:t.program,args:o}),await Fze(e,n),await n.fs_.writeFile(e,i(r,e,n),"utf8"),Lze(e,n)}function Pze(r,e,t){let n=Vs.relative(Vs.dirname(e),r).split("/").join("\\"),s=Vs.isAbsolute(n)?`"${n}"`:`"%~dp0\\${n}"`,o,a=t.prog,l=t.args||"",c=KL(t.nodePath).win32;a?(o=`"%~dp0\\${a}.exe"`,n=s):(a=s,l="",n="");let u=t.progArgs?`${t.progArgs.join(" ")} `:"",g=c?`@SET NODE_PATH=${c}\r +`:"";return o?g+=`@IF EXIST ${o} (\r + ${o} ${l} ${n} ${u}%*\r +) ELSE (\r + @SETLOCAL\r + @SET PATHEXT=%PATHEXT:;.JS;=;%\r + ${a} ${l} ${n} ${u}%*\r +)\r +`:g+=`@${a} ${l} ${n} ${u}%*\r +`,g}function Dze(r,e,t){let i=Vs.relative(Vs.dirname(e),r),n=t.prog&&t.prog.split("\\").join("/"),s;i=i.split("\\").join("/");let o=Vs.isAbsolute(i)?`"${i}"`:`"$basedir/${i}"`,a=t.args||"",l=KL(t.nodePath).posix;n?(s=`"$basedir/${t.prog}"`,i=o):(n=o,a="",i="");let c=t.progArgs?`${t.progArgs.join(" ")} `:"",u=`#!/bin/sh +basedir=$(dirname "$(echo "$0" | sed -e 's,\\\\,/,g')") + +case \`uname\` in + *CYGWIN*) basedir=\`cygpath -w "$basedir"\`;; +esac + +`,g=t.nodePath?`export NODE_PATH="${l}" +`:"";return s?u+=`${g}if [ -x ${s} ]; then + exec ${s} ${a} ${i} ${c}"$@" +else + exec ${n} ${a} ${i} ${c}"$@" +fi +`:u+=`${g}${n} ${a} ${i} ${c}"$@" +exit $? +`,u}function Rze(r,e,t){let i=Vs.relative(Vs.dirname(e),r),n=t.prog&&t.prog.split("\\").join("/"),s=n&&`"${n}$exe"`,o;i=i.split("\\").join("/");let a=Vs.isAbsolute(i)?`"${i}"`:`"$basedir/${i}"`,l=t.args||"",c=KL(t.nodePath),u=c.win32,g=c.posix;s?(o=`"$basedir/${t.prog}$exe"`,i=a):(s=a,l="",i="");let f=t.progArgs?`${t.progArgs.join(" ")} `:"",h=`#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +${t.nodePath?`$env_node_path=$env:NODE_PATH +$env:NODE_PATH="${u}" +`:""}if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +}`;return t.nodePath&&(h+=` else { + $env:NODE_PATH="${g}" +}`),o?h+=` +$ret=0 +if (Test-Path ${o}) { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & ${o} ${l} ${i} ${f}$args + } else { + & ${o} ${l} ${i} ${f}$args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & ${s} ${l} ${i} ${f}$args + } else { + & ${s} ${l} ${i} ${f}$args + } + $ret=$LASTEXITCODE +} +${t.nodePath?`$env:NODE_PATH=$env_node_path +`:""}exit $ret +`:h+=` +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & ${s} ${l} ${i} ${f}$args +} else { + & ${s} ${l} ${i} ${f}$args +} +${t.nodePath?`$env:NODE_PATH=$env_node_path +`:""}exit $LASTEXITCODE +`,h}function Nze(r,e){return e.fs_.chmod(r,493)}function KL(r){if(!r)return{win32:"",posix:""};let e=typeof r=="string"?r.split(Vs.delimiter):Array.from(r),t={};for(let i=0;i`/mnt/${a.toLowerCase()}`):e[i];t.win32=t.win32?`${t.win32};${n}`:n,t.posix=t.posix?`${t.posix}:${s}`:s,t[i]={win32:n,posix:s}}return t}_Ae.exports=UL});var tT=w((NSt,mle)=>{mle.exports=require("stream")});var wle=w((LSt,Ele)=>{"use strict";function Ile(r,e){var t=Object.keys(r);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(r);e&&(i=i.filter(function(n){return Object.getOwnPropertyDescriptor(r,n).enumerable})),t.push.apply(t,i)}return t}function t8e(r){for(var e=1;e0?this.tail.next=i:this.head=i,this.tail=i,++this.length}},{key:"unshift",value:function(t){var i={data:t,next:this.head};this.length===0&&(this.tail=i),this.head=i,++this.length}},{key:"shift",value:function(){if(this.length!==0){var t=this.head.data;return this.length===1?this.head=this.tail=null:this.head=this.head.next,--this.length,t}}},{key:"clear",value:function(){this.head=this.tail=null,this.length=0}},{key:"join",value:function(t){if(this.length===0)return"";for(var i=this.head,n=""+i.data;i=i.next;)n+=t+i.data;return n}},{key:"concat",value:function(t){if(this.length===0)return cb.alloc(0);for(var i=cb.allocUnsafe(t>>>0),n=this.head,s=0;n;)a8e(n.data,i,s),s+=n.data.length,n=n.next;return i}},{key:"consume",value:function(t,i){var n;return to.length?o.length:t;if(a===o.length?s+=o:s+=o.slice(0,t),t-=a,t===0){a===o.length?(++n,i.next?this.head=i.next:this.head=this.tail=null):(this.head=i,i.data=o.slice(a));break}++n}return this.length-=n,s}},{key:"_getBuffer",value:function(t){var i=cb.allocUnsafe(t),n=this.head,s=1;for(n.data.copy(i),t-=n.data.length;n=n.next;){var o=n.data,a=t>o.length?o.length:t;if(o.copy(i,i.length-t,0,a),t-=a,t===0){a===o.length?(++s,n.next?this.head=n.next:this.head=this.tail=null):(this.head=n,n.data=o.slice(a));break}++s}return this.length-=s,i}},{key:o8e,value:function(t,i){return rT(this,t8e({},i,{depth:0,customInspect:!1}))}}]),r}()});var nT=w((TSt,Ble)=>{"use strict";function A8e(r,e){var t=this,i=this._readableState&&this._readableState.destroyed,n=this._writableState&&this._writableState.destroyed;return i||n?(e?e(r):r&&(this._writableState?this._writableState.errorEmitted||(this._writableState.errorEmitted=!0,process.nextTick(iT,this,r)):process.nextTick(iT,this,r)),this):(this._readableState&&(this._readableState.destroyed=!0),this._writableState&&(this._writableState.destroyed=!0),this._destroy(r||null,function(s){!e&&s?t._writableState?t._writableState.errorEmitted?process.nextTick(ub,t):(t._writableState.errorEmitted=!0,process.nextTick(ble,t,s)):process.nextTick(ble,t,s):e?(process.nextTick(ub,t),e(s)):process.nextTick(ub,t)}),this)}function ble(r,e){iT(r,e),ub(r)}function ub(r){r._writableState&&!r._writableState.emitClose||r._readableState&&!r._readableState.emitClose||r.emit("close")}function l8e(){this._readableState&&(this._readableState.destroyed=!1,this._readableState.reading=!1,this._readableState.ended=!1,this._readableState.endEmitted=!1),this._writableState&&(this._writableState.destroyed=!1,this._writableState.ended=!1,this._writableState.ending=!1,this._writableState.finalCalled=!1,this._writableState.prefinished=!1,this._writableState.finished=!1,this._writableState.errorEmitted=!1)}function iT(r,e){r.emit("error",e)}function c8e(r,e){var t=r._readableState,i=r._writableState;t&&t.autoDestroy||i&&i.autoDestroy?r.destroy(e):r.emit("error",e)}Ble.exports={destroy:A8e,undestroy:l8e,errorOrDestroy:c8e}});var Hl=w((OSt,Qle)=>{"use strict";var Sle={};function Xs(r,e,t){t||(t=Error);function i(s,o,a){return typeof e=="string"?e:e(s,o,a)}class n extends t{constructor(o,a,l){super(i(o,a,l))}}n.prototype.name=t.name,n.prototype.code=r,Sle[r]=n}function vle(r,e){if(Array.isArray(r)){let t=r.length;return r=r.map(i=>String(i)),t>2?`one of ${e} ${r.slice(0,t-1).join(", ")}, or `+r[t-1]:t===2?`one of ${e} ${r[0]} or ${r[1]}`:`of ${e} ${r[0]}`}else return`of ${e} ${String(r)}`}function u8e(r,e,t){return r.substr(!t||t<0?0:+t,e.length)===e}function g8e(r,e,t){return(t===void 0||t>r.length)&&(t=r.length),r.substring(t-e.length,t)===e}function f8e(r,e,t){return typeof t!="number"&&(t=0),t+e.length>r.length?!1:r.indexOf(e,t)!==-1}Xs("ERR_INVALID_OPT_VALUE",function(r,e){return'The value "'+e+'" is invalid for option "'+r+'"'},TypeError);Xs("ERR_INVALID_ARG_TYPE",function(r,e,t){let i;typeof e=="string"&&u8e(e,"not ")?(i="must not be",e=e.replace(/^not /,"")):i="must be";let n;if(g8e(r," argument"))n=`The ${r} ${i} ${vle(e,"type")}`;else{let s=f8e(r,".")?"property":"argument";n=`The "${r}" ${s} ${i} ${vle(e,"type")}`}return n+=`. Received type ${typeof t}`,n},TypeError);Xs("ERR_STREAM_PUSH_AFTER_EOF","stream.push() after EOF");Xs("ERR_METHOD_NOT_IMPLEMENTED",function(r){return"The "+r+" method is not implemented"});Xs("ERR_STREAM_PREMATURE_CLOSE","Premature close");Xs("ERR_STREAM_DESTROYED",function(r){return"Cannot call "+r+" after a stream was destroyed"});Xs("ERR_MULTIPLE_CALLBACK","Callback called multiple times");Xs("ERR_STREAM_CANNOT_PIPE","Cannot pipe, not readable");Xs("ERR_STREAM_WRITE_AFTER_END","write after end");Xs("ERR_STREAM_NULL_VALUES","May not write null values to stream",TypeError);Xs("ERR_UNKNOWN_ENCODING",function(r){return"Unknown encoding: "+r},TypeError);Xs("ERR_STREAM_UNSHIFT_AFTER_END_EVENT","stream.unshift() after end event");Qle.exports.codes=Sle});var sT=w((MSt,kle)=>{"use strict";var h8e=Hl().codes.ERR_INVALID_OPT_VALUE;function p8e(r,e,t){return r.highWaterMark!=null?r.highWaterMark:e?r[t]:null}function d8e(r,e,t,i){var n=p8e(e,i,t);if(n!=null){if(!(isFinite(n)&&Math.floor(n)===n)||n<0){var s=i?t:"highWaterMark";throw new h8e(s,n)}return Math.floor(n)}return r.objectMode?16:16*1024}kle.exports={getHighWaterMark:d8e}});var xle=w((USt,oT)=>{typeof Object.create=="function"?oT.exports=function(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:oT.exports=function(e,t){if(t){e.super_=t;var i=function(){};i.prototype=t.prototype,e.prototype=new i,e.prototype.constructor=e}}});var jl=w((KSt,aT)=>{try{if(AT=require("util"),typeof AT.inherits!="function")throw"";aT.exports=AT.inherits}catch(r){aT.exports=xle()}var AT});var Dle=w((HSt,Ple)=>{Ple.exports=require("util").deprecate});var uT=w((jSt,Rle)=>{"use strict";Rle.exports=Gr;function Fle(r){var e=this;this.next=null,this.entry=null,this.finish=function(){C8e(e,r)}}var uh;Gr.WritableState=Ym;var m8e={deprecate:Dle()},Nle=tT(),gb=require("buffer").Buffer,E8e=global.Uint8Array||function(){};function I8e(r){return gb.from(r)}function y8e(r){return gb.isBuffer(r)||r instanceof E8e}var lT=nT(),w8e=sT(),B8e=w8e.getHighWaterMark,Gl=Hl().codes,b8e=Gl.ERR_INVALID_ARG_TYPE,Q8e=Gl.ERR_METHOD_NOT_IMPLEMENTED,S8e=Gl.ERR_MULTIPLE_CALLBACK,v8e=Gl.ERR_STREAM_CANNOT_PIPE,k8e=Gl.ERR_STREAM_DESTROYED,x8e=Gl.ERR_STREAM_NULL_VALUES,P8e=Gl.ERR_STREAM_WRITE_AFTER_END,D8e=Gl.ERR_UNKNOWN_ENCODING,gh=lT.errorOrDestroy;jl()(Gr,Nle);function R8e(){}function Ym(r,e,t){uh=uh||Pu(),r=r||{},typeof t!="boolean"&&(t=e instanceof uh),this.objectMode=!!r.objectMode,t&&(this.objectMode=this.objectMode||!!r.writableObjectMode),this.highWaterMark=B8e(this,r,"writableHighWaterMark",t),this.finalCalled=!1,this.needDrain=!1,this.ending=!1,this.ended=!1,this.finished=!1,this.destroyed=!1;var i=r.decodeStrings===!1;this.decodeStrings=!i,this.defaultEncoding=r.defaultEncoding||"utf8",this.length=0,this.writing=!1,this.corked=0,this.sync=!0,this.bufferProcessing=!1,this.onwrite=function(n){F8e(e,n)},this.writecb=null,this.writelen=0,this.bufferedRequest=null,this.lastBufferedRequest=null,this.pendingcb=0,this.prefinished=!1,this.errorEmitted=!1,this.emitClose=r.emitClose!==!1,this.autoDestroy=!!r.autoDestroy,this.bufferedRequestCount=0,this.corkedRequestsFree=new Fle(this)}Ym.prototype.getBuffer=function(){for(var e=this.bufferedRequest,t=[];e;)t.push(e),e=e.next;return t};(function(){try{Object.defineProperty(Ym.prototype,"buffer",{get:m8e.deprecate(function(){return this.getBuffer()},"_writableState.buffer is deprecated. Use _writableState.getBuffer instead.","DEP0003")})}catch(r){}})();var fb;typeof Symbol=="function"&&Symbol.hasInstance&&typeof Function.prototype[Symbol.hasInstance]=="function"?(fb=Function.prototype[Symbol.hasInstance],Object.defineProperty(Gr,Symbol.hasInstance,{value:function(e){return fb.call(this,e)?!0:this!==Gr?!1:e&&e._writableState instanceof Ym}})):fb=function(e){return e instanceof this};function Gr(r){uh=uh||Pu();var e=this instanceof uh;if(!e&&!fb.call(Gr,this))return new Gr(r);this._writableState=new Ym(r,this,e),this.writable=!0,r&&(typeof r.write=="function"&&(this._write=r.write),typeof r.writev=="function"&&(this._writev=r.writev),typeof r.destroy=="function"&&(this._destroy=r.destroy),typeof r.final=="function"&&(this._final=r.final)),Nle.call(this)}Gr.prototype.pipe=function(){gh(this,new v8e)};function N8e(r,e){var t=new P8e;gh(r,t),process.nextTick(e,t)}function L8e(r,e,t,i){var n;return t===null?n=new x8e:typeof t!="string"&&!e.objectMode&&(n=new b8e("chunk",["string","Buffer"],t)),n?(gh(r,n),process.nextTick(i,n),!1):!0}Gr.prototype.write=function(r,e,t){var i=this._writableState,n=!1,s=!i.objectMode&&y8e(r);return s&&!gb.isBuffer(r)&&(r=I8e(r)),typeof e=="function"&&(t=e,e=null),s?e="buffer":e||(e=i.defaultEncoding),typeof t!="function"&&(t=R8e),i.ending?N8e(this,t):(s||L8e(this,i,r,t))&&(i.pendingcb++,n=T8e(this,i,s,r,e,t)),n};Gr.prototype.cork=function(){this._writableState.corked++};Gr.prototype.uncork=function(){var r=this._writableState;r.corked&&(r.corked--,!r.writing&&!r.corked&&!r.bufferProcessing&&r.bufferedRequest&&Lle(this,r))};Gr.prototype.setDefaultEncoding=function(e){if(typeof e=="string"&&(e=e.toLowerCase()),!(["hex","utf8","utf-8","ascii","binary","base64","ucs2","ucs-2","utf16le","utf-16le","raw"].indexOf((e+"").toLowerCase())>-1))throw new D8e(e);return this._writableState.defaultEncoding=e,this};Object.defineProperty(Gr.prototype,"writableBuffer",{enumerable:!1,get:function(){return this._writableState&&this._writableState.getBuffer()}});function O8e(r,e,t){return!r.objectMode&&r.decodeStrings!==!1&&typeof e=="string"&&(e=gb.from(e,t)),e}Object.defineProperty(Gr.prototype,"writableHighWaterMark",{enumerable:!1,get:function(){return this._writableState.highWaterMark}});function T8e(r,e,t,i,n,s){if(!t){var o=O8e(e,i,n);i!==o&&(t=!0,n="buffer",i=o)}var a=e.objectMode?1:i.length;e.length+=a;var l=e.length{"use strict";var Y8e=Object.keys||function(r){var e=[];for(var t in r)e.push(t);return e};Mle.exports=Ia;var Ule=gT(),fT=uT();jl()(Ia,Ule);for(hT=Y8e(fT.prototype),hb=0;hb{var db=require("buffer"),kA=db.Buffer;function Hle(r,e){for(var t in r)e[t]=r[t]}kA.from&&kA.alloc&&kA.allocUnsafe&&kA.allocUnsafeSlow?Kle.exports=db:(Hle(db,pT),pT.Buffer=fh);function fh(r,e,t){return kA(r,e,t)}Hle(kA,fh);fh.from=function(r,e,t){if(typeof r=="number")throw new TypeError("Argument must not be a number");return kA(r,e,t)};fh.alloc=function(r,e,t){if(typeof r!="number")throw new TypeError("Argument must be a number");var i=kA(r);return e!==void 0?typeof t=="string"?i.fill(e,t):i.fill(e):i.fill(0),i};fh.allocUnsafe=function(r){if(typeof r!="number")throw new TypeError("Argument must be a number");return kA(r)};fh.allocUnsafeSlow=function(r){if(typeof r!="number")throw new TypeError("Argument must be a number");return db.SlowBuffer(r)}});var mT=w(Gle=>{"use strict";var dT=jle().Buffer,Yle=dT.isEncoding||function(r){switch(r=""+r,r&&r.toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":case"raw":return!0;default:return!1}};function W8e(r){if(!r)return"utf8";for(var e;;)switch(r){case"utf8":case"utf-8":return"utf8";case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return"utf16le";case"latin1":case"binary":return"latin1";case"base64":case"ascii":case"hex":return r;default:if(e)return;r=(""+r).toLowerCase(),e=!0}}function z8e(r){var e=W8e(r);if(typeof e!="string"&&(dT.isEncoding===Yle||!Yle(r)))throw new Error("Unknown encoding: "+r);return e||r}Gle.StringDecoder=Jm;function Jm(r){this.encoding=z8e(r);var e;switch(this.encoding){case"utf16le":this.text=V8e,this.end=X8e,e=4;break;case"utf8":this.fillLast=_8e,e=4;break;case"base64":this.text=Z8e,this.end=$8e,e=3;break;default:this.write=e5e,this.end=t5e;return}this.lastNeed=0,this.lastTotal=0,this.lastChar=dT.allocUnsafe(e)}Jm.prototype.write=function(r){if(r.length===0)return"";var e,t;if(this.lastNeed){if(e=this.fillLast(r),e===void 0)return"";t=this.lastNeed,this.lastNeed=0}else t=0;return t>5==6?2:r>>4==14?3:r>>3==30?4:r>>6==2?-1:-2}function n5e(r,e,t){var i=e.length-1;if(i=0?(n>0&&(r.lastNeed=n-1),n):--i=0?(n>0&&(r.lastNeed=n-2),n):--i=0?(n>0&&(n===2?n=0:r.lastNeed=n-3),n):0))}function s5e(r,e,t){if((e[0]&192)!=128)return r.lastNeed=0,"\uFFFD";if(r.lastNeed>1&&e.length>1){if((e[1]&192)!=128)return r.lastNeed=1,"\uFFFD";if(r.lastNeed>2&&e.length>2&&(e[2]&192)!=128)return r.lastNeed=2,"\uFFFD"}}function _8e(r){var e=this.lastTotal-this.lastNeed,t=s5e(this,r,e);if(t!==void 0)return t;if(this.lastNeed<=r.length)return r.copy(this.lastChar,e,0,this.lastNeed),this.lastChar.toString(this.encoding,0,this.lastTotal);r.copy(this.lastChar,e,0,r.length),this.lastNeed-=r.length}function i5e(r,e){var t=n5e(this,r,e);if(!this.lastNeed)return r.toString("utf8",e);this.lastTotal=t;var i=r.length-(t-this.lastNeed);return r.copy(this.lastChar,0,i),r.toString("utf8",e,i)}function r5e(r){var e=r&&r.length?this.write(r):"";return this.lastNeed?e+"\uFFFD":e}function V8e(r,e){if((r.length-e)%2==0){var t=r.toString("utf16le",e);if(t){var i=t.charCodeAt(t.length-1);if(i>=55296&&i<=56319)return this.lastNeed=2,this.lastTotal=4,this.lastChar[0]=r[r.length-2],this.lastChar[1]=r[r.length-1],t.slice(0,-1)}return t}return this.lastNeed=1,this.lastTotal=2,this.lastChar[0]=r[r.length-1],r.toString("utf16le",e,r.length-1)}function X8e(r){var e=r&&r.length?this.write(r):"";if(this.lastNeed){var t=this.lastTotal-this.lastNeed;return e+this.lastChar.toString("utf16le",0,t)}return e}function Z8e(r,e){var t=(r.length-e)%3;return t===0?r.toString("base64",e):(this.lastNeed=3-t,this.lastTotal=3,t===1?this.lastChar[0]=r[r.length-1]:(this.lastChar[0]=r[r.length-2],this.lastChar[1]=r[r.length-1]),r.toString("base64",e,r.length-t))}function $8e(r){var e=r&&r.length?this.write(r):"";return this.lastNeed?e+this.lastChar.toString("base64",0,3-this.lastNeed):e}function e5e(r){return r.toString(this.encoding)}function t5e(r){return r&&r.length?this.write(r):""}});var Cb=w((qSt,qle)=>{"use strict";var Jle=Hl().codes.ERR_STREAM_PREMATURE_CLOSE;function o5e(r){var e=!1;return function(){if(!e){e=!0;for(var t=arguments.length,i=new Array(t),n=0;n{"use strict";var mb;function Yl(r,e,t){return e in r?Object.defineProperty(r,e,{value:t,enumerable:!0,configurable:!0,writable:!0}):r[e]=t,r}var l5e=Cb(),ql=Symbol("lastResolve"),Du=Symbol("lastReject"),Wm=Symbol("error"),Eb=Symbol("ended"),Ru=Symbol("lastPromise"),ET=Symbol("handlePromise"),Fu=Symbol("stream");function Jl(r,e){return{value:r,done:e}}function c5e(r){var e=r[ql];if(e!==null){var t=r[Fu].read();t!==null&&(r[Ru]=null,r[ql]=null,r[Du]=null,e(Jl(t,!1)))}}function u5e(r){process.nextTick(c5e,r)}function g5e(r,e){return function(t,i){r.then(function(){if(e[Eb]){t(Jl(void 0,!0));return}e[ET](t,i)},i)}}var f5e=Object.getPrototypeOf(function(){}),h5e=Object.setPrototypeOf((mb={get stream(){return this[Fu]},next:function(){var e=this,t=this[Wm];if(t!==null)return Promise.reject(t);if(this[Eb])return Promise.resolve(Jl(void 0,!0));if(this[Fu].destroyed)return new Promise(function(o,a){process.nextTick(function(){e[Wm]?a(e[Wm]):o(Jl(void 0,!0))})});var i=this[Ru],n;if(i)n=new Promise(g5e(i,this));else{var s=this[Fu].read();if(s!==null)return Promise.resolve(Jl(s,!1));n=new Promise(this[ET])}return this[Ru]=n,n}},Yl(mb,Symbol.asyncIterator,function(){return this}),Yl(mb,"return",function(){var e=this;return new Promise(function(t,i){e[Fu].destroy(null,function(n){if(n){i(n);return}t(Jl(void 0,!0))})})}),mb),f5e),p5e=function(e){var t,i=Object.create(h5e,(t={},Yl(t,Fu,{value:e,writable:!0}),Yl(t,ql,{value:null,writable:!0}),Yl(t,Du,{value:null,writable:!0}),Yl(t,Wm,{value:null,writable:!0}),Yl(t,Eb,{value:e._readableState.endEmitted,writable:!0}),Yl(t,ET,{value:function(s,o){var a=i[Fu].read();a?(i[Ru]=null,i[ql]=null,i[Du]=null,s(Jl(a,!1))):(i[ql]=s,i[Du]=o)},writable:!0}),t));return i[Ru]=null,l5e(e,function(n){if(n&&n.code!=="ERR_STREAM_PREMATURE_CLOSE"){var s=i[Du];s!==null&&(i[Ru]=null,i[ql]=null,i[Du]=null,s(n)),i[Wm]=n;return}var o=i[ql];o!==null&&(i[Ru]=null,i[ql]=null,i[Du]=null,o(Jl(void 0,!0))),i[Eb]=!0}),e.on("readable",u5e.bind(null,i)),i};zle.exports=p5e});var $le=w((WSt,Vle)=>{"use strict";function Xle(r,e,t,i,n,s,o){try{var a=r[s](o),l=a.value}catch(c){t(c);return}a.done?e(l):Promise.resolve(l).then(i,n)}function d5e(r){return function(){var e=this,t=arguments;return new Promise(function(i,n){var s=r.apply(e,t);function o(l){Xle(s,i,n,o,a,"next",l)}function a(l){Xle(s,i,n,o,a,"throw",l)}o(void 0)})}}function Zle(r,e){var t=Object.keys(r);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(r);e&&(i=i.filter(function(n){return Object.getOwnPropertyDescriptor(r,n).enumerable})),t.push.apply(t,i)}return t}function m5e(r){for(var e=1;e{"use strict";ece.exports=Kt;var hh;Kt.ReadableState=tce;var zSt=require("events").EventEmitter,rce=function(e,t){return e.listeners(t).length},zm=tT(),Ib=require("buffer").Buffer,y5e=global.Uint8Array||function(){};function w5e(r){return Ib.from(r)}function B5e(r){return Ib.isBuffer(r)||r instanceof y5e}var IT=require("util"),Pt;IT&&IT.debuglog?Pt=IT.debuglog("stream"):Pt=function(){};var b5e=wle(),yT=nT(),Q5e=sT(),S5e=Q5e.getHighWaterMark,yb=Hl().codes,v5e=yb.ERR_INVALID_ARG_TYPE,k5e=yb.ERR_STREAM_PUSH_AFTER_EOF,x5e=yb.ERR_METHOD_NOT_IMPLEMENTED,P5e=yb.ERR_STREAM_UNSHIFT_AFTER_END_EVENT,ph,wT,BT;jl()(Kt,zm);var _m=yT.errorOrDestroy,bT=["error","close","destroy","pause","resume"];function D5e(r,e,t){if(typeof r.prependListener=="function")return r.prependListener(e,t);!r._events||!r._events[e]?r.on(e,t):Array.isArray(r._events[e])?r._events[e].unshift(t):r._events[e]=[t,r._events[e]]}function tce(r,e,t){hh=hh||Pu(),r=r||{},typeof t!="boolean"&&(t=e instanceof hh),this.objectMode=!!r.objectMode,t&&(this.objectMode=this.objectMode||!!r.readableObjectMode),this.highWaterMark=S5e(this,r,"readableHighWaterMark",t),this.buffer=new b5e,this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=null,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.resumeScheduled=!1,this.paused=!0,this.emitClose=r.emitClose!==!1,this.autoDestroy=!!r.autoDestroy,this.destroyed=!1,this.defaultEncoding=r.defaultEncoding||"utf8",this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,r.encoding&&(ph||(ph=mT().StringDecoder),this.decoder=new ph(r.encoding),this.encoding=r.encoding)}function Kt(r){if(hh=hh||Pu(),!(this instanceof Kt))return new Kt(r);var e=this instanceof hh;this._readableState=new tce(r,this,e),this.readable=!0,r&&(typeof r.read=="function"&&(this._read=r.read),typeof r.destroy=="function"&&(this._destroy=r.destroy)),zm.call(this)}Object.defineProperty(Kt.prototype,"destroyed",{enumerable:!1,get:function(){return this._readableState===void 0?!1:this._readableState.destroyed},set:function(e){!this._readableState||(this._readableState.destroyed=e)}});Kt.prototype.destroy=yT.destroy;Kt.prototype._undestroy=yT.undestroy;Kt.prototype._destroy=function(r,e){e(r)};Kt.prototype.push=function(r,e){var t=this._readableState,i;return t.objectMode?i=!0:typeof r=="string"&&(e=e||t.defaultEncoding,e!==t.encoding&&(r=Ib.from(r,e),e=""),i=!0),ice(this,r,e,!1,i)};Kt.prototype.unshift=function(r){return ice(this,r,null,!0,!1)};function ice(r,e,t,i,n){Pt("readableAddChunk",e);var s=r._readableState;if(e===null)s.reading=!1,F5e(r,s);else{var o;if(n||(o=R5e(s,e)),o)_m(r,o);else if(s.objectMode||e&&e.length>0)if(typeof e!="string"&&!s.objectMode&&Object.getPrototypeOf(e)!==Ib.prototype&&(e=w5e(e)),i)s.endEmitted?_m(r,new P5e):QT(r,s,e,!0);else if(s.ended)_m(r,new k5e);else{if(s.destroyed)return!1;s.reading=!1,s.decoder&&!t?(e=s.decoder.write(e),s.objectMode||e.length!==0?QT(r,s,e,!1):ST(r,s)):QT(r,s,e,!1)}else i||(s.reading=!1,ST(r,s))}return!s.ended&&(s.length=nce?r=nce:(r--,r|=r>>>1,r|=r>>>2,r|=r>>>4,r|=r>>>8,r|=r>>>16,r++),r}function sce(r,e){return r<=0||e.length===0&&e.ended?0:e.objectMode?1:r!==r?e.flowing&&e.length?e.buffer.head.data.length:e.length:(r>e.highWaterMark&&(e.highWaterMark=N5e(r)),r<=e.length?r:e.ended?e.length:(e.needReadable=!0,0))}Kt.prototype.read=function(r){Pt("read",r),r=parseInt(r,10);var e=this._readableState,t=r;if(r!==0&&(e.emittedReadable=!1),r===0&&e.needReadable&&((e.highWaterMark!==0?e.length>=e.highWaterMark:e.length>0)||e.ended))return Pt("read: emitReadable",e.length,e.ended),e.length===0&&e.ended?vT(this):wb(this),null;if(r=sce(r,e),r===0&&e.ended)return e.length===0&&vT(this),null;var i=e.needReadable;Pt("need readable",i),(e.length===0||e.length-r0?n=oce(r,e):n=null,n===null?(e.needReadable=e.length<=e.highWaterMark,r=0):(e.length-=r,e.awaitDrain=0),e.length===0&&(e.ended||(e.needReadable=!0),t!==r&&e.ended&&vT(this)),n!==null&&this.emit("data",n),n};function F5e(r,e){if(Pt("onEofChunk"),!e.ended){if(e.decoder){var t=e.decoder.end();t&&t.length&&(e.buffer.push(t),e.length+=e.objectMode?1:t.length)}e.ended=!0,e.sync?wb(r):(e.needReadable=!1,e.emittedReadable||(e.emittedReadable=!0,ace(r)))}}function wb(r){var e=r._readableState;Pt("emitReadable",e.needReadable,e.emittedReadable),e.needReadable=!1,e.emittedReadable||(Pt("emitReadable",e.flowing),e.emittedReadable=!0,process.nextTick(ace,r))}function ace(r){var e=r._readableState;Pt("emitReadable_",e.destroyed,e.length,e.ended),!e.destroyed&&(e.length||e.ended)&&(r.emit("readable"),e.emittedReadable=!1),e.needReadable=!e.flowing&&!e.ended&&e.length<=e.highWaterMark,kT(r)}function ST(r,e){e.readingMore||(e.readingMore=!0,process.nextTick(L5e,r,e))}function L5e(r,e){for(;!e.reading&&!e.ended&&(e.length1&&Ace(i.pipes,r)!==-1)&&!c&&(Pt("false write response, pause",i.awaitDrain),i.awaitDrain++),t.pause())}function f(y){Pt("onerror",y),m(),r.removeListener("error",f),rce(r,"error")===0&&_m(r,y)}D5e(r,"error",f);function h(){r.removeListener("finish",p),m()}r.once("close",h);function p(){Pt("onfinish"),r.removeListener("close",h),m()}r.once("finish",p);function m(){Pt("unpipe"),t.unpipe(r)}return r.emit("pipe",t),i.flowing||(Pt("pipe resume"),t.resume()),r};function T5e(r){return function(){var t=r._readableState;Pt("pipeOnDrain",t.awaitDrain),t.awaitDrain&&t.awaitDrain--,t.awaitDrain===0&&rce(r,"data")&&(t.flowing=!0,kT(r))}}Kt.prototype.unpipe=function(r){var e=this._readableState,t={hasUnpiped:!1};if(e.pipesCount===0)return this;if(e.pipesCount===1)return r&&r!==e.pipes?this:(r||(r=e.pipes),e.pipes=null,e.pipesCount=0,e.flowing=!1,r&&r.emit("unpipe",this,t),this);if(!r){var i=e.pipes,n=e.pipesCount;e.pipes=null,e.pipesCount=0,e.flowing=!1;for(var s=0;s0,i.flowing!==!1&&this.resume()):r==="readable"&&!i.endEmitted&&!i.readableListening&&(i.readableListening=i.needReadable=!0,i.flowing=!1,i.emittedReadable=!1,Pt("on readable",i.length,i.reading),i.length?wb(this):i.reading||process.nextTick(O5e,this)),t};Kt.prototype.addListener=Kt.prototype.on;Kt.prototype.removeListener=function(r,e){var t=zm.prototype.removeListener.call(this,r,e);return r==="readable"&&process.nextTick(lce,this),t};Kt.prototype.removeAllListeners=function(r){var e=zm.prototype.removeAllListeners.apply(this,arguments);return(r==="readable"||r===void 0)&&process.nextTick(lce,this),e};function lce(r){var e=r._readableState;e.readableListening=r.listenerCount("readable")>0,e.resumeScheduled&&!e.paused?e.flowing=!0:r.listenerCount("data")>0&&r.resume()}function O5e(r){Pt("readable nexttick read 0"),r.read(0)}Kt.prototype.resume=function(){var r=this._readableState;return r.flowing||(Pt("resume"),r.flowing=!r.readableListening,M5e(this,r)),r.paused=!1,this};function M5e(r,e){e.resumeScheduled||(e.resumeScheduled=!0,process.nextTick(U5e,r,e))}function U5e(r,e){Pt("resume",e.reading),e.reading||r.read(0),e.resumeScheduled=!1,r.emit("resume"),kT(r),e.flowing&&!e.reading&&r.read(0)}Kt.prototype.pause=function(){return Pt("call pause flowing=%j",this._readableState.flowing),this._readableState.flowing!==!1&&(Pt("pause"),this._readableState.flowing=!1,this.emit("pause")),this._readableState.paused=!0,this};function kT(r){var e=r._readableState;for(Pt("flow",e.flowing);e.flowing&&r.read()!==null;);}Kt.prototype.wrap=function(r){var e=this,t=this._readableState,i=!1;r.on("end",function(){if(Pt("wrapped end"),t.decoder&&!t.ended){var o=t.decoder.end();o&&o.length&&e.push(o)}e.push(null)}),r.on("data",function(o){if(Pt("wrapped data"),t.decoder&&(o=t.decoder.write(o)),!(t.objectMode&&o==null)&&!(!t.objectMode&&(!o||!o.length))){var a=e.push(o);a||(i=!0,r.pause())}});for(var n in r)this[n]===void 0&&typeof r[n]=="function"&&(this[n]=function(a){return function(){return r[a].apply(r,arguments)}}(n));for(var s=0;s=e.length?(e.decoder?t=e.buffer.join(""):e.buffer.length===1?t=e.buffer.first():t=e.buffer.concat(e.length),e.buffer.clear()):t=e.buffer.consume(r,e.decoder),t}function vT(r){var e=r._readableState;Pt("endReadable",e.endEmitted),e.endEmitted||(e.ended=!0,process.nextTick(K5e,e,r))}function K5e(r,e){if(Pt("endReadableNT",r.endEmitted,r.length),!r.endEmitted&&r.length===0&&(r.endEmitted=!0,e.readable=!1,e.emit("end"),r.autoDestroy)){var t=e._writableState;(!t||t.autoDestroy&&t.finished)&&e.destroy()}}typeof Symbol=="function"&&(Kt.from=function(r,e){return BT===void 0&&(BT=$le()),BT(Kt,r,e)});function Ace(r,e){for(var t=0,i=r.length;t{"use strict";cce.exports=xA;var Bb=Hl().codes,H5e=Bb.ERR_METHOD_NOT_IMPLEMENTED,j5e=Bb.ERR_MULTIPLE_CALLBACK,G5e=Bb.ERR_TRANSFORM_ALREADY_TRANSFORMING,Y5e=Bb.ERR_TRANSFORM_WITH_LENGTH_0,bb=Pu();jl()(xA,bb);function q5e(r,e){var t=this._transformState;t.transforming=!1;var i=t.writecb;if(i===null)return this.emit("error",new j5e);t.writechunk=null,t.writecb=null,e!=null&&this.push(e),i(r);var n=this._readableState;n.reading=!1,(n.needReadable||n.length{"use strict";gce.exports=Vm;var fce=xT();jl()(Vm,fce);function Vm(r){if(!(this instanceof Vm))return new Vm(r);fce.call(this,r)}Vm.prototype._transform=function(r,e,t){t(null,r)}});var Ece=w((ZSt,pce)=>{"use strict";var PT;function W5e(r){var e=!1;return function(){e||(e=!0,r.apply(void 0,arguments))}}var dce=Hl().codes,z5e=dce.ERR_MISSING_ARGS,_5e=dce.ERR_STREAM_DESTROYED;function Cce(r){if(r)throw r}function V5e(r){return r.setHeader&&typeof r.abort=="function"}function X5e(r,e,t,i){i=W5e(i);var n=!1;r.on("close",function(){n=!0}),PT===void 0&&(PT=Cb()),PT(r,{readable:e,writable:t},function(o){if(o)return i(o);n=!0,i()});var s=!1;return function(o){if(!n&&!s){if(s=!0,V5e(r))return r.abort();if(typeof r.destroy=="function")return r.destroy();i(o||new _5e("pipe"))}}}function mce(r){r()}function Z5e(r,e){return r.pipe(e)}function $5e(r){return!r.length||typeof r[r.length-1]!="function"?Cce:r.pop()}function e9e(){for(var r=arguments.length,e=new Array(r),t=0;t0;return X5e(o,l,c,function(u){n||(n=u),u&&s.forEach(mce),!l&&(s.forEach(mce),i(n))})});return e.reduce(Z5e)}pce.exports=e9e});var dh=w((Zs,Xm)=>{var Zm=require("stream");process.env.READABLE_STREAM==="disable"&&Zm?(Xm.exports=Zm.Readable,Object.assign(Xm.exports,Zm),Xm.exports.Stream=Zm):(Zs=Xm.exports=gT(),Zs.Stream=Zm||Zs,Zs.Readable=Zs,Zs.Writable=uT(),Zs.Duplex=Pu(),Zs.Transform=xT(),Zs.PassThrough=hce(),Zs.finished=Cb(),Zs.pipeline=Ece())});var wce=w(($St,Ice)=>{"use strict";var{Buffer:ko}=require("buffer"),yce=Symbol.for("BufferList");function mr(r){if(!(this instanceof mr))return new mr(r);mr._init.call(this,r)}mr._init=function(e){Object.defineProperty(this,yce,{value:!0}),this._bufs=[],this.length=0,e&&this.append(e)};mr.prototype._new=function(e){return new mr(e)};mr.prototype._offset=function(e){if(e===0)return[0,0];let t=0;for(let i=0;ithis.length||e<0)return;let t=this._offset(e);return this._bufs[t[0]][t[1]]};mr.prototype.slice=function(e,t){return typeof e=="number"&&e<0&&(e+=this.length),typeof t=="number"&&t<0&&(t+=this.length),this.copy(null,0,e,t)};mr.prototype.copy=function(e,t,i,n){if((typeof i!="number"||i<0)&&(i=0),(typeof n!="number"||n>this.length)&&(n=this.length),i>=this.length||n<=0)return e||ko.alloc(0);let s=!!e,o=this._offset(i),a=n-i,l=a,c=s&&t||0,u=o[1];if(i===0&&n===this.length){if(!s)return this._bufs.length===1?this._bufs[0]:ko.concat(this._bufs,this.length);for(let g=0;gf)this._bufs[g].copy(e,c,u),c+=f;else{this._bufs[g].copy(e,c,u,u+l),c+=f;break}l-=f,u&&(u=0)}return e.length>c?e.slice(0,c):e};mr.prototype.shallowSlice=function(e,t){if(e=e||0,t=typeof t!="number"?this.length:t,e<0&&(e+=this.length),t<0&&(t+=this.length),e===t)return this._new();let i=this._offset(e),n=this._offset(t),s=this._bufs.slice(i[0],n[0]+1);return n[1]===0?s.pop():s[s.length-1]=s[s.length-1].slice(0,n[1]),i[1]!==0&&(s[0]=s[0].slice(i[1])),this._new(s)};mr.prototype.toString=function(e,t,i){return this.slice(t,i).toString(e)};mr.prototype.consume=function(e){if(e=Math.trunc(e),Number.isNaN(e)||e<=0)return this;for(;this._bufs.length;)if(e>=this._bufs[0].length)e-=this._bufs[0].length,this.length-=this._bufs[0].length,this._bufs.shift();else{this._bufs[0]=this._bufs[0].slice(e),this.length-=e;break}return this};mr.prototype.duplicate=function(){let e=this._new();for(let t=0;tthis.length?this.length:e;let i=this._offset(e),n=i[0],s=i[1];for(;n=r.length){let l=o.indexOf(r,s);if(l!==-1)return this._reverseOffset([n,l]);s=o.length-r.length+1}else{let l=this._reverseOffset([n,s]);if(this._match(l,r))return l;s++}s=0}return-1};mr.prototype._match=function(r,e){if(this.length-r{"use strict";var DT=dh().Duplex,t9e=jl(),$m=wce();function Zi(r){if(!(this instanceof Zi))return new Zi(r);if(typeof r=="function"){this._callback=r;let e=function(i){this._callback&&(this._callback(i),this._callback=null)}.bind(this);this.on("pipe",function(i){i.on("error",e)}),this.on("unpipe",function(i){i.removeListener("error",e)}),r=null}$m._init.call(this,r),DT.call(this)}t9e(Zi,DT);Object.assign(Zi.prototype,$m.prototype);Zi.prototype._new=function(e){return new Zi(e)};Zi.prototype._write=function(e,t,i){this._appendBuffer(e),typeof i=="function"&&i()};Zi.prototype._read=function(e){if(!this.length)return this.push(null);e=Math.min(e,this.length),this.push(this.slice(0,e)),this.consume(e)};Zi.prototype.end=function(e){DT.prototype.end.call(this,e),this._callback&&(this._callback(null,this.slice()),this._callback=null)};Zi.prototype._destroy=function(e,t){this._bufs.length=0,this.length=0,t(e)};Zi.prototype._isBufferList=function(e){return e instanceof Zi||e instanceof $m||Zi.isBufferList(e)};Zi.isBufferList=$m.isBufferList;Qb.exports=Zi;Qb.exports.BufferListStream=Zi;Qb.exports.BufferList=$m});var NT=w(Ch=>{var r9e=Buffer.alloc,i9e="0000000000000000000",n9e="7777777777777777777",bce="0".charCodeAt(0),Qce=Buffer.from("ustar\0","binary"),s9e=Buffer.from("00","binary"),o9e=Buffer.from("ustar ","binary"),a9e=Buffer.from(" \0","binary"),A9e=parseInt("7777",8),eE=257,RT=263,l9e=function(r,e,t){return typeof r!="number"?t:(r=~~r,r>=e?e:r>=0||(r+=e,r>=0)?r:0)},c9e=function(r){switch(r){case 0:return"file";case 1:return"link";case 2:return"symlink";case 3:return"character-device";case 4:return"block-device";case 5:return"directory";case 6:return"fifo";case 7:return"contiguous-file";case 72:return"pax-header";case 55:return"pax-global-header";case 27:return"gnu-long-link-path";case 28:case 30:return"gnu-long-path"}return null},u9e=function(r){switch(r){case"file":return 0;case"link":return 1;case"symlink":return 2;case"character-device":return 3;case"block-device":return 4;case"directory":return 5;case"fifo":return 6;case"contiguous-file":return 7;case"pax-header":return 72}return 0},Sce=function(r,e,t,i){for(;te?n9e.slice(0,e)+" ":i9e.slice(0,e-r.length)+r+" "};function g9e(r){var e;if(r[0]===128)e=!0;else if(r[0]===255)e=!1;else return null;for(var t=[],i=r.length-1;i>0;i--){var n=r[i];e?t.push(n):t.push(255-n)}var s=0,o=t.length;for(i=0;i=Math.pow(10,t)&&t++,e+t+r};Ch.decodeLongPath=function(r,e){return mh(r,0,r.length,e)};Ch.encodePax=function(r){var e="";r.name&&(e+=FT(" path="+r.name+` +`)),r.linkname&&(e+=FT(" linkpath="+r.linkname+` +`));var t=r.pax;if(t)for(var i in t)e+=FT(" "+i+"="+t[i]+` +`);return Buffer.from(e)};Ch.decodePax=function(r){for(var e={};r.length;){for(var t=0;t100;){var n=t.indexOf("/");if(n===-1)return null;i+=i?"/"+t.slice(0,n):t.slice(0,n),t=t.slice(n+1)}return Buffer.byteLength(t)>100||Buffer.byteLength(i)>155||r.linkname&&Buffer.byteLength(r.linkname)>100?null:(e.write(t),e.write(Wl(r.mode&A9e,6),100),e.write(Wl(r.uid,6),108),e.write(Wl(r.gid,6),116),e.write(Wl(r.size,11),124),e.write(Wl(r.mtime.getTime()/1e3|0,11),136),e[156]=bce+u9e(r.type),r.linkname&&e.write(r.linkname,157),Qce.copy(e,eE),s9e.copy(e,RT),r.uname&&e.write(r.uname,265),r.gname&&e.write(r.gname,297),e.write(Wl(r.devmajor||0,6),329),e.write(Wl(r.devminor||0,6),337),i&&e.write(i,345),e.write(Wl(vce(e),6),148),e)};Ch.decode=function(r,e,t){var i=r[156]===0?0:r[156]-bce,n=mh(r,0,100,e),s=zl(r,100,8),o=zl(r,108,8),a=zl(r,116,8),l=zl(r,124,12),c=zl(r,136,12),u=c9e(i),g=r[157]===0?null:mh(r,157,100,e),f=mh(r,265,32),h=mh(r,297,32),p=zl(r,329,8),m=zl(r,337,8),y=vce(r);if(y===8*32)return null;if(y!==zl(r,148,8))throw new Error("Invalid tar header. Maybe the tar is corrupted or it needs to be gunzipped?");if(Qce.compare(r,eE,eE+6)===0)r[345]&&(n=mh(r,345,155,e)+"/"+n);else if(!(o9e.compare(r,eE,eE+6)===0&&a9e.compare(r,RT,RT+2)===0)){if(!t)throw new Error("Invalid tar header: unknown format.")}return i===0&&n&&n[n.length-1]==="/"&&(i=5),{name:n,mode:s,uid:o,gid:a,size:l,mtime:new Date(1e3*c),type:u,linkname:g,uname:f,gname:h,devmajor:p,devminor:m}}});var Nce=w((rvt,kce)=>{var xce=require("util"),f9e=Bce(),tE=NT(),Pce=dh().Writable,Dce=dh().PassThrough,Rce=function(){},Fce=function(r){return r&=511,r&&512-r},h9e=function(r,e){var t=new Sb(r,e);return t.end(),t},p9e=function(r,e){return e.path&&(r.name=e.path),e.linkpath&&(r.linkname=e.linkpath),e.size&&(r.size=parseInt(e.size,10)),r.pax=e,r},Sb=function(r,e){this._parent=r,this.offset=e,Dce.call(this,{autoDestroy:!1})};xce.inherits(Sb,Dce);Sb.prototype.destroy=function(r){this._parent.destroy(r)};var PA=function(r){if(!(this instanceof PA))return new PA(r);Pce.call(this,r),r=r||{},this._offset=0,this._buffer=f9e(),this._missing=0,this._partial=!1,this._onparse=Rce,this._header=null,this._stream=null,this._overflow=null,this._cb=null,this._locked=!1,this._destroyed=!1,this._pax=null,this._paxGlobal=null,this._gnuLongPath=null,this._gnuLongLinkPath=null;var e=this,t=e._buffer,i=function(){e._continue()},n=function(f){if(e._locked=!1,f)return e.destroy(f);e._stream||i()},s=function(){e._stream=null;var f=Fce(e._header.size);f?e._parse(f,o):e._parse(512,g),e._locked||i()},o=function(){e._buffer.consume(Fce(e._header.size)),e._parse(512,g),i()},a=function(){var f=e._header.size;e._paxGlobal=tE.decodePax(t.slice(0,f)),t.consume(f),s()},l=function(){var f=e._header.size;e._pax=tE.decodePax(t.slice(0,f)),e._paxGlobal&&(e._pax=Object.assign({},e._paxGlobal,e._pax)),t.consume(f),s()},c=function(){var f=e._header.size;this._gnuLongPath=tE.decodeLongPath(t.slice(0,f),r.filenameEncoding),t.consume(f),s()},u=function(){var f=e._header.size;this._gnuLongLinkPath=tE.decodeLongPath(t.slice(0,f),r.filenameEncoding),t.consume(f),s()},g=function(){var f=e._offset,h;try{h=e._header=tE.decode(t.slice(0,512),r.filenameEncoding,r.allowUnknownFormat)}catch(p){e.emit("error",p)}if(t.consume(512),!h){e._parse(512,g),i();return}if(h.type==="gnu-long-path"){e._parse(h.size,c),i();return}if(h.type==="gnu-long-link-path"){e._parse(h.size,u),i();return}if(h.type==="pax-global-header"){e._parse(h.size,a),i();return}if(h.type==="pax-header"){e._parse(h.size,l),i();return}if(e._gnuLongPath&&(h.name=e._gnuLongPath,e._gnuLongPath=null),e._gnuLongLinkPath&&(h.linkname=e._gnuLongLinkPath,e._gnuLongLinkPath=null),e._pax&&(e._header=h=p9e(h,e._pax),e._pax=null),e._locked=!0,!h.size||h.type==="directory"){e._parse(512,g),e.emit("entry",h,h9e(e,f),n);return}e._stream=new Sb(e,f),e.emit("entry",h,e._stream,n),e._parse(h.size,s),i()};this._onheader=g,this._parse(512,g)};xce.inherits(PA,Pce);PA.prototype.destroy=function(r){this._destroyed||(this._destroyed=!0,r&&this.emit("error",r),this.emit("close"),this._stream&&this._stream.emit("close"))};PA.prototype._parse=function(r,e){this._destroyed||(this._offset+=r,this._missing=r,e===this._onheader&&(this._partial=!1),this._onparse=e)};PA.prototype._continue=function(){if(!this._destroyed){var r=this._cb;this._cb=Rce,this._overflow?this._write(this._overflow,void 0,r):r()}};PA.prototype._write=function(r,e,t){if(!this._destroyed){var i=this._stream,n=this._buffer,s=this._missing;if(r.length&&(this._partial=!0),r.lengths&&(o=r.slice(s),r=r.slice(0,s)),i?i.end(r):n.append(r),this._overflow=o,this._onparse()}};PA.prototype._final=function(r){if(this._partial)return this.destroy(new Error("Unexpected end of data"));r()};kce.exports=PA});var Tce=w((ivt,Lce)=>{Lce.exports=require("fs").constants||require("constants")});var Hce=w((nvt,Oce)=>{var Eh=Tce(),Mce=Ux(),vb=jl(),d9e=Buffer.alloc,Uce=dh().Readable,Ih=dh().Writable,C9e=require("string_decoder").StringDecoder,kb=NT(),m9e=parseInt("755",8),E9e=parseInt("644",8),Kce=d9e(1024),LT=function(){},TT=function(r,e){e&=511,e&&r.push(Kce.slice(0,512-e))};function I9e(r){switch(r&Eh.S_IFMT){case Eh.S_IFBLK:return"block-device";case Eh.S_IFCHR:return"character-device";case Eh.S_IFDIR:return"directory";case Eh.S_IFIFO:return"fifo";case Eh.S_IFLNK:return"symlink"}return"file"}var xb=function(r){Ih.call(this),this.written=0,this._to=r,this._destroyed=!1};vb(xb,Ih);xb.prototype._write=function(r,e,t){if(this.written+=r.length,this._to.push(r))return t();this._to._drain=t};xb.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var Pb=function(){Ih.call(this),this.linkname="",this._decoder=new C9e("utf-8"),this._destroyed=!1};vb(Pb,Ih);Pb.prototype._write=function(r,e,t){this.linkname+=this._decoder.write(r),t()};Pb.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var rE=function(){Ih.call(this),this._destroyed=!1};vb(rE,Ih);rE.prototype._write=function(r,e,t){t(new Error("No body allowed for this entry"))};rE.prototype.destroy=function(){this._destroyed||(this._destroyed=!0,this.emit("close"))};var ya=function(r){if(!(this instanceof ya))return new ya(r);Uce.call(this,r),this._drain=LT,this._finalized=!1,this._finalizing=!1,this._destroyed=!1,this._stream=null};vb(ya,Uce);ya.prototype.entry=function(r,e,t){if(this._stream)throw new Error("already piping an entry");if(!(this._finalized||this._destroyed)){typeof e=="function"&&(t=e,e=null),t||(t=LT);var i=this;if((!r.size||r.type==="symlink")&&(r.size=0),r.type||(r.type=I9e(r.mode)),r.mode||(r.mode=r.type==="directory"?m9e:E9e),r.uid||(r.uid=0),r.gid||(r.gid=0),r.mtime||(r.mtime=new Date),typeof e=="string"&&(e=Buffer.from(e)),Buffer.isBuffer(e)){r.size=e.length,this._encode(r);var n=this.push(e);return TT(i,r.size),n?process.nextTick(t):this._drain=t,new rE}if(r.type==="symlink"&&!r.linkname){var s=new Pb;return Mce(s,function(a){if(a)return i.destroy(),t(a);r.linkname=s.linkname,i._encode(r),t()}),s}if(this._encode(r),r.type!=="file"&&r.type!=="contiguous-file")return process.nextTick(t),new rE;var o=new xb(this);return this._stream=o,Mce(o,function(a){if(i._stream=null,a)return i.destroy(),t(a);if(o.written!==r.size)return i.destroy(),t(new Error("size mismatch"));TT(i,r.size),i._finalizing&&i.finalize(),t()}),o}};ya.prototype.finalize=function(){if(this._stream){this._finalizing=!0;return}this._finalized||(this._finalized=!0,this.push(Kce),this.push(null))};ya.prototype.destroy=function(r){this._destroyed||(this._destroyed=!0,r&&this.emit("error",r),this.emit("close"),this._stream&&this._stream.destroy&&this._stream.destroy())};ya.prototype._encode=function(r){if(!r.pax){var e=kb.encode(r);if(e){this.push(e);return}}this._encodePax(r)};ya.prototype._encodePax=function(r){var e=kb.encodePax({name:r.name,linkname:r.linkname,pax:r.pax}),t={name:"PaxHeader",mode:r.mode,uid:r.uid,gid:r.gid,size:e.length,mtime:r.mtime,type:"pax-header",linkname:r.linkname&&"PaxHeader",uname:r.uname,gname:r.gname,devmajor:r.devmajor,devminor:r.devminor};this.push(kb.encode(t)),this.push(e),TT(this,e.length),t.size=r.size,t.type=r.type,this.push(kb.encode(t))};ya.prototype._read=function(r){var e=this._drain;this._drain=LT,e()};Oce.exports=ya});var jce=w(OT=>{OT.extract=Nce();OT.pack=Hce()});var tue=w((vvt,Xce)=>{"use strict";var yh=class{constructor(e,t,i){this.__specs=e||{},Object.keys(this.__specs).forEach(n=>{if(typeof this.__specs[n]=="string"){let s=this.__specs[n],o=this.__specs[s];if(o){let a=o.aliases||[];a.push(n,s),o.aliases=[...new Set(a)],this.__specs[n]=o}else throw new Error(`Alias refers to invalid key: ${s} -> ${n}`)}}),this.__opts=t||{},this.__providers=$ce(i.filter(n=>n!=null&&typeof n=="object")),this.__isFiggyPudding=!0}get(e){return GT(this,e,!0)}get[Symbol.toStringTag](){return"FiggyPudding"}forEach(e,t=this){for(let[i,n]of this.entries())e.call(t,n,i,this)}toJSON(){let e={};return this.forEach((t,i)=>{e[i]=t}),e}*entries(e){for(let i of Object.keys(this.__specs))yield[i,this.get(i)];let t=e||this.__opts.other;if(t){let i=new Set;for(let n of this.__providers){let s=n.entries?n.entries(t):F9e(n);for(let[o,a]of s)t(o)&&!i.has(o)&&(i.add(o),yield[o,a])}}}*[Symbol.iterator](){for(let[e,t]of this.entries())yield[e,t]}*keys(){for(let[e]of this.entries())yield e}*values(){for(let[,e]of this.entries())yield e}concat(...e){return new Proxy(new yh(this.__specs,this.__opts,$ce(this.__providers).concat(e)),Zce)}};try{let r=require("util");yh.prototype[r.inspect.custom]=function(e,t){return this[Symbol.toStringTag]+" "+r.inspect(this.toJSON(),t)}}catch(r){}function N9e(r){throw Object.assign(new Error(`invalid config key requested: ${r}`),{code:"EBADKEY"})}function GT(r,e,t){let i=r.__specs[e];if(t&&!i&&(!r.__opts.other||!r.__opts.other(e)))N9e(e);else{i||(i={});let n;for(let s of r.__providers){if(n=eue(e,s),n===void 0&&i.aliases&&i.aliases.length){for(let o of i.aliases)if(o!==e&&(n=eue(o,s),n!==void 0))break}if(n!==void 0)break}return n===void 0&&i.default!==void 0?typeof i.default=="function"?i.default(r):i.default:n}}function eue(r,e){let t;return e.__isFiggyPudding?t=GT(e,r,!1):typeof e.get=="function"?t=e.get(r):t=e[r],t}var Zce={has(r,e){return e in r.__specs&>(r,e,!1)!==void 0},ownKeys(r){return Object.keys(r.__specs)},get(r,e){return typeof e=="symbol"||e.slice(0,2)==="__"||e in yh.prototype?r[e]:r.get(e)},set(r,e,t){if(typeof e=="symbol"||e.slice(0,2)==="__")return r[e]=t,!0;throw new Error("figgyPudding options cannot be modified. Use .concat() instead.")},deleteProperty(){throw new Error("figgyPudding options cannot be deleted. Use .concat() and shadow them instead.")}};Xce.exports=L9e;function L9e(r,e){function t(...i){return new Proxy(new yh(r,e,i),Zce)}return t}function $ce(r){let e=[];return r.forEach(t=>e.unshift(t)),e}function F9e(r){return Object.keys(r).map(e=>[e,r[e]])}});var nue=w((kvt,wa)=>{"use strict";var nE=require("crypto"),T9e=tue(),O9e=require("stream").Transform,rue=["sha256","sha384","sha512"],M9e=/^[a-z0-9+/]+(?:=?=?)$/i,U9e=/^([^-]+)-([^?]+)([?\S*]*)$/,K9e=/^([^-]+)-([A-Za-z0-9+/=]{44,88})(\?[\x21-\x7E]*)*$/,H9e=/^[\x21-\x7E]+$/,Cn=T9e({algorithms:{default:["sha512"]},error:{default:!1},integrity:{},options:{default:[]},pickAlgorithm:{default:()=>j9e},Promise:{default:()=>Promise},sep:{default:" "},single:{default:!1},size:{},strict:{default:!1}}),Nu=class{get isHash(){return!0}constructor(e,t){t=Cn(t);let i=!!t.strict;this.source=e.trim();let n=this.source.match(i?K9e:U9e);if(!n||i&&!rue.some(o=>o===n[1]))return;this.algorithm=n[1],this.digest=n[2];let s=n[3];this.options=s?s.slice(1).split("?"):[]}hexDigest(){return this.digest&&Buffer.from(this.digest,"base64").toString("hex")}toJSON(){return this.toString()}toString(e){if(e=Cn(e),e.strict&&!(rue.some(i=>i===this.algorithm)&&this.digest.match(M9e)&&(this.options||[]).every(i=>i.match(H9e))))return"";let t=this.options&&this.options.length?`?${this.options.join("?")}`:"";return`${this.algorithm}-${this.digest}${t}`}},wh=class{get isIntegrity(){return!0}toJSON(){return this.toString()}toString(e){e=Cn(e);let t=e.sep||" ";return e.strict&&(t=t.replace(/\S+/g," ")),Object.keys(this).map(i=>this[i].map(n=>Nu.prototype.toString.call(n,e)).filter(n=>n.length).join(t)).filter(i=>i.length).join(t)}concat(e,t){t=Cn(t);let i=typeof e=="string"?e:sE(e,t);return Ba(`${this.toString(t)} ${i}`,t)}hexDigest(){return Ba(this,{single:!0}).hexDigest()}match(e,t){t=Cn(t);let i=Ba(e,t),n=i.pickAlgorithm(t);return this[n]&&i[n]&&this[n].find(s=>i[n].find(o=>s.digest===o.digest))||!1}pickAlgorithm(e){e=Cn(e);let t=e.pickAlgorithm,i=Object.keys(this);if(!i.length)throw new Error(`No algorithms available for ${JSON.stringify(this.toString())}`);return i.reduce((n,s)=>t(n,s)||n)}};wa.exports.parse=Ba;function Ba(r,e){if(e=Cn(e),typeof r=="string")return YT(r,e);if(r.algorithm&&r.digest){let t=new wh;return t[r.algorithm]=[r],YT(sE(t,e),e)}else return YT(sE(r,e),e)}function YT(r,e){return e.single?new Nu(r,e):r.trim().split(/\s+/).reduce((t,i)=>{let n=new Nu(i,e);if(n.algorithm&&n.digest){let s=n.algorithm;t[s]||(t[s]=[]),t[s].push(n)}return t},new wh)}wa.exports.stringify=sE;function sE(r,e){return e=Cn(e),r.algorithm&&r.digest?Nu.prototype.toString.call(r,e):typeof r=="string"?sE(Ba(r,e),e):wh.prototype.toString.call(r,e)}wa.exports.fromHex=G9e;function G9e(r,e,t){t=Cn(t);let i=t.options&&t.options.length?`?${t.options.join("?")}`:"";return Ba(`${e}-${Buffer.from(r,"hex").toString("base64")}${i}`,t)}wa.exports.fromData=Y9e;function Y9e(r,e){e=Cn(e);let t=e.algorithms,i=e.options&&e.options.length?`?${e.options.join("?")}`:"";return t.reduce((n,s)=>{let o=nE.createHash(s).update(r).digest("base64"),a=new Nu(`${s}-${o}${i}`,e);if(a.algorithm&&a.digest){let l=a.algorithm;n[l]||(n[l]=[]),n[l].push(a)}return n},new wh)}wa.exports.fromStream=q9e;function q9e(r,e){e=Cn(e);let t=e.Promise||Promise,i=qT(e);return new t((n,s)=>{r.pipe(i),r.on("error",s),i.on("error",s);let o;i.on("integrity",a=>{o=a}),i.on("end",()=>n(o)),i.on("data",()=>{})})}wa.exports.checkData=J9e;function J9e(r,e,t){if(t=Cn(t),e=Ba(e,t),!Object.keys(e).length){if(t.error)throw Object.assign(new Error("No valid integrity hashes to check against"),{code:"EINTEGRITY"});return!1}let i=e.pickAlgorithm(t),n=nE.createHash(i).update(r).digest("base64"),s=Ba({algorithm:i,digest:n}),o=s.match(e,t);if(o||!t.error)return o;if(typeof t.size=="number"&&r.length!==t.size){let a=new Error(`data size mismatch when checking ${e}. + Wanted: ${t.size} + Found: ${r.length}`);throw a.code="EBADSIZE",a.found=r.length,a.expected=t.size,a.sri=e,a}else{let a=new Error(`Integrity checksum failed when using ${i}: Wanted ${e}, but got ${s}. (${r.length} bytes)`);throw a.code="EINTEGRITY",a.found=s,a.expected=e,a.algorithm=i,a.sri=e,a}}wa.exports.checkStream=W9e;function W9e(r,e,t){t=Cn(t);let i=t.Promise||Promise,n=qT(t.concat({integrity:e}));return new i((s,o)=>{r.pipe(n),r.on("error",o),n.on("error",o);let a;n.on("verified",l=>{a=l}),n.on("end",()=>s(a)),n.on("data",()=>{})})}wa.exports.integrityStream=qT;function qT(r){r=Cn(r);let e=r.integrity&&Ba(r.integrity,r),t=e&&Object.keys(e).length,i=t&&e.pickAlgorithm(r),n=t&&e[i],s=Array.from(new Set(r.algorithms.concat(i?[i]:[]))),o=s.map(nE.createHash),a=0,l=new O9e({transform(c,u,g){a+=c.length,o.forEach(f=>f.update(c,u)),g(null,c,u)}}).on("end",()=>{let c=r.options&&r.options.length?`?${r.options.join("?")}`:"",u=Ba(o.map((f,h)=>`${s[h]}-${f.digest("base64")}${c}`).join(" "),r),g=t&&u.match(e,r);if(typeof r.size=="number"&&a!==r.size){let f=new Error(`stream size mismatch when checking ${e}. + Wanted: ${r.size} + Found: ${a}`);f.code="EBADSIZE",f.found=a,f.expected=r.size,f.sri=e,l.emit("error",f)}else if(r.integrity&&!g){let f=new Error(`${e} integrity checksum failed when using ${i}: wanted ${n} but got ${u}. (${a} bytes)`);f.code="EINTEGRITY",f.found=u,f.expected=n,f.algorithm=i,f.sri=e,l.emit("error",f)}else l.emit("size",a),l.emit("integrity",u),g&&l.emit("verified",g)});return l}wa.exports.create=z9e;function z9e(r){r=Cn(r);let e=r.algorithms,t=r.options.length?`?${r.options.join("?")}`:"",i=e.map(nE.createHash);return{update:function(n,s){return i.forEach(o=>o.update(n,s)),this},digest:function(n){return e.reduce((o,a)=>{let l=i.shift().digest("base64"),c=new Nu(`${a}-${l}${t}`,r);if(c.algorithm&&c.digest){let u=c.algorithm;o[u]||(o[u]=[]),o[u].push(c)}return o},new wh)}}}var _9e=new Set(nE.getHashes()),iue=["md5","whirlpool","sha1","sha224","sha256","sha384","sha512","sha3","sha3-256","sha3-384","sha3-512","sha3_256","sha3_384","sha3_512"].filter(r=>_9e.has(r));function j9e(r,e){return iue.indexOf(r.toLowerCase())>=iue.indexOf(e.toLowerCase())?r:e}});var IC={};ft(IC,{BuildType:()=>cs,Cache:()=>Nt,Configuration:()=>ye,DEFAULT_LOCK_FILENAME:()=>hx,DEFAULT_RC_FILENAME:()=>fx,FormatType:()=>Ri,InstallMode:()=>Ci,LightReport:()=>dA,LinkType:()=>Qt,Manifest:()=>At,MessageName:()=>X,MultiFetcher:()=>wd,PackageExtensionStatus:()=>qi,PackageExtensionType:()=>wi,Project:()=>ze,ProjectLookup:()=>gl,Report:()=>Ji,ReportError:()=>ct,SettingsType:()=>Ie,StreamReport:()=>Je,TAG_REGEXP:()=>_g,TelemetryManager:()=>EC,ThrowReport:()=>di,VirtualFetcher:()=>bd,Workspace:()=>mC,WorkspaceFetcher:()=>Qd,WorkspaceResolver:()=>oi,YarnVersion:()=>Kr,execUtils:()=>Nr,folderUtils:()=>ox,formatUtils:()=>ae,hashUtils:()=>Rn,httpUtils:()=>ir,miscUtils:()=>Se,nodeUtils:()=>Xg,parseMessageName:()=>yI,scriptUtils:()=>Zt,semverUtils:()=>Wt,stringifyMessageName:()=>VA,structUtils:()=>P,tgzUtils:()=>Bi,treeUtils:()=>ls});var Nr={};ft(Nr,{EndStrategy:()=>ss,ExecError:()=>yx,PipeError:()=>Bw,execvp:()=>mve,pipevp:()=>ia});var $h={};ft($h,{AliasFS:()=>La,CwdFS:()=>_t,DEFAULT_COMPRESSION_LEVEL:()=>cc,FakeFS:()=>qA,Filename:()=>xt,JailFS:()=>Ta,LazyFS:()=>Vh,LinkStrategy:()=>Yh,NoFS:()=>GE,NodeFS:()=>ar,PortablePath:()=>Me,PosixFS:()=>Xh,ProxiedFS:()=>Qi,VirtualFS:()=>Wr,ZipFS:()=>li,ZipOpenFS:()=>ys,constants:()=>Rr,extendFs:()=>zE,normalizeLineEndings:()=>oc,npath:()=>H,opendir:()=>KE,patchFs:()=>dQ,ppath:()=>x,statUtils:()=>iQ,toFilename:()=>Jr,xfs:()=>U});var Rr={};ft(Rr,{SAFE_TIME:()=>rQ,S_IFDIR:()=>Ra,S_IFLNK:()=>Na,S_IFMT:()=>Vn,S_IFREG:()=>Fa});var Vn=61440,Ra=16384,Fa=32768,Na=40960,rQ=456789e3;var iQ={};ft(iQ,{BigIntStatsEntry:()=>Hh,DEFAULT_MODE:()=>Kh,DirEntry:()=>GO,StatEntry:()=>GA,areStatsEqual:()=>sQ,clearStats:()=>FE,convertToBigIntStats:()=>NE,makeDefaultStats:()=>jh,makeEmptyStats:()=>dge});var nQ=ge(require("util"));var Kh=Fa|420,GO=class{constructor(){this.name="";this.mode=0}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&Vn)===Ra}isFIFO(){return!1}isFile(){return(this.mode&Vn)===Fa}isSocket(){return!1}isSymbolicLink(){return(this.mode&Vn)===Na}},GA=class{constructor(){this.uid=0;this.gid=0;this.size=0;this.blksize=0;this.atimeMs=0;this.mtimeMs=0;this.ctimeMs=0;this.birthtimeMs=0;this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=0;this.ino=0;this.mode=Kh;this.nlink=1;this.rdev=0;this.blocks=1}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&Vn)===Ra}isFIFO(){return!1}isFile(){return(this.mode&Vn)===Fa}isSocket(){return!1}isSymbolicLink(){return(this.mode&Vn)===Na}},Hh=class{constructor(){this.uid=BigInt(0);this.gid=BigInt(0);this.size=BigInt(0);this.blksize=BigInt(0);this.atimeMs=BigInt(0);this.mtimeMs=BigInt(0);this.ctimeMs=BigInt(0);this.birthtimeMs=BigInt(0);this.atimeNs=BigInt(0);this.mtimeNs=BigInt(0);this.ctimeNs=BigInt(0);this.birthtimeNs=BigInt(0);this.atime=new Date(0);this.mtime=new Date(0);this.ctime=new Date(0);this.birthtime=new Date(0);this.dev=BigInt(0);this.ino=BigInt(0);this.mode=BigInt(Kh);this.nlink=BigInt(1);this.rdev=BigInt(0);this.blocks=BigInt(1)}isBlockDevice(){return!1}isCharacterDevice(){return!1}isDirectory(){return(this.mode&BigInt(Vn))===BigInt(Ra)}isFIFO(){return!1}isFile(){return(this.mode&BigInt(Vn))===BigInt(Fa)}isSocket(){return!1}isSymbolicLink(){return(this.mode&BigInt(Vn))===BigInt(Na)}};function jh(){return new GA}function dge(){return FE(jh())}function FE(r){for(let e in r)if(Object.prototype.hasOwnProperty.call(r,e)){let t=r[e];typeof t=="number"?r[e]=0:typeof t=="bigint"?r[e]=BigInt(0):nQ.types.isDate(t)&&(r[e]=new Date(0))}return r}function NE(r){let e=new Hh;for(let t in r)if(Object.prototype.hasOwnProperty.call(r,t)){let i=r[t];typeof i=="number"?e[t]=BigInt(i):nQ.types.isDate(i)&&(e[t]=new Date(i))}return e.atimeNs=e.atimeMs*BigInt(1e6),e.mtimeNs=e.mtimeMs*BigInt(1e6),e.ctimeNs=e.ctimeMs*BigInt(1e6),e.birthtimeNs=e.birthtimeMs*BigInt(1e6),e}function sQ(r,e){if(r.atimeMs!==e.atimeMs||r.birthtimeMs!==e.birthtimeMs||r.blksize!==e.blksize||r.blocks!==e.blocks||r.ctimeMs!==e.ctimeMs||r.dev!==e.dev||r.gid!==e.gid||r.ino!==e.ino||r.isBlockDevice()!==e.isBlockDevice()||r.isCharacterDevice()!==e.isCharacterDevice()||r.isDirectory()!==e.isDirectory()||r.isFIFO()!==e.isFIFO()||r.isFile()!==e.isFile()||r.isSocket()!==e.isSocket()||r.isSymbolicLink()!==e.isSymbolicLink()||r.mode!==e.mode||r.mtimeMs!==e.mtimeMs||r.nlink!==e.nlink||r.rdev!==e.rdev||r.size!==e.size||r.uid!==e.uid)return!1;let t=r,i=e;return!(t.atimeNs!==i.atimeNs||t.mtimeNs!==i.mtimeNs||t.ctimeNs!==i.ctimeNs||t.birthtimeNs!==i.birthtimeNs)}var TE=ge(require("fs"));var Gh=ge(require("path")),YO;(function(i){i[i.File=0]="File",i[i.Portable=1]="Portable",i[i.Native=2]="Native"})(YO||(YO={}));var Me={root:"/",dot:"."},xt={nodeModules:"node_modules",manifest:"package.json",lockfile:"yarn.lock",virtual:"__virtual__",pnpJs:".pnp.js",pnpCjs:".pnp.cjs",rc:".yarnrc.yml"},H=Object.create(Gh.default),x=Object.create(Gh.default.posix);H.cwd=()=>process.cwd();x.cwd=()=>oQ(process.cwd());x.resolve=(...r)=>r.length>0&&x.isAbsolute(r[0])?Gh.default.posix.resolve(...r):Gh.default.posix.resolve(x.cwd(),...r);var qO=function(r,e,t){return e=r.normalize(e),t=r.normalize(t),e===t?".":(e.endsWith(r.sep)||(e=e+r.sep),t.startsWith(e)?t.slice(e.length):null)};H.fromPortablePath=JO;H.toPortablePath=oQ;H.contains=(r,e)=>qO(H,r,e);x.contains=(r,e)=>qO(x,r,e);var Cge=/^([a-zA-Z]:.*)$/,mge=/^\/\/(\.\/)?(.*)$/,Ege=/^\/([a-zA-Z]:.*)$/,Ige=/^\/unc\/(\.dot\/)?(.*)$/;function JO(r){if(process.platform!=="win32")return r;let e,t;if(e=r.match(Ege))r=e[1];else if(t=r.match(Ige))r=`\\\\${t[1]?".\\":""}${t[2]}`;else return r;return r.replace(/\//g,"\\")}function oQ(r){if(process.platform!=="win32")return r;r=r.replace(/\\/g,"/");let e,t;return(e=r.match(Cge))?r=`/${e[1]}`:(t=r.match(mge))&&(r=`/unc/${t[1]?".dot/":""}${t[2]}`),r}function LE(r,e){return r===H?JO(e):oQ(e)}function Jr(r){if(H.parse(r).dir!==""||x.parse(r).dir!=="")throw new Error(`Invalid filename: "${r}"`);return r}var OE=new Date(rQ*1e3),Yh;(function(t){t.Allow="allow",t.ReadOnly="readOnly"})(Yh||(Yh={}));async function WO(r,e,t,i,n){let s=r.pathUtils.normalize(e),o=t.pathUtils.normalize(i),a=[],l=[],{atime:c,mtime:u}=n.stableTime?{atime:OE,mtime:OE}:await t.lstatPromise(o);await r.mkdirpPromise(r.pathUtils.dirname(e),{utimes:[c,u]});let g=typeof r.lutimesPromise=="function"?r.lutimesPromise.bind(r):r.utimesPromise.bind(r);await aQ(a,l,g,r,s,t,o,te(N({},n),{didParentExist:!0}));for(let f of a)await f();await Promise.all(l.map(f=>f()))}async function aQ(r,e,t,i,n,s,o,a){var h,p;let l=a.didParentExist?await yge(i,n):null,c=await s.lstatPromise(o),{atime:u,mtime:g}=a.stableTime?{atime:OE,mtime:OE}:c,f;switch(!0){case c.isDirectory():f=await wge(r,e,t,i,n,l,s,o,c,a);break;case c.isFile():f=await Bge(r,e,t,i,n,l,s,o,c,a);break;case c.isSymbolicLink():f=await bge(r,e,t,i,n,l,s,o,c,a);break;default:throw new Error(`Unsupported file type (${c.mode})`)}return(f||((h=l==null?void 0:l.mtime)==null?void 0:h.getTime())!==g.getTime()||((p=l==null?void 0:l.atime)==null?void 0:p.getTime())!==u.getTime())&&(e.push(()=>t(n,u,g)),f=!0),(l===null||(l.mode&511)!=(c.mode&511))&&(e.push(()=>i.chmodPromise(n,c.mode&511)),f=!0),f}async function yge(r,e){try{return await r.lstatPromise(e)}catch(t){return null}}async function wge(r,e,t,i,n,s,o,a,l,c){if(s!==null&&!s.isDirectory())if(c.overwrite)r.push(async()=>i.removePromise(n)),s=null;else return!1;let u=!1;s===null&&(r.push(async()=>{try{await i.mkdirPromise(n,{mode:l.mode})}catch(h){if(h.code!=="EEXIST")throw h}}),u=!0);let g=await o.readdirPromise(a),f=c.didParentExist&&!s?te(N({},c),{didParentExist:!1}):c;if(c.stableSort)for(let h of g.sort())await aQ(r,e,t,i,i.pathUtils.join(n,h),o,o.pathUtils.join(a,h),f)&&(u=!0);else(await Promise.all(g.map(async p=>{await aQ(r,e,t,i,i.pathUtils.join(n,p),o,o.pathUtils.join(a,p),f)}))).some(p=>p)&&(u=!0);return u}var AQ=new WeakMap;function lQ(r,e,t,i,n){return async()=>{await r.linkPromise(t,e),n===Yh.ReadOnly&&(i.mode&=~146,await r.chmodPromise(e,i.mode))}}function Qge(r,e,t,i,n){let s=AQ.get(r);return typeof s=="undefined"?async()=>{try{await r.copyFilePromise(t,e,TE.default.constants.COPYFILE_FICLONE_FORCE),AQ.set(r,!0)}catch(o){if(o.code==="ENOSYS"||o.code==="ENOTSUP")AQ.set(r,!1),await lQ(r,e,t,i,n)();else throw o}}:s?async()=>r.copyFilePromise(t,e,TE.default.constants.COPYFILE_FICLONE_FORCE):lQ(r,e,t,i,n)}async function Bge(r,e,t,i,n,s,o,a,l,c){var f;if(s!==null)if(c.overwrite)r.push(async()=>i.removePromise(n)),s=null;else return!1;let u=(f=c.linkStrategy)!=null?f:null,g=i===o?u!==null?Qge(i,n,a,l,u):async()=>i.copyFilePromise(a,n,TE.default.constants.COPYFILE_FICLONE):u!==null?lQ(i,n,a,l,u):async()=>i.writeFilePromise(n,await o.readFilePromise(a));return r.push(async()=>g()),!0}async function bge(r,e,t,i,n,s,o,a,l,c){if(s!==null)if(c.overwrite)r.push(async()=>i.removePromise(n)),s=null;else return!1;return r.push(async()=>{await i.symlinkPromise(LE(i.pathUtils,await o.readlinkPromise(a)),n)}),!0}function Es(r,e){return Object.assign(new Error(`${r}: ${e}`),{code:r})}function ME(r){return Es("EBUSY",r)}function qh(r,e){return Es("ENOSYS",`${r}, ${e}`)}function YA(r){return Es("EINVAL",`invalid argument, ${r}`)}function Ai(r){return Es("EBADF",`bad file descriptor, ${r}`)}function so(r){return Es("ENOENT",`no such file or directory, ${r}`)}function No(r){return Es("ENOTDIR",`not a directory, ${r}`)}function Jh(r){return Es("EISDIR",`illegal operation on a directory, ${r}`)}function UE(r){return Es("EEXIST",`file already exists, ${r}`)}function In(r){return Es("EROFS",`read-only filesystem, ${r}`)}function zO(r){return Es("ENOTEMPTY",`directory not empty, ${r}`)}function _O(r){return Es("EOPNOTSUPP",`operation not supported, ${r}`)}function VO(){return Es("ERR_DIR_CLOSED","Directory handle was closed")}var cQ=class extends Error{constructor(e,t){super(e);this.name="Libzip Error",this.code=t}};var XO=class{constructor(e,t,i={}){this.path=e;this.nextDirent=t;this.opts=i;this.closed=!1}throwIfClosed(){if(this.closed)throw VO()}async*[Symbol.asyncIterator](){try{let e;for(;(e=await this.read())!==null;)yield e}finally{await this.close()}}read(e){let t=this.readSync();return typeof e!="undefined"?e(null,t):Promise.resolve(t)}readSync(){return this.throwIfClosed(),this.nextDirent()}close(e){return this.closeSync(),typeof e!="undefined"?e(null):Promise.resolve()}closeSync(){var e,t;this.throwIfClosed(),(t=(e=this.opts).onClose)==null||t.call(e),this.closed=!0}};function KE(r,e,t,i){let n=()=>{let s=t.shift();return typeof s=="undefined"?null:Object.assign(r.statSync(r.pathUtils.join(e,s)),{name:s})};return new XO(e,n,i)}var ZO=ge(require("os"));var qA=class{constructor(e){this.pathUtils=e}async*genTraversePromise(e,{stableSort:t=!1}={}){let i=[e];for(;i.length>0;){let n=i.shift();if((await this.lstatPromise(n)).isDirectory()){let o=await this.readdirPromise(n);if(t)for(let a of o.sort())i.push(this.pathUtils.join(n,a));else throw new Error("Not supported")}else yield n}}async removePromise(e,{recursive:t=!0,maxRetries:i=5}={}){let n;try{n=await this.lstatPromise(e)}catch(s){if(s.code==="ENOENT")return;throw s}if(n.isDirectory()){if(t){let s=await this.readdirPromise(e);await Promise.all(s.map(o=>this.removePromise(this.pathUtils.resolve(e,o))))}for(let s=0;s<=i;s++)try{await this.rmdirPromise(e);break}catch(o){if(o.code!=="EBUSY"&&o.code!=="ENOTEMPTY")throw o;ssetTimeout(a,s*100))}}else await this.unlinkPromise(e)}removeSync(e,{recursive:t=!0}={}){let i;try{i=this.lstatSync(e)}catch(n){if(n.code==="ENOENT")return;throw n}if(i.isDirectory()){if(t)for(let n of this.readdirSync(e))this.removeSync(this.pathUtils.resolve(e,n));this.rmdirSync(e)}else this.unlinkSync(e)}async mkdirpPromise(e,{chmod:t,utimes:i}={}){if(e=this.resolve(e),e===this.pathUtils.dirname(e))return;let n=e.split(this.pathUtils.sep),s;for(let o=2;o<=n.length;++o){let a=n.slice(0,o).join(this.pathUtils.sep);if(!this.existsSync(a)){try{await this.mkdirPromise(a)}catch(l){if(l.code==="EEXIST")continue;throw l}if(s!=null||(s=a),t!=null&&await this.chmodPromise(a,t),i!=null)await this.utimesPromise(a,i[0],i[1]);else{let l=await this.statPromise(this.pathUtils.dirname(a));await this.utimesPromise(a,l.atime,l.mtime)}}}return s}mkdirpSync(e,{chmod:t,utimes:i}={}){if(e=this.resolve(e),e===this.pathUtils.dirname(e))return;let n=e.split(this.pathUtils.sep),s;for(let o=2;o<=n.length;++o){let a=n.slice(0,o).join(this.pathUtils.sep);if(!this.existsSync(a)){try{this.mkdirSync(a)}catch(l){if(l.code==="EEXIST")continue;throw l}if(s!=null||(s=a),t!=null&&this.chmodSync(a,t),i!=null)this.utimesSync(a,i[0],i[1]);else{let l=this.statSync(this.pathUtils.dirname(a));this.utimesSync(a,l.atime,l.mtime)}}}return s}async copyPromise(e,t,{baseFs:i=this,overwrite:n=!0,stableSort:s=!1,stableTime:o=!1,linkStrategy:a=null}={}){return await WO(this,e,i,t,{overwrite:n,stableSort:s,stableTime:o,linkStrategy:a})}copySync(e,t,{baseFs:i=this,overwrite:n=!0}={}){let s=i.lstatSync(t),o=this.existsSync(e);if(s.isDirectory()){this.mkdirpSync(e);let l=i.readdirSync(t);for(let c of l)this.copySync(this.pathUtils.join(e,c),i.pathUtils.join(t,c),{baseFs:i,overwrite:n})}else if(s.isFile()){if(!o||n){o&&this.removeSync(e);let l=i.readFileSync(t);this.writeFileSync(e,l)}}else if(s.isSymbolicLink()){if(!o||n){o&&this.removeSync(e);let l=i.readlinkSync(t);this.symlinkSync(LE(this.pathUtils,l),e)}}else throw new Error(`Unsupported file type (file: ${t}, mode: 0o${s.mode.toString(8).padStart(6,"0")})`);let a=s.mode&511;this.chmodSync(e,a)}async changeFilePromise(e,t,i={}){return Buffer.isBuffer(t)?this.changeFileBufferPromise(e,t,i):this.changeFileTextPromise(e,t,i)}async changeFileBufferPromise(e,t,{mode:i}={}){let n=Buffer.alloc(0);try{n=await this.readFilePromise(e)}catch(s){}Buffer.compare(n,t)!==0&&await this.writeFilePromise(e,t,{mode:i})}async changeFileTextPromise(e,t,{automaticNewlines:i,mode:n}={}){let s="";try{s=await this.readFilePromise(e,"utf8")}catch(a){}let o=i?oc(s,t):t;s!==o&&await this.writeFilePromise(e,o,{mode:n})}changeFileSync(e,t,i={}){return Buffer.isBuffer(t)?this.changeFileBufferSync(e,t,i):this.changeFileTextSync(e,t,i)}changeFileBufferSync(e,t,{mode:i}={}){let n=Buffer.alloc(0);try{n=this.readFileSync(e)}catch(s){}Buffer.compare(n,t)!==0&&this.writeFileSync(e,t,{mode:i})}changeFileTextSync(e,t,{automaticNewlines:i=!1,mode:n}={}){let s="";try{s=this.readFileSync(e,"utf8")}catch(a){}let o=i?oc(s,t):t;s!==o&&this.writeFileSync(e,o,{mode:n})}async movePromise(e,t){try{await this.renamePromise(e,t)}catch(i){if(i.code==="EXDEV")await this.copyPromise(t,e),await this.removePromise(e);else throw i}}moveSync(e,t){try{this.renameSync(e,t)}catch(i){if(i.code==="EXDEV")this.copySync(t,e),this.removeSync(e);else throw i}}async lockPromise(e,t){let i=`${e}.flock`,n=1e3/60,s=Date.now(),o=null,a=async()=>{let l;try{[l]=await this.readJsonPromise(i)}catch(c){return Date.now()-s<500}try{return process.kill(l,0),!0}catch(c){return!1}};for(;o===null;)try{o=await this.openPromise(i,"wx")}catch(l){if(l.code==="EEXIST"){if(!await a())try{await this.unlinkPromise(i);continue}catch(c){}if(Date.now()-s<60*1e3)await new Promise(c=>setTimeout(c,n));else throw new Error(`Couldn't acquire a lock in a reasonable time (via ${i})`)}else throw l}await this.writePromise(o,JSON.stringify([process.pid]));try{return await t()}finally{try{await this.closePromise(o),await this.unlinkPromise(i)}catch(l){}}}async readJsonPromise(e){let t=await this.readFilePromise(e,"utf8");try{return JSON.parse(t)}catch(i){throw i.message+=` (in ${e})`,i}}readJsonSync(e){let t=this.readFileSync(e,"utf8");try{return JSON.parse(t)}catch(i){throw i.message+=` (in ${e})`,i}}async writeJsonPromise(e,t){return await this.writeFilePromise(e,`${JSON.stringify(t,null,2)} +`)}writeJsonSync(e,t){return this.writeFileSync(e,`${JSON.stringify(t,null,2)} +`)}async preserveTimePromise(e,t){let i=await this.lstatPromise(e),n=await t();typeof n!="undefined"&&(e=n),this.lutimesPromise?await this.lutimesPromise(e,i.atime,i.mtime):i.isSymbolicLink()||await this.utimesPromise(e,i.atime,i.mtime)}async preserveTimeSync(e,t){let i=this.lstatSync(e),n=t();typeof n!="undefined"&&(e=n),this.lutimesSync?this.lutimesSync(e,i.atime,i.mtime):i.isSymbolicLink()||this.utimesSync(e,i.atime,i.mtime)}},ac=class extends qA{constructor(){super(x)}};function Sge(r){let e=r.match(/\r?\n/g);if(e===null)return ZO.EOL;let t=e.filter(n=>n===`\r +`).length,i=e.length-t;return t>i?`\r +`:` +`}function oc(r,e){return e.replace(/\r?\n/g,Sge(r))}var _u=ge(require("fs")),uQ=ge(require("stream")),rM=ge(require("util")),gQ=ge(require("zlib"));var $O=ge(require("fs"));var ar=class extends ac{constructor(e=$O.default){super();this.realFs=e,typeof this.realFs.lutimes!="undefined"&&(this.lutimesPromise=this.lutimesPromiseImpl,this.lutimesSync=this.lutimesSyncImpl)}getExtractHint(){return!1}getRealPath(){return Me.root}resolve(e){return x.resolve(e)}async openPromise(e,t,i){return await new Promise((n,s)=>{this.realFs.open(H.fromPortablePath(e),t,i,this.makeCallback(n,s))})}openSync(e,t,i){return this.realFs.openSync(H.fromPortablePath(e),t,i)}async opendirPromise(e,t){return await new Promise((i,n)=>{typeof t!="undefined"?this.realFs.opendir(H.fromPortablePath(e),t,this.makeCallback(i,n)):this.realFs.opendir(H.fromPortablePath(e),this.makeCallback(i,n))}).then(i=>Object.defineProperty(i,"path",{value:e,configurable:!0,writable:!0}))}opendirSync(e,t){let i=typeof t!="undefined"?this.realFs.opendirSync(H.fromPortablePath(e),t):this.realFs.opendirSync(H.fromPortablePath(e));return Object.defineProperty(i,"path",{value:e,configurable:!0,writable:!0})}async readPromise(e,t,i=0,n=0,s=-1){return await new Promise((o,a)=>{this.realFs.read(e,t,i,n,s,(l,c)=>{l?a(l):o(c)})})}readSync(e,t,i,n,s){return this.realFs.readSync(e,t,i,n,s)}async writePromise(e,t,i,n,s){return await new Promise((o,a)=>typeof t=="string"?this.realFs.write(e,t,i,this.makeCallback(o,a)):this.realFs.write(e,t,i,n,s,this.makeCallback(o,a)))}writeSync(e,t,i,n,s){return typeof t=="string"?this.realFs.writeSync(e,t,i):this.realFs.writeSync(e,t,i,n,s)}async closePromise(e){await new Promise((t,i)=>{this.realFs.close(e,this.makeCallback(t,i))})}closeSync(e){this.realFs.closeSync(e)}createReadStream(e,t){let i=e!==null?H.fromPortablePath(e):e;return this.realFs.createReadStream(i,t)}createWriteStream(e,t){let i=e!==null?H.fromPortablePath(e):e;return this.realFs.createWriteStream(i,t)}async realpathPromise(e){return await new Promise((t,i)=>{this.realFs.realpath(H.fromPortablePath(e),{},this.makeCallback(t,i))}).then(t=>H.toPortablePath(t))}realpathSync(e){return H.toPortablePath(this.realFs.realpathSync(H.fromPortablePath(e),{}))}async existsPromise(e){return await new Promise(t=>{this.realFs.exists(H.fromPortablePath(e),t)})}accessSync(e,t){return this.realFs.accessSync(H.fromPortablePath(e),t)}async accessPromise(e,t){return await new Promise((i,n)=>{this.realFs.access(H.fromPortablePath(e),t,this.makeCallback(i,n))})}existsSync(e){return this.realFs.existsSync(H.fromPortablePath(e))}async statPromise(e,t){return await new Promise((i,n)=>{t?this.realFs.stat(H.fromPortablePath(e),t,this.makeCallback(i,n)):this.realFs.stat(H.fromPortablePath(e),this.makeCallback(i,n))})}statSync(e,t){return t?this.realFs.statSync(H.fromPortablePath(e),t):this.realFs.statSync(H.fromPortablePath(e))}async fstatPromise(e,t){return await new Promise((i,n)=>{t?this.realFs.fstat(e,t,this.makeCallback(i,n)):this.realFs.fstat(e,this.makeCallback(i,n))})}fstatSync(e,t){return t?this.realFs.fstatSync(e,t):this.realFs.fstatSync(e)}async lstatPromise(e,t){return await new Promise((i,n)=>{t?this.realFs.lstat(H.fromPortablePath(e),t,this.makeCallback(i,n)):this.realFs.lstat(H.fromPortablePath(e),this.makeCallback(i,n))})}lstatSync(e,t){return t?this.realFs.lstatSync(H.fromPortablePath(e),t):this.realFs.lstatSync(H.fromPortablePath(e))}async fchmodPromise(e,t){return await new Promise((i,n)=>{this.realFs.fchmod(e,t,this.makeCallback(i,n))})}fchmodSync(e,t){return this.realFs.fchmodSync(e,t)}async chmodPromise(e,t){return await new Promise((i,n)=>{this.realFs.chmod(H.fromPortablePath(e),t,this.makeCallback(i,n))})}chmodSync(e,t){return this.realFs.chmodSync(H.fromPortablePath(e),t)}async chownPromise(e,t,i){return await new Promise((n,s)=>{this.realFs.chown(H.fromPortablePath(e),t,i,this.makeCallback(n,s))})}chownSync(e,t,i){return this.realFs.chownSync(H.fromPortablePath(e),t,i)}async renamePromise(e,t){return await new Promise((i,n)=>{this.realFs.rename(H.fromPortablePath(e),H.fromPortablePath(t),this.makeCallback(i,n))})}renameSync(e,t){return this.realFs.renameSync(H.fromPortablePath(e),H.fromPortablePath(t))}async copyFilePromise(e,t,i=0){return await new Promise((n,s)=>{this.realFs.copyFile(H.fromPortablePath(e),H.fromPortablePath(t),i,this.makeCallback(n,s))})}copyFileSync(e,t,i=0){return this.realFs.copyFileSync(H.fromPortablePath(e),H.fromPortablePath(t),i)}async appendFilePromise(e,t,i){return await new Promise((n,s)=>{let o=typeof e=="string"?H.fromPortablePath(e):e;i?this.realFs.appendFile(o,t,i,this.makeCallback(n,s)):this.realFs.appendFile(o,t,this.makeCallback(n,s))})}appendFileSync(e,t,i){let n=typeof e=="string"?H.fromPortablePath(e):e;i?this.realFs.appendFileSync(n,t,i):this.realFs.appendFileSync(n,t)}async writeFilePromise(e,t,i){return await new Promise((n,s)=>{let o=typeof e=="string"?H.fromPortablePath(e):e;i?this.realFs.writeFile(o,t,i,this.makeCallback(n,s)):this.realFs.writeFile(o,t,this.makeCallback(n,s))})}writeFileSync(e,t,i){let n=typeof e=="string"?H.fromPortablePath(e):e;i?this.realFs.writeFileSync(n,t,i):this.realFs.writeFileSync(n,t)}async unlinkPromise(e){return await new Promise((t,i)=>{this.realFs.unlink(H.fromPortablePath(e),this.makeCallback(t,i))})}unlinkSync(e){return this.realFs.unlinkSync(H.fromPortablePath(e))}async utimesPromise(e,t,i){return await new Promise((n,s)=>{this.realFs.utimes(H.fromPortablePath(e),t,i,this.makeCallback(n,s))})}utimesSync(e,t,i){this.realFs.utimesSync(H.fromPortablePath(e),t,i)}async lutimesPromiseImpl(e,t,i){let n=this.realFs.lutimes;if(typeof n=="undefined")throw qh("unavailable Node binding",`lutimes '${e}'`);return await new Promise((s,o)=>{n.call(this.realFs,H.fromPortablePath(e),t,i,this.makeCallback(s,o))})}lutimesSyncImpl(e,t,i){let n=this.realFs.lutimesSync;if(typeof n=="undefined")throw qh("unavailable Node binding",`lutimes '${e}'`);n.call(this.realFs,H.fromPortablePath(e),t,i)}async mkdirPromise(e,t){return await new Promise((i,n)=>{this.realFs.mkdir(H.fromPortablePath(e),t,this.makeCallback(i,n))})}mkdirSync(e,t){return this.realFs.mkdirSync(H.fromPortablePath(e),t)}async rmdirPromise(e,t){return await new Promise((i,n)=>{t?this.realFs.rmdir(H.fromPortablePath(e),t,this.makeCallback(i,n)):this.realFs.rmdir(H.fromPortablePath(e),this.makeCallback(i,n))})}rmdirSync(e,t){return this.realFs.rmdirSync(H.fromPortablePath(e),t)}async linkPromise(e,t){return await new Promise((i,n)=>{this.realFs.link(H.fromPortablePath(e),H.fromPortablePath(t),this.makeCallback(i,n))})}linkSync(e,t){return this.realFs.linkSync(H.fromPortablePath(e),H.fromPortablePath(t))}async symlinkPromise(e,t,i){return await new Promise((n,s)=>{this.realFs.symlink(H.fromPortablePath(e.replace(/\/+$/,"")),H.fromPortablePath(t),i,this.makeCallback(n,s))})}symlinkSync(e,t,i){return this.realFs.symlinkSync(H.fromPortablePath(e.replace(/\/+$/,"")),H.fromPortablePath(t),i)}async readFilePromise(e,t){return await new Promise((i,n)=>{let s=typeof e=="string"?H.fromPortablePath(e):e;this.realFs.readFile(s,t,this.makeCallback(i,n))})}readFileSync(e,t){let i=typeof e=="string"?H.fromPortablePath(e):e;return this.realFs.readFileSync(i,t)}async readdirPromise(e,t){return await new Promise((i,n)=>{(t==null?void 0:t.withFileTypes)?this.realFs.readdir(H.fromPortablePath(e),{withFileTypes:!0},this.makeCallback(i,n)):this.realFs.readdir(H.fromPortablePath(e),this.makeCallback(s=>i(s),n))})}readdirSync(e,t){return(t==null?void 0:t.withFileTypes)?this.realFs.readdirSync(H.fromPortablePath(e),{withFileTypes:!0}):this.realFs.readdirSync(H.fromPortablePath(e))}async readlinkPromise(e){return await new Promise((t,i)=>{this.realFs.readlink(H.fromPortablePath(e),this.makeCallback(t,i))}).then(t=>H.toPortablePath(t))}readlinkSync(e){return H.toPortablePath(this.realFs.readlinkSync(H.fromPortablePath(e)))}async truncatePromise(e,t){return await new Promise((i,n)=>{this.realFs.truncate(H.fromPortablePath(e),t,this.makeCallback(i,n))})}truncateSync(e,t){return this.realFs.truncateSync(H.fromPortablePath(e),t)}async ftruncatePromise(e,t){return await new Promise((i,n)=>{this.realFs.ftruncate(e,t,this.makeCallback(i,n))})}ftruncateSync(e,t){return this.realFs.ftruncateSync(e,t)}watch(e,t,i){return this.realFs.watch(H.fromPortablePath(e),t,i)}watchFile(e,t,i){return this.realFs.watchFile(H.fromPortablePath(e),t,i)}unwatchFile(e,t){return this.realFs.unwatchFile(H.fromPortablePath(e),t)}makeCallback(e,t){return(i,n)=>{i?t(i):e(n)}}};var eM=ge(require("events"));var Ac;(function(t){t.Change="change",t.Stop="stop"})(Ac||(Ac={}));var lc;(function(i){i.Ready="ready",i.Running="running",i.Stopped="stopped"})(lc||(lc={}));function tM(r,e){if(r!==e)throw new Error(`Invalid StatWatcher status: expected '${e}', got '${r}'`)}var Wh=class extends eM.EventEmitter{constructor(e,t,{bigint:i=!1}={}){super();this.status=lc.Ready;this.changeListeners=new Map;this.startTimeout=null;this.fakeFs=e,this.path=t,this.bigint=i,this.lastStats=this.stat()}static create(e,t,i){let n=new Wh(e,t,i);return n.start(),n}start(){tM(this.status,lc.Ready),this.status=lc.Running,this.startTimeout=setTimeout(()=>{this.startTimeout=null,this.fakeFs.existsSync(this.path)||this.emit(Ac.Change,this.lastStats,this.lastStats)},3)}stop(){tM(this.status,lc.Running),this.status=lc.Stopped,this.startTimeout!==null&&(clearTimeout(this.startTimeout),this.startTimeout=null),this.emit(Ac.Stop)}stat(){try{return this.fakeFs.statSync(this.path,{bigint:this.bigint})}catch(e){let t=this.bigint?new Hh:new GA;return FE(t)}}makeInterval(e){let t=setInterval(()=>{let i=this.stat(),n=this.lastStats;sQ(i,n)||(this.lastStats=i,this.emit(Ac.Change,i,n))},e.interval);return e.persistent?t:t.unref()}registerChangeListener(e,t){this.addListener(Ac.Change,e),this.changeListeners.set(e,this.makeInterval(t))}unregisterChangeListener(e){this.removeListener(Ac.Change,e);let t=this.changeListeners.get(e);typeof t!="undefined"&&clearInterval(t),this.changeListeners.delete(e)}unregisterAllChangeListeners(){for(let e of this.changeListeners.keys())this.unregisterChangeListener(e)}hasChangeListeners(){return this.changeListeners.size>0}ref(){for(let e of this.changeListeners.values())e.ref();return this}unref(){for(let e of this.changeListeners.values())e.unref();return this}};var HE=new WeakMap;function jE(r,e,t,i){let n,s,o,a;switch(typeof t){case"function":n=!1,s=!0,o=5007,a=t;break;default:({bigint:n=!1,persistent:s=!0,interval:o=5007}=t),a=i;break}let l=HE.get(r);typeof l=="undefined"&&HE.set(r,l=new Map);let c=l.get(e);return typeof c=="undefined"&&(c=Wh.create(r,e,{bigint:n}),l.set(e,c)),c.registerChangeListener(a,{persistent:s,interval:o}),c}function zh(r,e,t){let i=HE.get(r);if(typeof i=="undefined")return;let n=i.get(e);typeof n!="undefined"&&(typeof t=="undefined"?n.unregisterAllChangeListeners():n.unregisterChangeListener(t),n.hasChangeListeners()||(n.stop(),i.delete(e)))}function _h(r){let e=HE.get(r);if(typeof e!="undefined")for(let t of e.keys())zh(r,t)}var cc="mixed";function vge(r){if(typeof r=="string"&&String(+r)===r)return+r;if(Number.isFinite(r))return r<0?Date.now()/1e3:r;if(rM.types.isDate(r))return r.getTime()/1e3;throw new Error("Invalid time")}function iM(){return Buffer.from([80,75,5,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0])}var li=class extends ac{constructor(e,t){super();this.lzSource=null;this.listings=new Map;this.entries=new Map;this.fileSources=new Map;this.fds=new Map;this.nextFd=0;this.ready=!1;this.readOnly=!1;this.libzip=t.libzip;let i=t;if(this.level=typeof i.level!="undefined"?i.level:cc,e!=null||(e=iM()),typeof e=="string"){let{baseFs:o=new ar}=i;this.baseFs=o,this.path=e}else this.path=null,this.baseFs=null;if(t.stats)this.stats=t.stats;else if(typeof e=="string")try{this.stats=this.baseFs.statSync(e)}catch(o){if(o.code==="ENOENT"&&i.create)this.stats=jh();else throw o}else this.stats=jh();let n=this.libzip.malloc(4);try{let o=0;if(typeof e=="string"&&i.create&&(o|=this.libzip.ZIP_CREATE|this.libzip.ZIP_TRUNCATE),t.readOnly&&(o|=this.libzip.ZIP_RDONLY,this.readOnly=!0),typeof e=="string")this.zip=this.libzip.open(H.fromPortablePath(e),o,n);else{let a=this.allocateUnattachedSource(e);try{this.zip=this.libzip.openFromSource(a,o,n),this.lzSource=a}catch(l){throw this.libzip.source.free(a),l}}if(this.zip===0){let a=this.libzip.struct.errorS();throw this.libzip.error.initWithCode(a,this.libzip.getValue(n,"i32")),this.makeLibzipError(a)}}finally{this.libzip.free(n)}this.listings.set(Me.root,new Set);let s=this.libzip.getNumEntries(this.zip,0);for(let o=0;oe)throw new Error("Overread");let n=this.libzip.HEAPU8.subarray(t,t+e);return Buffer.from(n)}finally{this.libzip.free(t)}}finally{this.libzip.source.close(this.lzSource),this.libzip.source.free(this.lzSource),this.ready=!1}}prepareClose(){if(!this.ready)throw ME("archive closed, close");_h(this)}saveAndClose(){if(!this.path||!this.baseFs)throw new Error("ZipFS cannot be saved and must be discarded when loaded from a buffer");if(this.prepareClose(),this.readOnly){this.discardAndClose();return}let e=this.baseFs.existsSync(this.path)||this.stats.mode===Kh?void 0:this.stats.mode;if(this.entries.size===0)this.discardAndClose(),this.baseFs.writeFileSync(this.path,iM(),{mode:e});else{if(this.libzip.close(this.zip)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));typeof e!="undefined"&&this.baseFs.chmodSync(this.path,e)}this.ready=!1}discardAndClose(){this.prepareClose(),this.libzip.discard(this.zip),this.ready=!1}resolve(e){return x.resolve(Me.root,e)}async openPromise(e,t,i){return this.openSync(e,t,i)}openSync(e,t,i){let n=this.nextFd++;return this.fds.set(n,{cursor:0,p:e}),n}hasOpenFileHandles(){return!!this.fds.size}async opendirPromise(e,t){return this.opendirSync(e,t)}opendirSync(e,t={}){let i=this.resolveFilename(`opendir '${e}'`,e);if(!this.entries.has(i)&&!this.listings.has(i))throw so(`opendir '${e}'`);let n=this.listings.get(i);if(!n)throw No(`opendir '${e}'`);let s=[...n],o=this.openSync(i,"r");return KE(this,i,s,{onClose:()=>{this.closeSync(o)}})}async readPromise(e,t,i,n,s){return this.readSync(e,t,i,n,s)}readSync(e,t,i=0,n=t.byteLength,s=-1){let o=this.fds.get(e);if(typeof o=="undefined")throw Ai("read");let a=s===-1||s===null?o.cursor:s,l=this.readFileSync(o.p);l.copy(t,i,a,a+n);let c=Math.max(0,Math.min(l.length-a,n));return(s===-1||s===null)&&(o.cursor+=c),c}async writePromise(e,t,i,n,s){return typeof t=="string"?this.writeSync(e,t,s):this.writeSync(e,t,i,n,s)}writeSync(e,t,i,n,s){throw typeof this.fds.get(e)=="undefined"?Ai("read"):new Error("Unimplemented")}async closePromise(e){return this.closeSync(e)}closeSync(e){if(typeof this.fds.get(e)=="undefined")throw Ai("read");this.fds.delete(e)}createReadStream(e,{encoding:t}={}){if(e===null)throw new Error("Unimplemented");let i=this.openSync(e,"r"),n=Object.assign(new uQ.PassThrough({emitClose:!0,autoDestroy:!0,destroy:(o,a)=>{clearImmediate(s),this.closeSync(i),a(o)}}),{close(){n.destroy()},bytesRead:0,path:e}),s=setImmediate(async()=>{try{let o=await this.readFilePromise(e,t);n.bytesRead=o.length,n.end(o)}catch(o){n.destroy(o)}});return n}createWriteStream(e,{encoding:t}={}){if(this.readOnly)throw In(`open '${e}'`);if(e===null)throw new Error("Unimplemented");let i=[],n=this.openSync(e,"w"),s=Object.assign(new uQ.PassThrough({autoDestroy:!0,emitClose:!0,destroy:(o,a)=>{try{o?a(o):(this.writeFileSync(e,Buffer.concat(i),t),a(null))}catch(l){a(l)}finally{this.closeSync(n)}}}),{bytesWritten:0,path:e,close(){s.destroy()}});return s.on("data",o=>{let a=Buffer.from(o);s.bytesWritten+=a.length,i.push(a)}),s}async realpathPromise(e){return this.realpathSync(e)}realpathSync(e){let t=this.resolveFilename(`lstat '${e}'`,e);if(!this.entries.has(t)&&!this.listings.has(t))throw so(`lstat '${e}'`);return t}async existsPromise(e){return this.existsSync(e)}existsSync(e){if(!this.ready)throw ME(`archive closed, existsSync '${e}'`);if(this.symlinkCount===0){let i=x.resolve(Me.root,e);return this.entries.has(i)||this.listings.has(i)}let t;try{t=this.resolveFilename(`stat '${e}'`,e,void 0,!1)}catch(i){return!1}return t===void 0?!1:this.entries.has(t)||this.listings.has(t)}async accessPromise(e,t){return this.accessSync(e,t)}accessSync(e,t=_u.constants.F_OK){let i=this.resolveFilename(`access '${e}'`,e);if(!this.entries.has(i)&&!this.listings.has(i))throw so(`access '${e}'`);if(this.readOnly&&t&_u.constants.W_OK)throw In(`access '${e}'`)}async statPromise(e,t={bigint:!1}){return t.bigint?this.statSync(e,{bigint:!0}):this.statSync(e)}statSync(e,t={bigint:!1,throwIfNoEntry:!0}){let i=this.resolveFilename(`stat '${e}'`,e,void 0,t.throwIfNoEntry);if(i!==void 0){if(!this.entries.has(i)&&!this.listings.has(i)){if(t.throwIfNoEntry===!1)return;throw so(`stat '${e}'`)}if(e[e.length-1]==="/"&&!this.listings.has(i))throw No(`stat '${e}'`);return this.statImpl(`stat '${e}'`,i,t)}}async fstatPromise(e,t){return this.fstatSync(e,t)}fstatSync(e,t){let i=this.fds.get(e);if(typeof i=="undefined")throw Ai("fstatSync");let{p:n}=i,s=this.resolveFilename(`stat '${n}'`,n);if(!this.entries.has(s)&&!this.listings.has(s))throw so(`stat '${n}'`);if(n[n.length-1]==="/"&&!this.listings.has(s))throw No(`stat '${n}'`);return this.statImpl(`fstat '${n}'`,s,t)}async lstatPromise(e,t={bigint:!1}){return t.bigint?this.lstatSync(e,{bigint:!0}):this.lstatSync(e)}lstatSync(e,t={bigint:!1,throwIfNoEntry:!0}){let i=this.resolveFilename(`lstat '${e}'`,e,!1,t.throwIfNoEntry);if(i!==void 0){if(!this.entries.has(i)&&!this.listings.has(i)){if(t.throwIfNoEntry===!1)return;throw so(`lstat '${e}'`)}if(e[e.length-1]==="/"&&!this.listings.has(i))throw No(`lstat '${e}'`);return this.statImpl(`lstat '${e}'`,i,t)}}statImpl(e,t,i={}){let n=this.entries.get(t);if(typeof n!="undefined"){let s=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,n,0,0,s)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let a=this.stats.uid,l=this.stats.gid,c=this.libzip.struct.statSize(s)>>>0,u=512,g=Math.ceil(c/u),f=(this.libzip.struct.statMtime(s)>>>0)*1e3,h=f,p=f,m=f,y=new Date(h),b=new Date(p),v=new Date(m),k=new Date(f),T=this.listings.has(t)?Ra:this.isSymbolicLink(n)?Na:Fa,Y=T===Ra?493:420,q=T|this.getUnixMode(n,Y)&511,$=this.libzip.struct.statCrc(s),z=Object.assign(new GA,{uid:a,gid:l,size:c,blksize:u,blocks:g,atime:y,birthtime:b,ctime:v,mtime:k,atimeMs:h,birthtimeMs:p,ctimeMs:m,mtimeMs:f,mode:q,crc:$});return i.bigint===!0?NE(z):z}if(this.listings.has(t)){let s=this.stats.uid,o=this.stats.gid,a=0,l=512,c=0,u=this.stats.mtimeMs,g=this.stats.mtimeMs,f=this.stats.mtimeMs,h=this.stats.mtimeMs,p=new Date(u),m=new Date(g),y=new Date(f),b=new Date(h),v=Ra|493,k=0,T=Object.assign(new GA,{uid:s,gid:o,size:a,blksize:l,blocks:c,atime:p,birthtime:m,ctime:y,mtime:b,atimeMs:u,birthtimeMs:g,ctimeMs:f,mtimeMs:h,mode:v,crc:k});return i.bigint===!0?NE(T):T}throw new Error("Unreachable")}getUnixMode(e,t){if(this.libzip.file.getExternalAttributes(this.zip,e,0,0,this.libzip.uint08S,this.libzip.uint32S)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return this.libzip.getValue(this.libzip.uint08S,"i8")>>>0!==this.libzip.ZIP_OPSYS_UNIX?t:this.libzip.getValue(this.libzip.uint32S,"i32")>>>16}registerListing(e){let t=this.listings.get(e);if(t)return t;this.registerListing(x.dirname(e)).add(x.basename(e));let n=new Set;return this.listings.set(e,n),n}registerEntry(e,t){this.registerListing(x.dirname(e)).add(x.basename(e)),this.entries.set(e,t)}unregisterListing(e){this.listings.delete(e);let t=this.listings.get(x.dirname(e));t==null||t.delete(x.basename(e))}unregisterEntry(e){this.unregisterListing(e);let t=this.entries.get(e);this.entries.delete(e),typeof t!="undefined"&&(this.fileSources.delete(t),this.isSymbolicLink(t)&&this.symlinkCount--)}deleteEntry(e,t){if(this.unregisterEntry(e),this.libzip.delete(this.zip,t)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}resolveFilename(e,t,i=!0,n=!0){if(!this.ready)throw ME(`archive closed, ${e}`);let s=x.resolve(Me.root,t);if(s==="/")return Me.root;let o=this.entries.get(s);if(i&&o!==void 0)if(this.symlinkCount!==0&&this.isSymbolicLink(o)){let a=this.getFileSource(o).toString();return this.resolveFilename(e,x.resolve(x.dirname(s),a),!0,n)}else return s;for(;;){let a=this.resolveFilename(e,x.dirname(s),!0,n);if(a===void 0)return a;let l=this.listings.has(a),c=this.entries.has(a);if(!l&&!c){if(n===!1)return;throw so(e)}if(!l)throw No(e);if(s=x.resolve(a,x.basename(s)),!i||this.symlinkCount===0)break;let u=this.libzip.name.locate(this.zip,s.slice(1));if(u===-1)break;if(this.isSymbolicLink(u)){let g=this.getFileSource(u).toString();s=x.resolve(x.dirname(s),g)}else break}return s}allocateBuffer(e){Buffer.isBuffer(e)||(e=Buffer.from(e));let t=this.libzip.malloc(e.byteLength);if(!t)throw new Error("Couldn't allocate enough memory");return new Uint8Array(this.libzip.HEAPU8.buffer,t,e.byteLength).set(e),{buffer:t,byteLength:e.byteLength}}allocateUnattachedSource(e){let t=this.libzip.struct.errorS(),{buffer:i,byteLength:n}=this.allocateBuffer(e),s=this.libzip.source.fromUnattachedBuffer(i,n,0,!0,t);if(s===0)throw this.libzip.free(t),this.makeLibzipError(t);return s}allocateSource(e){let{buffer:t,byteLength:i}=this.allocateBuffer(e),n=this.libzip.source.fromBuffer(this.zip,t,i,0,!0);if(n===0)throw this.libzip.free(t),this.makeLibzipError(this.libzip.getError(this.zip));return n}setFileSource(e,t){let i=Buffer.isBuffer(t)?t:Buffer.from(t),n=x.relative(Me.root,e),s=this.allocateSource(t);try{let o=this.libzip.file.add(this.zip,n,s,this.libzip.ZIP_FL_OVERWRITE);if(o===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));if(this.level!=="mixed"){let a=this.level===0?this.libzip.ZIP_CM_STORE:this.libzip.ZIP_CM_DEFLATE;if(this.libzip.file.setCompression(this.zip,o,0,a,this.level)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}return this.fileSources.set(o,i),o}catch(o){throw this.libzip.source.free(s),o}}isSymbolicLink(e){if(this.symlinkCount===0)return!1;if(this.libzip.file.getExternalAttributes(this.zip,e,0,0,this.libzip.uint08S,this.libzip.uint32S)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return this.libzip.getValue(this.libzip.uint08S,"i8")>>>0!==this.libzip.ZIP_OPSYS_UNIX?!1:(this.libzip.getValue(this.libzip.uint32S,"i32")>>>16&Vn)===Na}getFileSource(e,t={asyncDecompress:!1}){let i=this.fileSources.get(e);if(typeof i!="undefined")return i;let n=this.libzip.struct.statS();if(this.libzip.statIndex(this.zip,e,0,0,n)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));let o=this.libzip.struct.statCompSize(n),a=this.libzip.struct.statCompMethod(n),l=this.libzip.malloc(o);try{let c=this.libzip.fopenIndex(this.zip,e,0,this.libzip.ZIP_FL_COMPRESSED);if(c===0)throw this.makeLibzipError(this.libzip.getError(this.zip));try{let u=this.libzip.fread(c,l,o,0);if(u===-1)throw this.makeLibzipError(this.libzip.file.getError(c));if(uo)throw new Error("Overread");let g=this.libzip.HEAPU8.subarray(l,l+o),f=Buffer.from(g);if(a===0)return this.fileSources.set(e,f),f;if(t.asyncDecompress)return new Promise((h,p)=>{gQ.default.inflateRaw(f,(m,y)=>{m?p(m):(this.fileSources.set(e,y),h(y))})});{let h=gQ.default.inflateRawSync(f);return this.fileSources.set(e,h),h}}finally{this.libzip.fclose(c)}}finally{this.libzip.free(l)}}async fchmodPromise(e,t){return this.chmodPromise(this.fdToPath(e,"fchmod"),t)}fchmodSync(e,t){return this.chmodSync(this.fdToPath(e,"fchmodSync"),t)}async chmodPromise(e,t){return this.chmodSync(e,t)}chmodSync(e,t){if(this.readOnly)throw In(`chmod '${e}'`);t&=493;let i=this.resolveFilename(`chmod '${e}'`,e,!1),n=this.entries.get(i);if(typeof n=="undefined")throw new Error(`Assertion failed: The entry should have been registered (${i})`);let o=this.getUnixMode(n,Fa|0)&~511|t;if(this.libzip.file.setExternalAttributes(this.zip,n,0,0,this.libzip.ZIP_OPSYS_UNIX,o<<16)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}async chownPromise(e,t,i){return this.chownSync(e,t,i)}chownSync(e,t,i){throw new Error("Unimplemented")}async renamePromise(e,t){return this.renameSync(e,t)}renameSync(e,t){throw new Error("Unimplemented")}async copyFilePromise(e,t,i){let{indexSource:n,indexDest:s,resolvedDestP:o}=this.prepareCopyFile(e,t,i),a=await this.getFileSource(n,{asyncDecompress:!0}),l=this.setFileSource(o,a);l!==s&&this.registerEntry(o,l)}copyFileSync(e,t,i=0){let{indexSource:n,indexDest:s,resolvedDestP:o}=this.prepareCopyFile(e,t,i),a=this.getFileSource(n),l=this.setFileSource(o,a);l!==s&&this.registerEntry(o,l)}prepareCopyFile(e,t,i=0){if(this.readOnly)throw In(`copyfile '${e} -> '${t}'`);if((i&_u.constants.COPYFILE_FICLONE_FORCE)!=0)throw qh("unsupported clone operation",`copyfile '${e}' -> ${t}'`);let n=this.resolveFilename(`copyfile '${e} -> ${t}'`,e),s=this.entries.get(n);if(typeof s=="undefined")throw YA(`copyfile '${e}' -> '${t}'`);let o=this.resolveFilename(`copyfile '${e}' -> ${t}'`,t),a=this.entries.get(o);if((i&(_u.constants.COPYFILE_EXCL|_u.constants.COPYFILE_FICLONE_FORCE))!=0&&typeof a!="undefined")throw UE(`copyfile '${e}' -> '${t}'`);return{indexSource:s,resolvedDestP:o,indexDest:a}}async appendFilePromise(e,t,i){if(this.readOnly)throw In(`open '${e}'`);return typeof i=="undefined"?i={flag:"a"}:typeof i=="string"?i={flag:"a",encoding:i}:typeof i.flag=="undefined"&&(i=N({flag:"a"},i)),this.writeFilePromise(e,t,i)}appendFileSync(e,t,i={}){if(this.readOnly)throw In(`open '${e}'`);return typeof i=="undefined"?i={flag:"a"}:typeof i=="string"?i={flag:"a",encoding:i}:typeof i.flag=="undefined"&&(i=N({flag:"a"},i)),this.writeFileSync(e,t,i)}fdToPath(e,t){var n;let i=(n=this.fds.get(e))==null?void 0:n.p;if(typeof i=="undefined")throw Ai(t);return i}async writeFilePromise(e,t,i){let{encoding:n,mode:s,index:o,resolvedP:a}=this.prepareWriteFile(e,i);o!==void 0&&typeof i=="object"&&i.flag&&i.flag.includes("a")&&(t=Buffer.concat([await this.getFileSource(o,{asyncDecompress:!0}),Buffer.from(t)])),n!==null&&(t=t.toString(n));let l=this.setFileSource(a,t);l!==o&&this.registerEntry(a,l),s!==null&&await this.chmodPromise(a,s)}writeFileSync(e,t,i){let{encoding:n,mode:s,index:o,resolvedP:a}=this.prepareWriteFile(e,i);o!==void 0&&typeof i=="object"&&i.flag&&i.flag.includes("a")&&(t=Buffer.concat([this.getFileSource(o),Buffer.from(t)])),n!==null&&(t=t.toString(n));let l=this.setFileSource(a,t);l!==o&&this.registerEntry(a,l),s!==null&&this.chmodSync(a,s)}prepareWriteFile(e,t){if(typeof e=="number"&&(e=this.fdToPath(e,"read")),this.readOnly)throw In(`open '${e}'`);let i=this.resolveFilename(`open '${e}'`,e);if(this.listings.has(i))throw Jh(`open '${e}'`);let n=null,s=null;typeof t=="string"?n=t:typeof t=="object"&&({encoding:n=null,mode:s=null}=t);let o=this.entries.get(i);return{encoding:n,mode:s,resolvedP:i,index:o}}async unlinkPromise(e){return this.unlinkSync(e)}unlinkSync(e){if(this.readOnly)throw In(`unlink '${e}'`);let t=this.resolveFilename(`unlink '${e}'`,e);if(this.listings.has(t))throw Jh(`unlink '${e}'`);let i=this.entries.get(t);if(typeof i=="undefined")throw YA(`unlink '${e}'`);this.deleteEntry(t,i)}async utimesPromise(e,t,i){return this.utimesSync(e,t,i)}utimesSync(e,t,i){if(this.readOnly)throw In(`utimes '${e}'`);let n=this.resolveFilename(`utimes '${e}'`,e);this.utimesImpl(n,i)}async lutimesPromise(e,t,i){return this.lutimesSync(e,t,i)}lutimesSync(e,t,i){if(this.readOnly)throw In(`lutimes '${e}'`);let n=this.resolveFilename(`utimes '${e}'`,e,!1);this.utimesImpl(n,i)}utimesImpl(e,t){this.listings.has(e)&&(this.entries.has(e)||this.hydrateDirectory(e));let i=this.entries.get(e);if(i===void 0)throw new Error("Unreachable");if(this.libzip.file.setMtime(this.zip,i,0,vge(t),0)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip))}async mkdirPromise(e,t){return this.mkdirSync(e,t)}mkdirSync(e,{mode:t=493,recursive:i=!1}={}){if(i)return this.mkdirpSync(e,{chmod:t});if(this.readOnly)throw In(`mkdir '${e}'`);let n=this.resolveFilename(`mkdir '${e}'`,e);if(this.entries.has(n)||this.listings.has(n))throw UE(`mkdir '${e}'`);this.hydrateDirectory(n),this.chmodSync(n,t)}async rmdirPromise(e,t){return this.rmdirSync(e,t)}rmdirSync(e,{recursive:t=!1}={}){if(this.readOnly)throw In(`rmdir '${e}'`);if(t){this.removeSync(e);return}let i=this.resolveFilename(`rmdir '${e}'`,e),n=this.listings.get(i);if(!n)throw No(`rmdir '${e}'`);if(n.size>0)throw zO(`rmdir '${e}'`);let s=this.entries.get(i);if(typeof s=="undefined")throw YA(`rmdir '${e}'`);this.deleteEntry(e,s)}hydrateDirectory(e){let t=this.libzip.dir.add(this.zip,x.relative(Me.root,e));if(t===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));return this.registerListing(e),this.registerEntry(e,t),t}async linkPromise(e,t){return this.linkSync(e,t)}linkSync(e,t){throw _O(`link '${e}' -> '${t}'`)}async symlinkPromise(e,t){return this.symlinkSync(e,t)}symlinkSync(e,t){if(this.readOnly)throw In(`symlink '${e}' -> '${t}'`);let i=this.resolveFilename(`symlink '${e}' -> '${t}'`,t);if(this.listings.has(i))throw Jh(`symlink '${e}' -> '${t}'`);if(this.entries.has(i))throw UE(`symlink '${e}' -> '${t}'`);let n=this.setFileSource(i,e);if(this.registerEntry(i,n),this.libzip.file.setExternalAttributes(this.zip,n,0,0,this.libzip.ZIP_OPSYS_UNIX,(Na|511)<<16)===-1)throw this.makeLibzipError(this.libzip.getError(this.zip));this.symlinkCount+=1}async readFilePromise(e,t){typeof t=="object"&&(t=t?t.encoding:void 0);let i=await this.readFileBuffer(e,{asyncDecompress:!0});return t?i.toString(t):i}readFileSync(e,t){typeof t=="object"&&(t=t?t.encoding:void 0);let i=this.readFileBuffer(e);return t?i.toString(t):i}readFileBuffer(e,t={asyncDecompress:!1}){typeof e=="number"&&(e=this.fdToPath(e,"read"));let i=this.resolveFilename(`open '${e}'`,e);if(!this.entries.has(i)&&!this.listings.has(i))throw so(`open '${e}'`);if(e[e.length-1]==="/"&&!this.listings.has(i))throw No(`open '${e}'`);if(this.listings.has(i))throw Jh("read");let n=this.entries.get(i);if(n===void 0)throw new Error("Unreachable");return this.getFileSource(n,t)}async readdirPromise(e,t){return this.readdirSync(e,t)}readdirSync(e,t){let i=this.resolveFilename(`scandir '${e}'`,e);if(!this.entries.has(i)&&!this.listings.has(i))throw so(`scandir '${e}'`);let n=this.listings.get(i);if(!n)throw No(`scandir '${e}'`);let s=[...n];return(t==null?void 0:t.withFileTypes)?s.map(o=>Object.assign(this.statImpl("lstat",x.join(e,o)),{name:o})):s}async readlinkPromise(e){let t=this.prepareReadlink(e);return(await this.getFileSource(t,{asyncDecompress:!0})).toString()}readlinkSync(e){let t=this.prepareReadlink(e);return this.getFileSource(t).toString()}prepareReadlink(e){let t=this.resolveFilename(`readlink '${e}'`,e,!1);if(!this.entries.has(t)&&!this.listings.has(t))throw so(`readlink '${e}'`);if(e[e.length-1]==="/"&&!this.listings.has(t))throw No(`open '${e}'`);if(this.listings.has(t))throw YA(`readlink '${e}'`);let i=this.entries.get(t);if(i===void 0)throw new Error("Unreachable");if(!this.isSymbolicLink(i))throw YA(`readlink '${e}'`);return i}async truncatePromise(e,t=0){let i=this.resolveFilename(`open '${e}'`,e),n=this.entries.get(i);if(typeof n=="undefined")throw YA(`open '${e}'`);let s=await this.getFileSource(n,{asyncDecompress:!0}),o=Buffer.alloc(t,0);return s.copy(o),await this.writeFilePromise(e,o)}truncateSync(e,t=0){let i=this.resolveFilename(`open '${e}'`,e),n=this.entries.get(i);if(typeof n=="undefined")throw YA(`open '${e}'`);let s=this.getFileSource(n),o=Buffer.alloc(t,0);return s.copy(o),this.writeFileSync(e,o)}async ftruncatePromise(e,t){return this.truncatePromise(this.fdToPath(e,"ftruncate"),t)}ftruncateSync(e,t){return this.truncateSync(this.fdToPath(e,"ftruncateSync"),t)}watch(e,t,i){let n;switch(typeof t){case"function":case"string":case"undefined":n=!0;break;default:({persistent:n=!0}=t);break}if(!n)return{on:()=>{},close:()=>{}};let s=setInterval(()=>{},24*60*60*1e3);return{on:()=>{},close:()=>{clearInterval(s)}}}watchFile(e,t,i){let n=x.resolve(Me.root,e);return jE(this,n,t,i)}unwatchFile(e,t){let i=x.resolve(Me.root,e);return zh(this,i,t)}};var Qi=class extends qA{getExtractHint(e){return this.baseFs.getExtractHint(e)}resolve(e){return this.mapFromBase(this.baseFs.resolve(this.mapToBase(e)))}getRealPath(){return this.mapFromBase(this.baseFs.getRealPath())}async openPromise(e,t,i){return this.baseFs.openPromise(this.mapToBase(e),t,i)}openSync(e,t,i){return this.baseFs.openSync(this.mapToBase(e),t,i)}async opendirPromise(e,t){return Object.assign(await this.baseFs.opendirPromise(this.mapToBase(e),t),{path:e})}opendirSync(e,t){return Object.assign(this.baseFs.opendirSync(this.mapToBase(e),t),{path:e})}async readPromise(e,t,i,n,s){return await this.baseFs.readPromise(e,t,i,n,s)}readSync(e,t,i,n,s){return this.baseFs.readSync(e,t,i,n,s)}async writePromise(e,t,i,n,s){return typeof t=="string"?await this.baseFs.writePromise(e,t,i):await this.baseFs.writePromise(e,t,i,n,s)}writeSync(e,t,i,n,s){return typeof t=="string"?this.baseFs.writeSync(e,t,i):this.baseFs.writeSync(e,t,i,n,s)}async closePromise(e){return this.baseFs.closePromise(e)}closeSync(e){this.baseFs.closeSync(e)}createReadStream(e,t){return this.baseFs.createReadStream(e!==null?this.mapToBase(e):e,t)}createWriteStream(e,t){return this.baseFs.createWriteStream(e!==null?this.mapToBase(e):e,t)}async realpathPromise(e){return this.mapFromBase(await this.baseFs.realpathPromise(this.mapToBase(e)))}realpathSync(e){return this.mapFromBase(this.baseFs.realpathSync(this.mapToBase(e)))}async existsPromise(e){return this.baseFs.existsPromise(this.mapToBase(e))}existsSync(e){return this.baseFs.existsSync(this.mapToBase(e))}accessSync(e,t){return this.baseFs.accessSync(this.mapToBase(e),t)}async accessPromise(e,t){return this.baseFs.accessPromise(this.mapToBase(e),t)}async statPromise(e,t){return this.baseFs.statPromise(this.mapToBase(e),t)}statSync(e,t){return this.baseFs.statSync(this.mapToBase(e),t)}async fstatPromise(e,t){return this.baseFs.fstatPromise(e,t)}fstatSync(e,t){return this.baseFs.fstatSync(e,t)}lstatPromise(e,t){return this.baseFs.lstatPromise(this.mapToBase(e),t)}lstatSync(e,t){return this.baseFs.lstatSync(this.mapToBase(e),t)}async fchmodPromise(e,t){return this.baseFs.fchmodPromise(e,t)}fchmodSync(e,t){return this.baseFs.fchmodSync(e,t)}async chmodPromise(e,t){return this.baseFs.chmodPromise(this.mapToBase(e),t)}chmodSync(e,t){return this.baseFs.chmodSync(this.mapToBase(e),t)}async chownPromise(e,t,i){return this.baseFs.chownPromise(this.mapToBase(e),t,i)}chownSync(e,t,i){return this.baseFs.chownSync(this.mapToBase(e),t,i)}async renamePromise(e,t){return this.baseFs.renamePromise(this.mapToBase(e),this.mapToBase(t))}renameSync(e,t){return this.baseFs.renameSync(this.mapToBase(e),this.mapToBase(t))}async copyFilePromise(e,t,i=0){return this.baseFs.copyFilePromise(this.mapToBase(e),this.mapToBase(t),i)}copyFileSync(e,t,i=0){return this.baseFs.copyFileSync(this.mapToBase(e),this.mapToBase(t),i)}async appendFilePromise(e,t,i){return this.baseFs.appendFilePromise(this.fsMapToBase(e),t,i)}appendFileSync(e,t,i){return this.baseFs.appendFileSync(this.fsMapToBase(e),t,i)}async writeFilePromise(e,t,i){return this.baseFs.writeFilePromise(this.fsMapToBase(e),t,i)}writeFileSync(e,t,i){return this.baseFs.writeFileSync(this.fsMapToBase(e),t,i)}async unlinkPromise(e){return this.baseFs.unlinkPromise(this.mapToBase(e))}unlinkSync(e){return this.baseFs.unlinkSync(this.mapToBase(e))}async utimesPromise(e,t,i){return this.baseFs.utimesPromise(this.mapToBase(e),t,i)}utimesSync(e,t,i){return this.baseFs.utimesSync(this.mapToBase(e),t,i)}async mkdirPromise(e,t){return this.baseFs.mkdirPromise(this.mapToBase(e),t)}mkdirSync(e,t){return this.baseFs.mkdirSync(this.mapToBase(e),t)}async rmdirPromise(e,t){return this.baseFs.rmdirPromise(this.mapToBase(e),t)}rmdirSync(e,t){return this.baseFs.rmdirSync(this.mapToBase(e),t)}async linkPromise(e,t){return this.baseFs.linkPromise(this.mapToBase(e),this.mapToBase(t))}linkSync(e,t){return this.baseFs.linkSync(this.mapToBase(e),this.mapToBase(t))}async symlinkPromise(e,t,i){let n=this.mapToBase(t);if(this.pathUtils.isAbsolute(e))return this.baseFs.symlinkPromise(this.mapToBase(e),n,i);let s=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(t),e)),o=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(n),s);return this.baseFs.symlinkPromise(o,n,i)}symlinkSync(e,t,i){let n=this.mapToBase(t);if(this.pathUtils.isAbsolute(e))return this.baseFs.symlinkSync(this.mapToBase(e),n,i);let s=this.mapToBase(this.pathUtils.join(this.pathUtils.dirname(t),e)),o=this.baseFs.pathUtils.relative(this.baseFs.pathUtils.dirname(n),s);return this.baseFs.symlinkSync(o,n,i)}async readFilePromise(e,t){return t==="utf8"?this.baseFs.readFilePromise(this.fsMapToBase(e),t):this.baseFs.readFilePromise(this.fsMapToBase(e),t)}readFileSync(e,t){return t==="utf8"?this.baseFs.readFileSync(this.fsMapToBase(e),t):this.baseFs.readFileSync(this.fsMapToBase(e),t)}async readdirPromise(e,t){return this.baseFs.readdirPromise(this.mapToBase(e),t)}readdirSync(e,t){return this.baseFs.readdirSync(this.mapToBase(e),t)}async readlinkPromise(e){return this.mapFromBase(await this.baseFs.readlinkPromise(this.mapToBase(e)))}readlinkSync(e){return this.mapFromBase(this.baseFs.readlinkSync(this.mapToBase(e)))}async truncatePromise(e,t){return this.baseFs.truncatePromise(this.mapToBase(e),t)}truncateSync(e,t){return this.baseFs.truncateSync(this.mapToBase(e),t)}async ftruncatePromise(e,t){return this.baseFs.ftruncatePromise(e,t)}ftruncateSync(e,t){return this.baseFs.ftruncateSync(e,t)}watch(e,t,i){return this.baseFs.watch(this.mapToBase(e),t,i)}watchFile(e,t,i){return this.baseFs.watchFile(this.mapToBase(e),t,i)}unwatchFile(e,t){return this.baseFs.unwatchFile(this.mapToBase(e),t)}fsMapToBase(e){return typeof e=="number"?e:this.mapToBase(e)}};var La=class extends Qi{constructor(e,{baseFs:t,pathUtils:i}){super(i);this.target=e,this.baseFs=t}getRealPath(){return this.target}getBaseFs(){return this.baseFs}mapFromBase(e){return e}mapToBase(e){return e}};var _t=class extends Qi{constructor(e,{baseFs:t=new ar}={}){super(x);this.target=this.pathUtils.normalize(e),this.baseFs=t}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.target)}resolve(e){return this.pathUtils.isAbsolute(e)?x.normalize(e):this.baseFs.resolve(x.join(this.target,e))}mapFromBase(e){return e}mapToBase(e){return this.pathUtils.isAbsolute(e)?e:this.pathUtils.join(this.target,e)}};var nM=Me.root,Ta=class extends Qi{constructor(e,{baseFs:t=new ar}={}){super(x);this.target=this.pathUtils.resolve(Me.root,e),this.baseFs=t}getRealPath(){return this.pathUtils.resolve(this.baseFs.getRealPath(),this.pathUtils.relative(Me.root,this.target))}getTarget(){return this.target}getBaseFs(){return this.baseFs}mapToBase(e){let t=this.pathUtils.normalize(e);if(this.pathUtils.isAbsolute(e))return this.pathUtils.resolve(this.target,this.pathUtils.relative(nM,e));if(t.match(/^\.\.\/?/))throw new Error(`Resolving this path (${e}) would escape the jail`);return this.pathUtils.resolve(this.target,e)}mapFromBase(e){return this.pathUtils.resolve(nM,this.pathUtils.relative(this.target,e))}};var Vh=class extends Qi{constructor(e,t){super(t);this.instance=null;this.factory=e}get baseFs(){return this.instance||(this.instance=this.factory()),this.instance}set baseFs(e){this.instance=e}mapFromBase(e){return e}mapToBase(e){return e}};var et=()=>Object.assign(new Error("ENOSYS: unsupported filesystem access"),{code:"ENOSYS"}),fQ=class extends qA{constructor(){super(x)}getExtractHint(){throw et()}getRealPath(){throw et()}resolve(){throw et()}async openPromise(){throw et()}openSync(){throw et()}async opendirPromise(){throw et()}opendirSync(){throw et()}async readPromise(){throw et()}readSync(){throw et()}async writePromise(){throw et()}writeSync(){throw et()}async closePromise(){throw et()}closeSync(){throw et()}createWriteStream(){throw et()}createReadStream(){throw et()}async realpathPromise(){throw et()}realpathSync(){throw et()}async readdirPromise(){throw et()}readdirSync(){throw et()}async existsPromise(e){throw et()}existsSync(e){throw et()}async accessPromise(){throw et()}accessSync(){throw et()}async statPromise(){throw et()}statSync(){throw et()}async fstatPromise(e){throw et()}fstatSync(e){throw et()}async lstatPromise(e){throw et()}lstatSync(e){throw et()}async fchmodPromise(){throw et()}fchmodSync(){throw et()}async chmodPromise(){throw et()}chmodSync(){throw et()}async chownPromise(){throw et()}chownSync(){throw et()}async mkdirPromise(){throw et()}mkdirSync(){throw et()}async rmdirPromise(){throw et()}rmdirSync(){throw et()}async linkPromise(){throw et()}linkSync(){throw et()}async symlinkPromise(){throw et()}symlinkSync(){throw et()}async renamePromise(){throw et()}renameSync(){throw et()}async copyFilePromise(){throw et()}copyFileSync(){throw et()}async appendFilePromise(){throw et()}appendFileSync(){throw et()}async writeFilePromise(){throw et()}writeFileSync(){throw et()}async unlinkPromise(){throw et()}unlinkSync(){throw et()}async utimesPromise(){throw et()}utimesSync(){throw et()}async readFilePromise(){throw et()}readFileSync(){throw et()}async readlinkPromise(){throw et()}readlinkSync(){throw et()}async truncatePromise(){throw et()}truncateSync(){throw et()}async ftruncatePromise(e,t){throw et()}ftruncateSync(e,t){throw et()}watch(){throw et()}watchFile(){throw et()}unwatchFile(){throw et()}},GE=fQ;GE.instance=new fQ;var Xh=class extends Qi{constructor(e){super(H);this.baseFs=e}mapFromBase(e){return H.fromPortablePath(e)}mapToBase(e){return H.toPortablePath(e)}};var kge=/^[0-9]+$/,hQ=/^(\/(?:[^/]+\/)*?(?:\$\$virtual|__virtual__))((?:\/((?:[^/]+-)?[a-f0-9]+)(?:\/([^/]+))?)?((?:\/.*)?))$/,xge=/^([^/]+-)?[a-f0-9]+$/,Wr=class extends Qi{static makeVirtualPath(e,t,i){if(x.basename(e)!=="__virtual__")throw new Error('Assertion failed: Virtual folders must be named "__virtual__"');if(!x.basename(t).match(xge))throw new Error("Assertion failed: Virtual components must be ended by an hexadecimal hash");let s=x.relative(x.dirname(e),i).split("/"),o=0;for(;o{let t=r.indexOf(e);if(t<=0)return null;let i=t;for(;t>=0&&(i=t+e.length,r[i]!==x.sep);){if(r[t-1]===x.sep)return null;t=r.indexOf(e,i)}return r.length>i&&r[i]!==x.sep?null:r.slice(0,i)},ys=class extends ac{constructor({libzip:e,baseFs:t=new ar,filter:i=null,maxOpenFiles:n=Infinity,readOnlyArchives:s=!1,useCache:o=!0,maxAge:a=5e3,fileExtensions:l=null}){super();this.fdMap=new Map;this.nextFd=3;this.isZip=new Set;this.notZip=new Set;this.realPaths=new Map;this.limitOpenFilesTimeout=null;this.libzipFactory=typeof e!="function"?()=>e:e,this.baseFs=t,this.zipInstances=o?new Map:null,this.filter=i,this.maxOpenFiles=n,this.readOnlyArchives=s,this.maxAge=a,this.fileExtensions=l}static async openPromise(e,t){let i=new ys(t);try{return await e(i)}finally{i.saveAndClose()}}get libzip(){return typeof this.libzipInstance=="undefined"&&(this.libzipInstance=this.libzipFactory()),this.libzipInstance}getExtractHint(e){return this.baseFs.getExtractHint(e)}getRealPath(){return this.baseFs.getRealPath()}saveAndClose(){if(_h(this),this.zipInstances)for(let[e,{zipFs:t}]of this.zipInstances.entries())t.saveAndClose(),this.zipInstances.delete(e)}discardAndClose(){if(_h(this),this.zipInstances)for(let[e,{zipFs:t}]of this.zipInstances.entries())t.discardAndClose(),this.zipInstances.delete(e)}resolve(e){return this.baseFs.resolve(e)}remapFd(e,t){let i=this.nextFd++|Xn;return this.fdMap.set(i,[e,t]),i}async openPromise(e,t,i){return await this.makeCallPromise(e,async()=>await this.baseFs.openPromise(e,t,i),async(n,{subPath:s})=>this.remapFd(n,await n.openPromise(s,t,i)))}openSync(e,t,i){return this.makeCallSync(e,()=>this.baseFs.openSync(e,t,i),(n,{subPath:s})=>this.remapFd(n,n.openSync(s,t,i)))}async opendirPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.opendirPromise(e,t),async(i,{subPath:n})=>await i.opendirPromise(n,t),{requireSubpath:!1})}opendirSync(e,t){return this.makeCallSync(e,()=>this.baseFs.opendirSync(e,t),(i,{subPath:n})=>i.opendirSync(n,t),{requireSubpath:!1})}async readPromise(e,t,i,n,s){if((e&Is)!==Xn)return await this.baseFs.readPromise(e,t,i,n,s);let o=this.fdMap.get(e);if(typeof o=="undefined")throw Ai("read");let[a,l]=o;return await a.readPromise(l,t,i,n,s)}readSync(e,t,i,n,s){if((e&Is)!==Xn)return this.baseFs.readSync(e,t,i,n,s);let o=this.fdMap.get(e);if(typeof o=="undefined")throw Ai("readSync");let[a,l]=o;return a.readSync(l,t,i,n,s)}async writePromise(e,t,i,n,s){if((e&Is)!==Xn)return typeof t=="string"?await this.baseFs.writePromise(e,t,i):await this.baseFs.writePromise(e,t,i,n,s);let o=this.fdMap.get(e);if(typeof o=="undefined")throw Ai("write");let[a,l]=o;return typeof t=="string"?await a.writePromise(l,t,i):await a.writePromise(l,t,i,n,s)}writeSync(e,t,i,n,s){if((e&Is)!==Xn)return typeof t=="string"?this.baseFs.writeSync(e,t,i):this.baseFs.writeSync(e,t,i,n,s);let o=this.fdMap.get(e);if(typeof o=="undefined")throw Ai("writeSync");let[a,l]=o;return typeof t=="string"?a.writeSync(l,t,i):a.writeSync(l,t,i,n,s)}async closePromise(e){if((e&Is)!==Xn)return await this.baseFs.closePromise(e);let t=this.fdMap.get(e);if(typeof t=="undefined")throw Ai("close");this.fdMap.delete(e);let[i,n]=t;return await i.closePromise(n)}closeSync(e){if((e&Is)!==Xn)return this.baseFs.closeSync(e);let t=this.fdMap.get(e);if(typeof t=="undefined")throw Ai("closeSync");this.fdMap.delete(e);let[i,n]=t;return i.closeSync(n)}createReadStream(e,t){return e===null?this.baseFs.createReadStream(e,t):this.makeCallSync(e,()=>this.baseFs.createReadStream(e,t),(i,{archivePath:n,subPath:s})=>{let o=i.createReadStream(s,t);return o.path=H.fromPortablePath(this.pathUtils.join(n,s)),o})}createWriteStream(e,t){return e===null?this.baseFs.createWriteStream(e,t):this.makeCallSync(e,()=>this.baseFs.createWriteStream(e,t),(i,{subPath:n})=>i.createWriteStream(n,t))}async realpathPromise(e){return await this.makeCallPromise(e,async()=>await this.baseFs.realpathPromise(e),async(t,{archivePath:i,subPath:n})=>{let s=this.realPaths.get(i);return typeof s=="undefined"&&(s=await this.baseFs.realpathPromise(i),this.realPaths.set(i,s)),this.pathUtils.join(s,this.pathUtils.relative(Me.root,await t.realpathPromise(n)))})}realpathSync(e){return this.makeCallSync(e,()=>this.baseFs.realpathSync(e),(t,{archivePath:i,subPath:n})=>{let s=this.realPaths.get(i);return typeof s=="undefined"&&(s=this.baseFs.realpathSync(i),this.realPaths.set(i,s)),this.pathUtils.join(s,this.pathUtils.relative(Me.root,t.realpathSync(n)))})}async existsPromise(e){return await this.makeCallPromise(e,async()=>await this.baseFs.existsPromise(e),async(t,{subPath:i})=>await t.existsPromise(i))}existsSync(e){return this.makeCallSync(e,()=>this.baseFs.existsSync(e),(t,{subPath:i})=>t.existsSync(i))}async accessPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.accessPromise(e,t),async(i,{subPath:n})=>await i.accessPromise(n,t))}accessSync(e,t){return this.makeCallSync(e,()=>this.baseFs.accessSync(e,t),(i,{subPath:n})=>i.accessSync(n,t))}async statPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.statPromise(e,t),async(i,{subPath:n})=>await i.statPromise(n,t))}statSync(e,t){return this.makeCallSync(e,()=>this.baseFs.statSync(e,t),(i,{subPath:n})=>i.statSync(n,t))}async fstatPromise(e,t){if((e&Is)!==Xn)return this.baseFs.fstatPromise(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("fstat");let[n,s]=i;return n.fstatPromise(s,t)}fstatSync(e,t){if((e&Is)!==Xn)return this.baseFs.fstatSync(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("fstatSync");let[n,s]=i;return n.fstatSync(s,t)}async lstatPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.lstatPromise(e,t),async(i,{subPath:n})=>await i.lstatPromise(n,t))}lstatSync(e,t){return this.makeCallSync(e,()=>this.baseFs.lstatSync(e,t),(i,{subPath:n})=>i.lstatSync(n,t))}async fchmodPromise(e,t){if((e&Is)!==Xn)return this.baseFs.fchmodPromise(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("fchmod");let[n,s]=i;return n.fchmodPromise(s,t)}fchmodSync(e,t){if((e&Is)!==Xn)return this.baseFs.fchmodSync(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("fchmodSync");let[n,s]=i;return n.fchmodSync(s,t)}async chmodPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.chmodPromise(e,t),async(i,{subPath:n})=>await i.chmodPromise(n,t))}chmodSync(e,t){return this.makeCallSync(e,()=>this.baseFs.chmodSync(e,t),(i,{subPath:n})=>i.chmodSync(n,t))}async chownPromise(e,t,i){return await this.makeCallPromise(e,async()=>await this.baseFs.chownPromise(e,t,i),async(n,{subPath:s})=>await n.chownPromise(s,t,i))}chownSync(e,t,i){return this.makeCallSync(e,()=>this.baseFs.chownSync(e,t,i),(n,{subPath:s})=>n.chownSync(s,t,i))}async renamePromise(e,t){return await this.makeCallPromise(e,async()=>await this.makeCallPromise(t,async()=>await this.baseFs.renamePromise(e,t),async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),async(i,{subPath:n})=>await this.makeCallPromise(t,async()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},async(s,{subPath:o})=>{if(i!==s)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return await i.renamePromise(n,o)}))}renameSync(e,t){return this.makeCallSync(e,()=>this.makeCallSync(t,()=>this.baseFs.renameSync(e,t),()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})}),(i,{subPath:n})=>this.makeCallSync(t,()=>{throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"})},(s,{subPath:o})=>{if(i!==s)throw Object.assign(new Error("EEXDEV: cross-device link not permitted"),{code:"EEXDEV"});return i.renameSync(n,o)}))}async copyFilePromise(e,t,i=0){let n=async(s,o,a,l)=>{if((i&Zh.constants.COPYFILE_FICLONE_FORCE)!=0)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${o}' -> ${l}'`),{code:"EXDEV"});if(i&Zh.constants.COPYFILE_EXCL&&await this.existsPromise(o))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${o}' -> '${l}'`),{code:"EEXIST"});let c;try{c=await s.readFilePromise(o)}catch(u){throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${o}' -> '${l}'`),{code:"EINVAL"})}await a.writeFilePromise(l,c)};return await this.makeCallPromise(e,async()=>await this.makeCallPromise(t,async()=>await this.baseFs.copyFilePromise(e,t,i),async(s,{subPath:o})=>await n(this.baseFs,e,s,o)),async(s,{subPath:o})=>await this.makeCallPromise(t,async()=>await n(s,o,this.baseFs,t),async(a,{subPath:l})=>s!==a?await n(s,o,a,l):await s.copyFilePromise(o,l,i)))}copyFileSync(e,t,i=0){let n=(s,o,a,l)=>{if((i&Zh.constants.COPYFILE_FICLONE_FORCE)!=0)throw Object.assign(new Error(`EXDEV: cross-device clone not permitted, copyfile '${o}' -> ${l}'`),{code:"EXDEV"});if(i&Zh.constants.COPYFILE_EXCL&&this.existsSync(o))throw Object.assign(new Error(`EEXIST: file already exists, copyfile '${o}' -> '${l}'`),{code:"EEXIST"});let c;try{c=s.readFileSync(o)}catch(u){throw Object.assign(new Error(`EINVAL: invalid argument, copyfile '${o}' -> '${l}'`),{code:"EINVAL"})}a.writeFileSync(l,c)};return this.makeCallSync(e,()=>this.makeCallSync(t,()=>this.baseFs.copyFileSync(e,t,i),(s,{subPath:o})=>n(this.baseFs,e,s,o)),(s,{subPath:o})=>this.makeCallSync(t,()=>n(s,o,this.baseFs,t),(a,{subPath:l})=>s!==a?n(s,o,a,l):s.copyFileSync(o,l,i)))}async appendFilePromise(e,t,i){return await this.makeCallPromise(e,async()=>await this.baseFs.appendFilePromise(e,t,i),async(n,{subPath:s})=>await n.appendFilePromise(s,t,i))}appendFileSync(e,t,i){return this.makeCallSync(e,()=>this.baseFs.appendFileSync(e,t,i),(n,{subPath:s})=>n.appendFileSync(s,t,i))}async writeFilePromise(e,t,i){return await this.makeCallPromise(e,async()=>await this.baseFs.writeFilePromise(e,t,i),async(n,{subPath:s})=>await n.writeFilePromise(s,t,i))}writeFileSync(e,t,i){return this.makeCallSync(e,()=>this.baseFs.writeFileSync(e,t,i),(n,{subPath:s})=>n.writeFileSync(s,t,i))}async unlinkPromise(e){return await this.makeCallPromise(e,async()=>await this.baseFs.unlinkPromise(e),async(t,{subPath:i})=>await t.unlinkPromise(i))}unlinkSync(e){return this.makeCallSync(e,()=>this.baseFs.unlinkSync(e),(t,{subPath:i})=>t.unlinkSync(i))}async utimesPromise(e,t,i){return await this.makeCallPromise(e,async()=>await this.baseFs.utimesPromise(e,t,i),async(n,{subPath:s})=>await n.utimesPromise(s,t,i))}utimesSync(e,t,i){return this.makeCallSync(e,()=>this.baseFs.utimesSync(e,t,i),(n,{subPath:s})=>n.utimesSync(s,t,i))}async mkdirPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.mkdirPromise(e,t),async(i,{subPath:n})=>await i.mkdirPromise(n,t))}mkdirSync(e,t){return this.makeCallSync(e,()=>this.baseFs.mkdirSync(e,t),(i,{subPath:n})=>i.mkdirSync(n,t))}async rmdirPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.rmdirPromise(e,t),async(i,{subPath:n})=>await i.rmdirPromise(n,t))}rmdirSync(e,t){return this.makeCallSync(e,()=>this.baseFs.rmdirSync(e,t),(i,{subPath:n})=>i.rmdirSync(n,t))}async linkPromise(e,t){return await this.makeCallPromise(t,async()=>await this.baseFs.linkPromise(e,t),async(i,{subPath:n})=>await i.linkPromise(e,n))}linkSync(e,t){return this.makeCallSync(t,()=>this.baseFs.linkSync(e,t),(i,{subPath:n})=>i.linkSync(e,n))}async symlinkPromise(e,t,i){return await this.makeCallPromise(t,async()=>await this.baseFs.symlinkPromise(e,t,i),async(n,{subPath:s})=>await n.symlinkPromise(e,s))}symlinkSync(e,t,i){return this.makeCallSync(t,()=>this.baseFs.symlinkSync(e,t,i),(n,{subPath:s})=>n.symlinkSync(e,s))}async readFilePromise(e,t){return this.makeCallPromise(e,async()=>{switch(t){case"utf8":return await this.baseFs.readFilePromise(e,t);default:return await this.baseFs.readFilePromise(e,t)}},async(i,{subPath:n})=>await i.readFilePromise(n,t))}readFileSync(e,t){return this.makeCallSync(e,()=>{switch(t){case"utf8":return this.baseFs.readFileSync(e,t);default:return this.baseFs.readFileSync(e,t)}},(i,{subPath:n})=>i.readFileSync(n,t))}async readdirPromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.readdirPromise(e,t),async(i,{subPath:n})=>await i.readdirPromise(n,t),{requireSubpath:!1})}readdirSync(e,t){return this.makeCallSync(e,()=>this.baseFs.readdirSync(e,t),(i,{subPath:n})=>i.readdirSync(n,t),{requireSubpath:!1})}async readlinkPromise(e){return await this.makeCallPromise(e,async()=>await this.baseFs.readlinkPromise(e),async(t,{subPath:i})=>await t.readlinkPromise(i))}readlinkSync(e){return this.makeCallSync(e,()=>this.baseFs.readlinkSync(e),(t,{subPath:i})=>t.readlinkSync(i))}async truncatePromise(e,t){return await this.makeCallPromise(e,async()=>await this.baseFs.truncatePromise(e,t),async(i,{subPath:n})=>await i.truncatePromise(n,t))}truncateSync(e,t){return this.makeCallSync(e,()=>this.baseFs.truncateSync(e,t),(i,{subPath:n})=>i.truncateSync(n,t))}async ftruncatePromise(e,t){if((e&Is)!==Xn)return this.baseFs.ftruncatePromise(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("ftruncate");let[n,s]=i;return n.ftruncatePromise(s,t)}ftruncateSync(e,t){if((e&Is)!==Xn)return this.baseFs.ftruncateSync(e,t);let i=this.fdMap.get(e);if(typeof i=="undefined")throw Ai("ftruncateSync");let[n,s]=i;return n.ftruncateSync(s,t)}watch(e,t,i){return this.makeCallSync(e,()=>this.baseFs.watch(e,t,i),(n,{subPath:s})=>n.watch(s,t,i))}watchFile(e,t,i){return this.makeCallSync(e,()=>this.baseFs.watchFile(e,t,i),()=>jE(this,e,t,i))}unwatchFile(e,t){return this.makeCallSync(e,()=>this.baseFs.unwatchFile(e,t),()=>zh(this,e,t))}async makeCallPromise(e,t,i,{requireSubpath:n=!0}={}){if(typeof e!="string")return await t();let s=this.resolve(e),o=this.findZip(s);return o?n&&o.subPath==="/"?await t():await this.getZipPromise(o.archivePath,async a=>await i(a,o)):await t()}makeCallSync(e,t,i,{requireSubpath:n=!0}={}){if(typeof e!="string")return t();let s=this.resolve(e),o=this.findZip(s);return!o||n&&o.subPath==="/"?t():this.getZipSync(o.archivePath,a=>i(a,o))}findZip(e){if(this.filter&&!this.filter.test(e))return null;let t="";for(;;){let i=e.substring(t.length),n;if(!this.fileExtensions)n=sM(i,".zip");else for(let s of this.fileExtensions)if(n=sM(i,s),n)break;if(!n)return null;if(t=this.pathUtils.join(t,n),this.isZip.has(t)===!1){if(this.notZip.has(t))continue;try{if(!this.baseFs.lstatSync(t).isFile()){this.notZip.add(t);continue}}catch{return null}this.isZip.add(t)}return{archivePath:t,subPath:this.pathUtils.join(Me.root,e.substring(t.length))}}}limitOpenFiles(e){if(this.zipInstances===null)return;let t=Date.now(),i=t+this.maxAge,n=e===null?0:this.zipInstances.size-e;for(let[s,{zipFs:o,expiresAt:a,refCount:l}]of this.zipInstances.entries())if(!(l!==0||o.hasOpenFileHandles())){if(t>=a){o.saveAndClose(),this.zipInstances.delete(s),n-=1;continue}else if(e===null||n<=0){i=a;break}o.saveAndClose(),this.zipInstances.delete(s),n-=1}this.limitOpenFilesTimeout===null&&(e===null&&this.zipInstances.size>0||e!==null)&&(this.limitOpenFilesTimeout=setTimeout(()=>{this.limitOpenFilesTimeout=null,this.limitOpenFiles(null)},i-t).unref())}async getZipPromise(e,t){let i=async()=>({baseFs:this.baseFs,libzip:this.libzip,readOnly:this.readOnlyArchives,stats:await this.baseFs.statPromise(e)});if(this.zipInstances){let n=this.zipInstances.get(e);if(!n){let s=await i();n=this.zipInstances.get(e),n||(n={zipFs:new li(e,s),expiresAt:0,refCount:0})}this.zipInstances.delete(e),this.limitOpenFiles(this.maxOpenFiles-1),this.zipInstances.set(e,n),n.expiresAt=Date.now()+this.maxAge,n.refCount+=1;try{return await t(n.zipFs)}finally{n.refCount-=1}}else{let n=new li(e,await i());try{return await t(n)}finally{n.saveAndClose()}}}getZipSync(e,t){let i=()=>({baseFs:this.baseFs,libzip:this.libzip,readOnly:this.readOnlyArchives,stats:this.baseFs.statSync(e)});if(this.zipInstances){let n=this.zipInstances.get(e);return n||(n={zipFs:new li(e,i()),expiresAt:0,refCount:0}),this.zipInstances.delete(e),this.limitOpenFiles(this.maxOpenFiles-1),this.zipInstances.set(e,n),n.expiresAt=Date.now()+this.maxAge,t(n.zipFs)}else{let n=new li(e,i());try{return t(n)}finally{n.saveAndClose()}}}};var Xu=ge(require("util"));var YE=ge(require("url"));var pQ=class extends Qi{constructor(e){super(H);this.baseFs=e}mapFromBase(e){return e}mapToBase(e){return e instanceof YE.URL?(0,YE.fileURLToPath)(e):e}};var en=Symbol("kBaseFs"),Oa=Symbol("kFd"),JA=Symbol("kClosePromise"),qE=Symbol("kCloseResolve"),JE=Symbol("kCloseReject"),Vu=Symbol("kRefs"),Lo=Symbol("kRef"),To=Symbol("kUnref"),v6e,k6e,x6e,P6e,WE=class{constructor(e,t){this[v6e]=1;this[k6e]=void 0;this[x6e]=void 0;this[P6e]=void 0;this[en]=t,this[Oa]=e}get fd(){return this[Oa]}async appendFile(e,t){var i;try{this[Lo](this.appendFile);let n=(i=typeof t=="string"?t:t==null?void 0:t.encoding)!=null?i:void 0;return await this[en].appendFilePromise(this.fd,e,n?{encoding:n}:void 0)}finally{this[To]()}}chown(e,t){throw new Error("Method not implemented.")}async chmod(e){try{return this[Lo](this.chmod),await this[en].fchmodPromise(this.fd,e)}finally{this[To]()}}createReadStream(e){return this[en].createReadStream(null,te(N({},e),{fd:this.fd}))}createWriteStream(e){return this[en].createWriteStream(null,te(N({},e),{fd:this.fd}))}datasync(){throw new Error("Method not implemented.")}sync(){throw new Error("Method not implemented.")}async read(e,t,i,n){var s,o,a;try{this[Lo](this.read);let l;return Buffer.isBuffer(e)?l=e:(e!=null||(e={}),l=(s=e.buffer)!=null?s:Buffer.alloc(16384),t=e.offset||0,i=(o=e.length)!=null?o:l.byteLength,n=(a=e.position)!=null?a:null),t!=null||(t=0),i!=null||(i=0),i===0?{bytesRead:i,buffer:l}:{bytesRead:await this[en].readPromise(this.fd,l,t,i,n),buffer:l}}finally{this[To]()}}async readFile(e){var t;try{this[Lo](this.readFile);let i=(t=typeof e=="string"?e:e==null?void 0:e.encoding)!=null?t:void 0;return await this[en].readFilePromise(this.fd,i)}finally{this[To]()}}async stat(e){try{return this[Lo](this.stat),await this[en].fstatPromise(this.fd,e)}finally{this[To]()}}async truncate(e){try{return this[Lo](this.truncate),await this[en].ftruncatePromise(this.fd,e)}finally{this[To]()}}utimes(e,t){throw new Error("Method not implemented.")}async writeFile(e,t){var i;try{this[Lo](this.writeFile);let n=(i=typeof t=="string"?t:t==null?void 0:t.encoding)!=null?i:void 0;await this[en].writeFilePromise(this.fd,e,n)}finally{this[To]()}}async write(...e){try{if(this[Lo](this.write),ArrayBuffer.isView(e[0])){let[t,i,n,s]=e;return{bytesWritten:await this[en].writePromise(this.fd,t,i!=null?i:void 0,n!=null?n:void 0,s!=null?s:void 0),buffer:t}}else{let[t,i,n]=e;return{bytesWritten:await this[en].writePromise(this.fd,t,i,n),buffer:t}}}finally{this[To]()}}async writev(e,t){try{this[Lo](this.writev);let i=0;if(typeof t!="undefined")for(let n of e){let s=await this.write(n,void 0,void 0,t);i+=s.bytesWritten,t+=s.bytesWritten}else for(let n of e)i+=(await this.write(n)).bytesWritten;return{buffers:e,bytesWritten:i}}finally{this[To]()}}readv(e,t){throw new Error("Method not implemented.")}close(){if(this[Oa]===-1)return Promise.resolve();if(this[JA])return this[JA];if(this[Vu]--,this[Vu]===0){let e=this[Oa];this[Oa]=-1,this[JA]=this[en].closePromise(e).finally(()=>{this[JA]=void 0})}else this[JA]=new Promise((e,t)=>{this[qE]=e,this[JE]=t}).finally(()=>{this[JA]=void 0,this[JE]=void 0,this[qE]=void 0});return this[JA]}[(en,Oa,v6e=Vu,k6e=JA,x6e=qE,P6e=JE,Lo)](e){if(this[Oa]===-1){let t=new Error("file closed");throw t.code="EBADF",t.syscall=e.name,t}this[Vu]++}[To](){if(this[Vu]--,this[Vu]===0){let e=this[Oa];this[Oa]=-1,this[en].closePromise(e).then(this[qE],this[JE])}}};var Pge=new Set(["accessSync","appendFileSync","createReadStream","createWriteStream","chmodSync","fchmodSync","chownSync","closeSync","copyFileSync","linkSync","lstatSync","fstatSync","lutimesSync","mkdirSync","openSync","opendirSync","readlinkSync","readFileSync","readdirSync","readlinkSync","realpathSync","renameSync","rmdirSync","statSync","symlinkSync","truncateSync","ftruncateSync","unlinkSync","unwatchFile","utimesSync","watch","watchFile","writeFileSync","writeSync"]),oM=new Set(["accessPromise","appendFilePromise","fchmodPromise","chmodPromise","chownPromise","closePromise","copyFilePromise","linkPromise","fstatPromise","lstatPromise","lutimesPromise","mkdirPromise","openPromise","opendirPromise","readdirPromise","realpathPromise","readFilePromise","readdirPromise","readlinkPromise","renamePromise","rmdirPromise","statPromise","symlinkPromise","truncatePromise","ftruncatePromise","unlinkPromise","utimesPromise","writeFilePromise","writeSync"]);function dQ(r,e){e=new pQ(e);let t=(i,n,s)=>{let o=i[n];i[n]=s,typeof(o==null?void 0:o[Xu.promisify.custom])!="undefined"&&(s[Xu.promisify.custom]=o[Xu.promisify.custom])};{t(r,"exists",(i,...n)=>{let o=typeof n[n.length-1]=="function"?n.pop():()=>{};process.nextTick(()=>{e.existsPromise(i).then(a=>{o(a)},()=>{o(!1)})})}),t(r,"read",(...i)=>{let[n,s,o,a,l,c]=i;if(i.length<=3){let u={};i.length<3?c=i[1]:(u=i[1],c=i[2]),{buffer:s=Buffer.alloc(16384),offset:o=0,length:a=s.byteLength,position:l}=u}if(o==null&&(o=0),a|=0,a===0){process.nextTick(()=>{c(null,0,s)});return}l==null&&(l=-1),process.nextTick(()=>{e.readPromise(n,s,o,a,l).then(u=>{c(null,u,s)},u=>{c(u,0,s)})})});for(let i of oM){let n=i.replace(/Promise$/,"");if(typeof r[n]=="undefined")continue;let s=e[i];if(typeof s=="undefined")continue;t(r,n,(...a)=>{let c=typeof a[a.length-1]=="function"?a.pop():()=>{};process.nextTick(()=>{s.apply(e,a).then(u=>{c(null,u)},u=>{c(u)})})})}r.realpath.native=r.realpath}{t(r,"existsSync",i=>{try{return e.existsSync(i)}catch(n){return!1}}),t(r,"readSync",(...i)=>{let[n,s,o,a,l]=i;return i.length<=3&&({offset:o=0,length:a=s.byteLength,position:l}=i[2]||{}),o==null&&(o=0),a|=0,a===0?0:(l==null&&(l=-1),e.readSync(n,s,o,a,l))});for(let i of Pge){let n=i;if(typeof r[n]=="undefined")continue;let s=e[i];typeof s!="undefined"&&t(r,n,s.bind(e))}r.realpathSync.native=r.realpathSync}{let i=process.emitWarning;process.emitWarning=()=>{};let n;try{n=r.promises}finally{process.emitWarning=i}if(typeof n!="undefined"){for(let s of oM){let o=s.replace(/Promise$/,"");if(typeof n[o]=="undefined")continue;let a=e[s];typeof a!="undefined"&&s!=="open"&&t(n,o,(l,...c)=>l instanceof WE?l[o].apply(l,c):a.call(e,l,...c))}t(n,"open",async(...s)=>{let o=await e.openPromise(...s);return new WE(o,e)})}}r.read[Xu.promisify.custom]=async(i,n,...s)=>({bytesRead:await e.readPromise(i,n,...s),buffer:n}),r.write[Xu.promisify.custom]=async(i,n,...s)=>({bytesWritten:await e.writePromise(i,n,...s),buffer:n})}function zE(r,e){let t=Object.create(r);return dQ(t,e),t}var aM=ge(require("os"));function AM(r){let e=Math.ceil(Math.random()*4294967296).toString(16).padStart(8,"0");return`${r}${e}`}var oo=new Set,CQ=null;function lM(){if(CQ)return CQ;let r=H.toPortablePath(aM.default.tmpdir()),e=U.realpathSync(r);return process.once("exit",()=>{U.rmtempSync()}),CQ={tmpdir:r,realTmpdir:e}}var U=Object.assign(new ar,{detachTemp(r){oo.delete(r)},mktempSync(r){let{tmpdir:e,realTmpdir:t}=lM();for(;;){let i=AM("xfs-");try{this.mkdirSync(x.join(e,i))}catch(s){if(s.code==="EEXIST")continue;throw s}let n=x.join(t,i);if(oo.add(n),typeof r=="undefined")return n;try{return r(n)}finally{if(oo.has(n)){oo.delete(n);try{this.removeSync(n)}catch{}}}}},async mktempPromise(r){let{tmpdir:e,realTmpdir:t}=lM();for(;;){let i=AM("xfs-");try{await this.mkdirPromise(x.join(e,i))}catch(s){if(s.code==="EEXIST")continue;throw s}let n=x.join(t,i);if(oo.add(n),typeof r=="undefined")return n;try{return await r(n)}finally{if(oo.has(n)){oo.delete(n);try{await this.removePromise(n)}catch{}}}}},async rmtempPromise(){await Promise.all(Array.from(oo.values()).map(async r=>{try{await U.removePromise(r,{maxRetries:0}),oo.delete(r)}catch{}}))},rmtempSync(){for(let r of oo)try{U.removeSync(r),oo.delete(r)}catch{}}});var Ex=ge(vQ());var ap={};ft(ap,{parseResolution:()=>eI,parseShell:()=>VE,parseSyml:()=>Si,stringifyArgument:()=>DQ,stringifyArgumentSegment:()=>RQ,stringifyArithmeticExpression:()=>$E,stringifyCommand:()=>PQ,stringifyCommandChain:()=>tg,stringifyCommandChainThen:()=>xQ,stringifyCommandLine:()=>XE,stringifyCommandLineThen:()=>kQ,stringifyEnvSegment:()=>ZE,stringifyRedirectArgument:()=>ep,stringifyResolution:()=>tI,stringifyShell:()=>eg,stringifyShellLine:()=>eg,stringifySyml:()=>Ua,stringifyValueArgument:()=>gc});var $M=ge(ZM());function VE(r,e={isGlobPattern:()=>!1}){try{return(0,$M.parse)(r,e)}catch(t){throw t.location&&(t.message=t.message.replace(/(\.)?$/,` (line ${t.location.start.line}, column ${t.location.start.column})$1`)),t}}function eg(r,{endSemicolon:e=!1}={}){return r.map(({command:t,type:i},n)=>`${XE(t)}${i===";"?n!==r.length-1||e?";":"":" &"}`).join(" ")}function XE(r){return`${tg(r.chain)}${r.then?` ${kQ(r.then)}`:""}`}function kQ(r){return`${r.type} ${XE(r.line)}`}function tg(r){return`${PQ(r)}${r.then?` ${xQ(r.then)}`:""}`}function xQ(r){return`${r.type} ${tg(r.chain)}`}function PQ(r){switch(r.type){case"command":return`${r.envs.length>0?`${r.envs.map(e=>ZE(e)).join(" ")} `:""}${r.args.map(e=>DQ(e)).join(" ")}`;case"subshell":return`(${eg(r.subshell)})${r.args.length>0?` ${r.args.map(e=>ep(e)).join(" ")}`:""}`;case"group":return`{ ${eg(r.group,{endSemicolon:!0})} }${r.args.length>0?` ${r.args.map(e=>ep(e)).join(" ")}`:""}`;case"envs":return r.envs.map(e=>ZE(e)).join(" ");default:throw new Error(`Unsupported command type: "${r.type}"`)}}function ZE(r){return`${r.name}=${r.args[0]?gc(r.args[0]):""}`}function DQ(r){switch(r.type){case"redirection":return ep(r);case"argument":return gc(r);default:throw new Error(`Unsupported argument type: "${r.type}"`)}}function ep(r){return`${r.subtype} ${r.args.map(e=>gc(e)).join(" ")}`}function gc(r){return r.segments.map(e=>RQ(e)).join("")}function RQ(r){let e=(i,n)=>n?`"${i}"`:i,t=i=>i===""?'""':i.match(/[(){}<>$|&; \t"']/)?`$'${i.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\f/g,"\\f").replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t").replace(/\v/g,"\\v").replace(/\0/g,"\\0")}'`:i;switch(r.type){case"text":return t(r.text);case"glob":return r.pattern;case"shell":return e(`\${${eg(r.shell)}}`,r.quoted);case"variable":return e(typeof r.defaultValue=="undefined"?typeof r.alternativeValue=="undefined"?`\${${r.name}}`:r.alternativeValue.length===0?`\${${r.name}:+}`:`\${${r.name}:+${r.alternativeValue.map(i=>gc(i)).join(" ")}}`:r.defaultValue.length===0?`\${${r.name}:-}`:`\${${r.name}:-${r.defaultValue.map(i=>gc(i)).join(" ")}}`,r.quoted);case"arithmetic":return`$(( ${$E(r.arithmetic)} ))`;default:throw new Error(`Unsupported argument segment type: "${r.type}"`)}}function $E(r){let e=n=>{switch(n){case"addition":return"+";case"subtraction":return"-";case"multiplication":return"*";case"division":return"/";default:throw new Error(`Can't extract operator from arithmetic expression of type "${n}"`)}},t=(n,s)=>s?`( ${n} )`:n,i=n=>t($E(n),!["number","variable"].includes(n.type));switch(r.type){case"number":return String(r.value);case"variable":return r.name;default:return`${i(r.left)} ${e(r.type)} ${i(r.right)}`}}var r1=ge(t1());function eI(r){let e=r.match(/^\*{1,2}\/(.*)/);if(e)throw new Error(`The override for '${r}' includes a glob pattern. Glob patterns have been removed since their behaviours don't match what you'd expect. Set the override to '${e[1]}' instead.`);try{return(0,r1.parse)(r)}catch(t){throw t.location&&(t.message=t.message.replace(/(\.)?$/,` (line ${t.location.start.line}, column ${t.location.start.column})$1`)),t}}function tI(r){let e="";return r.from&&(e+=r.from.fullName,r.from.description&&(e+=`@${r.from.description}`),e+="/"),e+=r.descriptor.fullName,r.descriptor.description&&(e+=`@${r.descriptor.description}`),e}var gI=ge(JU()),_U=ge(zU()),Upe=/^(?![-?:,\][{}#&*!|>'"%@` \t\r\n]).([ \t]*(?![,\][{}:# \t\r\n]).)*$/,VU=["__metadata","version","resolution","dependencies","peerDependencies","dependenciesMeta","peerDependenciesMeta","binaries"],qQ=class{constructor(e){this.data=e}};function XU(r){return r.match(Upe)?r:JSON.stringify(r)}function ZU(r){return typeof r=="undefined"?!0:typeof r=="object"&&r!==null?Object.keys(r).every(e=>ZU(r[e])):!1}function JQ(r,e,t){if(r===null)return`null +`;if(typeof r=="number"||typeof r=="boolean")return`${r.toString()} +`;if(typeof r=="string")return`${XU(r)} +`;if(Array.isArray(r)){if(r.length===0)return`[] +`;let i=" ".repeat(e);return` +${r.map(s=>`${i}- ${JQ(s,e+1,!1)}`).join("")}`}if(typeof r=="object"&&r){let i,n;r instanceof qQ?(i=r.data,n=!1):(i=r,n=!0);let s=" ".repeat(e),o=Object.keys(i);n&&o.sort((l,c)=>{let u=VU.indexOf(l),g=VU.indexOf(c);return u===-1&&g===-1?lc?1:0:u!==-1&&g===-1?-1:u===-1&&g!==-1?1:u-g});let a=o.filter(l=>!ZU(i[l])).map((l,c)=>{let u=i[l],g=XU(l),f=JQ(u,e+1,!0),h=c>0||t?s:"",p=g.length>1024?`? ${g} +${h}:`:`${g}:`,m=f.startsWith(` +`)?f:` ${f}`;return`${h}${p}${m}`}).join(e===0?` +`:"")||` +`;return t?` +${a}`:`${a}`}throw new Error(`Unsupported value type (${r})`)}function Ua(r){try{let e=JQ(r,0,!1);return e!==` +`?e:""}catch(e){throw e.location&&(e.message=e.message.replace(/(\.)?$/,` (line ${e.location.start.line}, column ${e.location.start.column})$1`)),e}}Ua.PreserveOrdering=qQ;function Kpe(r){return r.endsWith(` +`)||(r+=` +`),(0,_U.parse)(r)}var Hpe=/^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;function jpe(r){if(Hpe.test(r))return Kpe(r);let e=(0,gI.safeLoad)(r,{schema:gI.FAILSAFE_SCHEMA,json:!0});if(e==null)return{};if(typeof e!="object")throw new Error(`Expected an indexed object, got a ${typeof e} instead. Does your file follow Yaml's rules?`);if(Array.isArray(e))throw new Error("Expected an indexed object, got an array instead. Does your file follow Yaml's rules?");return e}function Si(r){return jpe(r)}var K4=ge(eK()),Ew=ge(yc());var mp={};ft(mp,{Builtins:()=>aS,Cli:()=>Bs,Command:()=>Re,Option:()=>J,UsageError:()=>Pe,formatMarkdownish:()=>Ki});var wc=0,Ap=1,tn=2,zQ="",vi="\0",cg=-1,_Q=/^(-h|--help)(?:=([0-9]+))?$/,fI=/^(--[a-z]+(?:-[a-z]+)*|-[a-zA-Z]+)$/,sK=/^-[a-zA-Z]{2,}$/,VQ=/^([^=]+)=([\s\S]*)$/,XQ=process.env.DEBUG_CLI==="1";var Pe=class extends Error{constructor(e){super(e);this.clipanion={type:"usage"},this.name="UsageError"}},lp=class extends Error{constructor(e,t){super();if(this.input=e,this.candidates=t,this.clipanion={type:"none"},this.name="UnknownSyntaxError",this.candidates.length===0)this.message="Command not found, but we're not sure what's the alternative.";else if(this.candidates.every(i=>i.reason!==null&&i.reason===t[0].reason)){let[{reason:i}]=this.candidates;this.message=`${i} + +${this.candidates.map(({usage:n})=>`$ ${n}`).join(` +`)}`}else if(this.candidates.length===1){let[{usage:i}]=this.candidates;this.message=`Command not found; did you mean: + +$ ${i} +${ZQ(e)}`}else this.message=`Command not found; did you mean one of: + +${this.candidates.map(({usage:i},n)=>`${`${n}.`.padStart(4)} ${i}`).join(` +`)} + +${ZQ(e)}`}},$Q=class extends Error{constructor(e,t){super();this.input=e,this.usages=t,this.clipanion={type:"none"},this.name="AmbiguousSyntaxError",this.message=`Cannot find which to pick amongst the following alternatives: + +${this.usages.map((i,n)=>`${`${n}.`.padStart(4)} ${i}`).join(` +`)} + +${ZQ(e)}`}},ZQ=r=>`While running ${r.filter(e=>e!==vi).map(e=>{let t=JSON.stringify(e);return e.match(/\s/)||e.length===0||t!==`"${e}"`?t:e}).join(" ")}`;var cp=Symbol("clipanion/isOption");function rn(r){return te(N({},r),{[cp]:!0})}function Uo(r,e){return typeof r=="undefined"?[r,e]:typeof r=="object"&&r!==null&&!Array.isArray(r)?[void 0,r]:[r,e]}function hI(r,e=!1){let t=r.replace(/^\.: /,"");return e&&(t=t[0].toLowerCase()+t.slice(1)),t}function up(r,e){return e.length===1?new Pe(`${r}: ${hI(e[0],!0)}`):new Pe(`${r}: +${e.map(t=>` +- ${hI(t)}`).join("")}`)}function gp(r,e,t){if(typeof t=="undefined")return e;let i=[],n=[],s=a=>{let l=e;return e=a,s.bind(null,l)};if(!t(e,{errors:i,coercions:n,coercion:s}))throw up(`Invalid value for ${r}`,i);for(let[,a]of n)a();return e}var Re=class{constructor(){this.help=!1}static Usage(e){return e}async catch(e){throw e}async validateAndExecute(){let t=this.constructor.schema;if(Array.isArray(t)){let{isDict:n,isUnknown:s,applyCascade:o}=await Promise.resolve().then(()=>(ws(),ug)),a=o(n(s()),t),l=[],c=[];if(!a(this,{errors:l,coercions:c}))throw up("Invalid option schema",l);for(let[,g]of c)g()}else if(t!=null)throw new Error("Invalid command schema");let i=await this.execute();return typeof i!="undefined"?i:0}};Re.isOption=cp;Re.Default=[];var pK=80,rS=Array(pK).fill("\u2501");for(let r=0;r<=24;++r)rS[rS.length-r]=`[38;5;${232+r}m\u2501`;var iS={header:r=>`\u2501\u2501\u2501 ${r}${r.length`${r}`,error:r=>`${r}`,code:r=>`${r}`},dK={header:r=>r,bold:r=>r,error:r=>r,code:r=>r};function bde(r){let e=r.split(` +`),t=e.filter(n=>n.match(/\S/)),i=t.length>0?t.reduce((n,s)=>Math.min(n,s.length-s.trimStart().length),Number.MAX_VALUE):0;return e.map(n=>n.slice(i).trimRight()).join(` +`)}function Ki(r,{format:e,paragraphs:t}){return r=r.replace(/\r\n?/g,` +`),r=bde(r),r=r.replace(/^\n+|\n+$/g,""),r=r.replace(/^(\s*)-([^\n]*?)\n+/gm,`$1-$2 + +`),r=r.replace(/\n(\n)?\n*/g,"$1"),t&&(r=r.split(/\n/).map(i=>{let n=i.match(/^\s*[*-][\t ]+(.*)/);if(!n)return i.match(/(.{1,80})(?: |$)/g).join(` +`);let s=i.length-i.trimStart().length;return n[1].match(new RegExp(`(.{1,${78-s}})(?: |$)`,"g")).map((o,a)=>" ".repeat(s)+(a===0?"- ":" ")+o).join(` +`)}).join(` + +`)),r=r.replace(/(`+)((?:.|[\n])*?)\1/g,(i,n,s)=>e.code(n+s+n)),r=r.replace(/(\*\*)((?:.|[\n])*?)\1/g,(i,n,s)=>e.bold(n+s+n)),r?`${r} +`:""}var oS=ge(require("tty"));function wn(r){XQ&&console.log(r)}var CK={candidateUsage:null,requiredOptions:[],errorMessage:null,ignoreOptions:!1,path:[],positionals:[],options:[],remainder:null,selectedIndex:cg};function mK(){return{nodes:[sn(),sn(),sn()]}}function Sde(r){let e=mK(),t=[],i=e.nodes.length;for(let n of r){t.push(i);for(let s=0;s{if(e.has(i))return;e.add(i);let n=r.nodes[i];for(let o of Object.values(n.statics))for(let{to:a}of o)t(a);for(let[,{to:o}]of n.dynamics)t(o);for(let{to:o}of n.shortcuts)t(o);let s=new Set(n.shortcuts.map(({to:o})=>o));for(;n.shortcuts.length>0;){let{to:o}=n.shortcuts.shift(),a=r.nodes[o];for(let[l,c]of Object.entries(a.statics)){let u=Object.prototype.hasOwnProperty.call(n.statics,l)?n.statics[l]:n.statics[l]=[];for(let g of c)u.some(({to:f})=>g.to===f)||u.push(g)}for(let[l,c]of a.dynamics)n.dynamics.some(([u,{to:g}])=>l===u&&c.to===g)||n.dynamics.push([l,c]);for(let l of a.shortcuts)s.has(l.to)||(n.shortcuts.push(l),s.add(l.to))}};t(wc)}function kde(r,{prefix:e=""}={}){if(XQ){wn(`${e}Nodes are:`);for(let t=0;tl!==tn).map(({state:l})=>({usage:l.candidateUsage,reason:null})));if(a.every(({node:l})=>l===tn))throw new lp(e,a.map(({state:l})=>({usage:l.candidateUsage,reason:l.errorMessage})));i=xde(a)}if(i.length>0){wn(" Results:");for(let s of i)wn(` - ${s.node} -> ${JSON.stringify(s.state)}`)}else wn(" No results");return i}function Pde(r,e){if(e.selectedIndex!==null)return!0;if(Object.prototype.hasOwnProperty.call(r.statics,vi)){for(let{to:t}of r.statics[vi])if(t===Ap)return!0}return!1}function Rde(r,e,t){let i=t&&e.length>0?[""]:[],n=IK(r,e,t),s=[],o=new Set,a=(l,c,u=!0)=>{let g=[c];for(;g.length>0;){let h=g;g=[];for(let p of h){let m=r.nodes[p],y=Object.keys(m.statics);for(let b of Object.keys(m.statics)){let v=y[0];for(let{to:k,reducer:T}of m.statics[v])T==="pushPath"&&(u||l.push(v),g.push(k))}}u=!1}let f=JSON.stringify(l);o.has(f)||(s.push(l),o.add(f))};for(let{node:l,state:c}of n){if(c.remainder!==null){a([c.remainder],l);continue}let u=r.nodes[l],g=Pde(u,c);for(let[f,h]of Object.entries(u.statics))(g&&f!==vi||!f.startsWith("-")&&h.some(({reducer:p})=>p==="pushPath"))&&a([...i,f],l);if(!!g)for(let[f,{to:h}]of u.dynamics){if(h===tn)continue;let p=Dde(f,c);if(p!==null)for(let m of p)a([...i,m],l)}}return[...s].sort()}function Nde(r,e){let t=IK(r,[...e,vi]);return Fde(e,t.map(({state:i})=>i))}function xde(r){let e=0;for(let{state:t}of r)t.path.length>e&&(e=t.path.length);return r.filter(({state:t})=>t.path.length===e)}function Fde(r,e){let t=e.filter(g=>g.selectedIndex!==null);if(t.length===0)throw new Error;let i=t.filter(g=>g.requiredOptions.every(f=>f.some(h=>g.options.find(p=>p.name===h))));if(i.length===0)throw new lp(r,t.map(g=>({usage:g.candidateUsage,reason:null})));let n=0;for(let g of i)g.path.length>n&&(n=g.path.length);let s=i.filter(g=>g.path.length===n),o=g=>g.positionals.filter(({extra:f})=>!f).length+g.options.length,a=s.map(g=>({state:g,positionalCount:o(g)})),l=0;for(let{positionalCount:g}of a)g>l&&(l=g);let c=a.filter(({positionalCount:g})=>g===l).map(({state:g})=>g),u=Lde(c);if(u.length>1)throw new $Q(r,u.map(g=>g.candidateUsage));return u[0]}function Lde(r){let e=[],t=[];for(let i of r)i.selectedIndex===cg?t.push(i):e.push(i);return t.length>0&&e.push(te(N({},CK),{path:yK(...t.map(i=>i.path)),options:t.reduce((i,n)=>i.concat(n.options),[])})),e}function yK(r,e,...t){return e===void 0?Array.from(r):yK(r.filter((i,n)=>i===e[n]),...t)}function sn(){return{dynamics:[],shortcuts:[],statics:{}}}function EK(r){return r===Ap||r===tn}function sS(r,e=0){return{to:EK(r.to)?r.to:r.to>2?r.to+e-2:r.to+e,reducer:r.reducer}}function Qde(r,e=0){let t=sn();for(let[i,n]of r.dynamics)t.dynamics.push([i,sS(n,e)]);for(let i of r.shortcuts)t.shortcuts.push(sS(i,e));for(let[i,n]of Object.entries(r.statics))t.statics[i]=n.map(s=>sS(s,e));return t}function ki(r,e,t,i,n){r.nodes[e].dynamics.push([t,{to:i,reducer:n}])}function gg(r,e,t,i){r.nodes[e].shortcuts.push({to:t,reducer:i})}function Ka(r,e,t,i,n){(Object.prototype.hasOwnProperty.call(r.nodes[e].statics,t)?r.nodes[e].statics[t]:r.nodes[e].statics[t]=[]).push({to:i,reducer:n})}function dI(r,e,t,i){if(Array.isArray(e)){let[n,...s]=e;return r[n](t,i,...s)}else return r[e](t,i)}function Dde(r,e){let t=Array.isArray(r)?CI[r[0]]:CI[r];if(typeof t.suggest=="undefined")return null;let i=Array.isArray(r)?r.slice(1):[];return t.suggest(e,...i)}var CI={always:()=>!0,isOptionLike:(r,e)=>!r.ignoreOptions&&e!=="-"&&e.startsWith("-"),isNotOptionLike:(r,e)=>r.ignoreOptions||e==="-"||!e.startsWith("-"),isOption:(r,e,t,i)=>!r.ignoreOptions&&e===t,isBatchOption:(r,e,t)=>!r.ignoreOptions&&sK.test(e)&&[...e.slice(1)].every(i=>t.includes(`-${i}`)),isBoundOption:(r,e,t,i)=>{let n=e.match(VQ);return!r.ignoreOptions&&!!n&&fI.test(n[1])&&t.includes(n[1])&&i.filter(s=>s.names.includes(n[1])).every(s=>s.allowBinding)},isNegatedOption:(r,e,t)=>!r.ignoreOptions&&e===`--no-${t.slice(2)}`,isHelp:(r,e)=>!r.ignoreOptions&&_Q.test(e),isUnsupportedOption:(r,e,t)=>!r.ignoreOptions&&e.startsWith("-")&&fI.test(e)&&!t.includes(e),isInvalidOption:(r,e)=>!r.ignoreOptions&&e.startsWith("-")&&!fI.test(e)};CI.isOption.suggest=(r,e,t=!0)=>t?null:[e];var nS={setCandidateState:(r,e,t)=>N(N({},r),t),setSelectedIndex:(r,e,t)=>te(N({},r),{selectedIndex:t}),pushBatch:(r,e)=>te(N({},r),{options:r.options.concat([...e.slice(1)].map(t=>({name:`-${t}`,value:!0})))}),pushBound:(r,e)=>{let[,t,i]=e.match(VQ);return te(N({},r),{options:r.options.concat({name:t,value:i})})},pushPath:(r,e)=>te(N({},r),{path:r.path.concat(e)}),pushPositional:(r,e)=>te(N({},r),{positionals:r.positionals.concat({value:e,extra:!1})}),pushExtra:(r,e)=>te(N({},r),{positionals:r.positionals.concat({value:e,extra:!0})}),pushExtraNoLimits:(r,e)=>te(N({},r),{positionals:r.positionals.concat({value:e,extra:$n})}),pushTrue:(r,e,t=e)=>te(N({},r),{options:r.options.concat({name:e,value:!0})}),pushFalse:(r,e,t=e)=>te(N({},r),{options:r.options.concat({name:t,value:!1})}),pushUndefined:(r,e)=>te(N({},r),{options:r.options.concat({name:e,value:void 0})}),pushStringValue:(r,e)=>{var t;let i=te(N({},r),{options:[...r.options]}),n=r.options[r.options.length-1];return n.value=((t=n.value)!==null&&t!==void 0?t:[]).concat([e]),i},setStringValue:(r,e)=>{let t=te(N({},r),{options:[...r.options]}),i=r.options[r.options.length-1];return i.value=e,t},inhibateOptions:r=>te(N({},r),{ignoreOptions:!0}),useHelp:(r,e,t)=>{let[,,i]=e.match(_Q);return typeof i!="undefined"?te(N({},r),{options:[{name:"-c",value:String(t)},{name:"-i",value:i}]}):te(N({},r),{options:[{name:"-c",value:String(t)}]})},setError:(r,e,t)=>e===vi?te(N({},r),{errorMessage:`${t}.`}):te(N({},r),{errorMessage:`${t} ("${e}").`}),setOptionArityError:(r,e)=>{let t=r.options[r.options.length-1];return te(N({},r),{errorMessage:`Not enough arguments to option ${t.name}.`})}},$n=Symbol(),wK=class{constructor(e,t){this.allOptionNames=[],this.arity={leading:[],trailing:[],extra:[],proxy:!1},this.options=[],this.paths=[],this.cliIndex=e,this.cliOpts=t}addPath(e){this.paths.push(e)}setArity({leading:e=this.arity.leading,trailing:t=this.arity.trailing,extra:i=this.arity.extra,proxy:n=this.arity.proxy}){Object.assign(this.arity,{leading:e,trailing:t,extra:i,proxy:n})}addPositional({name:e="arg",required:t=!0}={}){if(!t&&this.arity.extra===$n)throw new Error("Optional parameters cannot be declared when using .rest() or .proxy()");if(!t&&this.arity.trailing.length>0)throw new Error("Optional parameters cannot be declared after the required trailing positional arguments");!t&&this.arity.extra!==$n?this.arity.extra.push(e):this.arity.extra!==$n&&this.arity.extra.length===0?this.arity.leading.push(e):this.arity.trailing.push(e)}addRest({name:e="arg",required:t=0}={}){if(this.arity.extra===$n)throw new Error("Infinite lists cannot be declared multiple times in the same command");if(this.arity.trailing.length>0)throw new Error("Infinite lists cannot be declared after the required trailing positional arguments");for(let i=0;i1)throw new Error("The arity cannot be higher than 1 when the option only supports the --arg=value syntax");if(!Number.isInteger(i))throw new Error(`The arity must be an integer, got ${i}`);if(i<0)throw new Error(`The arity must be positive, got ${i}`);this.allOptionNames.push(...e),this.options.push({names:e,description:t,arity:i,hidden:n,required:s,allowBinding:o})}setContext(e){this.context=e}usage({detailed:e=!0,inlineOptions:t=!0}={}){let i=[this.cliOpts.binaryName],n=[];if(this.paths.length>0&&i.push(...this.paths[0]),e){for(let{names:o,arity:a,hidden:l,description:c,required:u}of this.options){if(l)continue;let g=[];for(let h=0;h`:`[${f}]`)}i.push(...this.arity.leading.map(o=>`<${o}>`)),this.arity.extra===$n?i.push("..."):i.push(...this.arity.extra.map(o=>`[${o}]`)),i.push(...this.arity.trailing.map(o=>`<${o}>`))}return{usage:i.join(" "),options:n}}compile(){if(typeof this.context=="undefined")throw new Error("Assertion failed: No context attached");let e=mK(),t=wc,i=this.usage().usage,n=this.options.filter(a=>a.required).map(a=>a.names);t=ao(e,sn()),Ka(e,wc,zQ,t,["setCandidateState",{candidateUsage:i,requiredOptions:n}]);let s=this.arity.proxy?"always":"isNotOptionLike",o=this.paths.length>0?this.paths:[[]];for(let a of o){let l=t;if(a.length>0){let f=ao(e,sn());gg(e,l,f),this.registerOptions(e,f),l=f}for(let f=0;f0||!this.arity.proxy){let f=ao(e,sn());ki(e,l,"isHelp",f,["useHelp",this.cliIndex]),Ka(e,f,vi,Ap,["setSelectedIndex",cg]),this.registerOptions(e,l)}this.arity.leading.length>0&&Ka(e,l,vi,tn,["setError","Not enough positional arguments"]);let c=l;for(let f=0;f0||f+1!==this.arity.leading.length)&&Ka(e,h,vi,tn,["setError","Not enough positional arguments"]),ki(e,c,"isNotOptionLike",h,"pushPositional"),c=h}let u=c;if(this.arity.extra===$n||this.arity.extra.length>0){let f=ao(e,sn());if(gg(e,c,f),this.arity.extra===$n){let h=ao(e,sn());this.arity.proxy||this.registerOptions(e,h),ki(e,c,s,h,"pushExtraNoLimits"),ki(e,h,s,h,"pushExtraNoLimits"),gg(e,h,f)}else for(let h=0;h0&&Ka(e,u,vi,tn,["setError","Not enough positional arguments"]);let g=u;for(let f=0;fo.length>s.length?o:s,"");if(i.arity===0)for(let s of i.names)ki(e,t,["isOption",s,i.hidden||s!==n],t,"pushTrue"),s.startsWith("--")&&!s.startsWith("--no-")&&ki(e,t,["isNegatedOption",s],t,["pushFalse",s]);else{let s=ao(e,sn());for(let o of i.names)ki(e,t,["isOption",o,i.hidden||o!==n],s,"pushUndefined");for(let o=0;o=0&&eNde(i,n),suggest:(n,s)=>Rde(i,n,s)}}};var Cp=class extends Re{constructor(e){super();this.contexts=e,this.commands=[]}static from(e,t){let i=new Cp(t);i.path=e.path;for(let n of e.options)switch(n.name){case"-c":i.commands.push(Number(n.value));break;case"-i":i.index=Number(n.value);break}return i}async execute(){let e=this.commands;if(typeof this.index!="undefined"&&this.index>=0&&this.index1){this.context.stdout.write(`Multiple commands match your selection: +`),this.context.stdout.write(` +`);let t=0;for(let i of this.commands)this.context.stdout.write(this.cli.usage(this.contexts[i].commandClass,{prefix:`${t++}. `.padStart(5)}));this.context.stdout.write(` +`),this.context.stdout.write(`Run again with -h= to see the longer details of any of those commands. +`)}}};var BK=Symbol("clipanion/errorCommand");function Tde(){return process.env.FORCE_COLOR==="0"?1:process.env.FORCE_COLOR==="1"||typeof process.stdout!="undefined"&&process.stdout.isTTY?8:1}var Bs=class{constructor({binaryLabel:e,binaryName:t="...",binaryVersion:i,enableCapture:n=!1,enableColors:s}={}){this.registrations=new Map,this.builder=new dp({binaryName:t}),this.binaryLabel=e,this.binaryName=t,this.binaryVersion=i,this.enableCapture=n,this.enableColors=s}static from(e,t={}){let i=new Bs(t);for(let n of e)i.register(n);return i}register(e){var t;let i=new Map,n=new e;for(let l in n){let c=n[l];typeof c=="object"&&c!==null&&c[Re.isOption]&&i.set(l,c)}let s=this.builder.command(),o=s.cliIndex,a=(t=e.paths)!==null&&t!==void 0?t:n.paths;if(typeof a!="undefined")for(let l of a)s.addPath(l);this.registrations.set(e,{specs:i,builder:s,index:o});for(let[l,{definition:c}]of i.entries())c(s,l);s.setContext({commandClass:e})}process(e){let{contexts:t,process:i}=this.builder.compile(),n=i(e);switch(n.selectedIndex){case cg:return Cp.from(n,t);default:{let{commandClass:s}=t[n.selectedIndex],o=this.registrations.get(s);if(typeof o=="undefined")throw new Error("Assertion failed: Expected the command class to have been registered.");let a=new s;a.path=n.path;try{for(let[l,{transformer:c}]of o.specs.entries())a[l]=c(o.builder,l,n);return a}catch(l){throw l[BK]=a,l}}break}}async run(e,t){var i;let n,s=N(N({},Bs.defaultContext),t),o=(i=this.enableColors)!==null&&i!==void 0?i:s.colorDepth>1;if(!Array.isArray(e))n=e;else try{n=this.process(e)}catch(c){return s.stdout.write(this.error(c,{colored:o})),1}if(n.help)return s.stdout.write(this.usage(n,{colored:o,detailed:!0})),0;n.context=s,n.cli={binaryLabel:this.binaryLabel,binaryName:this.binaryName,binaryVersion:this.binaryVersion,enableCapture:this.enableCapture,enableColors:this.enableColors,definitions:()=>this.definitions(),error:(c,u)=>this.error(c,u),format:c=>this.format(c),process:c=>this.process(c),run:(c,u)=>this.run(c,N(N({},s),u)),usage:(c,u)=>this.usage(c,u)};let a=this.enableCapture?Ode(s):bK,l;try{l=await a(()=>n.validateAndExecute().catch(c=>n.catch(c).then(()=>0)))}catch(c){return s.stdout.write(this.error(c,{colored:o,command:n})),1}return l}async runExit(e,t){process.exitCode=await this.run(e,t)}suggest(e,t){let{suggest:i}=this.builder.compile();return i(e,t)}definitions({colored:e=!1}={}){let t=[];for(let[i,{index:n}]of this.registrations){if(typeof i.usage=="undefined")continue;let{usage:s}=this.getUsageByIndex(n,{detailed:!1}),{usage:o,options:a}=this.getUsageByIndex(n,{detailed:!0,inlineOptions:!1}),l=typeof i.usage.category!="undefined"?Ki(i.usage.category,{format:this.format(e),paragraphs:!1}):void 0,c=typeof i.usage.description!="undefined"?Ki(i.usage.description,{format:this.format(e),paragraphs:!1}):void 0,u=typeof i.usage.details!="undefined"?Ki(i.usage.details,{format:this.format(e),paragraphs:!0}):void 0,g=typeof i.usage.examples!="undefined"?i.usage.examples.map(([f,h])=>[Ki(f,{format:this.format(e),paragraphs:!1}),h.replace(/\$0/g,this.binaryName)]):void 0;t.push({path:s,usage:o,category:l,description:c,details:u,examples:g,options:a})}return t}usage(e=null,{colored:t,detailed:i=!1,prefix:n="$ "}={}){var s;if(e===null){for(let l of this.registrations.keys()){let c=l.paths,u=typeof l.usage!="undefined";if(!c||c.length===0||c.length===1&&c[0].length===0||((s=c==null?void 0:c.some(h=>h.length===0))!==null&&s!==void 0?s:!1))if(e){e=null;break}else e=l;else if(u){e=null;continue}}e&&(i=!0)}let o=e!==null&&e instanceof Re?e.constructor:e,a="";if(o)if(i){let{description:l="",details:c="",examples:u=[]}=o.usage||{};l!==""&&(a+=Ki(l,{format:this.format(t),paragraphs:!1}).replace(/^./,h=>h.toUpperCase()),a+=` +`),(c!==""||u.length>0)&&(a+=`${this.format(t).header("Usage")} +`,a+=` +`);let{usage:g,options:f}=this.getUsageByRegistration(o,{inlineOptions:!1});if(a+=`${this.format(t).bold(n)}${g} +`,f.length>0){a+=` +`,a+=`${iS.header("Options")} +`;let h=f.reduce((p,m)=>Math.max(p,m.definition.length),0);a+=` +`;for(let{definition:p,description:m}of f)a+=` ${this.format(t).bold(p.padEnd(h))} ${Ki(m,{format:this.format(t),paragraphs:!1})}`}if(c!==""&&(a+=` +`,a+=`${this.format(t).header("Details")} +`,a+=` +`,a+=Ki(c,{format:this.format(t),paragraphs:!0})),u.length>0){a+=` +`,a+=`${this.format(t).header("Examples")} +`;for(let[h,p]of u)a+=` +`,a+=Ki(h,{format:this.format(t),paragraphs:!1}),a+=`${p.replace(/^/m,` ${this.format(t).bold(n)}`).replace(/\$0/g,this.binaryName)} +`}}else{let{usage:l}=this.getUsageByRegistration(o);a+=`${this.format(t).bold(n)}${l} +`}else{let l=new Map;for(let[f,{index:h}]of this.registrations.entries()){if(typeof f.usage=="undefined")continue;let p=typeof f.usage.category!="undefined"?Ki(f.usage.category,{format:this.format(t),paragraphs:!1}):null,m=l.get(p);typeof m=="undefined"&&l.set(p,m=[]);let{usage:y}=this.getUsageByIndex(h);m.push({commandClass:f,usage:y})}let c=Array.from(l.keys()).sort((f,h)=>f===null?-1:h===null?1:f.localeCompare(h,"en",{usage:"sort",caseFirst:"upper"})),u=typeof this.binaryLabel!="undefined",g=typeof this.binaryVersion!="undefined";u||g?(u&&g?a+=`${this.format(t).header(`${this.binaryLabel} - ${this.binaryVersion}`)} + +`:u?a+=`${this.format(t).header(`${this.binaryLabel}`)} +`:a+=`${this.format(t).header(`${this.binaryVersion}`)} +`,a+=` ${this.format(t).bold(n)}${this.binaryName} +`):a+=`${this.format(t).bold(n)}${this.binaryName} +`;for(let f of c){let h=l.get(f).slice().sort((m,y)=>m.usage.localeCompare(y.usage,"en",{usage:"sort",caseFirst:"upper"})),p=f!==null?f.trim():"General commands";a+=` +`,a+=`${this.format(t).header(`${p}`)} +`;for(let{commandClass:m,usage:y}of h){let b=m.usage.description||"undocumented";a+=` +`,a+=` ${this.format(t).bold(y)} +`,a+=` ${Ki(b,{format:this.format(t),paragraphs:!1})}`}}a+=` +`,a+=Ki("You can also print more details about any of these commands by calling them with the `-h,--help` flag right after the command name.",{format:this.format(t),paragraphs:!0})}return a}error(e,t){var i,{colored:n,command:s=(i=e[BK])!==null&&i!==void 0?i:null}=t===void 0?{}:t;e instanceof Error||(e=new Error(`Execution failed with a non-error rejection (rejected value: ${JSON.stringify(e)})`));let o="",a=e.name.replace(/([a-z])([A-Z])/g,"$1 $2");a==="Error"&&(a="Internal Error"),o+=`${this.format(n).error(a)}: ${e.message} +`;let l=e.clipanion;return typeof l!="undefined"?l.type==="usage"&&(o+=` +`,o+=this.usage(s)):e.stack&&(o+=`${e.stack.replace(/^.*\n/,"")} +`),o}format(e){var t;return((t=e!=null?e:this.enableColors)!==null&&t!==void 0?t:Bs.defaultContext.colorDepth>1)?iS:dK}getUsageByRegistration(e,t){let i=this.registrations.get(e);if(typeof i=="undefined")throw new Error("Assertion failed: Unregistered command");return this.getUsageByIndex(i.index,t)}getUsageByIndex(e,t){return this.builder.getBuilderByIndex(e).usage(t)}};Bs.defaultContext={stdin:process.stdin,stdout:process.stdout,stderr:process.stderr,colorDepth:"getColorDepth"in oS.default.WriteStream.prototype?oS.default.WriteStream.prototype.getColorDepth():Tde()};var QK;function Ode(r){let e=QK;if(typeof e=="undefined"){if(r.stdout===process.stdout&&r.stderr===process.stderr)return bK;let{AsyncLocalStorage:t}=require("async_hooks");e=QK=new t;let i=process.stdout._write;process.stdout._write=function(s,o,a){let l=e.getStore();return typeof l=="undefined"?i.call(this,s,o,a):l.stdout.write(s,o,a)};let n=process.stderr._write;process.stderr._write=function(s,o,a){let l=e.getStore();return typeof l=="undefined"?n.call(this,s,o,a):l.stderr.write(s,o,a)}}return t=>e.run(r,t)}function bK(r){return r()}var aS={};ft(aS,{DefinitionsCommand:()=>mI,HelpCommand:()=>EI,VersionCommand:()=>II});var mI=class extends Re{async execute(){this.context.stdout.write(`${JSON.stringify(this.cli.definitions(),null,2)} +`)}};mI.paths=[["--clipanion=definitions"]];var EI=class extends Re{async execute(){this.context.stdout.write(this.cli.usage())}};EI.paths=[["-h"],["--help"]];var II=class extends Re{async execute(){var e;this.context.stdout.write(`${(e=this.cli.binaryVersion)!==null&&e!==void 0?e:""} +`)}};II.paths=[["-v"],["--version"]];var J={};ft(J,{Array:()=>SK,Boolean:()=>vK,Counter:()=>kK,Proxy:()=>xK,Rest:()=>PK,String:()=>DK,applyValidator:()=>gp,cleanValidationError:()=>hI,formatError:()=>up,isOptionSymbol:()=>cp,makeCommandOption:()=>rn,rerouteArguments:()=>Uo});function SK(r,e,t){let[i,n]=Uo(e,t!=null?t:{}),{arity:s=1}=n,o=r.split(","),a=new Set(o);return rn({definition(l){l.addOption({names:o,arity:s,hidden:n==null?void 0:n.hidden,description:n==null?void 0:n.description,required:n.required})},transformer(l,c,u){let g=typeof i!="undefined"?[...i]:void 0;for(let{name:f,value:h}of u.options)!a.has(f)||(g=g!=null?g:[],g.push(h));return g}})}function vK(r,e,t){let[i,n]=Uo(e,t!=null?t:{}),s=r.split(","),o=new Set(s);return rn({definition(a){a.addOption({names:s,allowBinding:!1,arity:0,hidden:n.hidden,description:n.description,required:n.required})},transformer(a,l,c){let u=i;for(let{name:g,value:f}of c.options)!o.has(g)||(u=f);return u}})}function kK(r,e,t){let[i,n]=Uo(e,t!=null?t:{}),s=r.split(","),o=new Set(s);return rn({definition(a){a.addOption({names:s,allowBinding:!1,arity:0,hidden:n.hidden,description:n.description,required:n.required})},transformer(a,l,c){let u=i;for(let{name:g,value:f}of c.options)!o.has(g)||(u!=null||(u=0),f?u+=1:u=0);return u}})}function xK(r={}){return rn({definition(e,t){var i;e.addProxy({name:(i=r.name)!==null&&i!==void 0?i:t,required:r.required})},transformer(e,t,i){return i.positionals.map(({value:n})=>n)}})}function PK(r={}){return rn({definition(e,t){var i;e.addRest({name:(i=r.name)!==null&&i!==void 0?i:t,required:r.required})},transformer(e,t,i){let n=o=>{let a=i.positionals[o];return a.extra===$n||a.extra===!1&&oo)}})}function Mde(r,e,t){let[i,n]=Uo(e,t!=null?t:{}),{arity:s=1}=n,o=r.split(","),a=new Set(o);return rn({definition(l){l.addOption({names:o,arity:n.tolerateBoolean?0:s,hidden:n.hidden,description:n.description,required:n.required})},transformer(l,c,u){let g,f=i;for(let{name:h,value:p}of u.options)!a.has(h)||(g=h,f=p);return typeof f=="string"?gp(g!=null?g:c,f,n.validator):f}})}function Ude(r={}){let{required:e=!0}=r;return rn({definition(t,i){var n;t.addPositional({name:(n=r.name)!==null&&n!==void 0?n:i,required:r.required})},transformer(t,i,n){var s;for(let o=0;oz3,areIdentsEqual:()=>hd,areLocatorsEqual:()=>pd,areVirtualPackagesEquivalent:()=>cSe,bindDescriptor:()=>ASe,bindLocator:()=>lSe,convertDescriptorToLocator:()=>lw,convertLocatorToDescriptor:()=>Vk,convertPackageToLocator:()=>aSe,convertToIdent:()=>oSe,convertToManifestRange:()=>fSe,copyPackage:()=>ud,devirtualizeDescriptor:()=>gd,devirtualizeLocator:()=>fd,getIdentVendorPath:()=>tx,isPackageCompatible:()=>fw,isVirtualDescriptor:()=>ll,isVirtualLocator:()=>ta,makeDescriptor:()=>rr,makeIdent:()=>ea,makeLocator:()=>cn,makeRange:()=>uw,parseDescriptor:()=>cl,parseFileStyleRange:()=>uSe,parseIdent:()=>An,parseLocator:()=>qc,parseRange:()=>Jg,prettyDependent:()=>Tv,prettyDescriptor:()=>sr,prettyIdent:()=>fi,prettyLocator:()=>It,prettyLocatorNoColors:()=>ex,prettyRange:()=>Aw,prettyReference:()=>Cd,prettyResolution:()=>Ov,prettyWorkspace:()=>md,renamePackage:()=>cd,slugifyIdent:()=>$k,slugifyLocator:()=>Wg,sortDescriptors:()=>zg,stringifyDescriptor:()=>Pn,stringifyIdent:()=>Ot,stringifyLocator:()=>Fs,tryParseDescriptor:()=>dd,tryParseIdent:()=>_3,tryParseLocator:()=>cw,virtualizeDescriptor:()=>Xk,virtualizePackage:()=>Zk});var qg=ge(require("querystring")),q3=ge(ri()),J3=ge(AY());var ae={};ft(ae,{LogLevel:()=>Co,Style:()=>Oc,Type:()=>Ye,addLogFilterSupport:()=>sd,applyColor:()=>Dn,applyHyperlink:()=>Ug,applyStyle:()=>Fy,json:()=>Mc,jsonOrPretty:()=>jBe,mark:()=>jv,pretty:()=>tt,prettyField:()=>Xo,prettyList:()=>Hv,supportsColor:()=>Dy,supportsHyperlinks:()=>Uv,tuple:()=>po});var id=ge(gv()),nd=ge(yc());var lJ=ge(ns()),cJ=ge(Vq());var Se={};ft(Se,{AsyncActions:()=>iJ,BufferStream:()=>rJ,CachingStrategy:()=>Tc,DefaultStream:()=>nJ,allSettledSafe:()=>ho,assertNever:()=>Dv,bufferStream:()=>Og,buildIgnorePattern:()=>MBe,convertMapsToIndexableObjects:()=>Py,dynamicRequire:()=>Mg,escapeRegExp:()=>FBe,getArrayWithDefault:()=>Ng,getFactoryWithDefault:()=>Va,getMapWithDefault:()=>Lg,getSetWithDefault:()=>Lc,isIndexableObject:()=>Rv,isPathLike:()=>UBe,isTaggedYarnVersion:()=>RBe,mapAndFilter:()=>Vo,mapAndFind:()=>ed,overrideType:()=>Pv,parseBoolean:()=>rd,parseOptionalBoolean:()=>AJ,prettifyAsyncErrors:()=>Tg,prettifySyncErrors:()=>Fv,releaseAfterUseAsync:()=>LBe,replaceEnvVariables:()=>Nv,sortMap:()=>xn,tryParseOptionalBoolean:()=>Lv,validateEnum:()=>NBe});var Xq=ge(ns()),Zq=ge(fg()),$q=ge(ri()),xv=ge(require("stream"));function RBe(r){return!!($q.default.valid(r)&&r.match(/^[^-]+(-rc\.[0-9]+)?$/))}function FBe(r){return r.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Pv(r){}function Dv(r){throw new Error(`Assertion failed: Unexpected object '${r}'`)}function NBe(r,e){let t=Object.values(r);if(!t.includes(e))throw new Pe(`Invalid value for enumeration: ${JSON.stringify(e)} (expected one of ${t.map(i=>JSON.stringify(i)).join(", ")})`);return e}function Vo(r,e){let t=[];for(let i of r){let n=e(i);n!==eJ&&t.push(n)}return t}var eJ=Symbol();Vo.skip=eJ;function ed(r,e){for(let t of r){let i=e(t);if(i!==tJ)return i}}var tJ=Symbol();ed.skip=tJ;function Rv(r){return typeof r=="object"&&r!==null}async function ho(r){let e=await Promise.allSettled(r),t=[];for(let i of e){if(i.status==="rejected")throw i.reason;t.push(i.value)}return t}function Py(r){if(r instanceof Map&&(r=Object.fromEntries(r)),Rv(r))for(let e of Object.keys(r)){let t=r[e];Rv(t)&&(r[e]=Py(t))}return r}function Va(r,e,t){let i=r.get(e);return typeof i=="undefined"&&r.set(e,i=t()),i}function Ng(r,e){let t=r.get(e);return typeof t=="undefined"&&r.set(e,t=[]),t}function Lc(r,e){let t=r.get(e);return typeof t=="undefined"&&r.set(e,t=new Set),t}function Lg(r,e){let t=r.get(e);return typeof t=="undefined"&&r.set(e,t=new Map),t}async function LBe(r,e){if(e==null)return await r();try{return await r()}finally{await e()}}async function Tg(r,e){try{return await r()}catch(t){throw t.message=e(t.message),t}}function Fv(r,e){try{return r()}catch(t){throw t.message=e(t.message),t}}async function Og(r){return await new Promise((e,t)=>{let i=[];r.on("error",n=>{t(n)}),r.on("data",n=>{i.push(n)}),r.on("end",()=>{e(Buffer.concat(i))})})}var rJ=class extends xv.Transform{constructor(){super(...arguments);this.chunks=[]}_transform(e,t,i){if(t!=="buffer"||!Buffer.isBuffer(e))throw new Error("Assertion failed: BufferStream only accept buffers");this.chunks.push(e),i(null,null)}_flush(e){e(null,Buffer.concat(this.chunks))}};function TBe(){let r,e;return{promise:new Promise((i,n)=>{r=i,e=n}),resolve:r,reject:e}}var iJ=class{constructor(e){this.deferred=new Map;this.promises=new Map;this.limit=(0,Zq.default)(e)}set(e,t){let i=this.deferred.get(e);typeof i=="undefined"&&this.deferred.set(e,i=TBe());let n=this.limit(()=>t());return this.promises.set(e,n),n.then(()=>{this.promises.get(e)===n&&i.resolve()},s=>{this.promises.get(e)===n&&i.reject(s)}),i.promise}reduce(e,t){var n;let i=(n=this.promises.get(e))!=null?n:Promise.resolve();this.set(e,()=>t(i))}async wait(){await Promise.all(this.promises.values())}},nJ=class extends xv.Transform{constructor(e=Buffer.alloc(0)){super();this.active=!0;this.ifEmpty=e}_transform(e,t,i){if(t!=="buffer"||!Buffer.isBuffer(e))throw new Error("Assertion failed: DefaultStream only accept buffers");this.active=!1,i(null,e)}_flush(e){this.active&&this.ifEmpty.length>0?e(null,this.ifEmpty):e(null)}},td=eval("require");function sJ(r){return td(H.fromPortablePath(r))}function oJ(path){let physicalPath=H.fromPortablePath(path),currentCacheEntry=td.cache[physicalPath];delete td.cache[physicalPath];let result;try{result=sJ(physicalPath);let freshCacheEntry=td.cache[physicalPath],dynamicModule=eval("module"),freshCacheIndex=dynamicModule.children.indexOf(freshCacheEntry);freshCacheIndex!==-1&&dynamicModule.children.splice(freshCacheIndex,1)}finally{td.cache[physicalPath]=currentCacheEntry}return result}var aJ=new Map;function OBe(r){let e=aJ.get(r),t=U.statSync(r);if((e==null?void 0:e.mtime)===t.mtimeMs)return e.instance;let i=oJ(r);return aJ.set(r,{mtime:t.mtimeMs,instance:i}),i}var Tc;(function(i){i[i.NoCache=0]="NoCache",i[i.FsTime=1]="FsTime",i[i.Node=2]="Node"})(Tc||(Tc={}));function Mg(r,{cachingStrategy:e=2}={}){switch(e){case 0:return oJ(r);case 1:return OBe(r);case 2:return sJ(r);default:throw new Error("Unsupported caching strategy")}}function xn(r,e){let t=Array.from(r);Array.isArray(e)||(e=[e]);let i=[];for(let s of e)i.push(t.map(o=>s(o)));let n=t.map((s,o)=>o);return n.sort((s,o)=>{for(let a of i){let l=a[s]a[o]?1:0;if(l!==0)return l}return 0}),n.map(s=>t[s])}function MBe(r){return r.length===0?null:r.map(e=>`(${Xq.default.makeRe(e,{windows:!1,dot:!0}).source})`).join("|")}function Nv(r,{env:e}){let t=/\${(?[\d\w_]+)(?:)?(?:-(?[^}]*))?}/g;return r.replace(t,(...i)=>{let{variableName:n,colon:s,fallback:o}=i[i.length-1],a=Object.prototype.hasOwnProperty.call(e,n),l=e[n];if(l||a&&!s)return l;if(o!=null)return o;throw new Pe(`Environment variable not found (${n})`)})}function rd(r){switch(r){case"true":case"1":case 1:case!0:return!0;case"false":case"0":case 0:case!1:return!1;default:throw new Error(`Couldn't parse "${r}" as a boolean`)}}function AJ(r){return typeof r=="undefined"?r:rd(r)}function Lv(r){try{return AJ(r)}catch{return null}}function UBe(r){return!!(H.isAbsolute(r)||r.match(/^(\.{1,2}|~)\//))}var Qt;(function(t){t.HARD="HARD",t.SOFT="SOFT"})(Qt||(Qt={}));var wi;(function(i){i.Dependency="Dependency",i.PeerDependency="PeerDependency",i.PeerDependencyMeta="PeerDependencyMeta"})(wi||(wi={}));var qi;(function(i){i.Inactive="inactive",i.Redundant="redundant",i.Active="active"})(qi||(qi={}));var Ye={NO_HINT:"NO_HINT",NULL:"NULL",SCOPE:"SCOPE",NAME:"NAME",RANGE:"RANGE",REFERENCE:"REFERENCE",NUMBER:"NUMBER",PATH:"PATH",URL:"URL",ADDED:"ADDED",REMOVED:"REMOVED",CODE:"CODE",DURATION:"DURATION",SIZE:"SIZE",IDENT:"IDENT",DESCRIPTOR:"DESCRIPTOR",LOCATOR:"LOCATOR",RESOLUTION:"RESOLUTION",DEPENDENT:"DEPENDENT",PACKAGE_EXTENSION:"PACKAGE_EXTENSION",SETTING:"SETTING",MARKDOWN:"MARKDOWN"},Oc;(function(e){e[e.BOLD=2]="BOLD"})(Oc||(Oc={}));var Mv=nd.default.GITHUB_ACTIONS?{level:2}:id.default.supportsColor?{level:id.default.supportsColor.level}:{level:0},Dy=Mv.level!==0,Uv=Dy&&!nd.default.GITHUB_ACTIONS&&!nd.default.CIRCLE&&!nd.default.GITLAB,Kv=new id.default.Instance(Mv),KBe=new Map([[Ye.NO_HINT,null],[Ye.NULL,["#a853b5",129]],[Ye.SCOPE,["#d75f00",166]],[Ye.NAME,["#d7875f",173]],[Ye.RANGE,["#00afaf",37]],[Ye.REFERENCE,["#87afff",111]],[Ye.NUMBER,["#ffd700",220]],[Ye.PATH,["#d75fd7",170]],[Ye.URL,["#d75fd7",170]],[Ye.ADDED,["#5faf00",70]],[Ye.REMOVED,["#d70000",160]],[Ye.CODE,["#87afff",111]],[Ye.SIZE,["#ffd700",220]]]),Ns=r=>r,Ry={[Ye.NUMBER]:Ns({pretty:(r,e)=>Dn(r,`${e}`,Ye.NUMBER),json:r=>r}),[Ye.IDENT]:Ns({pretty:(r,e)=>fi(r,e),json:r=>Ot(r)}),[Ye.LOCATOR]:Ns({pretty:(r,e)=>It(r,e),json:r=>Fs(r)}),[Ye.DESCRIPTOR]:Ns({pretty:(r,e)=>sr(r,e),json:r=>Pn(r)}),[Ye.RESOLUTION]:Ns({pretty:(r,{descriptor:e,locator:t})=>Ov(r,e,t),json:({descriptor:r,locator:e})=>({descriptor:Pn(r),locator:e!==null?Fs(e):null})}),[Ye.DEPENDENT]:Ns({pretty:(r,{locator:e,descriptor:t})=>Tv(r,e,t),json:({locator:r,descriptor:e})=>({locator:Fs(r),descriptor:Pn(e)})}),[Ye.PACKAGE_EXTENSION]:Ns({pretty:(r,e)=>{switch(e.type){case wi.Dependency:return`${fi(r,e.parentDescriptor)} \u27A4 ${Dn(r,"dependencies",Ye.CODE)} \u27A4 ${fi(r,e.descriptor)}`;case wi.PeerDependency:return`${fi(r,e.parentDescriptor)} \u27A4 ${Dn(r,"peerDependencies",Ye.CODE)} \u27A4 ${fi(r,e.descriptor)}`;case wi.PeerDependencyMeta:return`${fi(r,e.parentDescriptor)} \u27A4 ${Dn(r,"peerDependenciesMeta",Ye.CODE)} \u27A4 ${fi(r,An(e.selector))} \u27A4 ${Dn(r,e.key,Ye.CODE)}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${e.type}`)}},json:r=>{switch(r.type){case wi.Dependency:return`${Ot(r.parentDescriptor)} > ${Ot(r.descriptor)}`;case wi.PeerDependency:return`${Ot(r.parentDescriptor)} >> ${Ot(r.descriptor)}`;case wi.PeerDependencyMeta:return`${Ot(r.parentDescriptor)} >> ${r.selector} / ${r.key}`;default:throw new Error(`Assertion failed: Unsupported package extension type: ${r.type}`)}}}),[Ye.SETTING]:Ns({pretty:(r,e)=>(r.get(e),Ug(r,Dn(r,e,Ye.CODE),`https://yarnpkg.com/configuration/yarnrc#${e}`)),json:r=>r}),[Ye.DURATION]:Ns({pretty:(r,e)=>{if(e>1e3*60){let t=Math.floor(e/1e3/60),i=Math.ceil((e-t*60*1e3)/1e3);return i===0?`${t}m`:`${t}m ${i}s`}else{let t=Math.floor(e/1e3),i=e-t*1e3;return i===0?`${t}s`:`${t}s ${i}ms`}},json:r=>r}),[Ye.SIZE]:Ns({pretty:(r,e)=>{let t=["KB","MB","GB","TB"],i=t.length;for(;i>1&&e<1024**i;)i-=1;let n=1024**i,s=Math.floor(e*100/n)/100;return Dn(r,`${s} ${t[i-1]}`,Ye.NUMBER)},json:r=>r}),[Ye.PATH]:Ns({pretty:(r,e)=>Dn(r,H.fromPortablePath(e),Ye.PATH),json:r=>H.fromPortablePath(r)}),[Ye.MARKDOWN]:Ns({pretty:(r,{text:e,format:t,paragraphs:i})=>Ki(e,{format:t,paragraphs:i}),json:({text:r})=>r})};function po(r,e){return[e,r]}function Fy(r,e,t){return r.get("enableColors")&&t&2&&(e=id.default.bold(e)),e}function Dn(r,e,t){if(!r.get("enableColors"))return e;let i=KBe.get(t);if(i===null)return e;let n=typeof i=="undefined"?t:Mv.level>=3?i[0]:i[1],s=typeof n=="number"?Kv.ansi256(n):n.startsWith("#")?Kv.hex(n):Kv[n];if(typeof s!="function")throw new Error(`Invalid format type ${n}`);return s(e)}var HBe=!!process.env.KONSOLE_VERSION;function Ug(r,e,t){return r.get("enableHyperlinks")?HBe?`]8;;${t}\\${e}]8;;\\`:`]8;;${t}\x07${e}]8;;\x07`:e}function tt(r,e,t){if(e===null)return Dn(r,"null",Ye.NULL);if(Object.prototype.hasOwnProperty.call(Ry,t))return Ry[t].pretty(r,e);if(typeof e!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof e}`);return Dn(r,e,t)}function Hv(r,e,t,{separator:i=", "}={}){return[...e].map(n=>tt(r,n,t)).join(i)}function Mc(r,e){if(r===null)return null;if(Object.prototype.hasOwnProperty.call(Ry,e))return Pv(e),Ry[e].json(r);if(typeof r!="string")throw new Error(`Assertion failed: Expected the value to be a string, got ${typeof r}`);return r}function jBe(r,e,[t,i]){return r?Mc(t,i):tt(e,t,i)}function jv(r){return{Check:Dn(r,"\u2713","green"),Cross:Dn(r,"\u2718","red"),Question:Dn(r,"?","cyan")}}function Xo(r,{label:e,value:[t,i]}){return`${tt(r,e,Ye.CODE)}: ${tt(r,t,i)}`}var Co;(function(n){n.Error="error",n.Warning="warning",n.Info="info",n.Discard="discard"})(Co||(Co={}));function sd(r,{configuration:e}){let t=e.get("logFilters"),i=new Map,n=new Map,s=[];for(let g of t){let f=g.get("level");if(typeof f=="undefined")continue;let h=g.get("code");typeof h!="undefined"&&i.set(h,f);let p=g.get("text");typeof p!="undefined"&&n.set(p,f);let m=g.get("pattern");typeof m!="undefined"&&s.push([lJ.default.matcher(m,{contains:!0}),f])}s.reverse();let o=(g,f,h)=>{if(g===null||g===X.UNNAMED)return h;let p=n.size>0||s.length>0?(0,cJ.default)(f):f;if(n.size>0){let m=n.get(p);if(typeof m!="undefined")return m!=null?m:h}if(s.length>0){for(let[m,y]of s)if(m(p))return y!=null?y:h}if(i.size>0){let m=i.get(VA(g));if(typeof m!="undefined")return m!=null?m:h}return h},a=r.reportInfo,l=r.reportWarning,c=r.reportError,u=function(g,f,h,p){switch(o(f,h,p)){case Co.Info:a.call(g,f,h);break;case Co.Warning:l.call(g,f!=null?f:X.UNNAMED,h);break;case Co.Error:c.call(g,f!=null?f:X.UNNAMED,h);break}};r.reportInfo=function(...g){return u(this,...g,Co.Info)},r.reportWarning=function(...g){return u(this,...g,Co.Warning)},r.reportError=function(...g){return u(this,...g,Co.Error)}}var Rn={};ft(Rn,{checksumFile:()=>ow,checksumPattern:()=>aw,makeHash:()=>ln});var sw=ge(require("crypto")),_k=ge(zk());function ln(...r){let e=(0,sw.createHash)("sha512"),t="";for(let i of r)typeof i=="string"?t+=i:i&&(t&&(e.update(t),t=""),e.update(i));return t&&e.update(t),e.digest("hex")}async function ow(r,{baseFs:e,algorithm:t}={baseFs:U,algorithm:"sha512"}){let i=await e.openPromise(r,"r");try{let n=65536,s=Buffer.allocUnsafeSlow(n),o=(0,sw.createHash)(t),a=0;for(;(a=await e.readPromise(i,s,0,n))!==0;)o.update(a===n?s:s.slice(0,a));return o.digest("hex")}finally{await e.closePromise(i)}}async function aw(r,{cwd:e}){let i=(await(0,_k.default)(r,{cwd:H.fromPortablePath(e),expandDirectories:!1,onlyDirectories:!0,unique:!0})).map(a=>`${a}/**/*`),n=await(0,_k.default)([r,...i],{cwd:H.fromPortablePath(e),expandDirectories:!1,onlyFiles:!1,unique:!0});n.sort();let s=await Promise.all(n.map(async a=>{let l=[Buffer.from(a)],c=H.toPortablePath(a),u=await U.lstatPromise(c);return u.isSymbolicLink()?l.push(Buffer.from(await U.readlinkPromise(c))):u.isFile()&&l.push(await U.readFilePromise(c)),l.join("\0")})),o=(0,sw.createHash)("sha512");for(let a of s)o.update(a);return o.digest("hex")}var ld="virtual:",nSe=5,W3=/(os|cpu|libc)=([a-z0-9_-]+)/,sSe=(0,J3.makeParser)(W3);function ea(r,e){if(r==null?void 0:r.startsWith("@"))throw new Error("Invalid scope: don't prefix it with '@'");return{identHash:ln(r,e),scope:r,name:e}}function rr(r,e){return{identHash:r.identHash,scope:r.scope,name:r.name,descriptorHash:ln(r.identHash,e),range:e}}function cn(r,e){return{identHash:r.identHash,scope:r.scope,name:r.name,locatorHash:ln(r.identHash,e),reference:e}}function oSe(r){return{identHash:r.identHash,scope:r.scope,name:r.name}}function lw(r){return{identHash:r.identHash,scope:r.scope,name:r.name,locatorHash:r.descriptorHash,reference:r.range}}function Vk(r){return{identHash:r.identHash,scope:r.scope,name:r.name,descriptorHash:r.locatorHash,range:r.reference}}function aSe(r){return{identHash:r.identHash,scope:r.scope,name:r.name,locatorHash:r.locatorHash,reference:r.reference}}function cd(r,e){return{identHash:e.identHash,scope:e.scope,name:e.name,locatorHash:e.locatorHash,reference:e.reference,version:r.version,languageName:r.languageName,linkType:r.linkType,conditions:r.conditions,dependencies:new Map(r.dependencies),peerDependencies:new Map(r.peerDependencies),dependenciesMeta:new Map(r.dependenciesMeta),peerDependenciesMeta:new Map(r.peerDependenciesMeta),bin:new Map(r.bin)}}function ud(r){return cd(r,r)}function Xk(r,e){if(e.includes("#"))throw new Error("Invalid entropy");return rr(r,`virtual:${e}#${r.range}`)}function Zk(r,e){if(e.includes("#"))throw new Error("Invalid entropy");return cd(r,cn(r,`virtual:${e}#${r.reference}`))}function ll(r){return r.range.startsWith(ld)}function ta(r){return r.reference.startsWith(ld)}function gd(r){if(!ll(r))throw new Error("Not a virtual descriptor");return rr(r,r.range.replace(/^[^#]*#/,""))}function fd(r){if(!ta(r))throw new Error("Not a virtual descriptor");return cn(r,r.reference.replace(/^[^#]*#/,""))}function ASe(r,e){return r.range.includes("::")?r:rr(r,`${r.range}::${qg.default.stringify(e)}`)}function lSe(r,e){return r.reference.includes("::")?r:cn(r,`${r.reference}::${qg.default.stringify(e)}`)}function hd(r,e){return r.identHash===e.identHash}function z3(r,e){return r.descriptorHash===e.descriptorHash}function pd(r,e){return r.locatorHash===e.locatorHash}function cSe(r,e){if(!ta(r))throw new Error("Invalid package type");if(!ta(e))throw new Error("Invalid package type");if(!hd(r,e)||r.dependencies.size!==e.dependencies.size)return!1;for(let t of r.dependencies.values()){let i=e.dependencies.get(t.identHash);if(!i||!z3(t,i))return!1}return!0}function An(r){let e=_3(r);if(!e)throw new Error(`Invalid ident (${r})`);return e}function _3(r){let e=r.match(/^(?:@([^/]+?)\/)?([^/]+)$/);if(!e)return null;let[,t,i]=e,n=typeof t!="undefined"?t:null;return ea(n,i)}function cl(r,e=!1){let t=dd(r,e);if(!t)throw new Error(`Invalid descriptor (${r})`);return t}function dd(r,e=!1){let t=e?r.match(/^(?:@([^/]+?)\/)?([^/]+?)(?:@(.+))$/):r.match(/^(?:@([^/]+?)\/)?([^/]+?)(?:@(.+))?$/);if(!t)return null;let[,i,n,s]=t;if(s==="unknown")throw new Error(`Invalid range (${r})`);let o=typeof i!="undefined"?i:null,a=typeof s!="undefined"?s:"unknown";return rr(ea(o,n),a)}function qc(r,e=!1){let t=cw(r,e);if(!t)throw new Error(`Invalid locator (${r})`);return t}function cw(r,e=!1){let t=e?r.match(/^(?:@([^/]+?)\/)?([^/]+?)(?:@(.+))$/):r.match(/^(?:@([^/]+?)\/)?([^/]+?)(?:@(.+))?$/);if(!t)return null;let[,i,n,s]=t;if(s==="unknown")throw new Error(`Invalid reference (${r})`);let o=typeof i!="undefined"?i:null,a=typeof s!="undefined"?s:"unknown";return cn(ea(o,n),a)}function Jg(r,e){let t=r.match(/^([^#:]*:)?((?:(?!::)[^#])*)(?:#((?:(?!::).)*))?(?:::(.*))?$/);if(t===null)throw new Error(`Invalid range (${r})`);let i=typeof t[1]!="undefined"?t[1]:null;if(typeof(e==null?void 0:e.requireProtocol)=="string"&&i!==e.requireProtocol)throw new Error(`Invalid protocol (${i})`);if((e==null?void 0:e.requireProtocol)&&i===null)throw new Error(`Missing protocol (${i})`);let n=typeof t[3]!="undefined"?decodeURIComponent(t[2]):null;if((e==null?void 0:e.requireSource)&&n===null)throw new Error(`Missing source (${r})`);let s=typeof t[3]!="undefined"?decodeURIComponent(t[3]):decodeURIComponent(t[2]),o=(e==null?void 0:e.parseSelector)?qg.default.parse(s):s,a=typeof t[4]!="undefined"?qg.default.parse(t[4]):null;return{protocol:i,source:n,selector:o,params:a}}function uSe(r,{protocol:e}){let{selector:t,params:i}=Jg(r,{requireProtocol:e,requireBindings:!0});if(typeof i.locator!="string")throw new Error(`Assertion failed: Invalid bindings for ${r}`);return{parentLocator:qc(i.locator,!0),path:t}}function V3(r){return r=r.replace(/%/g,"%25"),r=r.replace(/:/g,"%3A"),r=r.replace(/#/g,"%23"),r}function gSe(r){return r===null?!1:Object.entries(r).length>0}function uw({protocol:r,source:e,selector:t,params:i}){let n="";return r!==null&&(n+=`${r}`),e!==null&&(n+=`${V3(e)}#`),n+=V3(t),gSe(i)&&(n+=`::${qg.default.stringify(i)}`),n}function fSe(r){let{params:e,protocol:t,source:i,selector:n}=Jg(r);for(let s in e)s.startsWith("__")&&delete e[s];return uw({protocol:t,source:i,params:e,selector:n})}function Ot(r){return r.scope?`@${r.scope}/${r.name}`:`${r.name}`}function Pn(r){return r.scope?`@${r.scope}/${r.name}@${r.range}`:`${r.name}@${r.range}`}function Fs(r){return r.scope?`@${r.scope}/${r.name}@${r.reference}`:`${r.name}@${r.reference}`}function $k(r){return r.scope!==null?`@${r.scope}-${r.name}`:r.name}function Wg(r){let{protocol:e,selector:t}=Jg(r.reference),i=e!==null?e.replace(/:$/,""):"exotic",n=q3.default.valid(t),s=n!==null?`${i}-${n}`:`${i}`,o=10,a=r.scope?`${$k(r)}-${s}-${r.locatorHash.slice(0,o)}`:`${$k(r)}-${s}-${r.locatorHash.slice(0,o)}`;return Jr(a)}function fi(r,e){return e.scope?`${tt(r,`@${e.scope}/`,Ye.SCOPE)}${tt(r,e.name,Ye.NAME)}`:`${tt(r,e.name,Ye.NAME)}`}function gw(r){if(r.startsWith(ld)){let e=gw(r.substring(r.indexOf("#")+1)),t=r.substring(ld.length,ld.length+nSe);return`${e} [${t}]`}else return r.replace(/\?.*/,"?[...]")}function Aw(r,e){return`${tt(r,gw(e),Ye.RANGE)}`}function sr(r,e){return`${fi(r,e)}${tt(r,"@",Ye.RANGE)}${Aw(r,e.range)}`}function Cd(r,e){return`${tt(r,gw(e),Ye.REFERENCE)}`}function It(r,e){return`${fi(r,e)}${tt(r,"@",Ye.REFERENCE)}${Cd(r,e.reference)}`}function ex(r){return`${Ot(r)}@${gw(r.reference)}`}function zg(r){return xn(r,[e=>Ot(e),e=>e.range])}function md(r,e){return fi(r,e.locator)}function Ov(r,e,t){let i=ll(e)?gd(e):e;return t===null?`${sr(r,i)} \u2192 ${jv(r).Cross}`:i.identHash===t.identHash?`${sr(r,i)} \u2192 ${Cd(r,t.reference)}`:`${sr(r,i)} \u2192 ${It(r,t)}`}function Tv(r,e,t){return t===null?`${It(r,e)}`:`${It(r,e)} (via ${Aw(r,t.range)})`}function tx(r){return`node_modules/${Ot(r)}`}function fw(r,e){return r.conditions?sSe(r.conditions,t=>{let[,i,n]=t.match(W3),s=e[i];return s?s.includes(n):!0}):!0}var X3={hooks:{reduceDependency:(r,e,t,i,{resolver:n,resolveOptions:s})=>{for(let{pattern:o,reference:a}of e.topLevelWorkspace.manifest.resolutions){if(o.from&&o.from.fullName!==Ot(t)||o.from&&o.from.description&&o.from.description!==t.reference||o.descriptor.fullName!==Ot(r)||o.descriptor.description&&o.descriptor.description!==r.range)continue;return n.bindDescriptor(rr(r,a),e.topLevelWorkspace.anchoredLocator,s)}return r},validateProject:async(r,e)=>{for(let t of r.workspaces){let i=md(r.configuration,t);await r.configuration.triggerHook(n=>n.validateWorkspace,t,{reportWarning:(n,s)=>e.reportWarning(n,`${i}: ${s}`),reportError:(n,s)=>e.reportError(n,`${i}: ${s}`)})}},validateWorkspace:async(r,e)=>{let{manifest:t}=r;t.resolutions.length&&r.cwd!==r.project.cwd&&t.errors.push(new Error("Resolutions field will be ignored"));for(let i of t.errors)e.reportWarning(X.INVALID_MANIFEST,i.message)}}};var t4=ge(ri());var Ed=class{supportsDescriptor(e,t){return!!(e.range.startsWith(Ed.protocol)||t.project.tryWorkspaceByDescriptor(e)!==null)}supportsLocator(e,t){return!!e.reference.startsWith(Ed.protocol)}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){return[i.project.getWorkspaceByDescriptor(e).anchoredLocator]}async getSatisfying(e,t,i){return null}async resolve(e,t){let i=t.project.getWorkspaceByCwd(e.reference.slice(Ed.protocol.length));return te(N({},e),{version:i.manifest.version||"0.0.0",languageName:"unknown",linkType:Qt.SOFT,conditions:null,dependencies:new Map([...i.manifest.dependencies,...i.manifest.devDependencies]),peerDependencies:new Map([...i.manifest.peerDependencies]),dependenciesMeta:i.manifest.dependenciesMeta,peerDependenciesMeta:i.manifest.peerDependenciesMeta,bin:i.manifest.bin})}},oi=Ed;oi.protocol="workspace:";var Wt={};ft(Wt,{SemVer:()=>Z3.SemVer,clean:()=>pSe,satisfiesWithPrereleases:()=>Jc,validRange:()=>mo});var hw=ge(ri()),Z3=ge(ri()),$3=new Map;function Jc(r,e,t=!1){if(!r)return!1;let i=`${e}${t}`,n=$3.get(i);if(typeof n=="undefined")try{n=new hw.default.Range(e,{includePrerelease:!0,loose:t})}catch{return!1}finally{$3.set(i,n||null)}else if(n===null)return!1;let s;try{s=new hw.default.SemVer(r,n)}catch(o){return!1}return n.test(s)?!0:(s.prerelease&&(s.prerelease=[]),n.set.some(o=>{for(let a of o)a.semver.prerelease&&(a.semver.prerelease=[]);return o.every(a=>a.test(s))}))}var e4=new Map;function mo(r){if(r.indexOf(":")!==-1)return null;let e=e4.get(r);if(typeof e!="undefined")return e;try{e=new hw.default.Range(r)}catch{e=null}return e4.set(r,e),e}var hSe=/^(?:[\sv=]*?)((0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)(?:\s*)$/;function pSe(r){let e=hSe.exec(r);return e?e[1]:null}var ul=class{constructor(){this.indent=" ";this.name=null;this.version=null;this.os=null;this.cpu=null;this.libc=null;this.type=null;this.packageManager=null;this.private=!1;this.license=null;this.main=null;this.module=null;this.browser=null;this.languageName=null;this.bin=new Map;this.scripts=new Map;this.dependencies=new Map;this.devDependencies=new Map;this.peerDependencies=new Map;this.workspaceDefinitions=[];this.dependenciesMeta=new Map;this.peerDependenciesMeta=new Map;this.resolutions=[];this.files=null;this.publishConfig=null;this.installConfig=null;this.preferUnplugged=null;this.raw={};this.errors=[]}static async tryFind(e,{baseFs:t=new ar}={}){let i=x.join(e,"package.json");try{return await ul.fromFile(i,{baseFs:t})}catch(n){if(n.code==="ENOENT")return null;throw n}}static async find(e,{baseFs:t}={}){let i=await ul.tryFind(e,{baseFs:t});if(i===null)throw new Error("Manifest not found");return i}static async fromFile(e,{baseFs:t=new ar}={}){let i=new ul;return await i.loadFile(e,{baseFs:t}),i}static fromText(e){let t=new ul;return t.loadFromText(e),t}static isManifestFieldCompatible(e,t){if(e===null)return!0;let i=!0,n=!1;for(let s of e)if(s[0]==="!"){if(n=!0,t===s.slice(1))return!1}else if(i=!1,s===t)return!0;return n&&i}loadFromText(e){let t;try{t=JSON.parse(i4(e)||"{}")}catch(i){throw i.message+=` (when parsing ${e})`,i}this.load(t),this.indent=r4(e)}async loadFile(e,{baseFs:t=new ar}){let i=await t.readFilePromise(e,"utf8"),n;try{n=JSON.parse(i4(i)||"{}")}catch(s){throw s.message+=` (when parsing ${e})`,s}this.load(n),this.indent=r4(i)}load(e,{yamlCompatibilityMode:t=!1}={}){if(typeof e!="object"||e===null)throw new Error(`Utterly invalid manifest data (${e})`);this.raw=e;let i=[];if(this.name=null,typeof e.name=="string")try{this.name=An(e.name)}catch(s){i.push(new Error("Parsing failed for the 'name' field"))}if(typeof e.version=="string"?this.version=e.version:this.version=null,Array.isArray(e.os)){let s=[];this.os=s;for(let o of e.os)typeof o!="string"?i.push(new Error("Parsing failed for the 'os' field")):s.push(o)}else this.os=null;if(Array.isArray(e.cpu)){let s=[];this.cpu=s;for(let o of e.cpu)typeof o!="string"?i.push(new Error("Parsing failed for the 'cpu' field")):s.push(o)}else this.cpu=null;if(Array.isArray(e.libc)){let s=[];this.libc=s;for(let o of e.libc)typeof o!="string"?i.push(new Error("Parsing failed for the 'libc' field")):s.push(o)}else this.libc=null;if(typeof e.type=="string"?this.type=e.type:this.type=null,typeof e.packageManager=="string"?this.packageManager=e.packageManager:this.packageManager=null,typeof e.private=="boolean"?this.private=e.private:this.private=!1,typeof e.license=="string"?this.license=e.license:this.license=null,typeof e.languageName=="string"?this.languageName=e.languageName:this.languageName=null,typeof e.main=="string"?this.main=un(e.main):this.main=null,typeof e.module=="string"?this.module=un(e.module):this.module=null,e.browser!=null)if(typeof e.browser=="string")this.browser=un(e.browser);else{this.browser=new Map;for(let[s,o]of Object.entries(e.browser))this.browser.set(un(s),typeof o=="string"?un(o):o)}else this.browser=null;if(this.bin=new Map,typeof e.bin=="string")this.name!==null?this.bin.set(this.name.name,un(e.bin)):i.push(new Error("String bin field, but no attached package name"));else if(typeof e.bin=="object"&&e.bin!==null)for(let[s,o]of Object.entries(e.bin)){if(typeof o!="string"){i.push(new Error(`Invalid bin definition for '${s}'`));continue}let a=An(s);this.bin.set(a.name,un(o))}if(this.scripts=new Map,typeof e.scripts=="object"&&e.scripts!==null)for(let[s,o]of Object.entries(e.scripts)){if(typeof o!="string"){i.push(new Error(`Invalid script definition for '${s}'`));continue}this.scripts.set(s,o)}if(this.dependencies=new Map,typeof e.dependencies=="object"&&e.dependencies!==null)for(let[s,o]of Object.entries(e.dependencies)){if(typeof o!="string"){i.push(new Error(`Invalid dependency range for '${s}'`));continue}let a;try{a=An(s)}catch(c){i.push(new Error(`Parsing failed for the dependency name '${s}'`));continue}let l=rr(a,o);this.dependencies.set(l.identHash,l)}if(this.devDependencies=new Map,typeof e.devDependencies=="object"&&e.devDependencies!==null)for(let[s,o]of Object.entries(e.devDependencies)){if(typeof o!="string"){i.push(new Error(`Invalid dependency range for '${s}'`));continue}let a;try{a=An(s)}catch(c){i.push(new Error(`Parsing failed for the dependency name '${s}'`));continue}let l=rr(a,o);this.devDependencies.set(l.identHash,l)}if(this.peerDependencies=new Map,typeof e.peerDependencies=="object"&&e.peerDependencies!==null)for(let[s,o]of Object.entries(e.peerDependencies)){let a;try{a=An(s)}catch(c){i.push(new Error(`Parsing failed for the dependency name '${s}'`));continue}(typeof o!="string"||!o.startsWith(oi.protocol)&&!mo(o))&&(i.push(new Error(`Invalid dependency range for '${s}'`)),o="*");let l=rr(a,o);this.peerDependencies.set(l.identHash,l)}typeof e.workspaces=="object"&&e.workspaces!==null&&e.workspaces.nohoist&&i.push(new Error("'nohoist' is deprecated, please use 'installConfig.hoistingLimits' instead"));let n=Array.isArray(e.workspaces)?e.workspaces:typeof e.workspaces=="object"&&e.workspaces!==null&&Array.isArray(e.workspaces.packages)?e.workspaces.packages:[];this.workspaceDefinitions=[];for(let s of n){if(typeof s!="string"){i.push(new Error(`Invalid workspace definition for '${s}'`));continue}this.workspaceDefinitions.push({pattern:s})}if(this.dependenciesMeta=new Map,typeof e.dependenciesMeta=="object"&&e.dependenciesMeta!==null)for(let[s,o]of Object.entries(e.dependenciesMeta)){if(typeof o!="object"||o===null){i.push(new Error(`Invalid meta field for '${s}`));continue}let a=cl(s),l=this.ensureDependencyMeta(a),c=pw(o.built,{yamlCompatibilityMode:t});if(c===null){i.push(new Error(`Invalid built meta field for '${s}'`));continue}let u=pw(o.optional,{yamlCompatibilityMode:t});if(u===null){i.push(new Error(`Invalid optional meta field for '${s}'`));continue}let g=pw(o.unplugged,{yamlCompatibilityMode:t});if(g===null){i.push(new Error(`Invalid unplugged meta field for '${s}'`));continue}Object.assign(l,{built:c,optional:u,unplugged:g})}if(this.peerDependenciesMeta=new Map,typeof e.peerDependenciesMeta=="object"&&e.peerDependenciesMeta!==null)for(let[s,o]of Object.entries(e.peerDependenciesMeta)){if(typeof o!="object"||o===null){i.push(new Error(`Invalid meta field for '${s}'`));continue}let a=cl(s),l=this.ensurePeerDependencyMeta(a),c=pw(o.optional,{yamlCompatibilityMode:t});if(c===null){i.push(new Error(`Invalid optional meta field for '${s}'`));continue}Object.assign(l,{optional:c})}if(this.resolutions=[],typeof e.resolutions=="object"&&e.resolutions!==null)for(let[s,o]of Object.entries(e.resolutions)){if(typeof o!="string"){i.push(new Error(`Invalid resolution entry for '${s}'`));continue}try{this.resolutions.push({pattern:eI(s),reference:o})}catch(a){i.push(a);continue}}if(Array.isArray(e.files)){this.files=new Set;for(let s of e.files){if(typeof s!="string"){i.push(new Error(`Invalid files entry for '${s}'`));continue}this.files.add(s)}}else this.files=null;if(typeof e.publishConfig=="object"&&e.publishConfig!==null){if(this.publishConfig={},typeof e.publishConfig.access=="string"&&(this.publishConfig.access=e.publishConfig.access),typeof e.publishConfig.main=="string"&&(this.publishConfig.main=un(e.publishConfig.main)),typeof e.publishConfig.module=="string"&&(this.publishConfig.module=un(e.publishConfig.module)),e.publishConfig.browser!=null)if(typeof e.publishConfig.browser=="string")this.publishConfig.browser=un(e.publishConfig.browser);else{this.publishConfig.browser=new Map;for(let[s,o]of Object.entries(e.publishConfig.browser))this.publishConfig.browser.set(un(s),typeof o=="string"?un(o):o)}if(typeof e.publishConfig.registry=="string"&&(this.publishConfig.registry=e.publishConfig.registry),typeof e.publishConfig.bin=="string")this.name!==null?this.publishConfig.bin=new Map([[this.name.name,un(e.publishConfig.bin)]]):i.push(new Error("String bin field, but no attached package name"));else if(typeof e.publishConfig.bin=="object"&&e.publishConfig.bin!==null){this.publishConfig.bin=new Map;for(let[s,o]of Object.entries(e.publishConfig.bin)){if(typeof o!="string"){i.push(new Error(`Invalid bin definition for '${s}'`));continue}this.publishConfig.bin.set(s,un(o))}}if(Array.isArray(e.publishConfig.executableFiles)){this.publishConfig.executableFiles=new Set;for(let s of e.publishConfig.executableFiles){if(typeof s!="string"){i.push(new Error("Invalid executable file definition"));continue}this.publishConfig.executableFiles.add(un(s))}}}else this.publishConfig=null;if(typeof e.installConfig=="object"&&e.installConfig!==null){this.installConfig={};for(let s of Object.keys(e.installConfig))s==="hoistingLimits"?typeof e.installConfig.hoistingLimits=="string"?this.installConfig.hoistingLimits=e.installConfig.hoistingLimits:i.push(new Error("Invalid hoisting limits definition")):s=="selfReferences"?typeof e.installConfig.selfReferences=="boolean"?this.installConfig.selfReferences=e.installConfig.selfReferences:i.push(new Error("Invalid selfReferences definition, must be a boolean value")):i.push(new Error(`Unrecognized installConfig key: ${s}`))}else this.installConfig=null;if(typeof e.optionalDependencies=="object"&&e.optionalDependencies!==null)for(let[s,o]of Object.entries(e.optionalDependencies)){if(typeof o!="string"){i.push(new Error(`Invalid dependency range for '${s}'`));continue}let a;try{a=An(s)}catch(g){i.push(new Error(`Parsing failed for the dependency name '${s}'`));continue}let l=rr(a,o);this.dependencies.set(l.identHash,l);let c=rr(a,"unknown"),u=this.ensureDependencyMeta(c);Object.assign(u,{optional:!0})}typeof e.preferUnplugged=="boolean"?this.preferUnplugged=e.preferUnplugged:this.preferUnplugged=null,this.errors=i}getForScope(e){switch(e){case"dependencies":return this.dependencies;case"devDependencies":return this.devDependencies;case"peerDependencies":return this.peerDependencies;default:throw new Error(`Unsupported value ("${e}")`)}}hasConsumerDependency(e){return!!(this.dependencies.has(e.identHash)||this.peerDependencies.has(e.identHash))}hasHardDependency(e){return!!(this.dependencies.has(e.identHash)||this.devDependencies.has(e.identHash))}hasSoftDependency(e){return!!this.peerDependencies.has(e.identHash)}hasDependency(e){return!!(this.hasHardDependency(e)||this.hasSoftDependency(e))}getConditions(){let e=[];return this.os&&this.os.length>0&&e.push(rx("os",this.os)),this.cpu&&this.cpu.length>0&&e.push(rx("cpu",this.cpu)),this.libc&&this.libc.length>0&&e.push(rx("libc",this.libc)),e.length>0?e.join(" & "):null}isCompatibleWithOS(e){return ul.isManifestFieldCompatible(this.os,e)}isCompatibleWithCPU(e){return ul.isManifestFieldCompatible(this.cpu,e)}ensureDependencyMeta(e){if(e.range!=="unknown"&&!t4.default.valid(e.range))throw new Error(`Invalid meta field range for '${Pn(e)}'`);let t=Ot(e),i=e.range!=="unknown"?e.range:null,n=this.dependenciesMeta.get(t);n||this.dependenciesMeta.set(t,n=new Map);let s=n.get(i);return s||n.set(i,s={}),s}ensurePeerDependencyMeta(e){if(e.range!=="unknown")throw new Error(`Invalid meta field range for '${Pn(e)}'`);let t=Ot(e),i=this.peerDependenciesMeta.get(t);return i||this.peerDependenciesMeta.set(t,i={}),i}setRawField(e,t,{after:i=[]}={}){let n=new Set(i.filter(s=>Object.prototype.hasOwnProperty.call(this.raw,s)));if(n.size===0||Object.prototype.hasOwnProperty.call(this.raw,e))this.raw[e]=t;else{let s=this.raw,o=this.raw={},a=!1;for(let l of Object.keys(s))o[l]=s[l],a||(n.delete(l),n.size===0&&(o[e]=t,a=!0))}}exportTo(e,{compatibilityMode:t=!0}={}){var s;if(Object.assign(e,this.raw),this.name!==null?e.name=Ot(this.name):delete e.name,this.version!==null?e.version=this.version:delete e.version,this.os!==null?e.os=this.os:delete e.os,this.cpu!==null?e.cpu=this.cpu:delete e.cpu,this.type!==null?e.type=this.type:delete e.type,this.packageManager!==null?e.packageManager=this.packageManager:delete e.packageManager,this.private?e.private=!0:delete e.private,this.license!==null?e.license=this.license:delete e.license,this.languageName!==null?e.languageName=this.languageName:delete e.languageName,this.main!==null?e.main=this.main:delete e.main,this.module!==null?e.module=this.module:delete e.module,this.browser!==null){let o=this.browser;typeof o=="string"?e.browser=o:o instanceof Map&&(e.browser=Object.assign({},...Array.from(o.keys()).sort().map(a=>({[a]:o.get(a)}))))}else delete e.browser;this.bin.size===1&&this.name!==null&&this.bin.has(this.name.name)?e.bin=this.bin.get(this.name.name):this.bin.size>0?e.bin=Object.assign({},...Array.from(this.bin.keys()).sort().map(o=>({[o]:this.bin.get(o)}))):delete e.bin,this.workspaceDefinitions.length>0?this.raw.workspaces&&!Array.isArray(this.raw.workspaces)?e.workspaces=te(N({},this.raw.workspaces),{packages:this.workspaceDefinitions.map(({pattern:o})=>o)}):e.workspaces=this.workspaceDefinitions.map(({pattern:o})=>o):this.raw.workspaces&&!Array.isArray(this.raw.workspaces)&&Object.keys(this.raw.workspaces).length>0?e.workspaces=this.raw.workspaces:delete e.workspaces;let i=[],n=[];for(let o of this.dependencies.values()){let a=this.dependenciesMeta.get(Ot(o)),l=!1;if(t&&a){let c=a.get(null);c&&c.optional&&(l=!0)}l?n.push(o):i.push(o)}i.length>0?e.dependencies=Object.assign({},...zg(i).map(o=>({[Ot(o)]:o.range}))):delete e.dependencies,n.length>0?e.optionalDependencies=Object.assign({},...zg(n).map(o=>({[Ot(o)]:o.range}))):delete e.optionalDependencies,this.devDependencies.size>0?e.devDependencies=Object.assign({},...zg(this.devDependencies.values()).map(o=>({[Ot(o)]:o.range}))):delete e.devDependencies,this.peerDependencies.size>0?e.peerDependencies=Object.assign({},...zg(this.peerDependencies.values()).map(o=>({[Ot(o)]:o.range}))):delete e.peerDependencies,e.dependenciesMeta={};for(let[o,a]of xn(this.dependenciesMeta.entries(),([l,c])=>l))for(let[l,c]of xn(a.entries(),([u,g])=>u!==null?`0${u}`:"1")){let u=l!==null?Pn(rr(An(o),l)):o,g=N({},c);t&&l===null&&delete g.optional,Object.keys(g).length!==0&&(e.dependenciesMeta[u]=g)}if(Object.keys(e.dependenciesMeta).length===0&&delete e.dependenciesMeta,this.peerDependenciesMeta.size>0?e.peerDependenciesMeta=Object.assign({},...xn(this.peerDependenciesMeta.entries(),([o,a])=>o).map(([o,a])=>({[o]:a}))):delete e.peerDependenciesMeta,this.resolutions.length>0?e.resolutions=Object.assign({},...this.resolutions.map(({pattern:o,reference:a})=>({[tI(o)]:a}))):delete e.resolutions,this.files!==null?e.files=Array.from(this.files):delete e.files,this.preferUnplugged!==null?e.preferUnplugged=this.preferUnplugged:delete e.preferUnplugged,this.scripts!==null&&this.scripts.size>0){(s=e.scripts)!=null||(e.scripts={});for(let o of Object.keys(e.scripts))this.scripts.has(o)||delete e.scripts[o];for(let[o,a]of this.scripts.entries())e.scripts[o]=a}else delete e.scripts;return e}},At=ul;At.fileName="package.json",At.allDependencies=["dependencies","devDependencies","peerDependencies"],At.hardDependencies=["dependencies","devDependencies"];function r4(r){let e=r.match(/^[ \t]+/m);return e?e[0]:" "}function i4(r){return r.charCodeAt(0)===65279?r.slice(1):r}function un(r){return r.replace(/\\/g,"/")}function pw(r,{yamlCompatibilityMode:e}){return e?Lv(r):typeof r=="undefined"||typeof r=="boolean"?r:null}function n4(r,e){let t=e.search(/[^!]/);if(t===-1)return"invalid";let i=t%2==0?"":"!",n=e.slice(t);return`${i}${r}=${n}`}function rx(r,e){return e.length===1?n4(r,e[0]):`(${e.map(t=>n4(r,t)).join(" | ")})`}var L4=ge(N4()),T4=ge(require("stream")),O4=ge(require("string_decoder"));var Ave=15,ct=class extends Error{constructor(e,t,i){super(t);this.reportExtra=i;this.reportCode=e}};function lve(r){return typeof r.reportCode!="undefined"}var Ji=class{constructor(){this.reportedInfos=new Set;this.reportedWarnings=new Set;this.reportedErrors=new Set}static progressViaCounter(e){let t=0,i,n=new Promise(l=>{i=l}),s=l=>{let c=i;n=new Promise(u=>{i=u}),t=l,c()},o=(l=0)=>{s(t+1)},a=async function*(){for(;t{t=o}),n=(0,L4.default)(o=>{let a=t;i=new Promise(l=>{t=l}),e=o,a()},1e3/Ave),s=async function*(){for(;;)await i,yield{title:e}}();return{[Symbol.asyncIterator](){return s},hasProgress:!1,hasTitle:!0,setTitle:n}}async startProgressPromise(e,t){let i=this.reportProgress(e);try{return await t(e)}finally{i.stop()}}startProgressSync(e,t){let i=this.reportProgress(e);try{return t(e)}finally{i.stop()}}reportInfoOnce(e,t,i){var s;let n=i&&i.key?i.key:t;this.reportedInfos.has(n)||(this.reportedInfos.add(n),this.reportInfo(e,t),(s=i==null?void 0:i.reportExtra)==null||s.call(i,this))}reportWarningOnce(e,t,i){var s;let n=i&&i.key?i.key:t;this.reportedWarnings.has(n)||(this.reportedWarnings.add(n),this.reportWarning(e,t),(s=i==null?void 0:i.reportExtra)==null||s.call(i,this))}reportErrorOnce(e,t,i){var s;let n=i&&i.key?i.key:t;this.reportedErrors.has(n)||(this.reportedErrors.add(n),this.reportError(e,t),(s=i==null?void 0:i.reportExtra)==null||s.call(i,this))}reportExceptionOnce(e){lve(e)?this.reportErrorOnce(e.reportCode,e.message,{key:e,reportExtra:e.reportExtra}):this.reportErrorOnce(X.EXCEPTION,e.stack||e.message,{key:e})}createStreamReporter(e=null){let t=new T4.PassThrough,i=new O4.StringDecoder,n="";return t.on("data",s=>{let o=i.write(s),a;do if(a=o.indexOf(` +`),a!==-1){let l=n+o.substring(0,a);o=o.substring(a+1),n="",e!==null?this.reportInfo(null,`${e} ${l}`):this.reportInfo(null,l)}while(a!==-1);n+=o}),t.on("end",()=>{let s=i.end();s!==""&&(e!==null?this.reportInfo(null,`${e} ${s}`):this.reportInfo(null,s))}),t}};var wd=class{constructor(e){this.fetchers=e}supports(e,t){return!!this.tryFetcher(e,t)}getLocalPath(e,t){return this.getFetcher(e,t).getLocalPath(e,t)}async fetch(e,t){return await this.getFetcher(e,t).fetch(e,t)}tryFetcher(e,t){let i=this.fetchers.find(n=>n.supports(e,t));return i||null}getFetcher(e,t){let i=this.fetchers.find(n=>n.supports(e,t));if(!i)throw new ct(X.FETCHER_NOT_FOUND,`${It(t.project.configuration,e)} isn't supported by any available fetcher`);return i}};var Bd=class{constructor(e){this.resolvers=e.filter(t=>t)}supportsDescriptor(e,t){return!!this.tryResolverByDescriptor(e,t)}supportsLocator(e,t){return!!this.tryResolverByLocator(e,t)}shouldPersistResolution(e,t){return this.getResolverByLocator(e,t).shouldPersistResolution(e,t)}bindDescriptor(e,t,i){return this.getResolverByDescriptor(e,i).bindDescriptor(e,t,i)}getResolutionDependencies(e,t){return this.getResolverByDescriptor(e,t).getResolutionDependencies(e,t)}async getCandidates(e,t,i){return await this.getResolverByDescriptor(e,i).getCandidates(e,t,i)}async getSatisfying(e,t,i){return this.getResolverByDescriptor(e,i).getSatisfying(e,t,i)}async resolve(e,t){return await this.getResolverByLocator(e,t).resolve(e,t)}tryResolverByDescriptor(e,t){let i=this.resolvers.find(n=>n.supportsDescriptor(e,t));return i||null}getResolverByDescriptor(e,t){let i=this.resolvers.find(n=>n.supportsDescriptor(e,t));if(!i)throw new Error(`${sr(t.project.configuration,e)} isn't supported by any available resolver`);return i}tryResolverByLocator(e,t){let i=this.resolvers.find(n=>n.supportsLocator(e,t));return i||null}getResolverByLocator(e,t){let i=this.resolvers.find(n=>n.supportsLocator(e,t));if(!i)throw new Error(`${It(t.project.configuration,e)} isn't supported by any available resolver`);return i}};var M4=ge(ri());var _g=/^(?!v)[a-z0-9._-]+$/i,sx=class{supportsDescriptor(e,t){return!!(mo(e.range)||_g.test(e.range))}supportsLocator(e,t){return!!(M4.default.valid(e.reference)||_g.test(e.reference))}shouldPersistResolution(e,t){return t.resolver.shouldPersistResolution(this.forwardLocator(e,t),t)}bindDescriptor(e,t,i){return i.resolver.bindDescriptor(this.forwardDescriptor(e,i),t,i)}getResolutionDependencies(e,t){return t.resolver.getResolutionDependencies(this.forwardDescriptor(e,t),t)}async getCandidates(e,t,i){return await i.resolver.getCandidates(this.forwardDescriptor(e,i),t,i)}async getSatisfying(e,t,i){return await i.resolver.getSatisfying(this.forwardDescriptor(e,i),t,i)}async resolve(e,t){let i=await t.resolver.resolve(this.forwardLocator(e,t),t);return cd(i,e)}forwardDescriptor(e,t){return rr(e,`${t.project.configuration.get("defaultProtocol")}${e.range}`)}forwardLocator(e,t){return cn(e,`${t.project.configuration.get("defaultProtocol")}${e.reference}`)}};var bd=class{supports(e){return!!e.reference.startsWith("virtual:")}getLocalPath(e,t){let i=e.reference.indexOf("#");if(i===-1)throw new Error("Invalid virtual package reference");let n=e.reference.slice(i+1),s=cn(e,n);return t.fetcher.getLocalPath(s,t)}async fetch(e,t){let i=e.reference.indexOf("#");if(i===-1)throw new Error("Invalid virtual package reference");let n=e.reference.slice(i+1),s=cn(e,n),o=await t.fetcher.fetch(s,t);return await this.ensureVirtualLink(e,o,t)}getLocatorFilename(e){return Wg(e)}async ensureVirtualLink(e,t,i){let n=t.packageFs.getRealPath(),s=i.project.configuration.get("virtualFolder"),o=this.getLocatorFilename(e),a=Wr.makeVirtualPath(s,o,n),l=new La(a,{baseFs:t.packageFs,pathUtils:x});return te(N({},t),{packageFs:l})}};var Vg=class{static isVirtualDescriptor(e){return!!e.range.startsWith(Vg.protocol)}static isVirtualLocator(e){return!!e.reference.startsWith(Vg.protocol)}supportsDescriptor(e,t){return Vg.isVirtualDescriptor(e)}supportsLocator(e,t){return Vg.isVirtualLocator(e)}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){throw new Error('Assertion failed: calling "bindDescriptor" on a virtual descriptor is unsupported')}getResolutionDependencies(e,t){throw new Error('Assertion failed: calling "getResolutionDependencies" on a virtual descriptor is unsupported')}async getCandidates(e,t,i){throw new Error('Assertion failed: calling "getCandidates" on a virtual descriptor is unsupported')}async getSatisfying(e,t,i){throw new Error('Assertion failed: calling "getSatisfying" on a virtual descriptor is unsupported')}async resolve(e,t){throw new Error('Assertion failed: calling "resolve" on a virtual locator is unsupported')}},dw=Vg;dw.protocol="virtual:";var Qd=class{supports(e){return!!e.reference.startsWith(oi.protocol)}getLocalPath(e,t){return this.getWorkspace(e,t).cwd}async fetch(e,t){let i=this.getWorkspace(e,t).cwd;return{packageFs:new _t(i),prefixPath:Me.dot,localPath:i}}getWorkspace(e,t){return t.project.getWorkspaceByCwd(e.reference.slice(oi.protocol.length))}};var ox={};ft(ox,{getDefaultGlobalFolder:()=>Ax,getHomeFolder:()=>Sd,isFolderInside:()=>lx});var ax=ge(require("os"));function Ax(){if(process.platform==="win32"){let r=H.toPortablePath(process.env.LOCALAPPDATA||H.join((0,ax.homedir)(),"AppData","Local"));return x.resolve(r,"Yarn/Berry")}if(process.env.XDG_DATA_HOME){let r=H.toPortablePath(process.env.XDG_DATA_HOME);return x.resolve(r,"yarn/berry")}return x.resolve(Sd(),".yarn/berry")}function Sd(){return H.toPortablePath((0,ax.homedir)()||"/usr/local/share")}function lx(r,e){let t=x.relative(e,r);return t&&!t.startsWith("..")&&!x.isAbsolute(t)}var Xg={};ft(Xg,{builtinModules:()=>cx,getArchitecture:()=>vd,getArchitectureName:()=>uve,getArchitectureSet:()=>ux});var U4=ge(require("module"));function cx(){return new Set(U4.default.builtinModules||Object.keys(process.binding("natives")))}function cve(){var i,n,s,o;if(process.platform==="win32")return null;let e=(s=((n=(i=process.report)==null?void 0:i.getReport())!=null?n:{}).sharedObjects)!=null?s:[],t=/\/(?:(ld-linux-|[^/]+-linux-gnu\/)|(libc.musl-|ld-musl-))/;return(o=ed(e,a=>{let l=a.match(t);if(!l)return ed.skip;if(l[1])return"glibc";if(l[2])return"musl";throw new Error("Assertion failed: Expected the libc variant to have been detected")}))!=null?o:null}var Cw,mw;function vd(){return Cw=Cw!=null?Cw:{os:process.platform,cpu:process.arch,libc:cve()}}function uve(r=vd()){return r.libc?`${r.os}-${r.cpu}-${r.libc}`:`${r.os}-${r.cpu}`}function ux(){let r=vd();return mw=mw!=null?mw:{os:[r.os],cpu:[r.cpu],libc:r.libc?[r.libc]:[]}}var gve=new Set(["binFolder","version","flags","profile","gpg","ignoreNode","wrapOutput","home","confDir"]),Iw="yarn_",fx=".yarnrc.yml",hx="yarn.lock",fve="********",Ie;(function(u){u.ANY="ANY",u.BOOLEAN="BOOLEAN",u.ABSOLUTE_PATH="ABSOLUTE_PATH",u.LOCATOR="LOCATOR",u.LOCATOR_LOOSE="LOCATOR_LOOSE",u.NUMBER="NUMBER",u.STRING="STRING",u.SECRET="SECRET",u.SHAPE="SHAPE",u.MAP="MAP"})(Ie||(Ie={}));var Ri=Ye,px={lastUpdateCheck:{description:"Last timestamp we checked whether new Yarn versions were available",type:Ie.STRING,default:null},yarnPath:{description:"Path to the local executable that must be used over the global one",type:Ie.ABSOLUTE_PATH,default:null},ignorePath:{description:"If true, the local executable will be ignored when using the global one",type:Ie.BOOLEAN,default:!1},ignoreCwd:{description:"If true, the `--cwd` flag will be ignored",type:Ie.BOOLEAN,default:!1},cacheKeyOverride:{description:"A global cache key override; used only for test purposes",type:Ie.STRING,default:null},globalFolder:{description:"Folder where all system-global files are stored",type:Ie.ABSOLUTE_PATH,default:Ax()},cacheFolder:{description:"Folder where the cache files must be written",type:Ie.ABSOLUTE_PATH,default:"./.yarn/cache"},compressionLevel:{description:"Zip files compression level, from 0 to 9 or mixed (a variant of 9, which stores some files uncompressed, when compression doesn't yield good results)",type:Ie.NUMBER,values:["mixed",0,1,2,3,4,5,6,7,8,9],default:cc},virtualFolder:{description:"Folder where the virtual packages (cf doc) will be mapped on the disk (must be named __virtual__)",type:Ie.ABSOLUTE_PATH,default:"./.yarn/__virtual__"},lockfileFilename:{description:"Name of the files where the Yarn dependency tree entries must be stored",type:Ie.STRING,default:hx},installStatePath:{description:"Path of the file where the install state will be persisted",type:Ie.ABSOLUTE_PATH,default:"./.yarn/install-state.gz"},immutablePatterns:{description:"Array of glob patterns; files matching them won't be allowed to change during immutable installs",type:Ie.STRING,default:[],isArray:!0},rcFilename:{description:"Name of the files where the configuration can be found",type:Ie.STRING,default:yw()},enableGlobalCache:{description:"If true, the system-wide cache folder will be used regardless of `cache-folder`",type:Ie.BOOLEAN,default:!1},enableColors:{description:"If true, the CLI is allowed to use colors in its output",type:Ie.BOOLEAN,default:Dy,defaultText:""},enableHyperlinks:{description:"If true, the CLI is allowed to use hyperlinks in its output",type:Ie.BOOLEAN,default:Uv,defaultText:""},enableInlineBuilds:{description:"If true, the CLI will print the build output on the command line",type:Ie.BOOLEAN,default:Ew.isCI,defaultText:""},enableMessageNames:{description:"If true, the CLI will prefix most messages with codes suitable for search engines",type:Ie.BOOLEAN,default:!0},enableProgressBars:{description:"If true, the CLI is allowed to show a progress bar for long-running events",type:Ie.BOOLEAN,default:!Ew.isCI,defaultText:""},enableTimers:{description:"If true, the CLI is allowed to print the time spent executing commands",type:Ie.BOOLEAN,default:!0},preferAggregateCacheInfo:{description:"If true, the CLI will only print a one-line report of any cache changes",type:Ie.BOOLEAN,default:Ew.isCI},preferInteractive:{description:"If true, the CLI will automatically use the interactive mode when called from a TTY",type:Ie.BOOLEAN,default:!1},preferTruncatedLines:{description:"If true, the CLI will truncate lines that would go beyond the size of the terminal",type:Ie.BOOLEAN,default:!1},progressBarStyle:{description:"Which style of progress bar should be used (only when progress bars are enabled)",type:Ie.STRING,default:void 0,defaultText:""},defaultLanguageName:{description:"Default language mode that should be used when a package doesn't offer any insight",type:Ie.STRING,default:"node"},defaultProtocol:{description:"Default resolution protocol used when resolving pure semver and tag ranges",type:Ie.STRING,default:"npm:"},enableTransparentWorkspaces:{description:"If false, Yarn won't automatically resolve workspace dependencies unless they use the `workspace:` protocol",type:Ie.BOOLEAN,default:!0},supportedArchitectures:{description:"Architectures that Yarn will fetch and inject into the resolver",type:Ie.SHAPE,properties:{os:{description:"Array of supported process.platform strings, or null to target them all",type:Ie.STRING,isArray:!0,isNullable:!0,default:["current"]},cpu:{description:"Array of supported process.arch strings, or null to target them all",type:Ie.STRING,isArray:!0,isNullable:!0,default:["current"]},libc:{description:"Array of supported libc libraries, or null to target them all",type:Ie.STRING,isArray:!0,isNullable:!0,default:["current"]}}},enableMirror:{description:"If true, the downloaded packages will be retrieved and stored in both the local and global folders",type:Ie.BOOLEAN,default:!0},enableNetwork:{description:"If false, the package manager will refuse to use the network if required to",type:Ie.BOOLEAN,default:!0},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:Ie.STRING,default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:Ie.STRING,default:null},unsafeHttpWhitelist:{description:"List of the hostnames for which http queries are allowed (glob patterns are supported)",type:Ie.STRING,default:[],isArray:!0},httpTimeout:{description:"Timeout of each http request in milliseconds",type:Ie.NUMBER,default:6e4},httpRetry:{description:"Retry times on http failure",type:Ie.NUMBER,default:3},networkConcurrency:{description:"Maximal number of concurrent requests",type:Ie.NUMBER,default:50},networkSettings:{description:"Network settings per hostname (glob patterns are supported)",type:Ie.MAP,valueDefinition:{description:"",type:Ie.SHAPE,properties:{caFilePath:{description:"Path to file containing one or multiple Certificate Authority signing certificates",type:Ie.ABSOLUTE_PATH,default:null},enableNetwork:{description:"If false, the package manager will refuse to use the network if required to",type:Ie.BOOLEAN,default:null},httpProxy:{description:"URL of the http proxy that must be used for outgoing http requests",type:Ie.STRING,default:null},httpsProxy:{description:"URL of the http proxy that must be used for outgoing https requests",type:Ie.STRING,default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:Ie.ABSOLUTE_PATH,default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:Ie.ABSOLUTE_PATH,default:null}}}},caFilePath:{description:"A path to a file containing one or multiple Certificate Authority signing certificates",type:Ie.ABSOLUTE_PATH,default:null},httpsKeyFilePath:{description:"Path to file containing private key in PEM format",type:Ie.ABSOLUTE_PATH,default:null},httpsCertFilePath:{description:"Path to file containing certificate chain in PEM format",type:Ie.ABSOLUTE_PATH,default:null},enableStrictSsl:{description:"If false, SSL certificate errors will be ignored",type:Ie.BOOLEAN,default:!0},logFilters:{description:"Overrides for log levels",type:Ie.SHAPE,isArray:!0,concatenateValues:!0,properties:{code:{description:"Code of the messages covered by this override",type:Ie.STRING,default:void 0},text:{description:"Code of the texts covered by this override",type:Ie.STRING,default:void 0},pattern:{description:"Code of the patterns covered by this override",type:Ie.STRING,default:void 0},level:{description:"Log level override, set to null to remove override",type:Ie.STRING,values:Object.values(Co),isNullable:!0,default:void 0}}},enableTelemetry:{description:"If true, telemetry will be periodically sent, following the rules in https://yarnpkg.com/advanced/telemetry",type:Ie.BOOLEAN,default:!0},telemetryInterval:{description:"Minimal amount of time between two telemetry uploads, in days",type:Ie.NUMBER,default:7},telemetryUserId:{description:"If you desire to tell us which project you are, you can set this field. Completely optional and opt-in.",type:Ie.STRING,default:null},enableScripts:{description:"If true, packages are allowed to have install scripts by default",type:Ie.BOOLEAN,default:!0},enableStrictSettings:{description:"If true, unknown settings will cause Yarn to abort",type:Ie.BOOLEAN,default:!0},enableImmutableCache:{description:"If true, the cache is reputed immutable and actions that would modify it will throw",type:Ie.BOOLEAN,default:!1},checksumBehavior:{description:"Enumeration defining what to do when a checksum doesn't match expectations",type:Ie.STRING,default:"throw"},packageExtensions:{description:"Map of package corrections to apply on the dependency tree",type:Ie.MAP,valueDefinition:{description:"The extension that will be applied to any package whose version matches the specified range",type:Ie.SHAPE,properties:{dependencies:{description:"The set of dependencies that must be made available to the current package in order for it to work properly",type:Ie.MAP,valueDefinition:{description:"A range",type:Ie.STRING}},peerDependencies:{description:"Inherited dependencies - the consumer of the package will be tasked to provide them",type:Ie.MAP,valueDefinition:{description:"A semver range",type:Ie.STRING}},peerDependenciesMeta:{description:"Extra information related to the dependencies listed in the peerDependencies field",type:Ie.MAP,valueDefinition:{description:"The peerDependency meta",type:Ie.SHAPE,properties:{optional:{description:"If true, the selected peer dependency will be marked as optional by the package manager and the consumer omitting it won't be reported as an error",type:Ie.BOOLEAN,default:!1}}}}}}}};function Cx(r,e,t,i,n){if(i.isArray||i.type===Ie.ANY&&Array.isArray(t))return Array.isArray(t)?t.map((s,o)=>dx(r,`${e}[${o}]`,s,i,n)):String(t).split(/,/).map(s=>dx(r,e,s,i,n));if(Array.isArray(t))throw new Error(`Non-array configuration settings "${e}" cannot be an array`);return dx(r,e,t,i,n)}function dx(r,e,t,i,n){var a;switch(i.type){case Ie.ANY:return t;case Ie.SHAPE:return hve(r,e,t,i,n);case Ie.MAP:return pve(r,e,t,i,n)}if(t===null&&!i.isNullable&&i.default!==null)throw new Error(`Non-nullable configuration settings "${e}" cannot be set to null`);if((a=i.values)==null?void 0:a.includes(t))return t;let o=(()=>{if(i.type===Ie.BOOLEAN&&typeof t!="string")return rd(t);if(typeof t!="string")throw new Error(`Expected value (${t}) to be a string`);let l=Nv(t,{env:process.env});switch(i.type){case Ie.ABSOLUTE_PATH:return x.resolve(n,H.toPortablePath(l));case Ie.LOCATOR_LOOSE:return qc(l,!1);case Ie.NUMBER:return parseInt(l);case Ie.LOCATOR:return qc(l);case Ie.BOOLEAN:return rd(l);default:return l}})();if(i.values&&!i.values.includes(o))throw new Error(`Invalid value, expected one of ${i.values.join(", ")}`);return o}function hve(r,e,t,i,n){if(typeof t!="object"||Array.isArray(t))throw new Pe(`Object configuration settings "${e}" must be an object`);let s=mx(r,i,{ignoreArrays:!0});if(t===null)return s;for(let[o,a]of Object.entries(t)){let l=`${e}.${o}`;if(!i.properties[o])throw new Pe(`Unrecognized configuration settings found: ${e}.${o} - run "yarn config -v" to see the list of settings supported in Yarn`);s.set(o,Cx(r,l,a,i.properties[o],n))}return s}function pve(r,e,t,i,n){let s=new Map;if(typeof t!="object"||Array.isArray(t))throw new Pe(`Map configuration settings "${e}" must be an object`);if(t===null)return s;for(let[o,a]of Object.entries(t)){let l=i.normalizeKeys?i.normalizeKeys(o):o,c=`${e}['${l}']`,u=i.valueDefinition;s.set(l,Cx(r,c,a,u,n))}return s}function mx(r,e,{ignoreArrays:t=!1}={}){switch(e.type){case Ie.SHAPE:{if(e.isArray&&!t)return[];let i=new Map;for(let[n,s]of Object.entries(e.properties))i.set(n,mx(r,s));return i}break;case Ie.MAP:return e.isArray&&!t?[]:new Map;case Ie.ABSOLUTE_PATH:return e.default===null?null:r.projectCwd===null?x.isAbsolute(e.default)?x.normalize(e.default):e.isNullable?null:void 0:Array.isArray(e.default)?e.default.map(i=>x.resolve(r.projectCwd,i)):x.resolve(r.projectCwd,e.default);default:return e.default}}function ww(r,e,t){if(e.type===Ie.SECRET&&typeof r=="string"&&t.hideSecrets)return fve;if(e.type===Ie.ABSOLUTE_PATH&&typeof r=="string"&&t.getNativePaths)return H.fromPortablePath(r);if(e.isArray&&Array.isArray(r)){let i=[];for(let n of r)i.push(ww(n,e,t));return i}if(e.type===Ie.MAP&&r instanceof Map){let i=new Map;for(let[n,s]of r.entries())i.set(n,ww(s,e.valueDefinition,t));return i}if(e.type===Ie.SHAPE&&r instanceof Map){let i=new Map;for(let[n,s]of r.entries()){let o=e.properties[n];i.set(n,ww(s,o,t))}return i}return r}function dve(){let r={};for(let[e,t]of Object.entries(process.env))e=e.toLowerCase(),!!e.startsWith(Iw)&&(e=(0,K4.default)(e.slice(Iw.length)),r[e]=t);return r}function yw(){let r=`${Iw}rc_filename`;for(let[e,t]of Object.entries(process.env))if(e.toLowerCase()===r&&typeof t=="string")return t;return fx}var gl;(function(i){i[i.LOCKFILE=0]="LOCKFILE",i[i.MANIFEST=1]="MANIFEST",i[i.NONE=2]="NONE"})(gl||(gl={}));var rA=class{constructor(e){this.projectCwd=null;this.plugins=new Map;this.settings=new Map;this.values=new Map;this.sources=new Map;this.invalid=new Map;this.packageExtensions=new Map;this.limits=new Map;this.startingCwd=e}static create(e,t,i){let n=new rA(e);typeof t!="undefined"&&!(t instanceof Map)&&(n.projectCwd=t),n.importSettings(px);let s=typeof i!="undefined"?i:t instanceof Map?t:new Map;for(let[o,a]of s)n.activatePlugin(o,a);return n}static async find(e,t,{lookup:i=0,strict:n=!0,usePath:s=!1,useRc:o=!0}={}){let a=dve();delete a.rcFilename;let l=await rA.findRcFiles(e),c=await rA.findHomeRcFile();if(c){let b=l.find(v=>v.path===c.path);b?b.strict=!1:l.push(te(N({},c),{strict:!1}))}let u=({ignoreCwd:b,yarnPath:v,ignorePath:k,lockfileFilename:T})=>({ignoreCwd:b,yarnPath:v,ignorePath:k,lockfileFilename:T}),g=q=>{var $=q,{ignoreCwd:b,yarnPath:v,ignorePath:k,lockfileFilename:T}=$,Y=Or($,["ignoreCwd","yarnPath","ignorePath","lockfileFilename"]);return Y},f=new rA(e);f.importSettings(u(px)),f.useWithSource("",u(a),e,{strict:!1});for(let{path:b,cwd:v,data:k}of l)f.useWithSource(b,u(k),v,{strict:!1});if(s){let b=f.get("yarnPath"),v=f.get("ignorePath");if(b!==null&&!v)return f}let h=f.get("lockfileFilename"),p;switch(i){case 0:p=await rA.findProjectCwd(e,h);break;case 1:p=await rA.findProjectCwd(e,null);break;case 2:U.existsSync(x.join(e,"package.json"))?p=x.resolve(e):p=null;break}f.startingCwd=e,f.projectCwd=p,f.importSettings(g(px));let m=new Map([["@@core",X3]]),y=b=>"default"in b?b.default:b;if(t!==null){for(let T of t.plugins.keys())m.set(T,y(t.modules.get(T)));let b=new Map;for(let T of cx())b.set(T,()=>Mg(T));for(let[T,Y]of t.modules)b.set(T,()=>Y);let v=new Set,k=async(T,Y)=>{let{factory:q,name:$}=Mg(T);if(v.has($))return;let z=new Map(b),ne=A=>{if(z.has(A))return z.get(A)();throw new Pe(`This plugin cannot access the package referenced via ${A} which is neither a builtin, nor an exposed entry`)},ee=await Tg(async()=>y(await q(ne)),A=>`${A} (when initializing ${$}, defined in ${Y})`);b.set($,()=>ee),v.add($),m.set($,ee)};if(a.plugins)for(let T of a.plugins.split(";")){let Y=x.resolve(e,H.toPortablePath(T));await k(Y,"")}for(let{path:T,cwd:Y,data:q}of l)if(!!o&&!!Array.isArray(q.plugins))for(let $ of q.plugins){let z=typeof $!="string"?$.path:$,ne=x.resolve(Y,H.toPortablePath(z));await k(ne,T)}}for(let[b,v]of m)f.activatePlugin(b,v);f.useWithSource("",g(a),e,{strict:n});for(let{path:b,cwd:v,data:k,strict:T}of l)f.useWithSource(b,g(k),v,{strict:T!=null?T:n});return f.get("enableGlobalCache")&&(f.values.set("cacheFolder",`${f.get("globalFolder")}/cache`),f.sources.set("cacheFolder","")),await f.refreshPackageExtensions(),f}static async findRcFiles(e){let t=yw(),i=[],n=e,s=null;for(;n!==s;){s=n;let o=x.join(s,t);if(U.existsSync(o)){let a=await U.readFilePromise(o,"utf8"),l;try{l=Si(a)}catch(c){let u="";throw a.match(/^\s+(?!-)[^:]+\s+\S+/m)&&(u=" (in particular, make sure you list the colons after each key name)"),new Pe(`Parse error when loading ${o}; please check it's proper Yaml${u}`)}i.push({path:o,cwd:s,data:l})}n=x.dirname(s)}return i}static async findHomeRcFile(){let e=yw(),t=Sd(),i=x.join(t,e);if(U.existsSync(i)){let n=await U.readFilePromise(i,"utf8"),s=Si(n);return{path:i,cwd:t,data:s}}return null}static async findProjectCwd(e,t){let i=null,n=e,s=null;for(;n!==s;){if(s=n,U.existsSync(x.join(s,"package.json"))&&(i=s),t!==null){if(U.existsSync(x.join(s,t))){i=s;break}}else if(i!==null)break;n=x.dirname(s)}return i}static async updateConfiguration(e,t){let i=yw(),n=x.join(e,i),s=U.existsSync(n)?Si(await U.readFilePromise(n,"utf8")):{},o=!1,a;if(typeof t=="function"){try{a=t(s)}catch{a=t({})}if(a===s)return}else{a=s;for(let l of Object.keys(t)){let c=s[l],u=t[l],g;if(typeof u=="function")try{g=u(c)}catch{g=u(void 0)}else g=u;c!==g&&(a[l]=g,o=!0)}if(!o)return}await U.changeFilePromise(n,Ua(a),{automaticNewlines:!0})}static async updateHomeConfiguration(e){let t=Sd();return await rA.updateConfiguration(t,e)}activatePlugin(e,t){this.plugins.set(e,t),typeof t.configuration!="undefined"&&this.importSettings(t.configuration)}importSettings(e){for(let[t,i]of Object.entries(e))if(i!=null){if(this.settings.has(t))throw new Error(`Cannot redefine settings "${t}"`);this.settings.set(t,i),this.values.set(t,mx(this,i))}}useWithSource(e,t,i,n){try{this.use(e,t,i,n)}catch(s){throw s.message+=` (in ${tt(this,e,Ye.PATH)})`,s}}use(e,t,i,{strict:n=!0,overwrite:s=!1}={}){n=n&&this.get("enableStrictSettings");for(let o of["enableStrictSettings",...Object.keys(t)]){if(typeof t[o]=="undefined"||o==="plugins"||e===""&&gve.has(o))continue;if(o==="rcFilename")throw new Pe(`The rcFilename settings can only be set via ${`${Iw}RC_FILENAME`.toUpperCase()}, not via a rc file`);let l=this.settings.get(o);if(!l){if(n)throw new Pe(`Unrecognized or legacy configuration settings found: ${o} - run "yarn config -v" to see the list of settings supported in Yarn`);this.invalid.set(o,e);continue}if(this.sources.has(o)&&!(s||l.type===Ie.MAP||l.isArray&&l.concatenateValues))continue;let c;try{c=Cx(this,o,t[o],l,i)}catch(u){throw u.message+=` in ${tt(this,e,Ye.PATH)}`,u}if(o==="enableStrictSettings"&&e!==""){n=c;continue}if(l.type===Ie.MAP){let u=this.values.get(o);this.values.set(o,new Map(s?[...u,...c]:[...c,...u])),this.sources.set(o,`${this.sources.get(o)}, ${e}`)}else if(l.isArray&&l.concatenateValues){let u=this.values.get(o);this.values.set(o,s?[...u,...c]:[...c,...u]),this.sources.set(o,`${this.sources.get(o)}, ${e}`)}else this.values.set(o,c),this.sources.set(o,e)}}get(e){if(!this.values.has(e))throw new Error(`Invalid configuration key "${e}"`);return this.values.get(e)}getSpecial(e,{hideSecrets:t=!1,getNativePaths:i=!1}){let n=this.get(e),s=this.settings.get(e);if(typeof s=="undefined")throw new Pe(`Couldn't find a configuration settings named "${e}"`);return ww(n,s,{hideSecrets:t,getNativePaths:i})}getSubprocessStreams(e,{header:t,prefix:i,report:n}){let s,o,a=U.createWriteStream(e);if(this.get("enableInlineBuilds")){let l=n.createStreamReporter(`${i} ${tt(this,"STDOUT","green")}`),c=n.createStreamReporter(`${i} ${tt(this,"STDERR","red")}`);s=new gx.PassThrough,s.pipe(l),s.pipe(a),o=new gx.PassThrough,o.pipe(c),o.pipe(a)}else s=a,o=a,typeof t!="undefined"&&s.write(`${t} +`);return{stdout:s,stderr:o}}makeResolver(){let e=[];for(let t of this.plugins.values())for(let i of t.resolvers||[])e.push(new i);return new Bd([new dw,new oi,new sx,...e])}makeFetcher(){let e=[];for(let t of this.plugins.values())for(let i of t.fetchers||[])e.push(new i);return new wd([new bd,new Qd,...e])}getLinkers(){let e=[];for(let t of this.plugins.values())for(let i of t.linkers||[])e.push(new i);return e}getSupportedArchitectures(){let e=vd(),t=this.get("supportedArchitectures"),i=t.get("os");i!==null&&(i=i.map(o=>o==="current"?e.os:o));let n=t.get("cpu");n!==null&&(n=n.map(o=>o==="current"?e.cpu:o));let s=t.get("libc");return s!==null&&(s=Vo(s,o=>{var a;return o==="current"?(a=e.libc)!=null?a:Vo.skip:o})),{os:i,cpu:n,libc:s}}async refreshPackageExtensions(){this.packageExtensions=new Map;let e=this.packageExtensions,t=(i,n,{userProvided:s=!1}={})=>{if(!mo(i.range))throw new Error("Only semver ranges are allowed as keys for the packageExtensions setting");let o=new At;o.load(n,{yamlCompatibilityMode:!0});let a=Ng(e,i.identHash),l=[];a.push([i.range,l]);let c={status:qi.Inactive,userProvided:s,parentDescriptor:i};for(let u of o.dependencies.values())l.push(te(N({},c),{type:wi.Dependency,descriptor:u}));for(let u of o.peerDependencies.values())l.push(te(N({},c),{type:wi.PeerDependency,descriptor:u}));for(let[u,g]of o.peerDependenciesMeta)for(let[f,h]of Object.entries(g))l.push(te(N({},c),{type:wi.PeerDependencyMeta,selector:u,key:f,value:h}))};await this.triggerHook(i=>i.registerPackageExtensions,this,t);for(let[i,n]of this.get("packageExtensions"))t(cl(i,!0),Py(n),{userProvided:!0})}normalizePackage(e){let t=ud(e);if(this.packageExtensions==null)throw new Error("refreshPackageExtensions has to be called before normalizing packages");let i=this.packageExtensions.get(e.identHash);if(typeof i!="undefined"){let s=e.version;if(s!==null){for(let[o,a]of i)if(!!Jc(s,o))for(let l of a)switch(l.status===qi.Inactive&&(l.status=qi.Redundant),l.type){case wi.Dependency:typeof t.dependencies.get(l.descriptor.identHash)=="undefined"&&(l.status=qi.Active,t.dependencies.set(l.descriptor.identHash,l.descriptor));break;case wi.PeerDependency:typeof t.peerDependencies.get(l.descriptor.identHash)=="undefined"&&(l.status=qi.Active,t.peerDependencies.set(l.descriptor.identHash,l.descriptor));break;case wi.PeerDependencyMeta:{let c=t.peerDependenciesMeta.get(l.selector);(typeof c=="undefined"||!Object.prototype.hasOwnProperty.call(c,l.key)||c[l.key]!==l.value)&&(l.status=qi.Active,Va(t.peerDependenciesMeta,l.selector,()=>({}))[l.key]=l.value)}break;default:Dv(l);break}}}let n=s=>s.scope?`${s.scope}__${s.name}`:`${s.name}`;for(let s of t.peerDependenciesMeta.keys()){let o=An(s);t.peerDependencies.has(o.identHash)||t.peerDependencies.set(o.identHash,rr(o,"*"))}for(let s of t.peerDependencies.values()){if(s.scope==="types")continue;let o=n(s),a=ea("types",o),l=Ot(a);t.peerDependencies.has(a.identHash)||t.peerDependenciesMeta.has(l)||(t.peerDependencies.set(a.identHash,rr(a,"*")),t.peerDependenciesMeta.set(l,{optional:!0}))}return t.dependencies=new Map(xn(t.dependencies,([,s])=>Pn(s))),t.peerDependencies=new Map(xn(t.peerDependencies,([,s])=>Pn(s))),t}getLimit(e){return Va(this.limits,e,()=>(0,H4.default)(this.get(e)))}async triggerHook(e,...t){for(let i of this.plugins.values()){let n=i.hooks;if(!n)continue;let s=e(n);!s||await s(...t)}}async triggerMultipleHooks(e,t){for(let i of t)await this.triggerHook(e,...i)}async reduceHook(e,t,...i){let n=t;for(let s of this.plugins.values()){let o=s.hooks;if(!o)continue;let a=e(o);!a||(n=await a(n,...i))}return n}async firstHook(e,...t){for(let i of this.plugins.values()){let n=i.hooks;if(!n)continue;let s=e(n);if(!s)continue;let o=await s(...t);if(typeof o!="undefined")return o}return null}},ye=rA;ye.telemetry=null;var ss;(function(i){i[i.Never=0]="Never",i[i.ErrorCode=1]="ErrorCode",i[i.Always=2]="Always"})(ss||(ss={}));var Bw=class extends ct{constructor({fileName:e,code:t,signal:i}){let n=ye.create(x.cwd()),s=tt(n,e,Ye.PATH);super(X.EXCEPTION,`Child ${s} reported an error`,o=>{Cve(t,i,{configuration:n,report:o})});this.code=Ix(t,i)}},yx=class extends Bw{constructor({fileName:e,code:t,signal:i,stdout:n,stderr:s}){super({fileName:e,code:t,signal:i});this.stdout=n,this.stderr=s}};function _c(r){return r!==null&&typeof r.fd=="number"}var Vc=new Set;function wx(){}function Bx(){for(let r of Vc)r.kill()}async function ia(r,e,{cwd:t,env:i=process.env,strict:n=!1,stdin:s=null,stdout:o,stderr:a,end:l=2}){let c=["pipe","pipe","pipe"];s===null?c[0]="ignore":_c(s)&&(c[0]=s),_c(o)&&(c[1]=o),_c(a)&&(c[2]=a);let u=(0,Ex.default)(r,e,{cwd:H.fromPortablePath(t),env:te(N({},i),{PWD:H.fromPortablePath(t)}),stdio:c});Vc.add(u),Vc.size===1&&(process.on("SIGINT",wx),process.on("SIGTERM",Bx)),!_c(s)&&s!==null&&s.pipe(u.stdin),_c(o)||u.stdout.pipe(o,{end:!1}),_c(a)||u.stderr.pipe(a,{end:!1});let g=()=>{for(let f of new Set([o,a]))_c(f)||f.end()};return new Promise((f,h)=>{u.on("error",p=>{Vc.delete(u),Vc.size===0&&(process.off("SIGINT",wx),process.off("SIGTERM",Bx)),(l===2||l===1)&&g(),h(p)}),u.on("close",(p,m)=>{Vc.delete(u),Vc.size===0&&(process.off("SIGINT",wx),process.off("SIGTERM",Bx)),(l===2||l===1&&p>0)&&g(),p===0||!n?f({code:Ix(p,m)}):h(new Bw({fileName:r,code:p,signal:m}))})})}async function mve(r,e,{cwd:t,env:i=process.env,encoding:n="utf8",strict:s=!1}){let o=["ignore","pipe","pipe"],a=[],l=[],c=H.fromPortablePath(t);typeof i.PWD!="undefined"&&(i=te(N({},i),{PWD:c}));let u=(0,Ex.default)(r,e,{cwd:c,env:i,stdio:o});return u.stdout.on("data",g=>{a.push(g)}),u.stderr.on("data",g=>{l.push(g)}),await new Promise((g,f)=>{u.on("error",h=>{let p=ye.create(t),m=tt(p,r,Ye.PATH);f(new ct(X.EXCEPTION,`Process ${m} failed to spawn`,y=>{y.reportError(X.EXCEPTION,` ${Xo(p,{label:"Thrown Error",value:po(Ye.NO_HINT,h.message)})}`)}))}),u.on("close",(h,p)=>{let m=n==="buffer"?Buffer.concat(a):Buffer.concat(a).toString(n),y=n==="buffer"?Buffer.concat(l):Buffer.concat(l).toString(n);h===0||!s?g({code:Ix(h,p),stdout:m,stderr:y}):f(new yx({fileName:r,code:h,signal:p,stdout:m,stderr:y}))})})}var Eve=new Map([["SIGINT",2],["SIGQUIT",3],["SIGKILL",9],["SIGTERM",15]]);function Ix(r,e){let t=Eve.get(e);return typeof t!="undefined"?128+t:r!=null?r:1}function Cve(r,e,{configuration:t,report:i}){i.reportError(X.EXCEPTION,` ${Xo(t,r!==null?{label:"Exit Code",value:po(Ye.NUMBER,r)}:{label:"Exit Signal",value:po(Ye.CODE,e)})}`)}var ir={};ft(ir,{Method:()=>ml,RequestError:()=>S5.RequestError,del:()=>DPe,get:()=>xPe,getNetworkSettings:()=>P5,post:()=>jP,put:()=>PPe,request:()=>Md});var B5=ge(Hw()),b5=ge(require("https")),Q5=ge(require("http")),UP=ge(ns()),KP=ge(w5()),jw=ge(require("url"));var S5=ge(Hw()),v5=new Map,k5=new Map,QPe=new Q5.Agent({keepAlive:!0}),SPe=new b5.Agent({keepAlive:!0});function x5(r){let e=new jw.URL(r),t={host:e.hostname,headers:{}};return e.port&&(t.port=Number(e.port)),{proxy:t}}async function HP(r){return Va(k5,r,()=>U.readFilePromise(r).then(e=>(k5.set(r,e),e)))}function vPe({statusCode:r,statusMessage:e},t){let i=tt(t,r,Ye.NUMBER),n=`https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/${r}`;return Ug(t,`${i}${e?` (${e})`:""}`,n)}async function Gw(r,{configuration:e,customErrorMessage:t}){var i,n;try{return await r}catch(s){if(s.name!=="HTTPError")throw s;let o=(n=t==null?void 0:t(s))!=null?n:(i=s.response.body)==null?void 0:i.error;o==null&&(s.message.startsWith("Response code")?o="The remote server failed to provide the requested resource":o=s.message),s instanceof B5.TimeoutError&&s.event==="socket"&&(o+=`(can be increased via ${tt(e,"httpTimeout",Ye.SETTING)})`);let a=new ct(X.NETWORK_ERROR,o,l=>{s.response&&l.reportError(X.NETWORK_ERROR,` ${Xo(e,{label:"Response Code",value:po(Ye.NO_HINT,vPe(s.response,e))})}`),s.request&&(l.reportError(X.NETWORK_ERROR,` ${Xo(e,{label:"Request Method",value:po(Ye.NO_HINT,s.request.options.method)})}`),l.reportError(X.NETWORK_ERROR,` ${Xo(e,{label:"Request URL",value:po(Ye.URL,s.request.requestUrl)})}`)),s.request.redirects.length>0&&l.reportError(X.NETWORK_ERROR,` ${Xo(e,{label:"Request Redirects",value:po(Ye.NO_HINT,Hv(e,s.request.redirects,Ye.URL))})}`),s.request.retryCount===s.request.options.retry.limit&&l.reportError(X.NETWORK_ERROR,` ${Xo(e,{label:"Request Retry Count",value:po(Ye.NO_HINT,`${tt(e,s.request.retryCount,Ye.NUMBER)} (can be increased via ${tt(e,"httpRetry",Ye.SETTING)})`)})}`)});throw a.originalError=s,a}}function P5(r,e){let t=[...e.configuration.get("networkSettings")].sort(([o],[a])=>a.length-o.length),i={enableNetwork:void 0,caFilePath:void 0,httpProxy:void 0,httpsProxy:void 0,httpsKeyFilePath:void 0,httpsCertFilePath:void 0},n=Object.keys(i),s=typeof r=="string"?new jw.URL(r):r;for(let[o,a]of t)if(UP.default.isMatch(s.hostname,o))for(let l of n){let c=a.get(l);c!==null&&typeof i[l]=="undefined"&&(i[l]=c)}for(let o of n)typeof i[o]=="undefined"&&(i[o]=e.configuration.get(o));return i}var ml;(function(n){n.GET="GET",n.PUT="PUT",n.POST="POST",n.DELETE="DELETE"})(ml||(ml={}));async function Md(r,e,{configuration:t,headers:i,jsonRequest:n,jsonResponse:s,method:o=ml.GET}){let a=async()=>await kPe(r,e,{configuration:t,headers:i,jsonRequest:n,jsonResponse:s,method:o});return await(await t.reduceHook(c=>c.wrapNetworkRequest,a,{target:r,body:e,configuration:t,headers:i,jsonRequest:n,jsonResponse:s,method:o}))()}async function xPe(r,n){var s=n,{configuration:e,jsonResponse:t}=s,i=Or(s,["configuration","jsonResponse"]);let o=Va(v5,r,()=>Gw(Md(r,null,N({configuration:e},i)),{configuration:e}).then(a=>(v5.set(r,a.body),a.body)));return Buffer.isBuffer(o)===!1&&(o=await o),t?JSON.parse(o.toString()):o}async function PPe(r,e,n){var s=n,{customErrorMessage:t}=s,i=Or(s,["customErrorMessage"]);return(await Gw(Md(r,e,te(N({},i),{method:ml.PUT})),i)).body}async function jP(r,e,n){var s=n,{customErrorMessage:t}=s,i=Or(s,["customErrorMessage"]);return(await Gw(Md(r,e,te(N({},i),{method:ml.POST})),i)).body}async function DPe(r,i){var n=i,{customErrorMessage:e}=n,t=Or(n,["customErrorMessage"]);return(await Gw(Md(r,null,te(N({},t),{method:ml.DELETE})),t)).body}async function kPe(r,e,{configuration:t,headers:i,jsonRequest:n,jsonResponse:s,method:o=ml.GET}){let a=typeof r=="string"?new jw.URL(r):r,l=P5(a,{configuration:t});if(l.enableNetwork===!1)throw new Error(`Request to '${a.href}' has been blocked because of your configuration settings`);if(a.protocol==="http:"&&!UP.default.isMatch(a.hostname,t.get("unsafeHttpWhitelist")))throw new Error(`Unsafe http requests must be explicitly whitelisted in your configuration (${a.hostname})`);let u={agent:{http:l.httpProxy?KP.default.httpOverHttp(x5(l.httpProxy)):QPe,https:l.httpsProxy?KP.default.httpsOverHttp(x5(l.httpsProxy)):SPe},headers:i,method:o};u.responseType=s?"json":"buffer",e!==null&&(Buffer.isBuffer(e)||!n&&typeof e=="string"?u.body=e:u.json=e);let g=t.get("httpTimeout"),f=t.get("httpRetry"),h=t.get("enableStrictSsl"),p=l.caFilePath,m=l.httpsCertFilePath,y=l.httpsKeyFilePath,{default:b}=await Promise.resolve().then(()=>ge(Hw())),v=p?await HP(p):void 0,k=m?await HP(m):void 0,T=y?await HP(y):void 0,Y=b.extend(N({timeout:{socket:g},retry:f,https:{rejectUnauthorized:h,certificateAuthority:v,certificate:k,key:T}},u));return t.getLimit("networkConcurrency")(()=>Y(a))}var Zt={};ft(Zt,{PackageManager:()=>hn,detectPackageManager:()=>G9,executePackageAccessibleBinary:()=>z9,executePackageScript:()=>sB,executePackageShellcode:()=>iD,executeWorkspaceAccessibleBinary:()=>VDe,executeWorkspaceLifecycleScript:()=>W9,executeWorkspaceScript:()=>J9,getPackageAccessibleBinaries:()=>oB,getWorkspaceAccessibleBinaries:()=>q9,hasPackageScript:()=>WDe,hasWorkspaceScript:()=>rD,makeScriptEnv:()=>qd,maybeExecuteWorkspaceLifecycleScript:()=>_De,prepareExternalProject:()=>JDe});var Ud={};ft(Ud,{getLibzipPromise:()=>fn,getLibzipSync:()=>L5});var N5=ge(R5());var El=["number","number"],qP;(function(L){L[L.ZIP_ER_OK=0]="ZIP_ER_OK",L[L.ZIP_ER_MULTIDISK=1]="ZIP_ER_MULTIDISK",L[L.ZIP_ER_RENAME=2]="ZIP_ER_RENAME",L[L.ZIP_ER_CLOSE=3]="ZIP_ER_CLOSE",L[L.ZIP_ER_SEEK=4]="ZIP_ER_SEEK",L[L.ZIP_ER_READ=5]="ZIP_ER_READ",L[L.ZIP_ER_WRITE=6]="ZIP_ER_WRITE",L[L.ZIP_ER_CRC=7]="ZIP_ER_CRC",L[L.ZIP_ER_ZIPCLOSED=8]="ZIP_ER_ZIPCLOSED",L[L.ZIP_ER_NOENT=9]="ZIP_ER_NOENT",L[L.ZIP_ER_EXISTS=10]="ZIP_ER_EXISTS",L[L.ZIP_ER_OPEN=11]="ZIP_ER_OPEN",L[L.ZIP_ER_TMPOPEN=12]="ZIP_ER_TMPOPEN",L[L.ZIP_ER_ZLIB=13]="ZIP_ER_ZLIB",L[L.ZIP_ER_MEMORY=14]="ZIP_ER_MEMORY",L[L.ZIP_ER_CHANGED=15]="ZIP_ER_CHANGED",L[L.ZIP_ER_COMPNOTSUPP=16]="ZIP_ER_COMPNOTSUPP",L[L.ZIP_ER_EOF=17]="ZIP_ER_EOF",L[L.ZIP_ER_INVAL=18]="ZIP_ER_INVAL",L[L.ZIP_ER_NOZIP=19]="ZIP_ER_NOZIP",L[L.ZIP_ER_INTERNAL=20]="ZIP_ER_INTERNAL",L[L.ZIP_ER_INCONS=21]="ZIP_ER_INCONS",L[L.ZIP_ER_REMOVE=22]="ZIP_ER_REMOVE",L[L.ZIP_ER_DELETED=23]="ZIP_ER_DELETED",L[L.ZIP_ER_ENCRNOTSUPP=24]="ZIP_ER_ENCRNOTSUPP",L[L.ZIP_ER_RDONLY=25]="ZIP_ER_RDONLY",L[L.ZIP_ER_NOPASSWD=26]="ZIP_ER_NOPASSWD",L[L.ZIP_ER_WRONGPASSWD=27]="ZIP_ER_WRONGPASSWD",L[L.ZIP_ER_OPNOTSUPP=28]="ZIP_ER_OPNOTSUPP",L[L.ZIP_ER_INUSE=29]="ZIP_ER_INUSE",L[L.ZIP_ER_TELL=30]="ZIP_ER_TELL",L[L.ZIP_ER_COMPRESSED_DATA=31]="ZIP_ER_COMPRESSED_DATA"})(qP||(qP={}));var F5=r=>({get HEAP8(){return r.HEAP8},get HEAPU8(){return r.HEAPU8},errors:qP,SEEK_SET:0,SEEK_CUR:1,SEEK_END:2,ZIP_CHECKCONS:4,ZIP_CREATE:1,ZIP_EXCL:2,ZIP_TRUNCATE:8,ZIP_RDONLY:16,ZIP_FL_OVERWRITE:8192,ZIP_FL_COMPRESSED:4,ZIP_OPSYS_DOS:0,ZIP_OPSYS_AMIGA:1,ZIP_OPSYS_OPENVMS:2,ZIP_OPSYS_UNIX:3,ZIP_OPSYS_VM_CMS:4,ZIP_OPSYS_ATARI_ST:5,ZIP_OPSYS_OS_2:6,ZIP_OPSYS_MACINTOSH:7,ZIP_OPSYS_Z_SYSTEM:8,ZIP_OPSYS_CPM:9,ZIP_OPSYS_WINDOWS_NTFS:10,ZIP_OPSYS_MVS:11,ZIP_OPSYS_VSE:12,ZIP_OPSYS_ACORN_RISC:13,ZIP_OPSYS_VFAT:14,ZIP_OPSYS_ALTERNATE_MVS:15,ZIP_OPSYS_BEOS:16,ZIP_OPSYS_TANDEM:17,ZIP_OPSYS_OS_400:18,ZIP_OPSYS_OS_X:19,ZIP_CM_DEFAULT:-1,ZIP_CM_STORE:0,ZIP_CM_DEFLATE:8,uint08S:r._malloc(1),uint16S:r._malloc(2),uint32S:r._malloc(4),uint64S:r._malloc(8),malloc:r._malloc,free:r._free,getValue:r.getValue,open:r.cwrap("zip_open","number",["string","number","number"]),openFromSource:r.cwrap("zip_open_from_source","number",["number","number","number"]),close:r.cwrap("zip_close","number",["number"]),discard:r.cwrap("zip_discard",null,["number"]),getError:r.cwrap("zip_get_error","number",["number"]),getName:r.cwrap("zip_get_name","string",["number","number","number"]),getNumEntries:r.cwrap("zip_get_num_entries","number",["number","number"]),delete:r.cwrap("zip_delete","number",["number","number"]),stat:r.cwrap("zip_stat","number",["number","string","number","number"]),statIndex:r.cwrap("zip_stat_index","number",["number",...El,"number","number"]),fopen:r.cwrap("zip_fopen","number",["number","string","number"]),fopenIndex:r.cwrap("zip_fopen_index","number",["number",...El,"number"]),fread:r.cwrap("zip_fread","number",["number","number","number","number"]),fclose:r.cwrap("zip_fclose","number",["number"]),dir:{add:r.cwrap("zip_dir_add","number",["number","string"])},file:{add:r.cwrap("zip_file_add","number",["number","string","number","number"]),getError:r.cwrap("zip_file_get_error","number",["number"]),getExternalAttributes:r.cwrap("zip_file_get_external_attributes","number",["number",...El,"number","number","number"]),setExternalAttributes:r.cwrap("zip_file_set_external_attributes","number",["number",...El,"number","number","number"]),setMtime:r.cwrap("zip_file_set_mtime","number",["number",...El,"number","number"]),setCompression:r.cwrap("zip_set_file_compression","number",["number",...El,"number","number"])},ext:{countSymlinks:r.cwrap("zip_ext_count_symlinks","number",["number"])},error:{initWithCode:r.cwrap("zip_error_init_with_code",null,["number","number"]),strerror:r.cwrap("zip_error_strerror","string",["number"])},name:{locate:r.cwrap("zip_name_locate","number",["number","string","number"])},source:{fromUnattachedBuffer:r.cwrap("zip_source_buffer_create","number",["number","number","number","number"]),fromBuffer:r.cwrap("zip_source_buffer","number",["number","number",...El,"number"]),free:r.cwrap("zip_source_free",null,["number"]),keep:r.cwrap("zip_source_keep",null,["number"]),open:r.cwrap("zip_source_open","number",["number"]),close:r.cwrap("zip_source_close","number",["number"]),seek:r.cwrap("zip_source_seek","number",["number",...El,"number"]),tell:r.cwrap("zip_source_tell","number",["number"]),read:r.cwrap("zip_source_read","number",["number","number","number"]),error:r.cwrap("zip_source_error","number",["number"]),setMtime:r.cwrap("zip_source_set_mtime","number",["number","number"])},struct:{stat:r.cwrap("zipstruct_stat","number",[]),statS:r.cwrap("zipstruct_statS","number",[]),statName:r.cwrap("zipstruct_stat_name","string",["number"]),statIndex:r.cwrap("zipstruct_stat_index","number",["number"]),statSize:r.cwrap("zipstruct_stat_size","number",["number"]),statCompSize:r.cwrap("zipstruct_stat_comp_size","number",["number"]),statCompMethod:r.cwrap("zipstruct_stat_comp_method","number",["number"]),statMtime:r.cwrap("zipstruct_stat_mtime","number",["number"]),statCrc:r.cwrap("zipstruct_stat_crc","number",["number"]),error:r.cwrap("zipstruct_error","number",[]),errorS:r.cwrap("zipstruct_errorS","number",[]),errorCodeZip:r.cwrap("zipstruct_error_code_zip","number",["number"])}});var JP=null;function L5(){return JP===null&&(JP=F5((0,N5.default)())),JP}async function fn(){return L5()}var Hd={};ft(Hd,{ShellError:()=>Ks,execute:()=>Zw,globUtils:()=>qw});var W5=ge(gv()),z5=ge(require("os")),os=ge(require("stream")),_5=ge(require("util"));var Ks=class extends Error{constructor(e){super(e);this.name="ShellError"}};var qw={};ft(qw,{fastGlobOptions:()=>M5,isBraceExpansion:()=>U5,isGlobPattern:()=>RPe,match:()=>FPe,micromatchOptions:()=>Ww});var T5=ge($y()),O5=ge(require("fs")),Jw=ge(ns()),Ww={strictBrackets:!0},M5={onlyDirectories:!1,onlyFiles:!1};function RPe(r){if(!Jw.default.scan(r,Ww).isGlob)return!1;try{Jw.default.parse(r,Ww)}catch{return!1}return!0}function FPe(r,{cwd:e,baseFs:t}){return(0,T5.default)(r,te(N({},M5),{cwd:H.fromPortablePath(e),fs:zE(O5.default,new Xh(t))}))}function U5(r){return Jw.default.scan(r,Ww).isBrace}var K5=ge(vQ()),sa=ge(require("stream")),H5=ge(require("string_decoder")),Nn;(function(i){i[i.STDIN=0]="STDIN",i[i.STDOUT=1]="STDOUT",i[i.STDERR=2]="STDERR"})(Nn||(Nn={}));var Zc=new Set;function WP(){}function zP(){for(let r of Zc)r.kill()}function j5(r,e,t,i){return n=>{let s=n[0]instanceof sa.Transform?"pipe":n[0],o=n[1]instanceof sa.Transform?"pipe":n[1],a=n[2]instanceof sa.Transform?"pipe":n[2],l=(0,K5.default)(r,e,te(N({},i),{stdio:[s,o,a]}));return Zc.add(l),Zc.size===1&&(process.on("SIGINT",WP),process.on("SIGTERM",zP)),n[0]instanceof sa.Transform&&n[0].pipe(l.stdin),n[1]instanceof sa.Transform&&l.stdout.pipe(n[1],{end:!1}),n[2]instanceof sa.Transform&&l.stderr.pipe(n[2],{end:!1}),{stdin:l.stdin,promise:new Promise(c=>{l.on("error",u=>{switch(Zc.delete(l),Zc.size===0&&(process.off("SIGINT",WP),process.off("SIGTERM",zP)),u.code){case"ENOENT":n[2].write(`command not found: ${r} +`),c(127);break;case"EACCES":n[2].write(`permission denied: ${r} +`),c(128);break;default:n[2].write(`uncaught error: ${u.message} +`),c(1);break}}),l.on("close",u=>{Zc.delete(l),Zc.size===0&&(process.off("SIGINT",WP),process.off("SIGTERM",zP)),c(u!==null?u:129)})})}}}function G5(r){return e=>{let t=e[0]==="pipe"?new sa.PassThrough:e[0];return{stdin:t,promise:Promise.resolve().then(()=>r({stdin:t,stdout:e[1],stderr:e[2]}))}}}var Io=class{constructor(e){this.stream=e}close(){}get(){return this.stream}},Y5=class{constructor(){this.stream=null}close(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");this.stream.end()}attach(e){this.stream=e}get(){if(this.stream===null)throw new Error("Assertion failed: No stream attached");return this.stream}},Kd=class{constructor(e,t){this.stdin=null;this.stdout=null;this.stderr=null;this.pipe=null;this.ancestor=e,this.implementation=t}static start(e,{stdin:t,stdout:i,stderr:n}){let s=new Kd(null,e);return s.stdin=t,s.stdout=i,s.stderr=n,s}pipeTo(e,t=1){let i=new Kd(this,e),n=new Y5;return i.pipe=n,i.stdout=this.stdout,i.stderr=this.stderr,(t&1)==1?this.stdout=n:this.ancestor!==null&&(this.stderr=this.ancestor.stdout),(t&2)==2?this.stderr=n:this.ancestor!==null&&(this.stderr=this.ancestor.stderr),i}async exec(){let e=["ignore","ignore","ignore"];if(this.pipe)e[0]="pipe";else{if(this.stdin===null)throw new Error("Assertion failed: No input stream registered");e[0]=this.stdin.get()}let t;if(this.stdout===null)throw new Error("Assertion failed: No output stream registered");t=this.stdout,e[1]=t.get();let i;if(this.stderr===null)throw new Error("Assertion failed: No error stream registered");i=this.stderr,e[2]=i.get();let n=this.implementation(e);return this.pipe&&this.pipe.attach(n.stdin),await n.promise.then(s=>(t.close(),i.close(),s))}async run(){let e=[];for(let i=this;i;i=i.ancestor)e.push(i.exec());return(await Promise.all(e))[0]}};function zw(r,e){return Kd.start(r,e)}function q5(r,e=null){let t=new sa.PassThrough,i=new H5.StringDecoder,n="";return t.on("data",s=>{let o=i.write(s),a;do if(a=o.indexOf(` +`),a!==-1){let l=n+o.substring(0,a);o=o.substring(a+1),n="",r(e!==null?`${e} ${l}`:l)}while(a!==-1);n+=o}),t.on("end",()=>{let s=i.end();s!==""&&r(e!==null?`${e} ${s}`:s)}),t}function J5(r,{prefix:e}){return{stdout:q5(t=>r.stdout.write(`${t} +`),r.stdout.isTTY?e:null),stderr:q5(t=>r.stderr.write(`${t} +`),r.stderr.isTTY?e:null)}}var NPe=(0,_5.promisify)(setTimeout);var zi;(function(t){t[t.Readable=1]="Readable",t[t.Writable=2]="Writable"})(zi||(zi={}));function V5(r,e,t){let i=new os.PassThrough({autoDestroy:!0});switch(r){case Nn.STDIN:(e&1)==1&&t.stdin.pipe(i,{end:!1}),(e&2)==2&&t.stdin instanceof os.Writable&&i.pipe(t.stdin,{end:!1});break;case Nn.STDOUT:(e&1)==1&&t.stdout.pipe(i,{end:!1}),(e&2)==2&&i.pipe(t.stdout,{end:!1});break;case Nn.STDERR:(e&1)==1&&t.stderr.pipe(i,{end:!1}),(e&2)==2&&i.pipe(t.stderr,{end:!1});break;default:throw new Ks(`Bad file descriptor: "${r}"`)}return i}function _w(r,e={}){let t=N(N({},r),e);return t.environment=N(N({},r.environment),e.environment),t.variables=N(N({},r.variables),e.variables),t}var LPe=new Map([["cd",async([r=(0,z5.homedir)(),...e],t,i)=>{let n=x.resolve(i.cwd,H.toPortablePath(r));if(!(await t.baseFs.statPromise(n).catch(o=>{throw o.code==="ENOENT"?new Ks(`cd: no such file or directory: ${r}`):o})).isDirectory())throw new Ks(`cd: not a directory: ${r}`);return i.cwd=n,0}],["pwd",async(r,e,t)=>(t.stdout.write(`${H.fromPortablePath(t.cwd)} +`),0)],[":",async(r,e,t)=>0],["true",async(r,e,t)=>0],["false",async(r,e,t)=>1],["exit",async([r,...e],t,i)=>i.exitCode=parseInt(r!=null?r:i.variables["?"],10)],["echo",async(r,e,t)=>(t.stdout.write(`${r.join(" ")} +`),0)],["sleep",async([r],e,t)=>{if(typeof r=="undefined")throw new Ks("sleep: missing operand");let i=Number(r);if(Number.isNaN(i))throw new Ks(`sleep: invalid time interval '${r}'`);return await NPe(1e3*i,0)}],["__ysh_run_procedure",async(r,e,t)=>{let i=t.procedures[r[0]];return await zw(i,{stdin:new Io(t.stdin),stdout:new Io(t.stdout),stderr:new Io(t.stderr)}).run()}],["__ysh_set_redirects",async(r,e,t)=>{let i=t.stdin,n=t.stdout,s=t.stderr,o=[],a=[],l=[],c=0;for(;r[c]!=="--";){let g=r[c++],{type:f,fd:h}=JSON.parse(g),p=v=>{switch(h){case null:case 0:o.push(v);break;default:throw new Error(`Unsupported file descriptor: "${h}"`)}},m=v=>{switch(h){case null:case 1:a.push(v);break;case 2:l.push(v);break;default:throw new Error(`Unsupported file descriptor: "${h}"`)}},y=Number(r[c++]),b=c+y;for(let v=c;ve.baseFs.createReadStream(x.resolve(t.cwd,H.toPortablePath(r[v]))));break;case"<<<":p(()=>{let k=new os.PassThrough;return process.nextTick(()=>{k.write(`${r[v]} +`),k.end()}),k});break;case"<&":p(()=>V5(Number(r[v]),1,t));break;case">":case">>":{let k=x.resolve(t.cwd,H.toPortablePath(r[v]));m(k==="/dev/null"?new os.Writable({autoDestroy:!0,emitClose:!0,write(T,Y,q){setImmediate(q)}}):e.baseFs.createWriteStream(k,f===">>"?{flags:"a"}:void 0))}break;case">&":m(V5(Number(r[v]),2,t));break;default:throw new Error(`Assertion failed: Unsupported redirection type: "${f}"`)}}if(o.length>0){let g=new os.PassThrough;i=g;let f=h=>{if(h===o.length)g.end();else{let p=o[h]();p.pipe(g,{end:!1}),p.on("end",()=>{f(h+1)})}};f(0)}if(a.length>0){let g=new os.PassThrough;n=g;for(let f of a)g.pipe(f)}if(l.length>0){let g=new os.PassThrough;s=g;for(let f of l)g.pipe(f)}let u=await zw(jd(r.slice(c+1),e,t),{stdin:new Io(i),stdout:new Io(n),stderr:new Io(s)}).run();return await Promise.all(a.map(g=>new Promise((f,h)=>{g.on("error",p=>{h(p)}),g.on("close",()=>{f()}),g.end()}))),await Promise.all(l.map(g=>new Promise((f,h)=>{g.on("error",p=>{h(p)}),g.on("close",()=>{f()}),g.end()}))),u}]]);async function TPe(r,e,t){let i=[],n=new os.PassThrough;return n.on("data",s=>i.push(s)),await Vw(r,e,_w(t,{stdout:n})),Buffer.concat(i).toString().replace(/[\r\n]+$/,"")}async function X5(r,e,t){let i=r.map(async s=>{let o=await AA(s.args,e,t);return{name:s.name,value:o.join(" ")}});return(await Promise.all(i)).reduce((s,o)=>(s[o.name]=o.value,s),{})}function Xw(r){return r.match(/[^ \r\n\t]+/g)||[]}async function Z5(r,e,t,i,n=i){switch(r.name){case"$":i(String(process.pid));break;case"#":i(String(e.args.length));break;case"@":if(r.quoted)for(let s of e.args)n(s);else for(let s of e.args){let o=Xw(s);for(let a=0;a=0&&sr+e,subtraction:(r,e)=>r-e,multiplication:(r,e)=>r*e,division:(r,e)=>Math.trunc(r/e)};async function Gd(r,e,t){if(r.type==="number"){if(Number.isInteger(r.value))return r.value;throw new Error(`Invalid number: "${r.value}", only integers are allowed`)}else if(r.type==="variable"){let i=[];await Z5(te(N({},r),{quoted:!0}),e,t,s=>i.push(s));let n=Number(i.join(" "));return Number.isNaN(n)?Gd({type:"variable",name:i.join(" ")},e,t):Gd({type:"number",value:n},e,t)}else return OPe[r.type](await Gd(r.left,e,t),await Gd(r.right,e,t))}async function AA(r,e,t){let i=new Map,n=[],s=[],o=u=>{s.push(u)},a=()=>{s.length>0&&n.push(s.join("")),s=[]},l=u=>{o(u),a()},c=(u,g,f)=>{let h=JSON.stringify({type:u,fd:g}),p=i.get(h);typeof p=="undefined"&&i.set(h,p=[]),p.push(f)};for(let u of r){let g=!1;switch(u.type){case"redirection":{let f=await AA(u.args,e,t);for(let h of f)c(u.subtype,u.fd,h)}break;case"argument":for(let f of u.segments)switch(f.type){case"text":o(f.text);break;case"glob":o(f.pattern),g=!0;break;case"shell":{let h=await TPe(f.shell,e,t);if(f.quoted)o(h);else{let p=Xw(h);for(let m=0;m0){let u=[];for(let[g,f]of i.entries())u.splice(u.length,0,g,String(f.length),...f);n.splice(0,0,"__ysh_set_redirects",...u,"--")}return n}function jd(r,e,t){e.builtins.has(r[0])||(r=["command",...r]);let i=H.fromPortablePath(t.cwd),n=t.environment;typeof n.PWD!="undefined"&&(n=te(N({},n),{PWD:i}));let[s,...o]=r;if(s==="command")return j5(o[0],o.slice(1),e,{cwd:i,env:n});let a=e.builtins.get(s);if(typeof a=="undefined")throw new Error(`Assertion failed: A builtin should exist for "${s}"`);return G5(async({stdin:l,stdout:c,stderr:u})=>{let{stdin:g,stdout:f,stderr:h}=t;t.stdin=l,t.stdout=c,t.stderr=u;try{return await a(o,e,t)}finally{t.stdin=g,t.stdout=f,t.stderr=h}})}function MPe(r,e,t){return i=>{let n=new os.PassThrough,s=Vw(r,e,_w(t,{stdin:n}));return{stdin:n,promise:s}}}function UPe(r,e,t){return i=>{let n=new os.PassThrough,s=Vw(r,e,t);return{stdin:n,promise:s}}}function $5(r,e,t,i){if(e.length===0)return r;{let n;do n=String(Math.random());while(Object.prototype.hasOwnProperty.call(i.procedures,n));return i.procedures=N({},i.procedures),i.procedures[n]=r,jd([...e,"__ysh_run_procedure",n],t,i)}}async function e9(r,e,t){let i=r,n=null,s=null;for(;i;){let o=i.then?N({},t):t,a;switch(i.type){case"command":{let l=await AA(i.args,e,t),c=await X5(i.envs,e,t);a=i.envs.length?jd(l,e,_w(o,{environment:c})):jd(l,e,o)}break;case"subshell":{let l=await AA(i.args,e,t),c=MPe(i.subshell,e,o);a=$5(c,l,e,o)}break;case"group":{let l=await AA(i.args,e,t),c=UPe(i.group,e,o);a=$5(c,l,e,o)}break;case"envs":{let l=await X5(i.envs,e,t);o.environment=N(N({},o.environment),l),a=jd(["true"],e,o)}break}if(typeof a=="undefined")throw new Error("Assertion failed: An action should have been generated");if(n===null)s=zw(a,{stdin:new Io(o.stdin),stdout:new Io(o.stdout),stderr:new Io(o.stderr)});else{if(s===null)throw new Error("Assertion failed: The execution pipeline should have been setup");switch(n){case"|":s=s.pipeTo(a,Nn.STDOUT);break;case"|&":s=s.pipeTo(a,Nn.STDOUT|Nn.STDERR);break}}i.then?(n=i.then.type,i=i.then.chain):i=null}if(s===null)throw new Error("Assertion failed: The execution pipeline should have been setup");return await s.run()}async function KPe(r,e,t,{background:i=!1}={}){function n(s){let o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],a=o[s%o.length];return W5.default.hex(a)}if(i){let s=t.nextBackgroundJobIndex++,o=n(s),a=`[${s}]`,l=o(a),{stdout:c,stderr:u}=J5(t,{prefix:l});return t.backgroundJobs.push(e9(r,e,_w(t,{stdout:c,stderr:u})).catch(g=>u.write(`${g.message} +`)).finally(()=>{t.stdout.isTTY&&t.stdout.write(`Job ${l}, '${o(tg(r))}' has ended +`)})),0}return await e9(r,e,t)}async function HPe(r,e,t,{background:i=!1}={}){let n,s=a=>{n=a,t.variables["?"]=String(a)},o=async a=>{try{return await KPe(a.chain,e,t,{background:i&&typeof a.then=="undefined"})}catch(l){if(!(l instanceof Ks))throw l;return t.stderr.write(`${l.message} +`),1}};for(s(await o(r));r.then;){if(t.exitCode!==null)return t.exitCode;switch(r.then.type){case"&&":n===0&&s(await o(r.then.line));break;case"||":n!==0&&s(await o(r.then.line));break;default:throw new Error(`Assertion failed: Unsupported command type: "${r.then.type}"`)}r=r.then.line}return n}async function Vw(r,e,t){let i=t.backgroundJobs;t.backgroundJobs=[];let n=0;for(let{command:s,type:o}of r){if(n=await HPe(s,e,t,{background:o==="&"}),t.exitCode!==null)return t.exitCode;t.variables["?"]=String(n)}return await Promise.all(t.backgroundJobs),t.backgroundJobs=i,n}function t9(r){switch(r.type){case"variable":return r.name==="@"||r.name==="#"||r.name==="*"||Number.isFinite(parseInt(r.name,10))||"defaultValue"in r&&!!r.defaultValue&&r.defaultValue.some(e=>Yd(e))||"alternativeValue"in r&&!!r.alternativeValue&&r.alternativeValue.some(e=>Yd(e));case"arithmetic":return _P(r.arithmetic);case"shell":return VP(r.shell);default:return!1}}function Yd(r){switch(r.type){case"redirection":return r.args.some(e=>Yd(e));case"argument":return r.segments.some(e=>t9(e));default:throw new Error(`Assertion failed: Unsupported argument type: "${r.type}"`)}}function _P(r){switch(r.type){case"variable":return t9(r);case"number":return!1;default:return _P(r.left)||_P(r.right)}}function VP(r){return r.some(({command:e})=>{for(;e;){let t=e.chain;for(;t;){let i;switch(t.type){case"subshell":i=VP(t.subshell);break;case"command":i=t.envs.some(n=>n.args.some(s=>Yd(s)))||t.args.some(n=>Yd(n));break}if(i)return!0;if(!t.then)break;t=t.then.chain}if(!e.then)break;e=e.then.line}return!1})}async function Zw(r,e=[],{baseFs:t=new ar,builtins:i={},cwd:n=H.toPortablePath(process.cwd()),env:s=process.env,stdin:o=process.stdin,stdout:a=process.stdout,stderr:l=process.stderr,variables:c={},glob:u=qw}={}){let g={};for(let[p,m]of Object.entries(s))typeof m!="undefined"&&(g[p]=m);let f=new Map(LPe);for(let[p,m]of Object.entries(i))f.set(p,m);o===null&&(o=new os.PassThrough,o.end());let h=VE(r,u);if(!VP(h)&&h.length>0&&e.length>0){let{command:p}=h[h.length-1];for(;p.then;)p=p.then.line;let m=p.chain;for(;m.then;)m=m.then.chain;m.type==="command"&&(m.args=m.args.concat(e.map(y=>({type:"argument",segments:[{type:"text",text:y}]}))))}return await Vw(h,{args:e,baseFs:t,builtins:f,initialStdin:o,initialStdout:a,initialStderr:l,glob:u},{cwd:n,environment:g,exitCode:null,procedures:{},stdin:o,stdout:a,stderr:l,variables:Object.assign({},c,{["?"]:0}),nextBackgroundJobIndex:1,backgroundJobs:[]})}var H9=ge($w()),j9=ge(fg()),Il=ge(require("stream"));var T9=ge(L9()),rB=ge(yc());var O9=["\u280B","\u2819","\u2839","\u2838","\u283C","\u2834","\u2826","\u2827","\u2807","\u280F"],M9=80,UDe=new Set([X.FETCH_NOT_CACHED,X.UNUSED_CACHE_ENTRY]),KDe=5,iB=rB.default.GITHUB_ACTIONS?{start:r=>`::group::${r} +`,end:r=>`::endgroup:: +`}:rB.default.TRAVIS?{start:r=>`travis_fold:start:${r} +`,end:r=>`travis_fold:end:${r} +`}:rB.default.GITLAB?{start:r=>`section_start:${Math.floor(Date.now()/1e3)}:${r.toLowerCase().replace(/\W+/g,"_")}[collapsed=true]\r${r} +`,end:r=>`section_end:${Math.floor(Date.now()/1e3)}:${r.toLowerCase().replace(/\W+/g,"_")}\r`}:null,U9=new Date,HDe=["iTerm.app","Apple_Terminal"].includes(process.env.TERM_PROGRAM)||!!process.env.WT_SESSION,jDe=r=>r,nB=jDe({patrick:{date:[17,3],chars:["\u{1F340}","\u{1F331}"],size:40},simba:{date:[19,7],chars:["\u{1F981}","\u{1F334}"],size:40},jack:{date:[31,10],chars:["\u{1F383}","\u{1F987}"],size:40},hogsfather:{date:[31,12],chars:["\u{1F389}","\u{1F384}"],size:40},default:{chars:["=","-"],size:80}}),GDe=HDe&&Object.keys(nB).find(r=>{let e=nB[r];return!(e.date&&(e.date[0]!==U9.getDate()||e.date[1]!==U9.getMonth()+1))})||"default";function K9(r,{configuration:e,json:t}){if(!e.get("enableMessageNames"))return"";let n=VA(r===null?0:r);return!t&&r===null?tt(e,n,"grey"):n}function tD(r,{configuration:e,json:t}){let i=K9(r,{configuration:e,json:t});if(!i||r===null||r===X.UNNAMED)return i;let n=X[r],s=`https://yarnpkg.com/advanced/error-codes#${i}---${n}`.toLowerCase();return Ug(e,i,s)}var Je=class extends Ji{constructor({configuration:e,stdout:t,json:i=!1,includeFooter:n=!0,includeLogs:s=!i,includeInfos:o=s,includeWarnings:a=s,forgettableBufferSize:l=KDe,forgettableNames:c=new Set}){super();this.uncommitted=new Set;this.cacheHitCount=0;this.cacheMissCount=0;this.lastCacheMiss=null;this.warningCount=0;this.errorCount=0;this.startTime=Date.now();this.indent=0;this.progress=new Map;this.progressTime=0;this.progressFrame=0;this.progressTimeout=null;this.progressStyle=null;this.progressMaxScaledSize=null;this.forgettableLines=[];if(sd(this,{configuration:e}),this.configuration=e,this.forgettableBufferSize=l,this.forgettableNames=new Set([...c,...UDe]),this.includeFooter=n,this.includeInfos=o,this.includeWarnings=a,this.json=i,this.stdout=t,e.get("enableProgressBars")&&!i&&t.isTTY&&t.columns>22){let u=e.get("progressBarStyle")||GDe;if(!Object.prototype.hasOwnProperty.call(nB,u))throw new Error("Assertion failed: Invalid progress bar style");this.progressStyle=nB[u];let g="\u27A4 YN0000: \u250C ".length,f=Math.max(0,Math.min(t.columns-g,80));this.progressMaxScaledSize=Math.floor(this.progressStyle.size*f/80)}}static async start(e,t){let i=new this(e),n=process.emitWarning;process.emitWarning=(s,o)=>{if(typeof s!="string"){let l=s;s=l.message,o=o!=null?o:l.name}let a=typeof o!="undefined"?`${o}: ${s}`:s;i.reportWarning(X.UNNAMED,a)};try{await t(i)}catch(s){i.reportExceptionOnce(s)}finally{await i.finalize(),process.emitWarning=n}return i}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}reportCacheHit(e){this.cacheHitCount+=1}reportCacheMiss(e,t){this.lastCacheMiss=e,this.cacheMissCount+=1,typeof t!="undefined"&&!this.configuration.get("preferAggregateCacheInfo")&&this.reportInfo(X.FETCH_NOT_CACHED,t)}startSectionSync({reportHeader:e,reportFooter:t,skipIfEmpty:i},n){let s={committed:!1,action:()=>{e==null||e()}};i?this.uncommitted.add(s):(s.action(),s.committed=!0);let o=Date.now();try{return n()}catch(a){throw this.reportExceptionOnce(a),a}finally{let a=Date.now();this.uncommitted.delete(s),s.committed&&(t==null||t(a-o))}}async startSectionPromise({reportHeader:e,reportFooter:t,skipIfEmpty:i},n){let s={committed:!1,action:()=>{e==null||e()}};i?this.uncommitted.add(s):(s.action(),s.committed=!0);let o=Date.now();try{return await n()}catch(a){throw this.reportExceptionOnce(a),a}finally{let a=Date.now();this.uncommitted.delete(s),s.committed&&(t==null||t(a-o))}}startTimerImpl(e,t,i){let n=typeof t=="function"?{}:t;return{cb:typeof t=="function"?t:i,reportHeader:()=>{this.reportInfo(null,`\u250C ${e}`),this.indent+=1,iB!==null&&!this.json&&this.includeInfos&&this.stdout.write(iB.start(e))},reportFooter:o=>{this.indent-=1,iB!==null&&!this.json&&this.includeInfos&&this.stdout.write(iB.end(e)),this.configuration.get("enableTimers")&&o>200?this.reportInfo(null,`\u2514 Completed in ${tt(this.configuration,o,Ye.DURATION)}`):this.reportInfo(null,"\u2514 Completed")},skipIfEmpty:n.skipIfEmpty}}startTimerSync(e,t,i){let o=this.startTimerImpl(e,t,i),{cb:n}=o,s=Or(o,["cb"]);return this.startSectionSync(s,n)}async startTimerPromise(e,t,i){let o=this.startTimerImpl(e,t,i),{cb:n}=o,s=Or(o,["cb"]);return this.startSectionPromise(s,n)}async startCacheReport(e){let t=this.configuration.get("preferAggregateCacheInfo")?{cacheHitCount:this.cacheHitCount,cacheMissCount:this.cacheMissCount}:null;try{return await e()}catch(i){throw this.reportExceptionOnce(i),i}finally{t!==null&&this.reportCacheChanges(t)}}reportSeparator(){this.indent===0?this.writeLineWithForgettableReset(""):this.reportInfo(null,"")}reportInfo(e,t){if(!this.includeInfos)return;this.commit();let i=this.formatNameWithHyperlink(e),n=i?`${i}: `:"",s=`${tt(this.configuration,"\u27A4","blueBright")} ${n}${this.formatIndent()}${t}`;if(this.json)this.reportJson({type:"info",name:e,displayName:this.formatName(e),indent:this.formatIndent(),data:t});else if(this.forgettableNames.has(e))if(this.forgettableLines.push(s),this.forgettableLines.length>this.forgettableBufferSize){for(;this.forgettableLines.length>this.forgettableBufferSize;)this.forgettableLines.shift();this.writeLines(this.forgettableLines,{truncate:!0})}else this.writeLine(s,{truncate:!0});else this.writeLineWithForgettableReset(s)}reportWarning(e,t){if(this.warningCount+=1,!this.includeWarnings)return;this.commit();let i=this.formatNameWithHyperlink(e),n=i?`${i}: `:"";this.json?this.reportJson({type:"warning",name:e,displayName:this.formatName(e),indent:this.formatIndent(),data:t}):this.writeLineWithForgettableReset(`${tt(this.configuration,"\u27A4","yellowBright")} ${n}${this.formatIndent()}${t}`)}reportError(e,t){this.errorCount+=1,this.commit();let i=this.formatNameWithHyperlink(e),n=i?`${i}: `:"";this.json?this.reportJson({type:"error",name:e,displayName:this.formatName(e),indent:this.formatIndent(),data:t}):this.writeLineWithForgettableReset(`${tt(this.configuration,"\u27A4","redBright")} ${n}${this.formatIndent()}${t}`,{truncate:!1})}reportProgress(e){if(this.progressStyle===null)return te(N({},Promise.resolve()),{stop:()=>{}});if(e.hasProgress&&e.hasTitle)throw new Error("Unimplemented: Progress bars can't have both progress and titles.");let t=!1,i=Promise.resolve().then(async()=>{let s={progress:e.hasProgress?0:void 0,title:e.hasTitle?"":void 0};this.progress.set(e,{definition:s,lastScaledSize:e.hasProgress?-1:void 0,lastTitle:void 0}),this.refreshProgress({delta:-1});for await(let{progress:o,title:a}of e)t||s.progress===o&&s.title===a||(s.progress=o,s.title=a,this.refreshProgress());n()}),n=()=>{t||(t=!0,this.progress.delete(e),this.refreshProgress({delta:1}))};return te(N({},i),{stop:n})}reportJson(e){this.json&&this.writeLineWithForgettableReset(`${JSON.stringify(e)}`)}async finalize(){if(!this.includeFooter)return;let e="";this.errorCount>0?e="Failed with errors":this.warningCount>0?e="Done with warnings":e="Done";let t=tt(this.configuration,Date.now()-this.startTime,Ye.DURATION),i=this.configuration.get("enableTimers")?`${e} in ${t}`:e;this.errorCount>0?this.reportError(X.UNNAMED,i):this.warningCount>0?this.reportWarning(X.UNNAMED,i):this.reportInfo(X.UNNAMED,i)}writeLine(e,{truncate:t}={}){this.clearProgress({clear:!0}),this.stdout.write(`${this.truncate(e,{truncate:t})} +`),this.writeProgress()}writeLineWithForgettableReset(e,{truncate:t}={}){this.forgettableLines=[],this.writeLine(e,{truncate:t})}writeLines(e,{truncate:t}={}){this.clearProgress({delta:e.length});for(let i of e)this.stdout.write(`${this.truncate(i,{truncate:t})} +`);this.writeProgress()}reportCacheChanges({cacheHitCount:e,cacheMissCount:t}){let i=this.cacheHitCount-e,n=this.cacheMissCount-t;if(i===0&&n===0)return;let s="";this.cacheHitCount>1?s+=`${this.cacheHitCount} packages were already cached`:this.cacheHitCount===1?s+=" - one package was already cached":s+="No packages were cached",this.cacheHitCount>0?this.cacheMissCount>1?s+=`, ${this.cacheMissCount} had to be fetched`:this.cacheMissCount===1&&(s+=`, one had to be fetched (${It(this.configuration,this.lastCacheMiss)})`):this.cacheMissCount>1?s+=` - ${this.cacheMissCount} packages had to be fetched`:this.cacheMissCount===1&&(s+=` - one package had to be fetched (${It(this.configuration,this.lastCacheMiss)})`),this.reportInfo(X.FETCH_NOT_CACHED,s)}commit(){let e=this.uncommitted;this.uncommitted=new Set;for(let t of e)t.committed=!0,t.action()}clearProgress({delta:e=0,clear:t=!1}){this.progressStyle!==null&&this.progress.size+e>0&&(this.stdout.write(`[${this.progress.size+e}A`),(e>0||t)&&this.stdout.write(""))}writeProgress(){if(this.progressStyle===null||(this.progressTimeout!==null&&clearTimeout(this.progressTimeout),this.progressTimeout=null,this.progress.size===0))return;let e=Date.now();e-this.progressTime>M9&&(this.progressFrame=(this.progressFrame+1)%O9.length,this.progressTime=e);let t=O9[this.progressFrame];for(let i of this.progress.values()){let n="";if(typeof i.lastScaledSize!="undefined"){let l=this.progressStyle.chars[0].repeat(i.lastScaledSize),c=this.progressStyle.chars[1].repeat(this.progressMaxScaledSize-i.lastScaledSize);n=` ${l}${c}`}let s=this.formatName(null),o=s?`${s}: `:"",a=i.definition.title?` ${i.definition.title}`:"";this.stdout.write(`${tt(this.configuration,"\u27A4","blueBright")} ${o}${t}${n}${a} +`)}this.progressTimeout=setTimeout(()=>{this.refreshProgress({force:!0})},M9)}refreshProgress({delta:e=0,force:t=!1}={}){let i=!1,n=!1;if(t||this.progress.size===0)i=!0;else for(let s of this.progress.values()){let o=typeof s.definition.progress!="undefined"?Math.trunc(this.progressMaxScaledSize*s.definition.progress):void 0,a=s.lastScaledSize;s.lastScaledSize=o;let l=s.lastTitle;if(s.lastTitle=s.definition.title,o!==a||(n=l!==s.definition.title)){i=!0;break}}i&&(this.clearProgress({delta:e,clear:n}),this.writeProgress())}truncate(e,{truncate:t}={}){return this.progressStyle===null&&(t=!1),typeof t=="undefined"&&(t=this.configuration.get("preferTruncatedLines")),t&&(e=(0,T9.default)(e,0,this.stdout.columns-1)),e}formatName(e){return K9(e,{configuration:this.configuration,json:this.json})}formatNameWithHyperlink(e){return tD(e,{configuration:this.configuration,json:this.json})}formatIndent(){return"\u2502 ".repeat(this.indent)}};var Kr="3.2.3";var hn;(function(n){n.Yarn1="Yarn Classic",n.Yarn2="Yarn",n.Npm="npm",n.Pnpm="pnpm"})(hn||(hn={}));async function lA(r,e,t,i=[]){if(process.platform==="win32"){let n=`@goto #_undefined_# 2>NUL || @title %COMSPEC% & @setlocal & @"${t}" ${i.map(s=>`"${s.replace('"','""')}"`).join(" ")} %*`;await U.writeFilePromise(x.format({dir:r,name:e,ext:".cmd"}),n)}await U.writeFilePromise(x.join(r,e),`#!/bin/sh +exec "${t}" ${i.map(n=>`'${n.replace(/'/g,`'"'"'`)}'`).join(" ")} "$@" +`,{mode:493})}async function G9(r){let e=await At.tryFind(r);if(e==null?void 0:e.packageManager){let i=cw(e.packageManager);if(i==null?void 0:i.name){let n=`found ${JSON.stringify({packageManager:e.packageManager})} in manifest`,[s]=i.reference.split(".");switch(i.name){case"yarn":return{packageManager:Number(s)===1?hn.Yarn1:hn.Yarn2,reason:n};case"npm":return{packageManager:hn.Npm,reason:n};case"pnpm":return{packageManager:hn.Pnpm,reason:n}}}}let t;try{t=await U.readFilePromise(x.join(r,xt.lockfile),"utf8")}catch{}return t!==void 0?t.match(/^__metadata:$/m)?{packageManager:hn.Yarn2,reason:'"__metadata" key found in yarn.lock'}:{packageManager:hn.Yarn1,reason:'"__metadata" key not found in yarn.lock, must be a Yarn classic lockfile'}:U.existsSync(x.join(r,"package-lock.json"))?{packageManager:hn.Npm,reason:`found npm's "package-lock.json" lockfile`}:U.existsSync(x.join(r,"pnpm-lock.yaml"))?{packageManager:hn.Pnpm,reason:`found pnpm's "pnpm-lock.yaml" lockfile`}:null}async function qd({project:r,locator:e,binFolder:t,lifecycleScript:i}){var l,c;let n={};for(let[u,g]of Object.entries(process.env))typeof g!="undefined"&&(n[u.toLowerCase()!=="path"?u:"PATH"]=g);let s=H.fromPortablePath(t);n.BERRY_BIN_FOLDER=H.fromPortablePath(s);let o=process.env.COREPACK_ROOT?H.join(process.env.COREPACK_ROOT,"dist/yarn.js"):process.argv[1];if(await Promise.all([lA(t,"node",process.execPath),...Kr!==null?[lA(t,"run",process.execPath,[o,"run"]),lA(t,"yarn",process.execPath,[o]),lA(t,"yarnpkg",process.execPath,[o]),lA(t,"node-gyp",process.execPath,[o,"run","--top-level","node-gyp"])]:[]]),r&&(n.INIT_CWD=H.fromPortablePath(r.configuration.startingCwd),n.PROJECT_CWD=H.fromPortablePath(r.cwd)),n.PATH=n.PATH?`${s}${H.delimiter}${n.PATH}`:`${s}`,n.npm_execpath=`${s}${H.sep}yarn`,n.npm_node_execpath=`${s}${H.sep}node`,e){if(!r)throw new Error("Assertion failed: Missing project");let u=r.tryWorkspaceByLocator(e),g=u?(l=u.manifest.version)!=null?l:"":(c=r.storedPackages.get(e.locatorHash).version)!=null?c:"";n.npm_package_name=Ot(e),n.npm_package_version=g;let f;if(u)f=u.cwd;else{let h=r.storedPackages.get(e.locatorHash);if(!h)throw new Error(`Package for ${It(r.configuration,e)} not found in the project`);let p=r.configuration.getLinkers(),m={project:r,report:new Je({stdout:new Il.PassThrough,configuration:r.configuration})},y=p.find(b=>b.supportsPackage(h,m));if(!y)throw new Error(`The package ${It(r.configuration,h)} isn't supported by any of the available linkers`);f=await y.findPackageLocation(h,m)}n.npm_package_json=H.fromPortablePath(x.join(f,xt.manifest))}let a=Kr!==null?`yarn/${Kr}`:`yarn/${Mg("@yarnpkg/core").version}-core`;return n.npm_config_user_agent=`${a} npm/? node/${process.version} ${process.platform} ${process.arch}`,i&&(n.npm_lifecycle_event=i),r&&await r.configuration.triggerHook(u=>u.setupScriptEnvironment,r,n,async(u,g,f)=>await lA(t,Jr(u),g,f)),n}var YDe=2,qDe=(0,j9.default)(YDe);async function JDe(r,e,{configuration:t,report:i,workspace:n=null,locator:s=null}){await qDe(async()=>{await U.mktempPromise(async o=>{let a=x.join(o,"pack.log"),l=null,{stdout:c,stderr:u}=t.getSubprocessStreams(a,{prefix:H.fromPortablePath(r),report:i}),g=s&&ta(s)?fd(s):s,f=g?Fs(g):"an external project";c.write(`Packing ${f} from sources +`);let h=await G9(r),p;h!==null?(c.write(`Using ${h.packageManager} for bootstrap. Reason: ${h.reason} + +`),p=h.packageManager):(c.write(`No package manager configuration detected; defaulting to Yarn + +`),p=hn.Yarn2),await U.mktempPromise(async m=>{let y=await qd({binFolder:m}),v=new Map([[hn.Yarn1,async()=>{let T=n!==null?["workspace",n]:[],Y=x.join(r,xt.manifest),q=await U.readFilePromise(Y),$=await ia(process.execPath,[process.argv[1],"set","version","classic","--only-if-needed"],{cwd:r,env:y,stdin:l,stdout:c,stderr:u,end:ss.ErrorCode});if($.code!==0)return $.code;await U.writeFilePromise(Y,q),await U.appendFilePromise(x.join(r,".npmignore"),`/.yarn +`),c.write(` +`),delete y.NODE_ENV;let z=await ia("yarn",["install"],{cwd:r,env:y,stdin:l,stdout:c,stderr:u,end:ss.ErrorCode});if(z.code!==0)return z.code;c.write(` +`);let ne=await ia("yarn",[...T,"pack","--filename",H.fromPortablePath(e)],{cwd:r,env:y,stdin:l,stdout:c,stderr:u});return ne.code!==0?ne.code:0}],[hn.Yarn2,async()=>{let T=n!==null?["workspace",n]:[];y.YARN_ENABLE_INLINE_BUILDS="1";let Y=x.join(r,xt.lockfile);await U.existsPromise(Y)||await U.writeFilePromise(Y,"");let q=await ia("yarn",[...T,"pack","--install-if-needed","--filename",H.fromPortablePath(e)],{cwd:r,env:y,stdin:l,stdout:c,stderr:u});return q.code!==0?q.code:0}],[hn.Npm,async()=>{if(n!==null){let A=new Il.PassThrough,oe=Og(A);A.pipe(c,{end:!1});let ce=await ia("npm",["--version"],{cwd:r,env:y,stdin:l,stdout:A,stderr:u,end:ss.Never});if(A.end(),ce.code!==0)return c.end(),u.end(),ce.code;let Z=(await oe).toString().trim();if(!Jc(Z,">=7.x")){let O=ea(null,"npm"),L=rr(O,Z),de=rr(O,">=7.x");throw new Error(`Workspaces aren't supported by ${sr(t,L)}; please upgrade to ${sr(t,de)} (npm has been detected as the primary package manager for ${tt(t,r,Ye.PATH)})`)}}let T=n!==null?["--workspace",n]:[];delete y.npm_config_user_agent,delete y.npm_config_production,delete y.NPM_CONFIG_PRODUCTION,delete y.NODE_ENV;let Y=await ia("npm",["install"],{cwd:r,env:y,stdin:l,stdout:c,stderr:u,end:ss.ErrorCode});if(Y.code!==0)return Y.code;let q=new Il.PassThrough,$=Og(q);q.pipe(c);let z=await ia("npm",["pack","--silent",...T],{cwd:r,env:y,stdin:l,stdout:q,stderr:u});if(z.code!==0)return z.code;let ne=(await $).toString().trim().replace(/^.*\n/s,""),ee=x.resolve(r,H.toPortablePath(ne));return await U.renamePromise(ee,e),0}]]).get(p);if(typeof v=="undefined")throw new Error("Assertion failed: Unsupported workflow");let k=await v();if(!(k===0||typeof k=="undefined"))throw U.detachTemp(o),new ct(X.PACKAGE_PREPARATION_FAILED,`Packing the package failed (exit code ${k}, logs can be found here: ${tt(t,a,Ye.PATH)})`)})})})}async function WDe(r,e,{project:t}){let i=t.tryWorkspaceByLocator(r);if(i!==null)return rD(i,e);let n=t.storedPackages.get(r.locatorHash);if(!n)throw new Error(`Package for ${It(t.configuration,r)} not found in the project`);return await ys.openPromise(async s=>{let o=t.configuration,a=t.configuration.getLinkers(),l={project:t,report:new Je({stdout:new Il.PassThrough,configuration:o})},c=a.find(h=>h.supportsPackage(n,l));if(!c)throw new Error(`The package ${It(t.configuration,n)} isn't supported by any of the available linkers`);let u=await c.findPackageLocation(n,l),g=new _t(u,{baseFs:s});return(await At.find(Me.dot,{baseFs:g})).scripts.has(e)},{libzip:await fn()})}async function sB(r,e,t,{cwd:i,project:n,stdin:s,stdout:o,stderr:a}){return await U.mktempPromise(async l=>{let{manifest:c,env:u,cwd:g}=await Y9(r,{project:n,binFolder:l,cwd:i,lifecycleScript:e}),f=c.scripts.get(e);if(typeof f=="undefined")return 1;let h=async()=>await Zw(f,t,{cwd:g,env:u,stdin:s,stdout:o,stderr:a});return await(await n.configuration.reduceHook(m=>m.wrapScriptExecution,h,n,r,e,{script:f,args:t,cwd:g,env:u,stdin:s,stdout:o,stderr:a}))()})}async function iD(r,e,t,{cwd:i,project:n,stdin:s,stdout:o,stderr:a}){return await U.mktempPromise(async l=>{let{env:c,cwd:u}=await Y9(r,{project:n,binFolder:l,cwd:i});return await Zw(e,t,{cwd:u,env:c,stdin:s,stdout:o,stderr:a})})}async function zDe(r,{binFolder:e,cwd:t,lifecycleScript:i}){let n=await qd({project:r.project,locator:r.anchoredLocator,binFolder:e,lifecycleScript:i});return await Promise.all(Array.from(await q9(r),([s,[,o]])=>lA(e,Jr(s),process.execPath,[o]))),typeof t=="undefined"&&(t=x.dirname(await U.realpathPromise(x.join(r.cwd,"package.json")))),{manifest:r.manifest,binFolder:e,env:n,cwd:t}}async function Y9(r,{project:e,binFolder:t,cwd:i,lifecycleScript:n}){let s=e.tryWorkspaceByLocator(r);if(s!==null)return zDe(s,{binFolder:t,cwd:i,lifecycleScript:n});let o=e.storedPackages.get(r.locatorHash);if(!o)throw new Error(`Package for ${It(e.configuration,r)} not found in the project`);return await ys.openPromise(async a=>{let l=e.configuration,c=e.configuration.getLinkers(),u={project:e,report:new Je({stdout:new Il.PassThrough,configuration:l})},g=c.find(y=>y.supportsPackage(o,u));if(!g)throw new Error(`The package ${It(e.configuration,o)} isn't supported by any of the available linkers`);let f=await qd({project:e,locator:r,binFolder:t,lifecycleScript:n});await Promise.all(Array.from(await oB(r,{project:e}),([y,[,b]])=>lA(t,Jr(y),process.execPath,[b])));let h=await g.findPackageLocation(o,u),p=new _t(h,{baseFs:a}),m=await At.find(Me.dot,{baseFs:p});return typeof i=="undefined"&&(i=h),{manifest:m,binFolder:t,env:f,cwd:i}},{libzip:await fn()})}async function J9(r,e,t,{cwd:i,stdin:n,stdout:s,stderr:o}){return await sB(r.anchoredLocator,e,t,{cwd:i,project:r.project,stdin:n,stdout:s,stderr:o})}function rD(r,e){return r.manifest.scripts.has(e)}async function W9(r,e,{cwd:t,report:i}){let{configuration:n}=r.project,s=null;await U.mktempPromise(async o=>{let a=x.join(o,`${e}.log`),l=`# This file contains the result of Yarn calling the "${e}" lifecycle script inside a workspace ("${H.fromPortablePath(r.cwd)}") +`,{stdout:c,stderr:u}=n.getSubprocessStreams(a,{report:i,prefix:It(n,r.anchoredLocator),header:l});i.reportInfo(X.LIFECYCLE_SCRIPT,`Calling the "${e}" lifecycle script`);let g=await J9(r,e,[],{cwd:t,stdin:s,stdout:c,stderr:u});if(c.end(),u.end(),g!==0)throw U.detachTemp(o),new ct(X.LIFECYCLE_SCRIPT,`${(0,H9.default)(e)} script failed (exit code ${tt(n,g,Ye.NUMBER)}, logs can be found here: ${tt(n,a,Ye.PATH)}); run ${tt(n,`yarn ${e}`,Ye.CODE)} to investigate`)})}async function _De(r,e,t){rD(r,e)&&await W9(r,e,t)}async function oB(r,{project:e}){let t=e.configuration,i=new Map,n=e.storedPackages.get(r.locatorHash);if(!n)throw new Error(`Package for ${It(t,r)} not found in the project`);let s=new Il.Writable,o=t.getLinkers(),a={project:e,report:new Je({configuration:t,stdout:s})},l=new Set([r.locatorHash]);for(let u of n.dependencies.values()){let g=e.storedResolutions.get(u.descriptorHash);if(!g)throw new Error(`Assertion failed: The resolution (${sr(t,u)}) should have been registered`);l.add(g)}let c=await Promise.all(Array.from(l,async u=>{let g=e.storedPackages.get(u);if(!g)throw new Error(`Assertion failed: The package (${u}) should have been registered`);if(g.bin.size===0)return Vo.skip;let f=o.find(p=>p.supportsPackage(g,a));if(!f)return Vo.skip;let h=null;try{h=await f.findPackageLocation(g,a)}catch(p){if(p.code==="LOCATOR_NOT_INSTALLED")return Vo.skip;throw p}return{dependency:g,packageLocation:h}}));for(let u of c){if(u===Vo.skip)continue;let{dependency:g,packageLocation:f}=u;for(let[h,p]of g.bin)i.set(h,[g,H.fromPortablePath(x.resolve(f,p))])}return i}async function q9(r){return await oB(r.anchoredLocator,{project:r.project})}async function z9(r,e,t,{cwd:i,project:n,stdin:s,stdout:o,stderr:a,nodeArgs:l=[],packageAccessibleBinaries:c}){c!=null||(c=await oB(r,{project:n}));let u=c.get(e);if(!u)throw new Error(`Binary not found (${e}) for ${It(n.configuration,r)}`);return await U.mktempPromise(async g=>{let[,f]=u,h=await qd({project:n,locator:r,binFolder:g});await Promise.all(Array.from(c,([m,[,y]])=>lA(h.BERRY_BIN_FOLDER,Jr(m),process.execPath,[y])));let p;try{p=await ia(process.execPath,[...l,f,...t],{cwd:i,env:h,stdin:s,stdout:o,stderr:a})}finally{await U.removePromise(h.BERRY_BIN_FOLDER)}return p.code})}async function VDe(r,e,t,{cwd:i,stdin:n,stdout:s,stderr:o,packageAccessibleBinaries:a}){return await z9(r.anchoredLocator,e,t,{project:r.project,cwd:i,stdin:n,stdout:s,stderr:o,packageAccessibleBinaries:a})}var Bi={};ft(Bi,{convertToZip:()=>oNe,extractArchiveTo:()=>ANe,makeArchiveFromDirectory:()=>sNe});var K6=ge(require("stream")),H6=ge(N6());var L6=ge(require("os")),T6=ge(fg()),O6=ge(require("worker_threads")),Rl=Symbol("kTaskInfo"),dR=class{constructor(e){this.source=e;this.workers=[];this.limit=(0,T6.default)(Math.max(1,(0,L6.cpus)().length));this.cleanupInterval=setInterval(()=>{if(this.limit.pendingCount===0&&this.limit.activeCount===0){let t=this.workers.pop();t?t.terminate():clearInterval(this.cleanupInterval)}},5e3).unref()}createWorker(){this.cleanupInterval.refresh();let e=new O6.Worker(this.source,{eval:!0,execArgv:[...process.execArgv,"--unhandled-rejections=strict"]});return e.on("message",t=>{if(!e[Rl])throw new Error("Assertion failed: Worker sent a result without having a task assigned");e[Rl].resolve(t),e[Rl]=null,e.unref(),this.workers.push(e)}),e.on("error",t=>{var i;(i=e[Rl])==null||i.reject(t),e[Rl]=null}),e.on("exit",t=>{var i;t!==0&&((i=e[Rl])==null||i.reject(new Error(`Worker exited with code ${t}`))),e[Rl]=null}),e}run(e){return this.limit(()=>{var i;let t=(i=this.workers.pop())!=null?i:this.createWorker();return t.ref(),new Promise((n,s)=>{t[Rl]={resolve:n,reject:s},t.postMessage(e)})})}};var j6=ge(U6());async function sNe(r,{baseFs:e=new ar,prefixPath:t=Me.root,compressionLevel:i,inMemory:n=!1}={}){let s=await fn(),o;if(n)o=new li(null,{libzip:s,level:i});else{let l=await U.mktempPromise(),c=x.join(l,"archive.zip");o=new li(c,{create:!0,libzip:s,level:i})}let a=x.resolve(Me.root,t);return await o.copyPromise(a,r,{baseFs:e,stableTime:!0,stableSort:!0}),o}var G6;async function oNe(r,e){let t=await U.mktempPromise(),i=x.join(t,"archive.zip");return G6||(G6=new dR((0,j6.getContent)())),await G6.run({tmpFile:i,tgz:r,opts:e}),new li(i,{libzip:await fn(),level:e.compressionLevel})}async function*aNe(r){let e=new H6.default.Parse,t=new K6.PassThrough({objectMode:!0,autoDestroy:!0,emitClose:!0});e.on("entry",i=>{t.write(i)}),e.on("error",i=>{t.destroy(i)}),e.on("close",()=>{t.destroyed||t.end()}),e.end(r);for await(let i of t){let n=i;yield n,n.resume()}}async function ANe(r,e,{stripComponents:t=0,prefixPath:i=Me.dot}={}){var s,o;function n(a){if(a.path[0]==="/")return!0;let l=a.path.split(/\//g);return!!(l.some(c=>c==="..")||l.length<=t)}for await(let a of aNe(r)){if(n(a))continue;let l=x.normalize(H.toPortablePath(a.path)).replace(/\/$/,"").split(/\//g);if(l.length<=t)continue;let c=l.slice(t).join("/"),u=x.join(i,c),g=420;switch((a.type==="Directory"||(((s=a.mode)!=null?s:0)&73)!=0)&&(g|=73),a.type){case"Directory":e.mkdirpSync(x.dirname(u),{chmod:493,utimes:[Rr.SAFE_TIME,Rr.SAFE_TIME]}),e.mkdirSync(u,{mode:g}),e.utimesSync(u,Rr.SAFE_TIME,Rr.SAFE_TIME);break;case"OldFile":case"File":e.mkdirpSync(x.dirname(u),{chmod:493,utimes:[Rr.SAFE_TIME,Rr.SAFE_TIME]}),e.writeFileSync(u,await Og(a),{mode:g}),e.utimesSync(u,Rr.SAFE_TIME,Rr.SAFE_TIME);break;case"SymbolicLink":e.mkdirpSync(x.dirname(u),{chmod:493,utimes:[Rr.SAFE_TIME,Rr.SAFE_TIME]}),e.symlinkSync(a.linkpath,u),(o=e.lutimesSync)==null||o.call(e,u,Rr.SAFE_TIME,Rr.SAFE_TIME);break}}return e}var ls={};ft(ls,{emitList:()=>lNe,emitTree:()=>_6,treeNodeToJson:()=>z6,treeNodeToTreeify:()=>W6});var J6=ge(q6());function W6(r,{configuration:e}){let t={},i=(n,s)=>{let o=Array.isArray(n)?n.entries():Object.entries(n);for(let[a,{label:l,value:c,children:u}]of o){let g=[];typeof l!="undefined"&&g.push(Fy(e,l,Oc.BOLD)),typeof c!="undefined"&&g.push(tt(e,c[0],c[1])),g.length===0&&g.push(Fy(e,`${a}`,Oc.BOLD));let f=g.join(": "),h=s[f]={};typeof u!="undefined"&&i(u,h)}};if(typeof r.children=="undefined")throw new Error("The root node must only contain children");return i(r.children,t),t}function z6(r){let e=t=>{var s;if(typeof t.children=="undefined"){if(typeof t.value=="undefined")throw new Error("Assertion failed: Expected a value to be set if the children are missing");return Mc(t.value[0],t.value[1])}let i=Array.isArray(t.children)?t.children.entries():Object.entries((s=t.children)!=null?s:{}),n=Array.isArray(t.children)?[]:{};for(let[o,a]of i)n[o]=e(a);return typeof t.value=="undefined"?n:{value:Mc(t.value[0],t.value[1]),children:n}};return e(r)}function lNe(r,{configuration:e,stdout:t,json:i}){let n=r.map(s=>({value:s}));_6({children:n},{configuration:e,stdout:t,json:i})}function _6(r,{configuration:e,stdout:t,json:i,separators:n=0}){var o;if(i){let a=Array.isArray(r.children)?r.children.values():Object.values((o=r.children)!=null?o:{});for(let l of a)t.write(`${JSON.stringify(z6(l))} +`);return}let s=(0,J6.asTree)(W6(r,{configuration:e}),!1,!1);if(n>=1&&(s=s.replace(/^([├└]─)/gm,`\u2502 +$1`).replace(/^│\n/,"")),n>=2)for(let a=0;a<2;++a)s=s.replace(/^([│ ].{2}[├│ ].{2}[^\n]+\n)(([│ ]).{2}[├└].{2}[^\n]*\n[│ ].{2}[│ ].{2}[├└]─)/gm,`$1$3 \u2502 +$2`).replace(/^│\n/,"");if(n>=3)throw new Error("Only the first two levels are accepted by treeUtils.emitTree");t.write(s)}var V6=ge(require("crypto")),ER=ge(require("fs"));var cNe=8,Nt=class{constructor(e,{configuration:t,immutable:i=t.get("enableImmutableCache"),check:n=!1}){this.markedFiles=new Set;this.mutexes=new Map;this.cacheId=`-${(0,V6.randomBytes)(8).toString("hex")}.tmp`;this.configuration=t,this.cwd=e,this.immutable=i,this.check=n;let s=t.get("cacheKeyOverride");if(s!==null)this.cacheKey=`${s}`;else{let o=t.get("compressionLevel"),a=o!==cc?`c${o}`:"";this.cacheKey=[cNe,a].join("")}}static async find(e,{immutable:t,check:i}={}){let n=new Nt(e.get("cacheFolder"),{configuration:e,immutable:t,check:i});return await n.setup(),n}get mirrorCwd(){if(!this.configuration.get("enableMirror"))return null;let e=`${this.configuration.get("globalFolder")}/cache`;return e!==this.cwd?e:null}getVersionFilename(e){return`${Wg(e)}-${this.cacheKey}.zip`}getChecksumFilename(e,t){let n=uNe(t).slice(0,10);return`${Wg(e)}-${n}.zip`}getLocatorPath(e,t,i={}){var s;return this.mirrorCwd===null||((s=i.unstablePackages)==null?void 0:s.has(e.locatorHash))?x.resolve(this.cwd,this.getVersionFilename(e)):t===null||IR(t)!==this.cacheKey?null:x.resolve(this.cwd,this.getChecksumFilename(e,t))}getLocatorMirrorPath(e){let t=this.mirrorCwd;return t!==null?x.resolve(t,this.getVersionFilename(e)):null}async setup(){if(!this.configuration.get("enableGlobalCache"))if(this.immutable){if(!await U.existsPromise(this.cwd))throw new ct(X.IMMUTABLE_CACHE,"Cache path does not exist.")}else{await U.mkdirPromise(this.cwd,{recursive:!0});let e=x.resolve(this.cwd,".gitignore");await U.changeFilePromise(e,`/.gitignore +*.flock +*.tmp +`)}(this.mirrorCwd||!this.immutable)&&await U.mkdirPromise(this.mirrorCwd||this.cwd,{recursive:!0})}async fetchPackageFromCache(e,t,a){var l=a,{onHit:i,onMiss:n,loader:s}=l,o=Or(l,["onHit","onMiss","loader"]);var A;let c=this.getLocatorMirrorPath(e),u=new ar,g=()=>{let oe=new li(null,{libzip:Y}),ce=x.join(Me.root,tx(e));return oe.mkdirSync(ce,{recursive:!0}),oe.writeJsonSync(x.join(ce,xt.manifest),{name:Ot(e),mocked:!0}),oe},f=async(oe,ce=null)=>{var O;if(ce===null&&((O=o.unstablePackages)==null?void 0:O.has(e.locatorHash)))return null;let Z=!o.skipIntegrityCheck||!t?`${this.cacheKey}/${await ow(oe)}`:t;if(ce!==null){let L=!o.skipIntegrityCheck||!t?`${this.cacheKey}/${await ow(ce)}`:t;if(Z!==L)throw new ct(X.CACHE_CHECKSUM_MISMATCH,"The remote archive doesn't match the local checksum - has the local cache been corrupted?")}if(t!==null&&Z!==t){let L;switch(this.check?L="throw":IR(t)!==IR(Z)?L="update":L=this.configuration.get("checksumBehavior"),L){case"ignore":return t;case"update":return Z;default:case"throw":throw new ct(X.CACHE_CHECKSUM_MISMATCH,"The remote archive doesn't match the expected checksum")}}return Z},h=async oe=>{if(!s)throw new Error(`Cache check required but no loader configured for ${It(this.configuration,e)}`);let ce=await s(),Z=ce.getRealPath();return ce.saveAndClose(),await U.chmodPromise(Z,420),await f(oe,Z)},p=async()=>{if(c===null||!await U.existsPromise(c)){let oe=await s(),ce=oe.getRealPath();return oe.saveAndClose(),{source:"loader",path:ce}}return{source:"mirror",path:c}},m=async()=>{if(!s)throw new Error(`Cache entry required but missing for ${It(this.configuration,e)}`);if(this.immutable)throw new ct(X.IMMUTABLE_CACHE,`Cache entry required but missing for ${It(this.configuration,e)}`);let{path:oe,source:ce}=await p(),Z=await f(oe),O=this.getLocatorPath(e,Z,o);if(!O)throw new Error("Assertion failed: Expected the cache path to be available");let L=[];ce!=="mirror"&&c!==null&&L.push(async()=>{let Be=`${c}${this.cacheId}`;await U.copyFilePromise(oe,Be,ER.default.constants.COPYFILE_FICLONE),await U.chmodPromise(Be,420),await U.renamePromise(Be,c)}),(!o.mirrorWriteOnly||c===null)&&L.push(async()=>{let Be=`${O}${this.cacheId}`;await U.copyFilePromise(oe,Be,ER.default.constants.COPYFILE_FICLONE),await U.chmodPromise(Be,420),await U.renamePromise(Be,O)});let de=o.mirrorWriteOnly&&c!=null?c:O;return await Promise.all(L.map(Be=>Be())),[!1,de,Z]},y=async()=>{let ce=(async()=>{var Ge;let Z=this.getLocatorPath(e,t,o),O=Z!==null?await u.existsPromise(Z):!1,L=!!((Ge=o.mockedPackages)==null?void 0:Ge.has(e.locatorHash))&&(!this.check||!O),de=L||O,Be=de?i:n;if(Be&&Be(),de){let re=null,se=Z;return L||(re=this.check?await h(se):await f(se)),[L,se,re]}else return m()})();this.mutexes.set(e.locatorHash,ce);try{return await ce}finally{this.mutexes.delete(e.locatorHash)}};for(let oe;oe=this.mutexes.get(e.locatorHash);)await oe;let[b,v,k]=await y();this.markedFiles.add(v);let T,Y=await fn(),q=b?()=>g():()=>new li(v,{baseFs:u,libzip:Y,readOnly:!0}),$=new Vh(()=>Fv(()=>T=q(),oe=>`Failed to open the cache entry for ${It(this.configuration,e)}: ${oe}`),x),z=new La(v,{baseFs:$,pathUtils:x}),ne=()=>{T==null||T.discardAndClose()},ee=((A=o.unstablePackages)==null?void 0:A.has(e.locatorHash))?null:k;return[z,ne,ee]}};function IR(r){let e=r.indexOf("/");return e!==-1?r.slice(0,e):null}function uNe(r){let e=r.indexOf("/");return e!==-1?r.slice(e+1):r}var cs;(function(t){t[t.SCRIPT=0]="SCRIPT",t[t.SHELLCODE=1]="SHELLCODE"})(cs||(cs={}));var dA=class extends Ji{constructor({configuration:e,stdout:t,suggestInstall:i=!0}){super();this.errorCount=0;sd(this,{configuration:e}),this.configuration=e,this.stdout=t,this.suggestInstall=i}static async start(e,t){let i=new this(e);try{await t(i)}catch(n){i.reportExceptionOnce(n)}finally{await i.finalize()}return i}hasErrors(){return this.errorCount>0}exitCode(){return this.hasErrors()?1:0}reportCacheHit(e){}reportCacheMiss(e){}startSectionSync(e,t){return t()}async startSectionPromise(e,t){return await t()}startTimerSync(e,t,i){return(typeof t=="function"?t:i)()}async startTimerPromise(e,t,i){return await(typeof t=="function"?t:i)()}async startCacheReport(e){return await e()}reportSeparator(){}reportInfo(e,t){}reportWarning(e,t){}reportError(e,t){this.errorCount+=1,this.stdout.write(`${tt(this.configuration,"\u27A4","redBright")} ${this.formatNameWithHyperlink(e)}: ${t} +`)}reportProgress(e){let t=Promise.resolve().then(async()=>{for await(let{}of e);}),i=()=>{};return te(N({},t),{stop:i})}reportJson(e){}async finalize(){this.errorCount>0&&(this.stdout.write(` +`),this.stdout.write(`${tt(this.configuration,"\u27A4","redBright")} Errors happened when preparing the environment required to run this command. +`),this.suggestInstall&&this.stdout.write(`${tt(this.configuration,"\u27A4","redBright")} This might be caused by packages being missing from the lockfile, in which case running "yarn install" might help. +`))}formatNameWithHyperlink(e){return tD(e,{configuration:this.configuration,json:!1})}};var n0=ge(require("crypto"));function CA(){}CA.prototype={diff:function(e,t){var i=arguments.length>2&&arguments[2]!==void 0?arguments[2]:{},n=i.callback;typeof i=="function"&&(n=i,i={}),this.options=i;var s=this;function o(m){return n?(setTimeout(function(){n(void 0,m)},0),!0):m}e=this.castInput(e),t=this.castInput(t),e=this.removeEmpty(this.tokenize(e)),t=this.removeEmpty(this.tokenize(t));var a=t.length,l=e.length,c=1,u=a+l;i.maxEditLength&&(u=Math.min(u,i.maxEditLength));var g=[{newPos:-1,components:[]}],f=this.extractCommon(g[0],t,e,0);if(g[0].newPos+1>=a&&f+1>=l)return o([{value:this.join(t),count:t.length}]);function h(){for(var m=-1*c;m<=c;m+=2){var y=void 0,b=g[m-1],v=g[m+1],k=(v?v.newPos:0)-m;b&&(g[m-1]=void 0);var T=b&&b.newPos+1=a&&k+1>=l)return o(gNe(s,y.components,t,e,s.useLongestToken));g[m]=y}c++}if(n)(function m(){setTimeout(function(){if(c>u)return n();h()||m()},0)})();else for(;c<=u;){var p=h();if(p)return p}},pushComponent:function(e,t,i){var n=e[e.length-1];n&&n.added===t&&n.removed===i?e[e.length-1]={count:n.count+1,added:t,removed:i}:e.push({count:1,added:t,removed:i})},extractCommon:function(e,t,i,n){for(var s=t.length,o=i.length,a=e.newPos,l=a-n,c=0;a+1h.length?m:h}),c.value=r.join(u)}else c.value=r.join(t.slice(a,a+c.count));a+=c.count,c.added||(l+=c.count)}}var f=e[o-1];return o>1&&typeof f.value=="string"&&(f.added||f.removed)&&r.equals("",f.value)&&(e[o-2].value+=f.value,e.pop()),e}function fNe(r){return{newPos:r.newPos,components:r.components.slice(0)}}var oAt=new CA;var X6=/^[A-Za-z\xC0-\u02C6\u02C8-\u02D7\u02DE-\u02FF\u1E00-\u1EFF]+$/,Z6=/\S/,$6=new CA;$6.equals=function(r,e){return this.options.ignoreCase&&(r=r.toLowerCase(),e=e.toLowerCase()),r===e||this.options.ignoreWhitespace&&!Z6.test(r)&&!Z6.test(e)};$6.tokenize=function(r){for(var e=r.split(/([^\S\r\n]+|[()[\]{}'"\r\n]|\b)/),t=0;tr.length)&&(e=r.length);for(var t=0,i=new Array(e);t0?l(q.lines.slice(-o.context)):[],u-=f.length,g-=f.length)}(Y=f).push.apply(Y,wR(T.map(function(Z){return(k.added?"+":"-")+Z}))),k.added?p+=T.length:h+=T.length}else{if(u)if(T.length<=o.context*2&&v=a.length-2&&T.length<=o.context){var A=/\n$/.test(t),oe=/\n$/.test(i),ce=T.length==0&&f.length>ee.oldLines;!A&&ce&&t.length>0&&f.splice(ee.oldLines,0,"\\ No newline at end of file"),(!A&&!ce||!oe)&&f.push("\\ No newline at end of file")}c.push(ee),u=0,g=0,f=[]}h+=T.length,p+=T.length}},y=0;y`${t}#commit=${i}`],[/^https:\/\/((?:[^/]+?)@)?codeload\.github\.com\/([^/]+\/[^/]+)\/tar\.gz\/([0-9a-f]+)$/,(r,e,t="",i,n)=>`https://${t}github.com/${i}.git#commit=${n}`],[/^https:\/\/((?:[^/]+?)@)?github\.com\/([^/]+\/[^/]+?)(?:\.git)?#([0-9a-f]+)$/,(r,e,t="",i,n)=>`https://${t}github.com/${i}.git#commit=${n}`],[/^https?:\/\/[^/]+\/(?:[^/]+\/)*(?:@.+(?:\/|(?:%2f)))?([^/]+)\/(?:-|download)\/\1-[^/]+\.tgz(?:#|$)/,r=>`npm:${r}`],[/^https:\/\/npm\.pkg\.github\.com\/download\/(?:@[^/]+)\/(?:[^/]+)\/(?:[^/]+)\/(?:[0-9a-f]+)(?:#|$)/,r=>`npm:${r}`],[/^https:\/\/npm\.fontawesome\.com\/(?:@[^/]+)\/([^/]+)\/-\/([^/]+)\/\1-\2.tgz(?:#|$)/,r=>`npm:${r}`],[/^https?:\/\/[^/]+\/.*\/(@[^/]+)\/([^/]+)\/-\/\1\/\2-(?:[.\d\w-]+)\.tgz(?:#|$)/,(r,e)=>uw({protocol:"npm:",source:null,selector:r,params:{__archiveUrl:e}})],[/^[^/]+\.tgz#[0-9a-f]+$/,r=>`npm:${r}`]],LR=class{constructor(e){this.resolver=e;this.resolutions=null}async setup(e,{report:t}){let i=x.join(e.cwd,e.configuration.get("lockfileFilename"));if(!U.existsSync(i))return;let n=await U.readFilePromise(i,"utf8"),s=Si(n);if(Object.prototype.hasOwnProperty.call(s,"__metadata"))return;let o=this.resolutions=new Map;for(let a of Object.keys(s)){let l=dd(a);if(!l){t.reportWarning(X.YARN_IMPORT_FAILED,`Failed to parse the string "${a}" into a proper descriptor`);continue}mo(l.range)&&(l=rr(l,`npm:${l.range}`));let{version:c,resolved:u}=s[a];if(!u)continue;let g;for(let[h,p]of _Oe){let m=u.match(h);if(m){g=p(c,...m);break}}if(!g){t.reportWarning(X.YARN_IMPORT_FAILED,`${sr(e.configuration,l)}: Only some patterns can be imported from legacy lockfiles (not "${u}")`);continue}let f=l;try{let h=Jg(l.range),p=dd(h.selector,!0);p&&(f=p)}catch{}o.set(l.descriptorHash,cn(f,g))}}supportsDescriptor(e,t){return this.resolutions?this.resolutions.has(e.descriptorHash):!1}supportsLocator(e,t){return!1}shouldPersistResolution(e,t){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){if(!this.resolutions)throw new Error("Assertion failed: The resolution store should have been setup");let n=this.resolutions.get(e.descriptorHash);if(!n)throw new Error("Assertion failed: The resolution should have been registered");return await this.resolver.getCandidates(Vk(n),t,i)}async getSatisfying(e,t,i){return null}async resolve(e,t){throw new Error("Assertion failed: This resolver doesn't support resolving locators to packages")}};var TR=class{constructor(e){this.resolver=e}supportsDescriptor(e,t){return!!(t.project.storedResolutions.get(e.descriptorHash)||t.project.originalPackages.has(lw(e).locatorHash))}supportsLocator(e,t){return!!(t.project.originalPackages.has(e.locatorHash)&&!t.project.lockfileNeedsRefresh)}shouldPersistResolution(e,t){throw new Error("The shouldPersistResolution method shouldn't be called on the lockfile resolver, which would always answer yes")}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return this.resolver.getResolutionDependencies(e,t)}async getCandidates(e,t,i){let n=i.project.originalPackages.get(lw(e).locatorHash);if(n)return[n];let s=i.project.storedResolutions.get(e.descriptorHash);if(!s)throw new Error("Expected the resolution to have been successful - resolution not found");if(n=i.project.originalPackages.get(s),!n)throw new Error("Expected the resolution to have been successful - package not found");return[n]}async getSatisfying(e,t,i){return null}async resolve(e,t){let i=t.project.originalPackages.get(e.locatorHash);if(!i)throw new Error("The lockfile resolver isn't meant to resolve packages - they should already have been stored into a cache");return i}};var OR=class{constructor(e){this.resolver=e}supportsDescriptor(e,t){return this.resolver.supportsDescriptor(e,t)}supportsLocator(e,t){return this.resolver.supportsLocator(e,t)}shouldPersistResolution(e,t){return this.resolver.shouldPersistResolution(e,t)}bindDescriptor(e,t,i){return this.resolver.bindDescriptor(e,t,i)}getResolutionDependencies(e,t){return this.resolver.getResolutionDependencies(e,t)}async getCandidates(e,t,i){throw new ct(X.MISSING_LOCKFILE_ENTRY,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async getSatisfying(e,t,i){throw new ct(X.MISSING_LOCKFILE_ENTRY,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}async resolve(e,t){throw new ct(X.MISSING_LOCKFILE_ENTRY,`This package doesn't seem to be present in your lockfile; run "yarn install" to update the lockfile`)}};var di=class extends Ji{reportCacheHit(e){}reportCacheMiss(e){}startSectionSync(e,t){return t()}async startSectionPromise(e,t){return await t()}startTimerSync(e,t,i){return(typeof t=="function"?t:i)()}async startTimerPromise(e,t,i){return await(typeof t=="function"?t:i)()}async startCacheReport(e){return await e()}reportSeparator(){}reportInfo(e,t){}reportWarning(e,t){}reportError(e,t){}reportProgress(e){let t=Promise.resolve().then(async()=>{for await(let{}of e);}),i=()=>{};return te(N({},t),{stop:i})}reportJson(e){}async finalize(){}};var aZ=ge(zk());var mC=class{constructor(e,{project:t}){this.workspacesCwds=new Set;this.dependencies=new Map;this.project=t,this.cwd=e}async setup(){var s;this.manifest=(s=await At.tryFind(this.cwd))!=null?s:new At,this.relativeCwd=x.relative(this.project.cwd,this.cwd)||Me.dot;let e=this.manifest.name?this.manifest.name:ea(null,`${this.computeCandidateName()}-${ln(this.relativeCwd).substring(0,6)}`),t=this.manifest.version?this.manifest.version:"0.0.0";this.locator=cn(e,t),this.anchoredDescriptor=rr(this.locator,`${oi.protocol}${this.relativeCwd}`),this.anchoredLocator=cn(this.locator,`${oi.protocol}${this.relativeCwd}`);let i=this.manifest.workspaceDefinitions.map(({pattern:o})=>o),n=await(0,aZ.default)(i,{cwd:H.fromPortablePath(this.cwd),expandDirectories:!1,onlyDirectories:!0,onlyFiles:!1,ignore:["**/node_modules","**/.git","**/.yarn"]});n.sort();for(let o of n){let a=x.resolve(this.cwd,H.toPortablePath(o));U.existsSync(x.join(a,"package.json"))&&this.workspacesCwds.add(a)}}accepts(e){var o;let t=e.indexOf(":"),i=t!==-1?e.slice(0,t+1):null,n=t!==-1?e.slice(t+1):e;if(i===oi.protocol&&x.normalize(n)===this.relativeCwd||i===oi.protocol&&(n==="*"||n==="^"||n==="~"))return!0;let s=mo(n);return s?i===oi.protocol?s.test((o=this.manifest.version)!=null?o:"0.0.0"):this.project.configuration.get("enableTransparentWorkspaces")&&this.manifest.version!==null?s.test(this.manifest.version):!1:!1}computeCandidateName(){return this.cwd===this.project.cwd?"root-workspace":`${x.basename(this.cwd)}`||"unnamed-workspace"}getRecursiveWorkspaceDependencies({dependencies:e=At.hardDependencies}={}){let t=new Set,i=n=>{for(let s of e)for(let o of n.manifest[s].values()){let a=this.project.tryWorkspaceByDescriptor(o);a===null||t.has(a)||(t.add(a),i(a))}};return i(this),t}getRecursiveWorkspaceDependents({dependencies:e=At.hardDependencies}={}){let t=new Set,i=n=>{for(let s of this.project.workspaces)e.some(a=>[...s.manifest[a].values()].some(l=>{let c=this.project.tryWorkspaceByDescriptor(l);return c!==null&&pd(c.anchoredLocator,n.anchoredLocator)}))&&!t.has(s)&&(t.add(s),i(s))};return i(this),t}getRecursiveWorkspaceChildren(){let e=[];for(let t of this.workspacesCwds){let i=this.project.workspacesByCwd.get(t);i&&e.push(i,...i.getRecursiveWorkspaceChildren())}return e}async persistManifest(){let e={};this.manifest.exportTo(e);let t=x.join(this.cwd,At.fileName),i=`${JSON.stringify(e,null,this.manifest.indent)} +`;await U.changeFilePromise(t,i,{automaticNewlines:!0}),this.manifest.raw=e}};var cZ=6,VOe=1,XOe=/ *, */g,uZ=/\/$/,ZOe=32,$Oe=(0,MR.promisify)(KR.default.gzip),eMe=(0,MR.promisify)(KR.default.gunzip),Ci;(function(t){t.UpdateLockfile="update-lockfile",t.SkipBuild="skip-build"})(Ci||(Ci={}));var HR={restoreInstallersCustomData:["installersCustomData"],restoreResolutions:["accessibleLocators","conditionalLocators","disabledLocators","optionalBuilds","storedDescriptors","storedResolutions","storedPackages","lockFileChecksum"],restoreBuildState:["storedBuildState"]},gZ=r=>ln(`${VOe}`,r),ze=class{constructor(e,{configuration:t}){this.resolutionAliases=new Map;this.workspaces=[];this.workspacesByCwd=new Map;this.workspacesByIdent=new Map;this.storedResolutions=new Map;this.storedDescriptors=new Map;this.storedPackages=new Map;this.storedChecksums=new Map;this.storedBuildState=new Map;this.accessibleLocators=new Set;this.conditionalLocators=new Set;this.disabledLocators=new Set;this.originalPackages=new Map;this.optionalBuilds=new Set;this.lockfileNeedsRefresh=!1;this.peerRequirements=new Map;this.installersCustomData=new Map;this.lockFileChecksum=null;this.installStateChecksum=null;this.configuration=t,this.cwd=e}static async find(e,t){var p,m,y;if(!e.projectCwd)throw new Pe(`No project found in ${t}`);let i=e.projectCwd,n=t,s=null;for(;s!==e.projectCwd;){if(s=n,U.existsSync(x.join(s,xt.manifest))){i=s;break}n=x.dirname(s)}let o=new ze(e.projectCwd,{configuration:e});(p=ye.telemetry)==null||p.reportProject(o.cwd),await o.setupResolutions(),await o.setupWorkspaces(),(m=ye.telemetry)==null||m.reportWorkspaceCount(o.workspaces.length),(y=ye.telemetry)==null||y.reportDependencyCount(o.workspaces.reduce((b,v)=>b+v.manifest.dependencies.size+v.manifest.devDependencies.size,0));let a=o.tryWorkspaceByCwd(i);if(a)return{project:o,workspace:a,locator:a.anchoredLocator};let l=await o.findLocatorForLocation(`${i}/`,{strict:!0});if(l)return{project:o,locator:l,workspace:null};let c=tt(e,o.cwd,Ye.PATH),u=tt(e,x.relative(o.cwd,i),Ye.PATH),g=`- If ${c} isn't intended to be a project, remove any yarn.lock and/or package.json file there.`,f=`- If ${c} is intended to be a project, it might be that you forgot to list ${u} in its workspace configuration.`,h=`- Finally, if ${c} is fine and you intend ${u} to be treated as a completely separate project (not even a workspace), create an empty yarn.lock file in it.`;throw new Pe(`The nearest package directory (${tt(e,i,Ye.PATH)}) doesn't seem to be part of the project declared in ${tt(e,o.cwd,Ye.PATH)}. + +${[g,f,h].join(` +`)}`)}async setupResolutions(){var i;this.storedResolutions=new Map,this.storedDescriptors=new Map,this.storedPackages=new Map,this.lockFileChecksum=null;let e=x.join(this.cwd,this.configuration.get("lockfileFilename")),t=this.configuration.get("defaultLanguageName");if(U.existsSync(e)){let n=await U.readFilePromise(e,"utf8");this.lockFileChecksum=gZ(n);let s=Si(n);if(s.__metadata){let o=s.__metadata.version,a=s.__metadata.cacheKey;this.lockfileNeedsRefresh=o0;){let t=e;e=[];for(let i of t){if(this.workspacesByCwd.has(i))continue;let n=await this.addWorkspace(i),s=this.storedPackages.get(n.anchoredLocator.locatorHash);s&&(n.dependencies=s.dependencies);for(let o of n.workspacesCwds)e.push(o)}}}async addWorkspace(e){let t=new mC(e,{project:this});await t.setup();let i=this.workspacesByIdent.get(t.locator.identHash);if(typeof i!="undefined")throw new Error(`Duplicate workspace name ${fi(this.configuration,t.locator)}: ${H.fromPortablePath(e)} conflicts with ${H.fromPortablePath(i.cwd)}`);return this.workspaces.push(t),this.workspacesByCwd.set(e,t),this.workspacesByIdent.set(t.locator.identHash,t),t}get topLevelWorkspace(){return this.getWorkspaceByCwd(this.cwd)}tryWorkspaceByCwd(e){x.isAbsolute(e)||(e=x.resolve(this.cwd,e)),e=x.normalize(e).replace(/\/+$/,"");let t=this.workspacesByCwd.get(e);return t||null}getWorkspaceByCwd(e){let t=this.tryWorkspaceByCwd(e);if(!t)throw new Error(`Workspace not found (${e})`);return t}tryWorkspaceByFilePath(e){let t=null;for(let i of this.workspaces)x.relative(i.cwd,e).startsWith("../")||t&&t.cwd.length>=i.cwd.length||(t=i);return t||null}getWorkspaceByFilePath(e){let t=this.tryWorkspaceByFilePath(e);if(!t)throw new Error(`Workspace not found (${e})`);return t}tryWorkspaceByIdent(e){let t=this.workspacesByIdent.get(e.identHash);return typeof t=="undefined"?null:t}getWorkspaceByIdent(e){let t=this.tryWorkspaceByIdent(e);if(!t)throw new Error(`Workspace not found (${fi(this.configuration,e)})`);return t}tryWorkspaceByDescriptor(e){let t=this.tryWorkspaceByIdent(e);return t===null||(ll(e)&&(e=gd(e)),!t.accepts(e.range))?null:t}getWorkspaceByDescriptor(e){let t=this.tryWorkspaceByDescriptor(e);if(t===null)throw new Error(`Workspace not found (${sr(this.configuration,e)})`);return t}tryWorkspaceByLocator(e){let t=this.tryWorkspaceByIdent(e);return t===null||(ta(e)&&(e=fd(e)),t.locator.locatorHash!==e.locatorHash&&t.anchoredLocator.locatorHash!==e.locatorHash)?null:t}getWorkspaceByLocator(e){let t=this.tryWorkspaceByLocator(e);if(!t)throw new Error(`Workspace not found (${It(this.configuration,e)})`);return t}refreshWorkspaceDependencies(){for(let e of this.workspaces){let t=this.storedPackages.get(e.anchoredLocator.locatorHash);if(!t)throw new Error(`Assertion failed: Expected workspace ${md(this.configuration,e)} (${tt(this.configuration,x.join(e.cwd,xt.manifest),Ye.PATH)}) to have been resolved. Run "yarn install" to update the lockfile`);e.dependencies=new Map(t.dependencies)}}forgetResolution(e){let t=n=>{this.storedResolutions.delete(n),this.storedDescriptors.delete(n)},i=n=>{this.originalPackages.delete(n),this.storedPackages.delete(n),this.accessibleLocators.delete(n)};if("descriptorHash"in e){let n=this.storedResolutions.get(e.descriptorHash);t(e.descriptorHash);let s=new Set(this.storedResolutions.values());typeof n!="undefined"&&!s.has(n)&&i(n)}if("locatorHash"in e){i(e.locatorHash);for(let[n,s]of this.storedResolutions)s===e.locatorHash&&t(n)}}forgetTransientResolutions(){let e=this.configuration.makeResolver();for(let t of this.originalPackages.values()){let i;try{i=e.shouldPersistResolution(t,{project:this,resolver:e})}catch{i=!1}i||this.forgetResolution(t)}}forgetVirtualResolutions(){for(let e of this.storedPackages.values())for(let[t,i]of e.dependencies)ll(i)&&e.dependencies.set(t,gd(i))}getDependencyMeta(e,t){let i={},s=this.topLevelWorkspace.manifest.dependenciesMeta.get(Ot(e));if(!s)return i;let o=s.get(null);if(o&&Object.assign(i,o),t===null||!lZ.default.valid(t))return i;for(let[a,l]of s)a!==null&&a===t&&Object.assign(i,l);return i}async findLocatorForLocation(e,{strict:t=!1}={}){let i=new di,n=this.configuration.getLinkers(),s={project:this,report:i};for(let o of n){let a=await o.findPackageLocator(e,s);if(a){if(t&&(await o.findPackageLocation(a,s)).replace(uZ,"")!==e.replace(uZ,""))continue;return a}}return null}async resolveEverything(e){if(!this.workspacesByCwd||!this.workspacesByIdent)throw new Error("Workspaces must have been setup before calling this function");this.forgetVirtualResolutions(),e.lockfileOnly||this.forgetTransientResolutions();let t=e.resolver||this.configuration.makeResolver(),i=new LR(t);await i.setup(this,{report:e.report});let n=e.lockfileOnly?[new OR(t)]:[i,t],s=new Bd([new TR(t),...n]),o=this.configuration.makeFetcher(),a=e.lockfileOnly?{project:this,report:e.report,resolver:s}:{project:this,report:e.report,resolver:s,fetchOptions:{project:this,cache:e.cache,checksums:this.storedChecksums,report:e.report,fetcher:o,cacheOptions:{mirrorWriteOnly:!0}}},l=new Map,c=new Map,u=new Map,g=new Map,f=new Map,h=new Map,p=this.topLevelWorkspace.anchoredLocator,m=new Set,y=[],b=ux(),v=this.configuration.getSupportedArchitectures();await e.report.startProgressPromise(Ji.progressViaTitle(),async ne=>{let ee=async O=>{let L=await Tg(async()=>await s.resolve(O,a),Ge=>`${It(this.configuration,O)}: ${Ge}`);if(!pd(O,L))throw new Error(`Assertion failed: The locator cannot be changed by the resolver (went from ${It(this.configuration,O)} to ${It(this.configuration,L)})`);g.set(L.locatorHash,L);let de=this.configuration.normalizePackage(L);for(let[Ge,re]of de.dependencies){let se=await this.configuration.reduceHook(he=>he.reduceDependency,re,this,de,re,{resolver:s,resolveOptions:a});if(!hd(re,se))throw new Error("Assertion failed: The descriptor ident cannot be changed through aliases");let be=s.bindDescriptor(se,O,a);de.dependencies.set(Ge,be)}let Be=ho([...de.dependencies.values()].map(Ge=>Z(Ge)));return y.push(Be),Be.catch(()=>{}),c.set(de.locatorHash,de),de},A=async O=>{let L=f.get(O.locatorHash);if(typeof L!="undefined")return L;let de=Promise.resolve().then(()=>ee(O));return f.set(O.locatorHash,de),de},oe=async(O,L)=>{let de=await Z(L);return l.set(O.descriptorHash,O),u.set(O.descriptorHash,de.locatorHash),de},ce=async O=>{ne.setTitle(sr(this.configuration,O));let L=this.resolutionAliases.get(O.descriptorHash);if(typeof L!="undefined")return oe(O,this.storedDescriptors.get(L));let de=s.getResolutionDependencies(O,a),Be=new Map(await ho(de.map(async se=>{let be=s.bindDescriptor(se,p,a),he=await Z(be);return m.add(he.locatorHash),[se.descriptorHash,he]}))),re=(await Tg(async()=>await s.getCandidates(O,Be,a),se=>`${sr(this.configuration,O)}: ${se}`))[0];if(typeof re=="undefined")throw new Error(`${sr(this.configuration,O)}: No candidates found`);return l.set(O.descriptorHash,O),u.set(O.descriptorHash,re.locatorHash),A(re)},Z=O=>{let L=h.get(O.descriptorHash);if(typeof L!="undefined")return L;l.set(O.descriptorHash,O);let de=Promise.resolve().then(()=>ce(O));return h.set(O.descriptorHash,de),de};for(let O of this.workspaces){let L=O.anchoredDescriptor;y.push(Z(L))}for(;y.length>0;){let O=[...y];y.length=0,await ho(O)}});let k=new Set(this.resolutionAliases.values()),T=new Set(c.keys()),Y=new Set,q=new Map;tMe({project:this,report:e.report,accessibleLocators:Y,volatileDescriptors:k,optionalBuilds:T,peerRequirements:q,allDescriptors:l,allResolutions:u,allPackages:c});for(let ne of m)T.delete(ne);for(let ne of k)l.delete(ne),u.delete(ne);let $=new Set,z=new Set;for(let ne of c.values())ne.conditions!=null&&(!T.has(ne.locatorHash)||(fw(ne,v)||(fw(ne,b)&&e.report.reportWarningOnce(X.GHOST_ARCHITECTURE,`${It(this.configuration,ne)}: Your current architecture (${process.platform}-${process.arch}) is supported by this package, but is missing from the ${tt(this.configuration,"supportedArchitectures",Ri.SETTING)} setting`),z.add(ne.locatorHash)),$.add(ne.locatorHash)));this.storedResolutions=u,this.storedDescriptors=l,this.storedPackages=c,this.accessibleLocators=Y,this.conditionalLocators=$,this.disabledLocators=z,this.originalPackages=g,this.optionalBuilds=T,this.peerRequirements=q,this.refreshWorkspaceDependencies()}async fetchEverything({cache:e,report:t,fetcher:i,mode:n}){let s={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators},o=i||this.configuration.makeFetcher(),a={checksums:this.storedChecksums,project:this,cache:e,fetcher:o,report:t,cacheOptions:s},l=Array.from(new Set(xn(this.storedResolutions.values(),[f=>{let h=this.storedPackages.get(f);if(!h)throw new Error("Assertion failed: The locator should have been registered");return Fs(h)}])));n===Ci.UpdateLockfile&&(l=l.filter(f=>!this.storedChecksums.has(f)));let c=!1,u=Ji.progressViaCounter(l.length);t.reportProgress(u);let g=(0,AZ.default)(ZOe);if(await t.startCacheReport(async()=>{await ho(l.map(f=>g(async()=>{let h=this.storedPackages.get(f);if(!h)throw new Error("Assertion failed: The locator should have been registered");if(ta(h))return;let p;try{p=await o.fetch(h,a)}catch(m){m.message=`${It(this.configuration,h)}: ${m.message}`,t.reportExceptionOnce(m),c=m;return}p.checksum!=null?this.storedChecksums.set(h.locatorHash,p.checksum):this.storedChecksums.delete(h.locatorHash),p.releaseFs&&p.releaseFs()}).finally(()=>{u.tick()})))}),c)throw c}async linkEverything({cache:e,report:t,fetcher:i,mode:n}){var A,oe,ce;let s={mockedPackages:this.disabledLocators,unstablePackages:this.conditionalLocators,skipIntegrityCheck:!0},o=i||this.configuration.makeFetcher(),a={checksums:this.storedChecksums,project:this,cache:e,fetcher:o,report:t,skipIntegrityCheck:!0,cacheOptions:s},l=this.configuration.getLinkers(),c={project:this,report:t},u=new Map(l.map(Z=>{let O=Z.makeInstaller(c),L=O.getCustomDataKey(),de=this.installersCustomData.get(L);return typeof de!="undefined"&&O.attachCustomData(de),[Z,O]})),g=new Map,f=new Map,h=new Map,p=new Map(await ho([...this.accessibleLocators].map(async Z=>{let O=this.storedPackages.get(Z);if(!O)throw new Error("Assertion failed: The locator should have been registered");return[Z,await o.fetch(O,a)]}))),m=[];for(let Z of this.accessibleLocators){let O=this.storedPackages.get(Z);if(typeof O=="undefined")throw new Error("Assertion failed: The locator should have been registered");let L=p.get(O.locatorHash);if(typeof L=="undefined")throw new Error("Assertion failed: The fetch result should have been registered");let de=[],Be=re=>{de.push(re)},Ge=this.tryWorkspaceByLocator(O);if(Ge!==null){let re=[],{scripts:se}=Ge.manifest;for(let he of["preinstall","install","postinstall"])se.has(he)&&re.push([cs.SCRIPT,he]);try{for(let[he,Fe]of u)if(he.supportsPackage(O,c)&&(await Fe.installPackage(O,L,{holdFetchResult:Be})).buildDirective!==null)throw new Error("Assertion failed: Linkers can't return build directives for workspaces; this responsibility befalls to the Yarn core")}finally{de.length===0?(A=L.releaseFs)==null||A.call(L):m.push(ho(de).catch(()=>{}).then(()=>{var he;(he=L.releaseFs)==null||he.call(L)}))}let be=x.join(L.packageFs.getRealPath(),L.prefixPath);f.set(O.locatorHash,be),!ta(O)&&re.length>0&&h.set(O.locatorHash,{directives:re,buildLocations:[be]})}else{let re=l.find(he=>he.supportsPackage(O,c));if(!re)throw new ct(X.LINKER_NOT_FOUND,`${It(this.configuration,O)} isn't supported by any available linker`);let se=u.get(re);if(!se)throw new Error("Assertion failed: The installer should have been registered");let be;try{be=await se.installPackage(O,L,{holdFetchResult:Be})}finally{de.length===0?(oe=L.releaseFs)==null||oe.call(L):m.push(ho(de).then(()=>{}).then(()=>{var he;(he=L.releaseFs)==null||he.call(L)}))}g.set(O.locatorHash,re),f.set(O.locatorHash,be.packageLocation),be.buildDirective&&be.buildDirective.length>0&&be.packageLocation&&h.set(O.locatorHash,{directives:be.buildDirective,buildLocations:[be.packageLocation]})}}let y=new Map;for(let Z of this.accessibleLocators){let O=this.storedPackages.get(Z);if(!O)throw new Error("Assertion failed: The locator should have been registered");let L=this.tryWorkspaceByLocator(O)!==null,de=async(Be,Ge)=>{let re=f.get(O.locatorHash);if(typeof re=="undefined")throw new Error(`Assertion failed: The package (${It(this.configuration,O)}) should have been registered`);let se=[];for(let be of O.dependencies.values()){let he=this.storedResolutions.get(be.descriptorHash);if(typeof he=="undefined")throw new Error(`Assertion failed: The resolution (${sr(this.configuration,be)}, from ${It(this.configuration,O)})should have been registered`);let Fe=this.storedPackages.get(he);if(typeof Fe=="undefined")throw new Error(`Assertion failed: The package (${he}, resolved from ${sr(this.configuration,be)}) should have been registered`);let Ue=this.tryWorkspaceByLocator(Fe)===null?g.get(he):null;if(typeof Ue=="undefined")throw new Error(`Assertion failed: The package (${he}, resolved from ${sr(this.configuration,be)}) should have been registered`);Ue===Be||Ue===null?f.get(Fe.locatorHash)!==null&&se.push([be,Fe]):!L&&re!==null&&Ng(y,he).push(re)}re!==null&&await Ge.attachInternalDependencies(O,se)};if(L)for(let[Be,Ge]of u)Be.supportsPackage(O,c)&&await de(Be,Ge);else{let Be=g.get(O.locatorHash);if(!Be)throw new Error("Assertion failed: The linker should have been found");let Ge=u.get(Be);if(!Ge)throw new Error("Assertion failed: The installer should have been registered");await de(Be,Ge)}}for(let[Z,O]of y){let L=this.storedPackages.get(Z);if(!L)throw new Error("Assertion failed: The package should have been registered");let de=g.get(L.locatorHash);if(!de)throw new Error("Assertion failed: The linker should have been found");let Be=u.get(de);if(!Be)throw new Error("Assertion failed: The installer should have been registered");await Be.attachExternalDependents(L,O)}let b=new Map;for(let Z of u.values()){let O=await Z.finalizeInstall();for(let L of(ce=O==null?void 0:O.records)!=null?ce:[])h.set(L.locatorHash,{directives:L.buildDirective,buildLocations:L.buildLocations});typeof(O==null?void 0:O.customData)!="undefined"&&b.set(Z.getCustomDataKey(),O.customData)}if(this.installersCustomData=b,await ho(m),n===Ci.SkipBuild)return;let v=new Set(this.storedPackages.keys()),k=new Set(h.keys());for(let Z of k)v.delete(Z);let T=(0,n0.createHash)("sha512");T.update(process.versions.node),await this.configuration.triggerHook(Z=>Z.globalHashGeneration,this,Z=>{T.update("\0"),T.update(Z)});let Y=T.digest("hex"),q=new Map,$=Z=>{let O=q.get(Z.locatorHash);if(typeof O!="undefined")return O;let L=this.storedPackages.get(Z.locatorHash);if(typeof L=="undefined")throw new Error("Assertion failed: The package should have been registered");let de=(0,n0.createHash)("sha512");de.update(Z.locatorHash),q.set(Z.locatorHash,"");for(let Be of L.dependencies.values()){let Ge=this.storedResolutions.get(Be.descriptorHash);if(typeof Ge=="undefined")throw new Error(`Assertion failed: The resolution (${sr(this.configuration,Be)}) should have been registered`);let re=this.storedPackages.get(Ge);if(typeof re=="undefined")throw new Error("Assertion failed: The package should have been registered");de.update($(re))}return O=de.digest("hex"),q.set(Z.locatorHash,O),O},z=(Z,O)=>{let L=(0,n0.createHash)("sha512");L.update(Y),L.update($(Z));for(let de of O)L.update(de);return L.digest("hex")},ne=new Map,ee=!1;for(;k.size>0;){let Z=k.size,O=[];for(let L of k){let de=this.storedPackages.get(L);if(!de)throw new Error("Assertion failed: The package should have been registered");let Be=!0;for(let se of de.dependencies.values()){let be=this.storedResolutions.get(se.descriptorHash);if(!be)throw new Error(`Assertion failed: The resolution (${sr(this.configuration,se)}) should have been registered`);if(k.has(be)){Be=!1;break}}if(!Be)continue;k.delete(L);let Ge=h.get(de.locatorHash);if(!Ge)throw new Error("Assertion failed: The build directive should have been registered");let re=z(de,Ge.buildLocations);if(this.storedBuildState.get(de.locatorHash)===re){ne.set(de.locatorHash,re);continue}ee||(await this.persistInstallStateFile(),ee=!0),this.storedBuildState.has(de.locatorHash)?t.reportInfo(X.MUST_REBUILD,`${It(this.configuration,de)} must be rebuilt because its dependency tree changed`):t.reportInfo(X.MUST_BUILD,`${It(this.configuration,de)} must be built because it never has been before or the last one failed`);for(let se of Ge.buildLocations){if(!x.isAbsolute(se))throw new Error(`Assertion failed: Expected the build location to be absolute (not ${se})`);O.push((async()=>{for(let[be,he]of Ge.directives){let Fe=`# This file contains the result of Yarn building a package (${Fs(de)}) +`;switch(be){case cs.SCRIPT:Fe+=`# Script name: ${he} +`;break;case cs.SHELLCODE:Fe+=`# Script code: ${he} +`;break}let Ue=null;if(!await U.mktempPromise(async ve=>{let pe=x.join(ve,"build.log"),{stdout:V,stderr:Qe}=this.configuration.getSubprocessStreams(pe,{header:Fe,prefix:It(this.configuration,de),report:t}),le;try{switch(be){case cs.SCRIPT:le=await sB(de,he,[],{cwd:se,project:this,stdin:Ue,stdout:V,stderr:Qe});break;case cs.SHELLCODE:le=await iD(de,he,[],{cwd:se,project:this,stdin:Ue,stdout:V,stderr:Qe});break}}catch(gt){Qe.write(gt.stack),le=1}if(V.end(),Qe.end(),le===0)return ne.set(de.locatorHash,re),!0;U.detachTemp(ve);let fe=`${It(this.configuration,de)} couldn't be built successfully (exit code ${tt(this.configuration,le,Ye.NUMBER)}, logs can be found here: ${tt(this.configuration,pe,Ye.PATH)})`;return this.optionalBuilds.has(de.locatorHash)?(t.reportInfo(X.BUILD_FAILED,fe),ne.set(de.locatorHash,re),!0):(t.reportError(X.BUILD_FAILED,fe),!1)}))return}})())}}if(await ho(O),Z===k.size){let L=Array.from(k).map(de=>{let Be=this.storedPackages.get(de);if(!Be)throw new Error("Assertion failed: The package should have been registered");return It(this.configuration,Be)}).join(", ");t.reportError(X.CYCLIC_DEPENDENCIES,`Some packages have circular dependencies that make their build order unsatisfiable - as a result they won't be built (affected packages are: ${L})`);break}}this.storedBuildState=ne}async install(e){var a,l;let t=this.configuration.get("nodeLinker");(a=ye.telemetry)==null||a.reportInstall(t),await e.report.startTimerPromise("Project validation",{skipIfEmpty:!0},async()=>{await this.configuration.triggerHook(c=>c.validateProject,this,{reportWarning:e.report.reportWarning.bind(e.report),reportError:e.report.reportError.bind(e.report)})});for(let c of this.configuration.packageExtensions.values())for(let[,u]of c)for(let g of u)g.status=qi.Inactive;let i=x.join(this.cwd,this.configuration.get("lockfileFilename")),n=null;if(e.immutable)try{n=await U.readFilePromise(i,"utf8")}catch(c){throw c.code==="ENOENT"?new ct(X.FROZEN_LOCKFILE_EXCEPTION,"The lockfile would have been created by this install, which is explicitly forbidden."):c}await e.report.startTimerPromise("Resolution step",async()=>{await this.resolveEverything(e)}),await e.report.startTimerPromise("Post-resolution validation",{skipIfEmpty:!0},async()=>{for(let[,c]of this.configuration.packageExtensions)for(let[,u]of c)for(let g of u)if(g.userProvided){let f=tt(this.configuration,g,Ye.PACKAGE_EXTENSION);switch(g.status){case qi.Inactive:e.report.reportWarning(X.UNUSED_PACKAGE_EXTENSION,`${f}: No matching package in the dependency tree; you may not need this rule anymore.`);break;case qi.Redundant:e.report.reportWarning(X.REDUNDANT_PACKAGE_EXTENSION,`${f}: This rule seems redundant when applied on the original package; the extension may have been applied upstream.`);break}}if(n!==null){let c=oc(n,this.generateLockfile());if(c!==n){let u=e7(i,i,n,c,void 0,void 0,{maxEditLength:100});if(u){e.report.reportSeparator();for(let g of u.hunks){e.report.reportInfo(null,`@@ -${g.oldStart},${g.oldLines} +${g.newStart},${g.newLines} @@`);for(let f of g.lines)f.startsWith("+")?e.report.reportError(X.FROZEN_LOCKFILE_EXCEPTION,tt(this.configuration,f,Ye.ADDED)):f.startsWith("-")?e.report.reportError(X.FROZEN_LOCKFILE_EXCEPTION,tt(this.configuration,f,Ye.REMOVED)):e.report.reportInfo(null,tt(this.configuration,f,"grey"))}e.report.reportSeparator()}throw new ct(X.FROZEN_LOCKFILE_EXCEPTION,"The lockfile would have been modified by this install, which is explicitly forbidden.")}}});for(let c of this.configuration.packageExtensions.values())for(let[,u]of c)for(let g of u)g.userProvided&&g.status===qi.Active&&((l=ye.telemetry)==null||l.reportPackageExtension(Mc(g,Ye.PACKAGE_EXTENSION)));await e.report.startTimerPromise("Fetch step",async()=>{await this.fetchEverything(e),(typeof e.persistProject=="undefined"||e.persistProject)&&e.mode!==Ci.UpdateLockfile&&await this.cacheCleanup(e)});let s=e.immutable?[...new Set(this.configuration.get("immutablePatterns"))].sort():[],o=await Promise.all(s.map(async c=>aw(c,{cwd:this.cwd})));(typeof e.persistProject=="undefined"||e.persistProject)&&await this.persist(),await e.report.startTimerPromise("Link step",async()=>{if(e.mode===Ci.UpdateLockfile){e.report.reportWarning(X.UPDATE_LOCKFILE_ONLY_SKIP_LINK,`Skipped due to ${tt(this.configuration,"mode=update-lockfile",Ye.CODE)}`);return}await this.linkEverything(e);let c=await Promise.all(s.map(async u=>aw(u,{cwd:this.cwd})));for(let u=0;uc.afterAllInstalled,this,e)}generateLockfile(){let e=new Map;for(let[n,s]of this.storedResolutions.entries()){let o=e.get(s);o||e.set(s,o=new Set),o.add(n)}let t={};t.__metadata={version:cZ,cacheKey:void 0};for(let[n,s]of e.entries()){let o=this.originalPackages.get(n);if(!o)continue;let a=[];for(let f of s){let h=this.storedDescriptors.get(f);if(!h)throw new Error("Assertion failed: The descriptor should have been registered");a.push(h)}let l=a.map(f=>Pn(f)).sort().join(", "),c=new At;c.version=o.linkType===Qt.HARD?o.version:"0.0.0-use.local",c.languageName=o.languageName,c.dependencies=new Map(o.dependencies),c.peerDependencies=new Map(o.peerDependencies),c.dependenciesMeta=new Map(o.dependenciesMeta),c.peerDependenciesMeta=new Map(o.peerDependenciesMeta),c.bin=new Map(o.bin);let u,g=this.storedChecksums.get(o.locatorHash);if(typeof g!="undefined"){let f=g.indexOf("/");if(f===-1)throw new Error("Assertion failed: Expected the checksum to reference its cache key");let h=g.slice(0,f),p=g.slice(f+1);typeof t.__metadata.cacheKey=="undefined"&&(t.__metadata.cacheKey=h),h===t.__metadata.cacheKey?u=p:u=g}t[l]=te(N({},c.exportTo({},{compatibilityMode:!1})),{linkType:o.linkType.toLowerCase(),resolution:Fs(o),checksum:u,conditions:o.conditions||void 0})}return`${[`# This file is generated by running "yarn install" inside your project. +`,`# Manual changes might be lost - proceed with caution! +`].join("")} +`+Ua(t)}async persistLockfile(){let e=x.join(this.cwd,this.configuration.get("lockfileFilename")),t="";try{t=await U.readFilePromise(e,"utf8")}catch(s){}let i=this.generateLockfile(),n=oc(t,i);n!==t&&(await U.writeFilePromise(e,n),this.lockFileChecksum=gZ(n),this.lockfileNeedsRefresh=!1)}async persistInstallStateFile(){let e=[];for(let o of Object.values(HR))e.push(...o);let t=(0,s0.default)(this,e),i=UR.default.serialize(t),n=ln(i);if(this.installStateChecksum===n)return;let s=this.configuration.get("installStatePath");await U.mkdirPromise(x.dirname(s),{recursive:!0}),await U.writeFilePromise(s,await $Oe(i)),this.installStateChecksum=n}async restoreInstallState({restoreInstallersCustomData:e=!0,restoreResolutions:t=!0,restoreBuildState:i=!0}={}){let n=this.configuration.get("installStatePath"),s;try{let o=await eMe(await U.readFilePromise(n));s=UR.default.deserialize(o),this.installStateChecksum=ln(o)}catch{t&&await this.applyLightResolution();return}e&&typeof s.installersCustomData!="undefined"&&(this.installersCustomData=s.installersCustomData),i&&Object.assign(this,(0,s0.default)(s,HR.restoreBuildState)),t&&(s.lockFileChecksum===this.lockFileChecksum?(Object.assign(this,(0,s0.default)(s,HR.restoreResolutions)),this.refreshWorkspaceDependencies()):await this.applyLightResolution())}async applyLightResolution(){await this.resolveEverything({lockfileOnly:!0,report:new di}),await this.persistInstallStateFile()}async persist(){await this.persistLockfile();for(let e of this.workspacesByCwd.values())await e.persistManifest()}async cacheCleanup({cache:e,report:t}){if(this.configuration.get("enableGlobalCache"))return;let i=new Set([".gitignore"]);if(!lx(e.cwd,this.cwd)||!await U.existsPromise(e.cwd))return;let n=this.configuration.get("preferAggregateCacheInfo"),s=0,o=null;for(let a of await U.readdirPromise(e.cwd)){if(i.has(a))continue;let l=x.resolve(e.cwd,a);e.markedFiles.has(l)||(o=a,e.immutable?t.reportError(X.IMMUTABLE_CACHE,`${tt(this.configuration,x.basename(l),"magenta")} appears to be unused and would be marked for deletion, but the cache is immutable`):(n?s+=1:t.reportInfo(X.UNUSED_CACHE_ENTRY,`${tt(this.configuration,x.basename(l),"magenta")} appears to be unused - removing`),await U.removePromise(l)))}n&&s!==0&&t.reportInfo(X.UNUSED_CACHE_ENTRY,s>1?`${s} packages appeared to be unused and were removed`:`${o} appeared to be unused and was removed`),e.markedFiles.clear()}};function tMe({project:r,allDescriptors:e,allResolutions:t,allPackages:i,accessibleLocators:n=new Set,optionalBuilds:s=new Set,peerRequirements:o=new Map,volatileDescriptors:a=new Set,report:l,tolerateMissingPackages:c=!1}){var ne;let u=new Map,g=[],f=new Map,h=new Map,p=new Map,m=new Map,y=new Map,b=new Map(r.workspaces.map(ee=>{let A=ee.anchoredLocator.locatorHash,oe=i.get(A);if(typeof oe=="undefined"){if(c)return[A,null];throw new Error("Assertion failed: The workspace should have an associated package")}return[A,ud(oe)]})),v=()=>{let ee=U.mktempSync(),A=x.join(ee,"stacktrace.log"),oe=String(g.length+1).length,ce=g.map((Z,O)=>`${`${O+1}.`.padStart(oe," ")} ${Fs(Z)} +`).join("");throw U.writeFileSync(A,ce),U.detachTemp(ee),new ct(X.STACK_OVERFLOW_RESOLUTION,`Encountered a stack overflow when resolving peer dependencies; cf ${H.fromPortablePath(A)}`)},k=ee=>{let A=t.get(ee.descriptorHash);if(typeof A=="undefined")throw new Error("Assertion failed: The resolution should have been registered");let oe=i.get(A);if(!oe)throw new Error("Assertion failed: The package could not be found");return oe},T=(ee,A,oe,{top:ce,optional:Z})=>{g.length>1e3&&v(),g.push(A);let O=Y(ee,A,oe,{top:ce,optional:Z});return g.pop(),O},Y=(ee,A,oe,{top:ce,optional:Z})=>{if(n.has(A.locatorHash))return;n.add(A.locatorHash),Z||s.delete(A.locatorHash);let O=i.get(A.locatorHash);if(!O){if(c)return;throw new Error(`Assertion failed: The package (${It(r.configuration,A)}) should have been registered`)}let L=[],de=[],Be=[],Ge=[],re=[];for(let be of Array.from(O.dependencies.values())){if(O.peerDependencies.has(be.identHash)&&O.locatorHash!==ce)continue;if(ll(be))throw new Error("Assertion failed: Virtual packages shouldn't be encountered when virtualizing a branch");a.delete(be.descriptorHash);let he=Z;if(!he){let Qe=O.dependenciesMeta.get(Ot(be));if(typeof Qe!="undefined"){let le=Qe.get(null);typeof le!="undefined"&&le.optional&&(he=!0)}}let Fe=t.get(be.descriptorHash);if(!Fe){if(c)continue;throw new Error(`Assertion failed: The resolution (${sr(r.configuration,be)}) should have been registered`)}let Ue=b.get(Fe)||i.get(Fe);if(!Ue)throw new Error(`Assertion failed: The package (${Fe}, resolved from ${sr(r.configuration,be)}) should have been registered`);if(Ue.peerDependencies.size===0){T(be,Ue,new Map,{top:ce,optional:he});continue}let xe,ve,pe=new Set,V;de.push(()=>{xe=Xk(be,A.locatorHash),ve=Zk(Ue,A.locatorHash),O.dependencies.delete(be.identHash),O.dependencies.set(xe.identHash,xe),t.set(xe.descriptorHash,ve.locatorHash),e.set(xe.descriptorHash,xe),i.set(ve.locatorHash,ve),L.push([Ue,xe,ve])}),Be.push(()=>{var Qe;V=new Map;for(let le of ve.peerDependencies.values()){let fe=O.dependencies.get(le.identHash);if(!fe&&hd(A,le)&&(ee.identHash===A.identHash?fe=ee:(fe=rr(A,ee.range),e.set(fe.descriptorHash,fe),t.set(fe.descriptorHash,A.locatorHash),a.delete(fe.descriptorHash))),(!fe||fe.range==="missing:")&&ve.dependencies.has(le.identHash)){ve.peerDependencies.delete(le.identHash);continue}fe||(fe=rr(le,"missing:")),ve.dependencies.set(fe.identHash,fe),ll(fe)&&Lc(p,fe.descriptorHash).add(ve.locatorHash),f.set(fe.identHash,fe),fe.range==="missing:"&&pe.add(fe.identHash),V.set(le.identHash,(Qe=oe.get(le.identHash))!=null?Qe:ve.locatorHash)}ve.dependencies=new Map(xn(ve.dependencies,([le,fe])=>Ot(fe)))}),Ge.push(()=>{if(!i.has(ve.locatorHash))return;let Qe=u.get(Ue.locatorHash);typeof Qe=="number"&&Qe>=2&&v();let le=u.get(Ue.locatorHash),fe=typeof le!="undefined"?le+1:1;u.set(Ue.locatorHash,fe),T(xe,ve,V,{top:ce,optional:he}),u.set(Ue.locatorHash,fe-1)}),re.push(()=>{let Qe=O.dependencies.get(be.identHash);if(typeof Qe=="undefined")throw new Error("Assertion failed: Expected the peer dependency to have been turned into a dependency");let le=t.get(Qe.descriptorHash);if(typeof le=="undefined")throw new Error("Assertion failed: Expected the descriptor to be registered");if(Lc(y,le).add(A.locatorHash),!!i.has(ve.locatorHash)){for(let fe of ve.peerDependencies.values()){let gt=V.get(fe.identHash);if(typeof gt=="undefined")throw new Error("Assertion failed: Expected the peer dependency ident to be registered");Ng(Lg(m,gt),Ot(fe)).push(ve.locatorHash)}for(let fe of pe)ve.dependencies.delete(fe)}})}for(let be of[...de,...Be])be();let se;do{se=!0;for(let[be,he,Fe]of L){let Ue=Lg(h,be.locatorHash),xe=ln(...[...Fe.dependencies.values()].map(Qe=>{let le=Qe.range!=="missing:"?t.get(Qe.descriptorHash):"missing:";if(typeof le=="undefined")throw new Error(`Assertion failed: Expected the resolution for ${sr(r.configuration,Qe)} to have been registered`);return le===ce?`${le} (top)`:le}),he.identHash),ve=Ue.get(xe);if(typeof ve=="undefined"){Ue.set(xe,he);continue}if(ve===he)continue;i.delete(Fe.locatorHash),e.delete(he.descriptorHash),t.delete(he.descriptorHash),n.delete(Fe.locatorHash);let pe=p.get(he.descriptorHash)||[],V=[O.locatorHash,...pe];p.delete(he.descriptorHash);for(let Qe of V){let le=i.get(Qe);typeof le!="undefined"&&(le.dependencies.get(he.identHash).descriptorHash!==ve.descriptorHash&&(se=!1),le.dependencies.set(he.identHash,ve))}}}while(!se);for(let be of[...Ge,...re])be()};for(let ee of r.workspaces){let A=ee.anchoredLocator;a.delete(ee.anchoredDescriptor.descriptorHash),T(ee.anchoredDescriptor,A,new Map,{top:A.locatorHash,optional:!1})}var q;(function(oe){oe[oe.NotProvided=0]="NotProvided",oe[oe.NotCompatible=1]="NotCompatible"})(q||(q={}));let $=[];for(let[ee,A]of y){let oe=i.get(ee);if(typeof oe=="undefined")throw new Error("Assertion failed: Expected the root to be registered");let ce=m.get(ee);if(typeof ce!="undefined")for(let Z of A){let O=i.get(Z);if(typeof O!="undefined")for(let[L,de]of ce){let Be=An(L);if(O.peerDependencies.has(Be.identHash))continue;let Ge=`p${ln(Z,L,ee).slice(0,5)}`;o.set(Ge,{subject:Z,requested:Be,rootRequester:ee,allRequesters:de});let re=oe.dependencies.get(Be.identHash);if(typeof re!="undefined"){let se=k(re),be=(ne=se.version)!=null?ne:"0.0.0",he=new Set;for(let Ue of de){let xe=i.get(Ue);if(typeof xe=="undefined")throw new Error("Assertion failed: Expected the link to be registered");let ve=xe.peerDependencies.get(Be.identHash);if(typeof ve=="undefined")throw new Error("Assertion failed: Expected the ident to be registered");he.add(ve.range)}[...he].every(Ue=>{if(Ue.startsWith(oi.protocol)){if(!r.tryWorkspaceByLocator(se))return!1;Ue=Ue.slice(oi.protocol.length),(Ue==="^"||Ue==="~")&&(Ue="*")}return Jc(be,Ue)})||$.push({type:1,subject:O,requested:Be,requester:oe,version:be,hash:Ge,requirementCount:de.length})}else{let se=oe.peerDependenciesMeta.get(L);(se==null?void 0:se.optional)||$.push({type:0,subject:O,requested:Be,requester:oe,hash:Ge})}}}}let z=[ee=>ex(ee.subject),ee=>Ot(ee.requested),ee=>`${ee.type}`];l==null||l.startSectionSync({reportFooter:()=>{l.reportWarning(X.UNNAMED,`Some peer dependencies are incorrectly met; run ${tt(r.configuration,"yarn explain peer-requirements ",Ye.CODE)} for details, where ${tt(r.configuration,"",Ye.CODE)} is the six-letter p-prefixed code`)},skipIfEmpty:!0},()=>{for(let ee of xn($,z))switch(ee.type){case 0:l.reportWarning(X.MISSING_PEER_DEPENDENCY,`${It(r.configuration,ee.subject)} doesn't provide ${fi(r.configuration,ee.requested)} (${tt(r.configuration,ee.hash,Ye.CODE)}), requested by ${fi(r.configuration,ee.requester)}`);break;case 1:{let A=ee.requirementCount>1?"and some of its descendants request":"requests";l.reportWarning(X.INCOMPATIBLE_PEER_DEPENDENCY,`${It(r.configuration,ee.subject)} provides ${fi(r.configuration,ee.requested)} (${tt(r.configuration,ee.hash,Ye.CODE)}) with version ${Cd(r.configuration,ee.version)}, which doesn't satisfy what ${fi(r.configuration,ee.requester)} ${A}`)}break}})}var ua;(function(l){l.VERSION="version",l.COMMAND_NAME="commandName",l.PLUGIN_NAME="pluginName",l.INSTALL_COUNT="installCount",l.PROJECT_COUNT="projectCount",l.WORKSPACE_COUNT="workspaceCount",l.DEPENDENCY_COUNT="dependencyCount",l.EXTENSION="packageExtension"})(ua||(ua={}));var EC=class{constructor(e,t){this.values=new Map;this.hits=new Map;this.enumerators=new Map;this.configuration=e;let i=this.getRegistryPath();this.isNew=!U.existsSync(i),this.sendReport(t),this.startBuffer()}reportVersion(e){this.reportValue(ua.VERSION,e.replace(/-git\..*/,"-git"))}reportCommandName(e){this.reportValue(ua.COMMAND_NAME,e||"")}reportPluginName(e){this.reportValue(ua.PLUGIN_NAME,e)}reportProject(e){this.reportEnumerator(ua.PROJECT_COUNT,e)}reportInstall(e){this.reportHit(ua.INSTALL_COUNT,e)}reportPackageExtension(e){this.reportValue(ua.EXTENSION,e)}reportWorkspaceCount(e){this.reportValue(ua.WORKSPACE_COUNT,String(e))}reportDependencyCount(e){this.reportValue(ua.DEPENDENCY_COUNT,String(e))}reportValue(e,t){Lc(this.values,e).add(t)}reportEnumerator(e,t){Lc(this.enumerators,e).add(ln(t))}reportHit(e,t="*"){let i=Lg(this.hits,e),n=Va(i,t,()=>0);i.set(t,n+1)}getRegistryPath(){let e=this.configuration.get("globalFolder");return x.join(e,"telemetry.json")}sendReport(e){var u,g,f;let t=this.getRegistryPath(),i;try{i=U.readJsonSync(t)}catch{i={}}let n=Date.now(),s=this.configuration.get("telemetryInterval")*24*60*60*1e3,a=((u=i.lastUpdate)!=null?u:n+s+Math.floor(s*Math.random()))+s;if(a>n&&i.lastUpdate!=null)return;try{U.mkdirSync(x.dirname(t),{recursive:!0}),U.writeJsonSync(t,{lastUpdate:n})}catch{return}if(a>n||!i.blocks)return;let l=`https://browser-http-intake.logs.datadoghq.eu/v1/input/${e}?ddsource=yarn`,c=h=>jP(l,h,{configuration:this.configuration}).catch(()=>{});for(let[h,p]of Object.entries((g=i.blocks)!=null?g:{})){if(Object.keys(p).length===0)continue;let m=p;m.userId=h,m.reportType="primary";for(let v of Object.keys((f=m.enumerators)!=null?f:{}))m.enumerators[v]=m.enumerators[v].length;c(m);let y=new Map,b=20;for(let[v,k]of Object.entries(m.values))k.length>0&&y.set(v,k.slice(0,b));for(;y.size>0;){let v={};v.userId=h,v.reportType="secondary",v.metrics={};for(let[k,T]of y)v.metrics[k]=T.shift(),T.length===0&&y.delete(k);c(v)}}}applyChanges(){var o,a,l,c,u,g,f,h,p;let e=this.getRegistryPath(),t;try{t=U.readJsonSync(e)}catch{t={}}let i=(o=this.configuration.get("telemetryUserId"))!=null?o:"*",n=t.blocks=(a=t.blocks)!=null?a:{},s=n[i]=(l=n[i])!=null?l:{};for(let m of this.hits.keys()){let y=s.hits=(c=s.hits)!=null?c:{},b=y[m]=(u=y[m])!=null?u:{};for(let[v,k]of this.hits.get(m))b[v]=((g=b[v])!=null?g:0)+k}for(let m of["values","enumerators"])for(let y of this[m].keys()){let b=s[m]=(f=s[m])!=null?f:{};b[y]=[...new Set([...(h=b[y])!=null?h:[],...(p=this[m].get(y))!=null?p:[]])]}U.mkdirSync(x.dirname(e),{recursive:!0}),U.writeJsonSync(e,t)}startBuffer(){process.on("exit",()=>{try{this.applyChanges()}catch{}})}};var jR=ge(require("child_process")),fZ=ge(yc());var GR=ge(require("fs"));var Tf=new Map([["constraints",[["constraints","query"],["constraints","source"],["constraints"]]],["exec",[]],["interactive-tools",[["search"],["upgrade-interactive"]]],["stage",[["stage"]]],["typescript",[]],["version",[["version","apply"],["version","check"],["version"]]],["workspace-tools",[["workspaces","focus"],["workspaces","foreach"]]]]);function rMe(r){let e=H.fromPortablePath(r);process.on("SIGINT",()=>{}),e?(0,jR.execFileSync)(process.execPath,[e,...process.argv.slice(2)],{stdio:"inherit",env:te(N({},process.env),{YARN_IGNORE_PATH:"1",YARN_IGNORE_CWD:"1"})}):(0,jR.execFileSync)(e,process.argv.slice(2),{stdio:"inherit",env:te(N({},process.env),{YARN_IGNORE_PATH:"1",YARN_IGNORE_CWD:"1"})})}async function o0({binaryVersion:r,pluginConfiguration:e}){async function t(){let n=new Bs({binaryLabel:"Yarn Package Manager",binaryName:"yarn",binaryVersion:r});try{await i(n)}catch(s){process.stdout.write(n.error(s)),process.exitCode=1}}async function i(n){var m,y,b,v,k;let s=process.versions.node,o=">=12 <14 || 14.2 - 14.9 || >14.10.0";if(!Se.parseOptionalBoolean(process.env.YARN_IGNORE_NODE)&&!Wt.satisfiesWithPrereleases(s,o))throw new Pe(`This tool requires a Node version compatible with ${o} (got ${s}). Upgrade Node, or set \`YARN_IGNORE_NODE=1\` in your environment.`);let l=await ye.find(H.toPortablePath(process.cwd()),e,{usePath:!0,strict:!1}),c=l.get("yarnPath"),u=l.get("ignorePath"),g=l.get("ignoreCwd"),f=H.toPortablePath(H.resolve(process.argv[1])),h=T=>U.readFilePromise(T).catch(()=>Buffer.of());if(!u&&!g&&await(async()=>c===f||Buffer.compare(...await Promise.all([h(c),h(f)]))===0)()){process.env.YARN_IGNORE_PATH="1",process.env.YARN_IGNORE_CWD="1",await i(n);return}else if(c!==null&&!u)if(!U.existsSync(c))process.stdout.write(n.error(new Error(`The "yarn-path" option has been set (in ${l.sources.get("yarnPath")}), but the specified location doesn't exist (${c}).`))),process.exitCode=1;else try{rMe(c)}catch(T){process.exitCode=T.code||1}else{u&&delete process.env.YARN_IGNORE_PATH,l.get("enableTelemetry")&&!fZ.isCI&&process.stdout.isTTY&&(ye.telemetry=new EC(l,"puba9cdc10ec5790a2cf4969dd413a47270")),(m=ye.telemetry)==null||m.reportVersion(r);for(let[$,z]of l.plugins.entries()){Tf.has((b=(y=$.match(/^@yarnpkg\/plugin-(.*)$/))==null?void 0:y[1])!=null?b:"")&&((v=ye.telemetry)==null||v.reportPluginName($));for(let ne of z.commands||[])n.register(ne)}let Y=n.process(process.argv.slice(2));Y.help||(k=ye.telemetry)==null||k.reportCommandName(Y.path.join(" "));let q=Y.cwd;if(typeof q!="undefined"&&!g){let $=(0,GR.realpathSync)(process.cwd()),z=(0,GR.realpathSync)(q);if($!==z){process.chdir(q),await t();return}}await n.runExit(Y,{cwd:H.toPortablePath(process.cwd()),plugins:e,quiet:!1,stdin:process.stdin,stdout:process.stdout,stderr:process.stderr})}}return t().catch(n=>{process.stdout.write(n.stack||n.message),process.exitCode=1}).finally(()=>U.rmtempPromise())}function hZ(r){r.Command.Path=(...e)=>t=>{t.paths=t.paths||[],t.paths.push(e)};for(let e of["Array","Boolean","String","Proxy","Rest","Counter"])r.Command[e]=(...t)=>(i,n)=>{let s=r.Option[e](...t);Object.defineProperty(i,`__${n}`,{configurable:!1,enumerable:!0,get(){return s},set(o){this[n]=o}})};return r}var YC={};ft(YC,{BaseCommand:()=>Le,WorkspaceRequiredError:()=>ht,getDynamicLibs:()=>kre,getPluginConfiguration:()=>T0,main:()=>o0,openWorkspace:()=>zf,pluginCommands:()=>Tf});var Le=class extends Re{constructor(){super(...arguments);this.cwd=J.String("--cwd",{hidden:!0})}};var ht=class extends Pe{constructor(e,t){let i=x.relative(e,t),n=x.join(e,At.fileName);super(`This command can only be run from within a workspace of your project (${i} isn't a workspace of ${n}).`)}};var AGe=ge(ri());ws();var lGe=ge(HF()),kre=()=>new Map([["@yarnpkg/cli",YC],["@yarnpkg/core",IC],["@yarnpkg/fslib",$h],["@yarnpkg/libzip",Ud],["@yarnpkg/parsers",ap],["@yarnpkg/shell",Hd],["clipanion",mp],["semver",AGe],["typanion",ug],["yup",lGe]]);async function zf(r,e){let{project:t,workspace:i}=await ze.find(r,e);if(!i)throw new ht(t.cwd,e);return i}var v_e=ge(ri());ws();var k_e=ge(HF());var YN={};ft(YN,{dedupeUtils:()=>BN,default:()=>y3e,suggestUtils:()=>cN});var Pae=ge(yc());var One=ge(zC());ws();var cN={};ft(cN,{Modifier:()=>da,Strategy:()=>Vr,Target:()=>Hr,WorkspaceModifier:()=>Zf,applyModifier:()=>Fne,extractDescriptorFromPath:()=>fN,extractRangeModifier:()=>Rne,fetchDescriptorFrom:()=>gN,findProjectDescriptors:()=>Tne,getModifier:()=>_C,getSuggestedDescriptors:()=>VC,makeWorkspaceDescriptor:()=>Lne,toWorkspaceModifier:()=>Nne});var uN=ge(ri()),vYe="workspace:",Hr;(function(i){i.REGULAR="dependencies",i.DEVELOPMENT="devDependencies",i.PEER="peerDependencies"})(Hr||(Hr={}));var da;(function(i){i.CARET="^",i.TILDE="~",i.EXACT=""})(da||(da={}));var Zf;(function(i){i.CARET="^",i.TILDE="~",i.EXACT="*"})(Zf||(Zf={}));var Vr;(function(s){s.KEEP="keep",s.REUSE="reuse",s.PROJECT="project",s.LATEST="latest",s.CACHE="cache"})(Vr||(Vr={}));function _C(r,e){return r.exact?da.EXACT:r.caret?da.CARET:r.tilde?da.TILDE:e.configuration.get("defaultSemverRangePrefix")}var kYe=/^([\^~]?)[0-9]+(?:\.[0-9]+){0,2}(?:-\S+)?$/;function Rne(r,{project:e}){let t=r.match(kYe);return t?t[1]:e.configuration.get("defaultSemverRangePrefix")}function Fne(r,e){let{protocol:t,source:i,params:n,selector:s}=P.parseRange(r.range);return uN.default.valid(s)&&(s=`${e}${r.range}`),P.makeDescriptor(r,P.makeRange({protocol:t,source:i,params:n,selector:s}))}function Nne(r){switch(r){case da.CARET:return Zf.CARET;case da.TILDE:return Zf.TILDE;case da.EXACT:return Zf.EXACT;default:throw new Error(`Assertion failed: Unknown modifier: "${r}"`)}}function Lne(r,e){return P.makeDescriptor(r.anchoredDescriptor,`${vYe}${Nne(e)}`)}async function Tne(r,{project:e,target:t}){let i=new Map,n=s=>{let o=i.get(s.descriptorHash);return o||i.set(s.descriptorHash,o={descriptor:s,locators:[]}),o};for(let s of e.workspaces)if(t===Hr.PEER){let o=s.manifest.peerDependencies.get(r.identHash);o!==void 0&&n(o).locators.push(s.locator)}else{let o=s.manifest.dependencies.get(r.identHash),a=s.manifest.devDependencies.get(r.identHash);t===Hr.DEVELOPMENT?a!==void 0?n(a).locators.push(s.locator):o!==void 0&&n(o).locators.push(s.locator):o!==void 0?n(o).locators.push(s.locator):a!==void 0&&n(a).locators.push(s.locator)}return i}async function fN(r,{cwd:e,workspace:t}){return await xYe(async i=>{x.isAbsolute(r)||(r=x.relative(t.cwd,x.resolve(e,r)),r.match(/^\.{0,2}\//)||(r=`./${r}`));let{project:n}=t,s=await gN(P.makeIdent(null,"archive"),r,{project:t.project,cache:i,workspace:t});if(!s)throw new Error("Assertion failed: The descriptor should have been found");let o=new di,a=n.configuration.makeResolver(),l=n.configuration.makeFetcher(),c={checksums:n.storedChecksums,project:n,cache:i,fetcher:l,report:o,resolver:a},u=a.bindDescriptor(s,t.anchoredLocator,c),g=P.convertDescriptorToLocator(u),f=await l.fetch(g,c),h=await At.find(f.prefixPath,{baseFs:f.packageFs});if(!h.name)throw new Error("Target path doesn't have a name");return P.makeDescriptor(h.name,r)})}async function VC(r,{project:e,workspace:t,cache:i,target:n,modifier:s,strategies:o,maxResults:a=Infinity}){if(!(a>=0))throw new Error(`Invalid maxResults (${a})`);if(r.range!=="unknown")return{suggestions:[{descriptor:r,name:`Use ${P.prettyDescriptor(e.configuration,r)}`,reason:"(unambiguous explicit request)"}],rejections:[]};let l=typeof t!="undefined"&&t!==null&&t.manifest[n].get(r.identHash)||null,c=[],u=[],g=async f=>{try{await f()}catch(h){u.push(h)}};for(let f of o){if(c.length>=a)break;switch(f){case Vr.KEEP:await g(async()=>{l&&c.push({descriptor:l,name:`Keep ${P.prettyDescriptor(e.configuration,l)}`,reason:"(no changes)"})});break;case Vr.REUSE:await g(async()=>{for(let{descriptor:h,locators:p}of(await Tne(r,{project:e,target:n})).values()){if(p.length===1&&p[0].locatorHash===t.anchoredLocator.locatorHash&&o.includes(Vr.KEEP))continue;let m=`(originally used by ${P.prettyLocator(e.configuration,p[0])}`;m+=p.length>1?` and ${p.length-1} other${p.length>2?"s":""})`:")",c.push({descriptor:h,name:`Reuse ${P.prettyDescriptor(e.configuration,h)}`,reason:m})}});break;case Vr.CACHE:await g(async()=>{for(let h of e.storedDescriptors.values())h.identHash===r.identHash&&c.push({descriptor:h,name:`Reuse ${P.prettyDescriptor(e.configuration,h)}`,reason:"(already used somewhere in the lockfile)"})});break;case Vr.PROJECT:await g(async()=>{if(t.manifest.name!==null&&r.identHash===t.manifest.name.identHash)return;let h=e.tryWorkspaceByIdent(r);if(h===null)return;let p=Lne(h,s);c.push({descriptor:p,name:`Attach ${P.prettyDescriptor(e.configuration,p)}`,reason:`(local workspace at ${ae.pretty(e.configuration,h.relativeCwd,ae.Type.PATH)})`})});break;case Vr.LATEST:await g(async()=>{if(r.range!=="unknown")c.push({descriptor:r,name:`Use ${P.prettyRange(e.configuration,r.range)}`,reason:"(explicit range requested)"});else if(n===Hr.PEER)c.push({descriptor:P.makeDescriptor(r,"*"),name:"Use *",reason:"(catch-all peer dependency pattern)"});else if(!e.configuration.get("enableNetwork"))c.push({descriptor:null,name:"Resolve from latest",reason:ae.pretty(e.configuration,"(unavailable because enableNetwork is toggled off)","grey")});else{let h=await gN(r,"latest",{project:e,cache:i,workspace:t,preserveModifier:!1});h&&(h=Fne(h,s),c.push({descriptor:h,name:`Use ${P.prettyDescriptor(e.configuration,h)}`,reason:"(resolved from latest)"}))}});break}}return{suggestions:c.slice(0,a),rejections:u.slice(0,a)}}async function gN(r,e,{project:t,cache:i,workspace:n,preserveModifier:s=!0}){let o=P.makeDescriptor(r,e),a=new di,l=t.configuration.makeFetcher(),c=t.configuration.makeResolver(),u={project:t,fetcher:l,cache:i,checksums:t.storedChecksums,report:a,cacheOptions:{skipIntegrityCheck:!0},skipIntegrityCheck:!0},g=te(N({},u),{resolver:c,fetchOptions:u}),f=c.bindDescriptor(o,n.anchoredLocator,g),h=await c.getCandidates(f,new Map,g);if(h.length===0)return null;let p=h[0],{protocol:m,source:y,params:b,selector:v}=P.parseRange(P.convertToManifestRange(p.reference));if(m===t.configuration.get("defaultProtocol")&&(m=null),uN.default.valid(v)&&s!==!1){let k=typeof s=="string"?s:o.range;v=Rne(k,{project:t})+v}return P.makeDescriptor(p,P.makeRange({protocol:m,source:y,params:b,selector:v}))}async function xYe(r){return await U.mktempPromise(async e=>{let t=ye.create(e);return t.useWithSource(e,{enableMirror:!1,compressionLevel:0},e,{overwrite:!0}),await r(new Nt(e,{configuration:t,check:!1,immutable:!1}))})}var XC=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.exact=J.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=J.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=J.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.dev=J.Boolean("-D,--dev",!1,{description:"Add a package as a dev dependency"});this.peer=J.Boolean("-P,--peer",!1,{description:"Add a package as a peer dependency"});this.optional=J.Boolean("-O,--optional",!1,{description:"Add / upgrade a package to an optional regular / peer dependency"});this.preferDev=J.Boolean("--prefer-dev",!1,{description:"Add / upgrade a package to a dev dependency"});this.interactive=J.Boolean("-i,--interactive",{description:"Reuse the specified package from other workspaces in the project"});this.cached=J.Boolean("--cached",!1,{description:"Reuse the highest version already used somewhere within the project"});this.mode=J.String("--mode",{description:"Change what artifacts installs generate",validator:nn(Ci)});this.silent=J.Boolean("--silent",{hidden:!0});this.packages=J.Rest()}async execute(){var m;let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState({restoreResolutions:!1});let s=(m=this.interactive)!=null?m:e.get("preferInteractive"),o=_C(this,t),a=[...s?[Vr.REUSE]:[],Vr.PROJECT,...this.cached?[Vr.CACHE]:[],Vr.LATEST],l=s?Infinity:1,c=await Promise.all(this.packages.map(async y=>{let b=y.match(/^\.{0,2}\//)?await fN(y,{cwd:this.context.cwd,workspace:i}):P.tryParseDescriptor(y),v=y.match(/^(https?:|git@github)/);if(v)throw new Pe(`It seems you are trying to add a package using a ${ae.pretty(e,`${v[0]}...`,Ri.RANGE)} url; we now require package names to be explicitly specified. +Try running the command again with the package name prefixed: ${ae.pretty(e,"yarn add",Ri.CODE)} ${ae.pretty(e,P.makeDescriptor(P.makeIdent(null,"my-package"),`${v[0]}...`),Ri.DESCRIPTOR)}`);if(!b)throw new Pe(`The ${ae.pretty(e,y,Ri.CODE)} string didn't match the required format (package-name@range). Did you perhaps forget to explicitly reference the package name?`);let k=PYe(i,b,{dev:this.dev,peer:this.peer,preferDev:this.preferDev,optional:this.optional}),T=await VC(b,{project:t,workspace:i,cache:n,target:k,modifier:o,strategies:a,maxResults:l});return[b,T,k]})),u=await dA.start({configuration:e,stdout:this.context.stdout,suggestInstall:!1},async y=>{for(let[b,{suggestions:v,rejections:k}]of c)if(v.filter(Y=>Y.descriptor!==null).length===0){let[Y]=k;if(typeof Y=="undefined")throw new Error("Assertion failed: Expected an error to have been set");t.configuration.get("enableNetwork")?y.reportError(X.CANT_SUGGEST_RESOLUTIONS,`${P.prettyDescriptor(e,b)} can't be resolved to a satisfying range`):y.reportError(X.CANT_SUGGEST_RESOLUTIONS,`${P.prettyDescriptor(e,b)} can't be resolved to a satisfying range (note: network resolution has been disabled)`),y.reportSeparator(),y.reportExceptionOnce(Y)}});if(u.hasErrors())return u.exitCode();let g=!1,f=[],h=[];for(let[,{suggestions:y},b]of c){let v,k=y.filter($=>$.descriptor!==null),T=k[0].descriptor,Y=k.every($=>P.areDescriptorsEqual($.descriptor,T));k.length===1||Y?v=T:(g=!0,{answer:v}=await(0,One.prompt)({type:"select",name:"answer",message:"Which range do you want to use?",choices:y.map(({descriptor:$,name:z,reason:ne})=>$?{name:z,hint:ne,descriptor:$}:{name:z,hint:ne,disabled:!0}),onCancel:()=>process.exit(130),result($){return this.find($,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let q=i.manifest[b].get(v.identHash);(typeof q=="undefined"||q.descriptorHash!==v.descriptorHash)&&(i.manifest[b].set(v.identHash,v),this.optional&&(b==="dependencies"?i.manifest.ensureDependencyMeta(te(N({},v),{range:"unknown"})).optional=!0:b==="peerDependencies"&&(i.manifest.ensurePeerDependencyMeta(te(N({},v),{range:"unknown"})).optional=!0)),typeof q=="undefined"?f.push([i,b,v,a]):h.push([i,b,q,v]))}return await e.triggerMultipleHooks(y=>y.afterWorkspaceDependencyAddition,f),await e.triggerMultipleHooks(y=>y.afterWorkspaceDependencyReplacement,h),g&&this.context.stdout.write(` +`),(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout,includeLogs:!this.context.quiet},async y=>{await t.install({cache:n,report:y,mode:this.mode})})).exitCode()}};XC.paths=[["add"]],XC.usage=Re.Usage({description:"add dependencies to the project",details:"\n This command adds a package to the package.json for the nearest workspace.\n\n - If it didn't exist before, the package will by default be added to the regular `dependencies` field, but this behavior can be overriden thanks to the `-D,--dev` flag (which will cause the dependency to be added to the `devDependencies` field instead) and the `-P,--peer` flag (which will do the same but for `peerDependencies`).\n\n - If the package was already listed in your dependencies, it will by default be upgraded whether it's part of your `dependencies` or `devDependencies` (it won't ever update `peerDependencies`, though).\n\n - If set, the `--prefer-dev` flag will operate as a more flexible `-D,--dev` in that it will add the package to your `devDependencies` if it isn't already listed in either `dependencies` or `devDependencies`, but it will also happily upgrade your `dependencies` if that's what you already use (whereas `-D,--dev` would throw an exception).\n\n - If set, the `-O,--optional` flag will add the package to the `optionalDependencies` field and, in combination with the `-P,--peer` flag, it will add the package as an optional peer dependency. If the package was already listed in your `dependencies`, it will be upgraded to `optionalDependencies`. If the package was already listed in your `peerDependencies`, in combination with the `-P,--peer` flag, it will be upgraded to an optional peer dependency: `\"peerDependenciesMeta\": { \"\": { \"optional\": true } }`\n\n - If the added package doesn't specify a range at all its `latest` tag will be resolved and the returned version will be used to generate a new semver range (using the `^` modifier by default unless otherwise configured via the `defaultSemverRangePrefix` configuration, or the `~` modifier if `-T,--tilde` is specified, or no modifier at all if `-E,--exact` is specified). Two exceptions to this rule: the first one is that if the package is a workspace then its local version will be used, and the second one is that if you use `-P,--peer` the default range will be `*` and won't be resolved at all.\n\n - If the added package specifies a range (such as `^1.0.0`, `latest`, or `rc`), Yarn will add this range as-is in the resulting package.json entry (in particular, tags such as `rc` will be encoded as-is rather than being converted into a semver range).\n\n If the `--cached` option is used, Yarn will preferably reuse the highest version already used somewhere within the project, even if through a transitive dependency.\n\n If the `-i,--interactive` option is used (or if the `preferInteractive` settings is toggled on) the command will first try to check whether other workspaces in the project use the specified package and, if so, will offer to reuse them.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n For a compilation of all the supported protocols, please consult the dedicated page from our website: https://yarnpkg.com/features/protocols.\n ",examples:[["Add a regular package to the current workspace","$0 add lodash"],["Add a specific version for a package to the current workspace","$0 add lodash@1.2.3"],["Add a package from a GitHub repository (the master branch) to the current workspace using a URL","$0 add lodash@https://github.com/lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol","$0 add lodash@github:lodash/lodash"],["Add a package from a GitHub repository (the master branch) to the current workspace using the GitHub protocol (shorthand)","$0 add lodash@lodash/lodash"],["Add a package from a specific branch of a GitHub repository to the current workspace using the GitHub protocol (shorthand)","$0 add lodash-es@lodash/lodash#es"]]});var Mne=XC;function PYe(r,e,{dev:t,peer:i,preferDev:n,optional:s}){let o=r.manifest[Hr.REGULAR].has(e.identHash),a=r.manifest[Hr.DEVELOPMENT].has(e.identHash),l=r.manifest[Hr.PEER].has(e.identHash);if((t||i)&&o)throw new Pe(`Package "${P.prettyIdent(r.project.configuration,e)}" is already listed as a regular dependency - remove the -D,-P flags or remove it from your dependencies first`);if(!t&&!i&&l)throw new Pe(`Package "${P.prettyIdent(r.project.configuration,e)}" is already listed as a peer dependency - use either of -D or -P, or remove it from your peer dependencies first`);if(s&&a)throw new Pe(`Package "${P.prettyIdent(r.project.configuration,e)}" is already listed as a dev dependency - remove the -O flag or remove it from your dev dependencies first`);if(s&&!i&&l)throw new Pe(`Package "${P.prettyIdent(r.project.configuration,e)}" is already listed as a peer dependency - remove the -O flag or add the -P flag or remove it from your peer dependencies first`);if((t||n)&&s)throw new Pe(`Package "${P.prettyIdent(r.project.configuration,e)}" cannot simultaneously be a dev dependency and an optional dependency`);return i?Hr.PEER:t||n?Hr.DEVELOPMENT:o?Hr.REGULAR:a?Hr.DEVELOPMENT:Hr.REGULAR}var ZC=class extends Le{constructor(){super(...arguments);this.verbose=J.Boolean("-v,--verbose",!1,{description:"Print both the binary name and the locator of the package that provides the binary"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.name=J.String({required:!1})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,locator:i}=await ze.find(e,this.context.cwd);if(await t.restoreInstallState(),this.name){let o=(await Zt.getPackageAccessibleBinaries(i,{project:t})).get(this.name);if(!o)throw new Pe(`Couldn't find a binary named "${this.name}" for package "${P.prettyLocator(e,i)}"`);let[,a]=o;return this.context.stdout.write(`${a} +`),0}return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async s=>{let o=await Zt.getPackageAccessibleBinaries(i,{project:t}),l=Array.from(o.keys()).reduce((c,u)=>Math.max(c,u.length),0);for(let[c,[u,g]]of o)s.reportJson({name:c,source:P.stringifyIdent(u),path:g});if(this.verbose)for(let[c,[u]]of o)s.reportInfo(null,`${c.padEnd(l," ")} ${P.prettyLocator(e,u)}`);else for(let c of o.keys())s.reportInfo(null,c)})).exitCode()}};ZC.paths=[["bin"]],ZC.usage=Re.Usage({description:"get the path to a binary script",details:` + When used without arguments, this command will print the list of all the binaries available in the current workspace. Adding the \`-v,--verbose\` flag will cause the output to contain both the binary name and the locator of the package that provides the binary. + + When an argument is specified, this command will just print the path to the binary on the standard output and exit. Note that the reported path may be stored within a zip archive. + `,examples:[["List all the available binaries","$0 bin"],["Print the path to a specific binary","$0 bin eslint"]]});var Une=ZC;var $C=class extends Le{constructor(){super(...arguments);this.mirror=J.Boolean("--mirror",!1,{description:"Remove the global cache files instead of the local cache files"});this.all=J.Boolean("--all",!1,{description:"Remove both the global cache files and the local cache files of the current project"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=await Nt.find(e);return(await Je.start({configuration:e,stdout:this.context.stdout},async()=>{let n=(this.all||this.mirror)&&t.mirrorCwd!==null,s=!this.mirror;n&&(await U.removePromise(t.mirrorCwd),await e.triggerHook(o=>o.cleanGlobalArtifacts,e)),s&&await U.removePromise(t.cwd)})).exitCode()}};$C.paths=[["cache","clean"],["cache","clear"]],$C.usage=Re.Usage({description:"remove the shared cache files",details:` + This command will remove all the files from the cache. + `,examples:[["Remove all the local archives","$0 cache clean"],["Remove all the archives stored in the ~/.yarn directory","$0 cache clean --mirror"]]});var Kne=$C;var Hne=ge(m0()),hN=ge(require("util")),em=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.unsafe=J.Boolean("--no-redacted",!1,{description:"Don't redact secrets (such as tokens) from the output"});this.name=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=this.name.replace(/[.[].*$/,""),i=this.name.replace(/^[^.[]*/,"");if(typeof e.settings.get(t)=="undefined")throw new Pe(`Couldn't find a configuration settings named "${t}"`);let s=e.getSpecial(t,{hideSecrets:!this.unsafe,getNativePaths:!0}),o=Se.convertMapsToIndexableObjects(s),a=i?(0,Hne.default)(o,i):o,l=await Je.start({configuration:e,includeFooter:!1,json:this.json,stdout:this.context.stdout},async c=>{c.reportJson(a)});if(!this.json){if(typeof a=="string")return this.context.stdout.write(`${a} +`),l.exitCode();hN.inspect.styles.name="cyan",this.context.stdout.write(`${(0,hN.inspect)(a,{depth:Infinity,colors:e.get("enableColors"),compact:!1})} +`)}return l.exitCode()}};em.paths=[["config","get"]],em.usage=Re.Usage({description:"read a configuration settings",details:` + This command will print a configuration setting. + + Secrets (such as tokens) will be redacted from the output by default. If this behavior isn't desired, set the \`--no-redacted\` to get the untransformed value. + `,examples:[["Print a simple configuration setting","yarn config get yarnPath"],["Print a complex configuration setting","yarn config get packageExtensions"],["Print a nested field from the configuration",`yarn config get 'npmScopes["my-company"].npmRegistryServer'`],["Print a token from the configuration","yarn config get npmAuthToken --no-redacted"],["Print a configuration setting as JSON","yarn config get packageExtensions --json"]]});var jne=em;var eoe=ge(IN()),toe=ge(m0()),roe=ge($se()),yN=ge(require("util")),rm=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Set complex configuration settings to JSON values"});this.home=J.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=J.String();this.value=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=()=>{if(!e.projectCwd)throw new Pe("This command must be run from within a project folder");return e.projectCwd},i=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof e.settings.get(i)=="undefined")throw new Pe(`Couldn't find a configuration settings named "${i}"`);if(i==="enableStrictSettings")throw new Pe("This setting only affects the file it's in, and thus cannot be set from the CLI");let o=this.json?JSON.parse(this.value):this.value;await(this.home?h=>ye.updateHomeConfiguration(h):h=>ye.updateConfiguration(t(),h))(h=>{if(n){let p=(0,eoe.default)(h);return(0,roe.default)(p,this.name,o),p}else return te(N({},h),{[i]:o})});let c=(await ye.find(this.context.cwd,this.context.plugins)).getSpecial(i,{hideSecrets:!0,getNativePaths:!0}),u=Se.convertMapsToIndexableObjects(c),g=n?(0,toe.default)(u,n):u;return(await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout},async h=>{yN.inspect.styles.name="cyan",h.reportInfo(X.UNNAMED,`Successfully set ${this.name} to ${(0,yN.inspect)(g,{depth:Infinity,colors:e.get("enableColors"),compact:!1})}`)})).exitCode()}};rm.paths=[["config","set"]],rm.usage=Re.Usage({description:"change a configuration settings",details:` + This command will set a configuration setting. + + When used without the \`--json\` flag, it can only set a simple configuration setting (a string, a number, or a boolean). + + When used with the \`--json\` flag, it can set both simple and complex configuration settings, including Arrays and Objects. + `,examples:[["Set a simple configuration setting (a string, a number, or a boolean)","yarn config set initScope myScope"],["Set a simple configuration setting (a string, a number, or a boolean) using the `--json` flag",'yarn config set initScope --json \\"myScope\\"'],["Set a complex configuration setting (an Array) using the `--json` flag",`yarn config set unsafeHttpWhitelist --json '["*.example.com", "example.com"]'`],["Set a complex configuration setting (an Object) using the `--json` flag",`yarn config set packageExtensions --json '{ "@babel/parser@*": { "dependencies": { "@babel/types": "*" } } }'`],["Set a nested configuration setting",'yarn config set npmScopes.company.npmRegistryServer "https://npm.example.com"'],["Set a nested configuration setting using indexed access for non-simple keys",`yarn config set 'npmRegistries["//npm.example.com"].npmAuthToken' "ffffffff-ffff-ffff-ffff-ffffffffffff"`]]});var ioe=rm;var goe=ge(IN()),foe=ge(wC()),hoe=ge(uoe()),im=class extends Le{constructor(){super(...arguments);this.home=J.Boolean("-H,--home",!1,{description:"Update the home configuration instead of the project configuration"});this.name=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=()=>{if(!e.projectCwd)throw new Pe("This command must be run from within a project folder");return e.projectCwd},i=this.name.replace(/[.[].*$/,""),n=this.name.replace(/^[^.[]*\.?/,"");if(typeof e.settings.get(i)=="undefined")throw new Pe(`Couldn't find a configuration settings named "${i}"`);let o=this.home?l=>ye.updateHomeConfiguration(l):l=>ye.updateConfiguration(t(),l);return(await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout},async l=>{let c=!1;await o(u=>{if(!(0,foe.default)(u,this.name))return l.reportWarning(X.UNNAMED,`Configuration doesn't contain setting ${this.name}; there is nothing to unset`),c=!0,u;let g=n?(0,goe.default)(u):N({},u);return(0,hoe.default)(g,this.name),g}),c||l.reportInfo(X.UNNAMED,`Successfully unset ${this.name}`)})).exitCode()}};im.paths=[["config","unset"]],im.usage=Re.Usage({description:"unset a configuration setting",details:` + This command will unset a configuration setting. + `,examples:[["Unset a simple configuration setting","yarn config unset initScope"],["Unset a complex configuration setting","yarn config unset packageExtensions"],["Unset a nested configuration setting","yarn config unset npmScopes.company.npmRegistryServer"]]});var poe=im;var wN=ge(require("util")),nm=class extends Le{constructor(){super(...arguments);this.verbose=J.Boolean("-v,--verbose",!1,{description:"Print the setting description on top of the regular key/value information"});this.why=J.Boolean("--why",!1,{description:"Print the reason why a setting is set a particular way"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins,{strict:!1});return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async i=>{if(e.invalid.size>0&&!this.json){for(let[n,s]of e.invalid)i.reportError(X.INVALID_CONFIGURATION_KEY,`Invalid configuration key "${n}" in ${s}`);i.reportSeparator()}if(this.json){let n=Se.sortMap(e.settings.keys(),s=>s);for(let s of n){let o=e.settings.get(s),a=e.getSpecial(s,{hideSecrets:!0,getNativePaths:!0}),l=e.sources.get(s);this.verbose?i.reportJson({key:s,effective:a,source:l}):i.reportJson(N({key:s,effective:a,source:l},o))}}else{let n=Se.sortMap(e.settings.keys(),a=>a),s=n.reduce((a,l)=>Math.max(a,l.length),0),o={breakLength:Infinity,colors:e.get("enableColors"),maxArrayLength:2};if(this.why||this.verbose){let a=n.map(c=>{let u=e.settings.get(c);if(!u)throw new Error(`Assertion failed: This settings ("${c}") should have been registered`);let g=this.why?e.sources.get(c)||"":u.description;return[c,g]}),l=a.reduce((c,[,u])=>Math.max(c,u.length),0);for(let[c,u]of a)i.reportInfo(null,`${c.padEnd(s," ")} ${u.padEnd(l," ")} ${(0,wN.inspect)(e.getSpecial(c,{hideSecrets:!0,getNativePaths:!0}),o)}`)}else for(let a of n)i.reportInfo(null,`${a.padEnd(s," ")} ${(0,wN.inspect)(e.getSpecial(a,{hideSecrets:!0,getNativePaths:!0}),o)}`)}})).exitCode()}};nm.paths=[["config"]],nm.usage=Re.Usage({description:"display the current configuration",details:` + This command prints the current active configuration settings. + `,examples:[["Print the active configuration settings","$0 config"]]});var doe=nm;ws();var BN={};ft(BN,{Strategy:()=>bu,acceptedStrategies:()=>FWe,dedupe:()=>bN});var Coe=ge(ns()),bu;(function(e){e.HIGHEST="highest"})(bu||(bu={}));var FWe=new Set(Object.values(bu)),NWe={highest:async(r,e,{resolver:t,fetcher:i,resolveOptions:n,fetchOptions:s})=>{let o=new Map;for(let[a,l]of r.storedResolutions){let c=r.storedDescriptors.get(a);if(typeof c=="undefined")throw new Error(`Assertion failed: The descriptor (${a}) should have been registered`);Se.getSetWithDefault(o,c.identHash).add(l)}return Array.from(r.storedDescriptors.values(),async a=>{if(e.length&&!Coe.default.isMatch(P.stringifyIdent(a),e))return null;let l=r.storedResolutions.get(a.descriptorHash);if(typeof l=="undefined")throw new Error(`Assertion failed: The resolution (${a.descriptorHash}) should have been registered`);let c=r.originalPackages.get(l);if(typeof c=="undefined"||!t.shouldPersistResolution(c,n))return null;let u=o.get(a.identHash);if(typeof u=="undefined")throw new Error(`Assertion failed: The resolutions (${a.identHash}) should have been registered`);if(u.size===1)return null;let g=[...u].map(y=>{let b=r.originalPackages.get(y);if(typeof b=="undefined")throw new Error(`Assertion failed: The package (${y}) should have been registered`);return b.reference}),f=await t.getSatisfying(a,g,n),h=f==null?void 0:f[0];if(typeof h=="undefined")return null;let p=h.locatorHash,m=r.originalPackages.get(p);if(typeof m=="undefined")throw new Error(`Assertion failed: The package (${p}) should have been registered`);return p===l?null:{descriptor:a,currentPackage:c,updatedPackage:m}})}};async function bN(r,{strategy:e,patterns:t,cache:i,report:n}){let{configuration:s}=r,o=new di,a=s.makeResolver(),l=s.makeFetcher(),c={cache:i,checksums:r.storedChecksums,fetcher:l,project:r,report:o,skipIntegrityCheck:!0,cacheOptions:{skipIntegrityCheck:!0}},u={project:r,resolver:a,report:o,fetchOptions:c};return await n.startTimerPromise("Deduplication step",async()=>{let f=await NWe[e](r,t,{resolver:a,resolveOptions:u,fetcher:l,fetchOptions:c}),h=Ji.progressViaCounter(f.length);n.reportProgress(h);let p=0;await Promise.all(f.map(b=>b.then(v=>{if(v===null)return;p++;let{descriptor:k,currentPackage:T,updatedPackage:Y}=v;n.reportInfo(X.UNNAMED,`${P.prettyDescriptor(s,k)} can be deduped from ${P.prettyLocator(s,T)} to ${P.prettyLocator(s,Y)}`),n.reportJson({descriptor:P.stringifyDescriptor(k),currentResolution:P.stringifyLocator(T),updatedResolution:P.stringifyLocator(Y)}),r.storedResolutions.set(k.descriptorHash,Y.locatorHash)}).finally(()=>h.tick())));let m;switch(p){case 0:m="No packages";break;case 1:m="One package";break;default:m=`${p} packages`}let y=ae.pretty(s,e,ae.Type.CODE);return n.reportInfo(X.UNNAMED,`${m} can be deduped using the ${y} strategy`),p})}var sm=class extends Le{constructor(){super(...arguments);this.strategy=J.String("-s,--strategy",bu.HIGHEST,{description:"The strategy to use when deduping dependencies",validator:nn(bu)});this.check=J.Boolean("-c,--check",!1,{description:"Exit with exit code 1 when duplicates are found, without persisting the dependency tree"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.mode=J.String("--mode",{description:"Change what artifacts installs generate",validator:nn(Ci)});this.patterns=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd),i=await Nt.find(e);await t.restoreInstallState({restoreResolutions:!1});let n=0,s=await Je.start({configuration:e,includeFooter:!1,stdout:this.context.stdout,json:this.json},async o=>{n=await bN(t,{strategy:this.strategy,patterns:this.patterns,cache:i,report:o})});return s.hasErrors()?s.exitCode():this.check?n?1:0:(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async a=>{await t.install({cache:i,report:a,mode:this.mode})})).exitCode()}};sm.paths=[["dedupe"]],sm.usage=Re.Usage({description:"deduplicate dependencies with overlapping ranges",details:"\n Duplicates are defined as descriptors with overlapping ranges being resolved and locked to different locators. They are a natural consequence of Yarn's deterministic installs, but they can sometimes pile up and unnecessarily increase the size of your project.\n\n This command dedupes dependencies in the current project using different strategies (only one is implemented at the moment):\n\n - `highest`: Reuses (where possible) the locators with the highest versions. This means that dependencies can only be upgraded, never downgraded. It's also guaranteed that it never takes more than a single pass to dedupe the entire dependency tree.\n\n **Note:** Even though it never produces a wrong dependency tree, this command should be used with caution, as it modifies the dependency tree, which can sometimes cause problems when packages don't strictly follow semver recommendations. Because of this, it is recommended to also review the changes manually.\n\n If set, the `-c,--check` flag will only report the found duplicates, without persisting the modified dependency tree. If changes are found, the command will exit with a non-zero exit code, making it suitable for CI purposes.\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n ### In-depth explanation:\n\n Yarn doesn't deduplicate dependencies by default, otherwise installs wouldn't be deterministic and the lockfile would be useless. What it actually does is that it tries to not duplicate dependencies in the first place.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@*`will cause Yarn to reuse `foo@2.3.4`, even if the latest `foo` is actually `foo@2.10.14`, thus preventing unnecessary duplication.\n\n Duplication happens when Yarn can't unlock dependencies that have already been locked inside the lockfile.\n\n **Example:** If `foo@^2.3.4` (a dependency of a dependency) has already been resolved to `foo@2.3.4`, running `yarn add foo@2.10.14` will cause Yarn to install `foo@2.10.14` because the existing resolution doesn't satisfy the range `2.10.14`. This behavior can lead to (sometimes) unwanted duplication, since now the lockfile contains 2 separate resolutions for the 2 `foo` descriptors, even though they have overlapping ranges, which means that the lockfile can be simplified so that both descriptors resolve to `foo@2.10.14`.\n ",examples:[["Dedupe all packages","$0 dedupe"],["Dedupe all packages using a specific strategy","$0 dedupe --strategy highest"],["Dedupe a specific package","$0 dedupe lodash"],["Dedupe all packages with the `@babel/*` scope","$0 dedupe '@babel/*'"],["Check for duplicates (can be used as a CI step)","$0 dedupe --check"]]});var moe=sm;var W0=class extends Le{async execute(){let{plugins:e}=await ye.find(this.context.cwd,this.context.plugins),t=[];for(let o of e){let{commands:a}=o[1];if(a){let c=Bs.from(a).definitions();t.push([o[0],c])}}let i=this.cli.definitions(),n=(o,a)=>o.split(" ").slice(1).join()===a.split(" ").slice(1).join(),s=Ioe()["@yarnpkg/builder"].bundles.standard;for(let o of t){let a=o[1];for(let l of a)i.find(c=>n(c.path,l.path)).plugin={name:o[0],isDefault:s.includes(o[0])}}this.context.stdout.write(`${JSON.stringify(i,null,2)} +`)}};W0.paths=[["--clipanion=definitions"]];var yoe=W0;var z0=class extends Le{async execute(){this.context.stdout.write(this.cli.usage(null))}};z0.paths=[["help"],["--help"],["-h"]];var woe=z0;var QN=class extends Le{constructor(){super(...arguments);this.leadingArgument=J.String();this.args=J.Proxy()}async execute(){if(this.leadingArgument.match(/[\\/]/)&&!P.tryParseIdent(this.leadingArgument)){let e=x.resolve(this.context.cwd,H.toPortablePath(this.leadingArgument));return await this.cli.run(this.args,{cwd:e})}else return await this.cli.run(["run",this.leadingArgument,...this.args])}},Boe=QN;var _0=class extends Le{async execute(){this.context.stdout.write(`${Kr||""} +`)}};_0.paths=[["-v"],["--version"]];var boe=_0;var om=class extends Le{constructor(){super(...arguments);this.commandName=J.String();this.args=J.Proxy()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,locator:i}=await ze.find(e,this.context.cwd);return await t.restoreInstallState(),await Zt.executePackageShellcode(i,this.commandName,this.args,{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,project:t})}};om.paths=[["exec"]],om.usage=Re.Usage({description:"execute a shell script",details:` + This command simply executes a shell script within the context of the root directory of the active workspace using the portable shell. + + It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). + `,examples:[["Execute a single shell command","$0 exec echo Hello World"],["Execute a shell script",'$0 exec "tsc & babel src --out-dir lib"']]});var Qoe=om;ws();var am=class extends Le{constructor(){super(...arguments);this.hash=J.String({required:!1,validator:hp(fp(),[pp(/^p[0-9a-f]{5}$/)])})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd);return await t.restoreInstallState({restoreResolutions:!1}),await t.applyLightResolution(),typeof this.hash!="undefined"?await LWe(this.hash,t,{stdout:this.context.stdout}):(await Je.start({configuration:e,stdout:this.context.stdout,includeFooter:!1},async n=>{var o;let s=[([,a])=>P.stringifyLocator(t.storedPackages.get(a.subject)),([,a])=>P.stringifyIdent(a.requested)];for(let[a,l]of Se.sortMap(t.peerRequirements,s)){let c=t.storedPackages.get(l.subject);if(typeof c=="undefined")throw new Error("Assertion failed: Expected the subject package to have been registered");let u=t.storedPackages.get(l.rootRequester);if(typeof u=="undefined")throw new Error("Assertion failed: Expected the root package to have been registered");let g=(o=c.dependencies.get(l.requested.identHash))!=null?o:null,f=ae.pretty(e,a,ae.Type.CODE),h=P.prettyLocator(e,c),p=P.prettyIdent(e,l.requested),m=P.prettyIdent(e,u),y=l.allRequesters.length-1,b=`descendant${y===1?"":"s"}`,v=y>0?` and ${y} ${b}`:"",k=g!==null?"provides":"doesn't provide";n.reportInfo(null,`${f} \u2192 ${h} ${k} ${p} to ${m}${v}`)}})).exitCode()}};am.paths=[["explain","peer-requirements"]],am.usage=Re.Usage({description:"explain a set of peer requirements",details:` + A set of peer requirements represents all peer requirements that a dependent must satisfy when providing a given peer request to a requester and its descendants. + + When the hash argument is specified, this command prints a detailed explanation of all requirements of the set corresponding to the hash and whether they're satisfied or not. + + When used without arguments, this command lists all sets of peer requirements and the corresponding hash that can be used to get detailed information about a given set. + + **Note:** A hash is a six-letter p-prefixed code that can be obtained from peer dependency warnings or from the list of all peer requirements (\`yarn explain peer-requirements\`). + `,examples:[["Explain the corresponding set of peer requirements for a hash","$0 explain peer-requirements p1a4ed"],["List all sets of peer requirements","$0 explain peer-requirements"]]});var Soe=am;async function LWe(r,e,t){let{configuration:i}=e,n=e.peerRequirements.get(r);if(typeof n=="undefined")throw new Error(`No peerDependency requirements found for hash: "${r}"`);return(await Je.start({configuration:i,stdout:t.stdout,includeFooter:!1},async o=>{var b,v;let a=e.storedPackages.get(n.subject);if(typeof a=="undefined")throw new Error("Assertion failed: Expected the subject package to have been registered");let l=e.storedPackages.get(n.rootRequester);if(typeof l=="undefined")throw new Error("Assertion failed: Expected the root package to have been registered");let c=(b=a.dependencies.get(n.requested.identHash))!=null?b:null,u=c!==null?e.storedResolutions.get(c.descriptorHash):null;if(typeof u=="undefined")throw new Error("Assertion failed: Expected the resolution to have been registered");let g=u!==null?e.storedPackages.get(u):null;if(typeof g=="undefined")throw new Error("Assertion failed: Expected the provided package to have been registered");let f=[...n.allRequesters.values()].map(k=>{let T=e.storedPackages.get(k);if(typeof T=="undefined")throw new Error("Assertion failed: Expected the package to be registered");let Y=P.devirtualizeLocator(T),q=e.storedPackages.get(Y.locatorHash);if(typeof q=="undefined")throw new Error("Assertion failed: Expected the package to be registered");let $=q.peerDependencies.get(n.requested.identHash);if(typeof $=="undefined")throw new Error("Assertion failed: Expected the peer dependency to be registered");return{pkg:T,peerDependency:$}});if(g!==null){let k=f.every(({peerDependency:T})=>Wt.satisfiesWithPrereleases(g.version,T.range));o.reportInfo(X.UNNAMED,`${P.prettyLocator(i,a)} provides ${P.prettyLocator(i,g)} with version ${P.prettyReference(i,(v=g.version)!=null?v:"")}, which ${k?"satisfies":"doesn't satisfy"} the following requirements:`)}else o.reportInfo(X.UNNAMED,`${P.prettyLocator(i,a)} doesn't provide ${P.prettyIdent(i,n.requested)}, breaking the following requirements:`);o.reportSeparator();let h=ae.mark(i),p=[];for(let{pkg:k,peerDependency:T}of Se.sortMap(f,Y=>P.stringifyLocator(Y.pkg))){let q=(g!==null?Wt.satisfiesWithPrereleases(g.version,T.range):!1)?h.Check:h.Cross;p.push({stringifiedLocator:P.stringifyLocator(k),prettyLocator:P.prettyLocator(i,k),prettyRange:P.prettyRange(i,T.range),mark:q})}let m=Math.max(...p.map(({stringifiedLocator:k})=>k.length)),y=Math.max(...p.map(({prettyRange:k})=>k.length));for(let{stringifiedLocator:k,prettyLocator:T,prettyRange:Y,mark:q}of Se.sortMap(p,({stringifiedLocator:$})=>$))o.reportInfo(null,`${T.padEnd(m+(T.length-k.length)," ")} \u2192 ${Y.padEnd(y," ")} ${q}`);p.length>1&&(o.reportSeparator(),o.reportInfo(X.UNNAMED,`Note: these requirements start with ${P.prettyLocator(e.configuration,l)}`))})).exitCode()}ws();var voe=ge(ri()),Am=class extends Le{constructor(){super(...arguments);this.onlyIfNeeded=J.Boolean("--only-if-needed",!1,{description:"Only lock the Yarn version if it isn't already locked"});this.version=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins);if(e.get("yarnPath")&&this.onlyIfNeeded)return 0;let t=()=>{if(typeof Kr=="undefined")throw new Pe("The --install flag can only be used without explicit version specifier from the Yarn CLI");return`file://${process.argv[1]}`},i;if(this.version==="self")i=t();else if(this.version==="latest"||this.version==="berry"||this.version==="stable")i=`https://repo.yarnpkg.com/${await lm(e,"stable")}/packages/yarnpkg-cli/bin/yarn.js`;else if(this.version==="canary")i=`https://repo.yarnpkg.com/${await lm(e,"canary")}/packages/yarnpkg-cli/bin/yarn.js`;else if(this.version==="classic")i="https://nightly.yarnpkg.com/latest.js";else if(this.version.match(/^https?:/))i=this.version;else if(this.version.match(/^\.{0,2}[\\/]/)||H.isAbsolute(this.version))i=`file://${H.resolve(this.version)}`;else if(Wt.satisfiesWithPrereleases(this.version,">=2.0.0"))i=`https://repo.yarnpkg.com/${this.version}/packages/yarnpkg-cli/bin/yarn.js`;else if(Wt.satisfiesWithPrereleases(this.version,"^0.x || ^1.x"))i=`https://github.com/yarnpkg/yarn/releases/download/v${this.version}/yarn-${this.version}.js`;else if(Wt.validRange(this.version))i=`https://repo.yarnpkg.com/${await TWe(e,this.version)}/packages/yarnpkg-cli/bin/yarn.js`;else throw new Pe(`Invalid version descriptor "${this.version}"`);return(await Je.start({configuration:e,stdout:this.context.stdout,includeLogs:!this.context.quiet},async s=>{let o="file://",a;i.startsWith(o)?(s.reportInfo(X.UNNAMED,`Downloading ${ae.pretty(e,i,Ri.URL)}`),a=await U.readFilePromise(H.toPortablePath(i.slice(o.length)))):(s.reportInfo(X.UNNAMED,`Retrieving ${ae.pretty(e,i,Ri.PATH)}`),a=await ir.get(i,{configuration:e})),await SN(e,null,a,{report:s})})).exitCode()}};Am.paths=[["set","version"]],Am.usage=Re.Usage({description:"lock the Yarn version used by the project",details:"\n This command will download a specific release of Yarn directly from the Yarn GitHub repository, will store it inside your project, and will change the `yarnPath` settings from your project `.yarnrc.yml` file to point to the new file.\n\n A very good use case for this command is to enforce the version of Yarn used by the any single member of your team inside a same project - by doing this you ensure that you have control on Yarn upgrades and downgrades (including on your deployment servers), and get rid of most of the headaches related to someone using a slightly different version and getting a different behavior than you.\n\n The version specifier can be:\n\n - a tag:\n - `latest` / `berry` / `stable` -> the most recent stable berry (`>=2.0.0`) release\n - `canary` -> the most recent canary (release candidate) berry (`>=2.0.0`) release\n - `classic` -> the most recent classic (`^0.x || ^1.x`) release\n\n - a semver range (e.g. `2.x`) -> the most recent version satisfying the range (limited to berry releases)\n\n - a semver version (e.g. `2.4.1`, `1.22.1`)\n\n - a local file referenced through either a relative or absolute path\n\n - `self` -> the version used to invoke the command\n ",examples:[["Download the latest release from the Yarn repository","$0 set version latest"],["Download the latest canary release from the Yarn repository","$0 set version canary"],["Download the latest classic release from the Yarn repository","$0 set version classic"],["Download the most recent Yarn 3 build","$0 set version 3.x"],["Download a specific Yarn 2 build","$0 set version 2.0.0-rc.30"],["Switch back to a specific Yarn 1 release","$0 set version 1.22.1"],["Use a release from the local filesystem","$0 set version ./yarn.cjs"],["Use a release from a URL","$0 set version https://repo.yarnpkg.com/3.1.0/packages/yarnpkg-cli/bin/yarn.js"],["Download the version used to invoke the command","$0 set version self"]]});var koe=Am;async function TWe(r,e){let i=(await ir.get("https://repo.yarnpkg.com/tags",{configuration:r,jsonResponse:!0})).tags.filter(n=>Wt.satisfiesWithPrereleases(n,e));if(i.length===0)throw new Pe(`No matching release found for range ${ae.pretty(r,e,ae.Type.RANGE)}.`);return i[0]}async function lm(r,e){let t=await ir.get("https://repo.yarnpkg.com/tags",{configuration:r,jsonResponse:!0});if(!t.latest[e])throw new Pe(`Tag ${ae.pretty(r,e,ae.Type.RANGE)} not found`);return t.latest[e]}async function SN(r,e,t,{report:i}){var g;e===null&&await U.mktempPromise(async f=>{let h=x.join(f,"yarn.cjs");await U.writeFilePromise(h,t);let{stdout:p}=await Nr.execvp(process.execPath,[H.fromPortablePath(h),"--version"],{cwd:f,env:te(N({},process.env),{YARN_IGNORE_PATH:"1"})});if(e=p.trim(),!voe.default.valid(e))throw new Error(`Invalid semver version. ${ae.pretty(r,"yarn --version",ae.Type.CODE)} returned: +${e}`)});let n=(g=r.projectCwd)!=null?g:r.startingCwd,s=x.resolve(n,".yarn/releases"),o=x.resolve(s,`yarn-${e}.cjs`),a=x.relative(r.startingCwd,o),l=x.relative(n,o),c=r.get("yarnPath"),u=c===null||c.startsWith(`${s}/`);if(i.reportInfo(X.UNNAMED,`Saving the new release in ${ae.pretty(r,a,"magenta")}`),await U.removePromise(x.dirname(o)),await U.mkdirPromise(x.dirname(o),{recursive:!0}),await U.writeFilePromise(o,t,{mode:493}),u){await ye.updateConfiguration(n,{yarnPath:l});let f=await At.tryFind(n)||new At;f.packageManager=`yarn@${e&&Se.isTaggedYarnVersion(e)?e:await lm(r,"stable")}`;let h={};f.exportTo(h);let p=x.join(n,At.fileName),m=`${JSON.stringify(h,null,f.indent)} +`;await U.changeFilePromise(p,m,{automaticNewlines:!0})}}function xoe(r){return X[yI(r)]}var OWe=/## (?YN[0-9]{4}) - `(?[A-Z_]+)`\n\n(?
(?:.(?!##))+)/gs;async function MWe(r){let t=`https://repo.yarnpkg.com/${Se.isTaggedYarnVersion(Kr)?Kr:await lm(r,"canary")}/packages/gatsby/content/advanced/error-codes.md`,i=await ir.get(t,{configuration:r});return new Map(Array.from(i.toString().matchAll(OWe),({groups:n})=>{if(!n)throw new Error("Assertion failed: Expected the match to have been successful");let s=xoe(n.code);if(n.name!==s)throw new Error(`Assertion failed: Invalid error code data: Expected "${n.name}" to be named "${s}"`);return[n.code,n.details]}))}var cm=class extends Le{constructor(){super(...arguments);this.code=J.String({required:!1,validator:hp(fp(),[pp(/^YN[0-9]{4}$/)])});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins);if(typeof this.code!="undefined"){let t=xoe(this.code),i=ae.pretty(e,t,ae.Type.CODE),n=this.cli.format().header(`${this.code} - ${i}`),o=(await MWe(e)).get(this.code),a=typeof o!="undefined"?ae.jsonOrPretty(this.json,e,ae.tuple(ae.Type.MARKDOWN,{text:o,format:this.cli.format(),paragraphs:!0})):`This error code does not have a description. + +You can help us by editing this page on GitHub \u{1F642}: +${ae.jsonOrPretty(this.json,e,ae.tuple(ae.Type.URL,"https://github.com/yarnpkg/berry/blob/master/packages/gatsby/content/advanced/error-codes.md"))} +`;this.json?this.context.stdout.write(`${JSON.stringify({code:this.code,name:t,details:a})} +`):this.context.stdout.write(`${n} + +${a} +`)}else{let t={children:Se.mapAndFilter(Object.entries(X),([i,n])=>Number.isNaN(Number(i))?Se.mapAndFilter.skip:{label:VA(Number(i)),value:ae.tuple(ae.Type.CODE,n)})};ls.emitTree(t,{configuration:e,stdout:this.context.stdout,json:this.json})}}};cm.paths=[["explain"]],cm.usage=Re.Usage({description:"explain an error code",details:` + When the code argument is specified, this command prints its name and its details. + + When used without arguments, this command lists all error codes and their names. + `,examples:[["Explain an error code","$0 explain YN0006"],["List all error codes","$0 explain"]]});var Poe=cm;var Doe=ge(ns()),um=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Print versions of a package from the whole project"});this.recursive=J.Boolean("-R,--recursive",!1,{description:"Print information for all packages, including transitive dependencies"});this.extra=J.Array("-X,--extra",[],{description:"An array of requests of extra data provided by plugins"});this.cache=J.Boolean("--cache",!1,{description:"Print information about the cache entry of a package (path, size, checksum)"});this.dependents=J.Boolean("--dependents",!1,{description:"Print all dependents for each matching package"});this.manifest=J.Boolean("--manifest",!1,{description:"Print data obtained by looking at the package archive (license, homepage, ...)"});this.nameOnly=J.Boolean("--name-only",!1,{description:"Only print the name for the matching packages"});this.virtuals=J.Boolean("--virtuals",!1,{description:"Print each instance of the virtual packages"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i&&!this.all)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState();let s=new Set(this.extra);this.cache&&s.add("cache"),this.dependents&&s.add("dependents"),this.manifest&&s.add("manifest");let o=(k,{recursive:T})=>{let Y=k.anchoredLocator.locatorHash,q=new Map,$=[Y];for(;$.length>0;){let z=$.shift();if(q.has(z))continue;let ne=t.storedPackages.get(z);if(typeof ne=="undefined")throw new Error("Assertion failed: Expected the package to be registered");if(q.set(z,ne),P.isVirtualLocator(ne)&&$.push(P.devirtualizeLocator(ne).locatorHash),!(!T&&z!==Y))for(let ee of ne.dependencies.values()){let A=t.storedResolutions.get(ee.descriptorHash);if(typeof A=="undefined")throw new Error("Assertion failed: Expected the resolution to be registered");$.push(A)}}return q.values()},a=({recursive:k})=>{let T=new Map;for(let Y of t.workspaces)for(let q of o(Y,{recursive:k}))T.set(q.locatorHash,q);return T.values()},l=({all:k,recursive:T})=>k&&T?t.storedPackages.values():k?a({recursive:T}):o(i,{recursive:T}),c=({all:k,recursive:T})=>{let Y=l({all:k,recursive:T}),q=this.patterns.map(ne=>{let ee=P.parseLocator(ne),A=Doe.default.makeRe(P.stringifyIdent(ee)),oe=P.isVirtualLocator(ee),ce=oe?P.devirtualizeLocator(ee):ee;return Z=>{let O=P.stringifyIdent(Z);if(!A.test(O))return!1;if(ee.reference==="unknown")return!0;let L=P.isVirtualLocator(Z),de=L?P.devirtualizeLocator(Z):Z;return!(oe&&L&&ee.reference!==Z.reference||ce.reference!==de.reference)}}),$=Se.sortMap([...Y],ne=>P.stringifyLocator(ne));return{selection:$.filter(ne=>q.length===0||q.some(ee=>ee(ne))),sortedLookup:$}},{selection:u,sortedLookup:g}=c({all:this.all,recursive:this.recursive});if(u.length===0)throw new Pe("No package matched your request");let f=new Map;if(this.dependents)for(let k of g)for(let T of k.dependencies.values()){let Y=t.storedResolutions.get(T.descriptorHash);if(typeof Y=="undefined")throw new Error("Assertion failed: Expected the resolution to be registered");Se.getArrayWithDefault(f,Y).push(k)}let h=new Map;for(let k of g){if(!P.isVirtualLocator(k))continue;let T=P.devirtualizeLocator(k);Se.getArrayWithDefault(h,T.locatorHash).push(k)}let p={},m={children:p},y=e.makeFetcher(),b={project:t,fetcher:y,cache:n,checksums:t.storedChecksums,report:new di,cacheOptions:{skipIntegrityCheck:!0},skipIntegrityCheck:!0},v=[async(k,T,Y)=>{var z,ne;if(!T.has("manifest"))return;let q=await y.fetch(k,b),$;try{$=await At.find(q.prefixPath,{baseFs:q.packageFs})}finally{(z=q.releaseFs)==null||z.call(q)}Y("Manifest",{License:ae.tuple(ae.Type.NO_HINT,$.license),Homepage:ae.tuple(ae.Type.URL,(ne=$.raw.homepage)!=null?ne:null)})},async(k,T,Y)=>{var A;if(!T.has("cache"))return;let q={mockedPackages:t.disabledLocators,unstablePackages:t.conditionalLocators},$=(A=t.storedChecksums.get(k.locatorHash))!=null?A:null,z=n.getLocatorPath(k,$,q),ne;if(z!==null)try{ne=U.statSync(z)}catch{}let ee=typeof ne!="undefined"?[ne.size,ae.Type.SIZE]:void 0;Y("Cache",{Checksum:ae.tuple(ae.Type.NO_HINT,$),Path:ae.tuple(ae.Type.PATH,z),Size:ee})}];for(let k of u){let T=P.isVirtualLocator(k);if(!this.virtuals&&T)continue;let Y={},q={value:[k,ae.Type.LOCATOR],children:Y};if(p[P.stringifyLocator(k)]=q,this.nameOnly){delete q.children;continue}let $=h.get(k.locatorHash);typeof $!="undefined"&&(Y.Instances={label:"Instances",value:ae.tuple(ae.Type.NUMBER,$.length)}),Y.Version={label:"Version",value:ae.tuple(ae.Type.NO_HINT,k.version)};let z=(ee,A)=>{let oe={};if(Y[ee]=oe,Array.isArray(A))oe.children=A.map(ce=>({value:ce}));else{let ce={};oe.children=ce;for(let[Z,O]of Object.entries(A))typeof O!="undefined"&&(ce[Z]={label:Z,value:O})}};if(!T){for(let ee of v)await ee(k,s,z);await e.triggerHook(ee=>ee.fetchPackageInfo,k,s,z)}k.bin.size>0&&!T&&z("Exported Binaries",[...k.bin.keys()].map(ee=>ae.tuple(ae.Type.PATH,ee)));let ne=f.get(k.locatorHash);typeof ne!="undefined"&&ne.length>0&&z("Dependents",ne.map(ee=>ae.tuple(ae.Type.LOCATOR,ee))),k.dependencies.size>0&&!T&&z("Dependencies",[...k.dependencies.values()].map(ee=>{var ce;let A=t.storedResolutions.get(ee.descriptorHash),oe=typeof A!="undefined"&&(ce=t.storedPackages.get(A))!=null?ce:null;return ae.tuple(ae.Type.RESOLUTION,{descriptor:ee,locator:oe})})),k.peerDependencies.size>0&&T&&z("Peer dependencies",[...k.peerDependencies.values()].map(ee=>{var Z,O;let A=k.dependencies.get(ee.identHash),oe=typeof A!="undefined"&&(Z=t.storedResolutions.get(A.descriptorHash))!=null?Z:null,ce=oe!==null&&(O=t.storedPackages.get(oe))!=null?O:null;return ae.tuple(ae.Type.RESOLUTION,{descriptor:ee,locator:ce})}))}ls.emitTree(m,{configuration:e,json:this.json,stdout:this.context.stdout,separators:this.nameOnly?0:2})}};um.paths=[["info"]],um.usage=Re.Usage({description:"see information related to packages",details:"\n This command prints various information related to the specified packages, accepting glob patterns.\n\n By default, if the locator reference is missing, Yarn will default to print the information about all the matching direct dependencies of the package for the active workspace. To instead print all versions of the package that are direct dependencies of any of your workspaces, use the `-A,--all` flag. Adding the `-R,--recursive` flag will also report transitive dependencies.\n\n Some fields will be hidden by default in order to keep the output readable, but can be selectively displayed by using additional options (`--dependents`, `--manifest`, `--virtuals`, ...) described in the option descriptions.\n\n Note that this command will only print the information directly related to the selected packages - if you wish to know why the package is there in the first place, use `yarn why` which will do just that (it also provides a `-R,--recursive` flag that may be of some help).\n ",examples:[["Show information about Lodash","$0 info lodash"]]});var Roe=um;var V0=ge(yc());ws();var gm=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.immutable=J.Boolean("--immutable",{description:"Abort with an error exit code if the lockfile was to be modified"});this.immutableCache=J.Boolean("--immutable-cache",{description:"Abort with an error exit code if the cache folder was to be modified"});this.checkCache=J.Boolean("--check-cache",!1,{description:"Always refetch the packages and ensure that their checksums are consistent"});this.inlineBuilds=J.Boolean("--inline-builds",{description:"Verbosely print the output of the build steps of dependencies"});this.mode=J.String("--mode",{description:"Change what artifacts installs generate",validator:nn(Ci)});this.cacheFolder=J.String("--cache-folder",{hidden:!0});this.frozenLockfile=J.Boolean("--frozen-lockfile",{hidden:!0});this.ignoreEngines=J.Boolean("--ignore-engines",{hidden:!0});this.nonInteractive=J.Boolean("--non-interactive",{hidden:!0});this.preferOffline=J.Boolean("--prefer-offline",{hidden:!0});this.production=J.Boolean("--production",{hidden:!0});this.registry=J.String("--registry",{hidden:!0});this.silent=J.Boolean("--silent",{hidden:!0});this.networkTimeout=J.String("--network-timeout",{hidden:!0})}async execute(){var g;let e=await ye.find(this.context.cwd,this.context.plugins);typeof this.inlineBuilds!="undefined"&&e.useWithSource("",{enableInlineBuilds:this.inlineBuilds},e.startingCwd,{overwrite:!0});let t=!!process.env.FUNCTION_TARGET||!!process.env.GOOGLE_RUNTIME,i=async(f,{error:h})=>{let p=await Je.start({configuration:e,stdout:this.context.stdout,includeFooter:!1},async m=>{h?m.reportError(X.DEPRECATED_CLI_SETTINGS,f):m.reportWarning(X.DEPRECATED_CLI_SETTINGS,f)});return p.hasErrors()?p.exitCode():null};if(typeof this.ignoreEngines!="undefined"){let f=await i("The --ignore-engines option is deprecated; engine checking isn't a core feature anymore",{error:!V0.default.VERCEL});if(f!==null)return f}if(typeof this.registry!="undefined"){let f=await i("The --registry option is deprecated; prefer setting npmRegistryServer in your .yarnrc.yml file",{error:!1});if(f!==null)return f}if(typeof this.preferOffline!="undefined"){let f=await i("The --prefer-offline flag is deprecated; use the --cached flag with 'yarn add' instead",{error:!V0.default.VERCEL});if(f!==null)return f}if(typeof this.production!="undefined"){let f=await i("The --production option is deprecated on 'install'; use 'yarn workspaces focus' instead",{error:!0});if(f!==null)return f}if(typeof this.nonInteractive!="undefined"){let f=await i("The --non-interactive option is deprecated",{error:!t});if(f!==null)return f}if(typeof this.frozenLockfile!="undefined"&&(await i("The --frozen-lockfile option is deprecated; use --immutable and/or --immutable-cache instead",{error:!1}),this.immutable=this.frozenLockfile),typeof this.cacheFolder!="undefined"){let f=await i("The cache-folder option has been deprecated; use rc settings instead",{error:!V0.default.NETLIFY});if(f!==null)return f}let n=this.mode===Ci.UpdateLockfile;if(n&&(this.immutable||this.immutableCache))throw new Pe(`${ae.pretty(e,"--immutable",ae.Type.CODE)} and ${ae.pretty(e,"--immutable-cache",ae.Type.CODE)} cannot be used with ${ae.pretty(e,"--mode=update-lockfile",ae.Type.CODE)}`);let s=((g=this.immutable)!=null?g:e.get("enableImmutableInstalls"))&&!n,o=this.immutableCache&&!n;if(e.projectCwd!==null){let f=await Je.start({configuration:e,json:this.json,stdout:this.context.stdout,includeFooter:!1},async h=>{await UWe(e,s)&&(h.reportInfo(X.AUTOMERGE_SUCCESS,"Automatically fixed merge conflicts \u{1F44D}"),h.reportSeparator())});if(f.hasErrors())return f.exitCode()}if(e.projectCwd!==null&&typeof e.sources.get("nodeLinker")=="undefined"){let f=e.projectCwd,h;try{h=await U.readFilePromise(x.join(f,xt.lockfile),"utf8")}catch{}if(h==null?void 0:h.includes("yarn lockfile v1")){let p=await Je.start({configuration:e,json:this.json,stdout:this.context.stdout,includeFooter:!1},async m=>{m.reportInfo(X.AUTO_NM_SUCCESS,"Migrating from Yarn 1; automatically enabling the compatibility node-modules linker \u{1F44D}"),m.reportSeparator(),e.use("",{nodeLinker:"node-modules"},f,{overwrite:!0}),await ye.updateConfiguration(f,{nodeLinker:"node-modules"})});if(p.hasErrors())return p.exitCode()}}if(e.projectCwd!==null){let f=await Je.start({configuration:e,json:this.json,stdout:this.context.stdout,includeFooter:!1},async h=>{var p;((p=ye.telemetry)==null?void 0:p.isNew)&&(h.reportInfo(X.TELEMETRY_NOTICE,"Yarn will periodically gather anonymous telemetry: https://yarnpkg.com/advanced/telemetry"),h.reportInfo(X.TELEMETRY_NOTICE,`Run ${ae.pretty(e,"yarn config set --home enableTelemetry 0",ae.Type.CODE)} to disable`),h.reportSeparator())});if(f.hasErrors())return f.exitCode()}let{project:a,workspace:l}=await ze.find(e,this.context.cwd),c=await Nt.find(e,{immutable:o,check:this.checkCache});if(!l)throw new ht(a.cwd,this.context.cwd);return await a.restoreInstallState({restoreResolutions:!1}),(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout,includeLogs:!0},async f=>{await a.install({cache:c,report:f,immutable:s,mode:this.mode})})).exitCode()}};gm.paths=[["install"],Re.Default],gm.usage=Re.Usage({description:"install the project dependencies",details:` + This command sets up your project if needed. The installation is split into four different steps that each have their own characteristics: + + - **Resolution:** First the package manager will resolve your dependencies. The exact way a dependency version is privileged over another isn't standardized outside of the regular semver guarantees. If a package doesn't resolve to what you would expect, check that all dependencies are correctly declared (also check our website for more information: ). + + - **Fetch:** Then we download all the dependencies if needed, and make sure that they're all stored within our cache (check the value of \`cacheFolder\` in \`yarn config\` to see where the cache files are stored). + + - **Link:** Then we send the dependency tree information to internal plugins tasked with writing them on the disk in some form (for example by generating the .pnp.cjs file you might know). + + - **Build:** Once the dependency tree has been written on the disk, the package manager will now be free to run the build scripts for all packages that might need it, in a topological order compatible with the way they depend on one another. See https://yarnpkg.com/advanced/lifecycle-scripts for detail. + + Note that running this command is not part of the recommended workflow. Yarn supports zero-installs, which means that as long as you store your cache and your .pnp.cjs file inside your repository, everything will work without requiring any install right after cloning your repository or switching branches. + + If the \`--immutable\` option is set (defaults to true on CI), Yarn will abort with an error exit code if the lockfile was to be modified (other paths can be added using the \`immutablePatterns\` configuration setting). For backward compatibility we offer an alias under the name of \`--frozen-lockfile\`, but it will be removed in a later release. + + If the \`--immutable-cache\` option is set, Yarn will abort with an error exit code if the cache folder was to be modified (either because files would be added, or because they'd be removed). + + If the \`--check-cache\` option is set, Yarn will always refetch the packages and will ensure that their checksum matches what's 1/ described in the lockfile 2/ inside the existing cache files (if present). This is recommended as part of your CI workflow if you're both following the Zero-Installs model and accepting PRs from third-parties, as they'd otherwise have the ability to alter the checked-in packages before submitting them. + + If the \`--inline-builds\` option is set, Yarn will verbosely print the output of the build steps of your dependencies (instead of writing them into individual files). This is likely useful mostly for debug purposes only when using Docker-like environments. + + If the \`--mode=\` option is set, Yarn will change which artifacts are generated. The modes currently supported are: + + - \`skip-build\` will not run the build scripts at all. Note that this is different from setting \`enableScripts\` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run. + + - \`update-lockfile\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost. + `,examples:[["Install the project","$0 install"],["Validate a project when using Zero-Installs","$0 install --immutable --immutable-cache"],["Validate a project when using Zero-Installs (slightly safer if you accept external PRs)","$0 install --immutable --immutable-cache --check-cache"]]});var Foe=gm,KWe="|||||||",HWe=">>>>>>>",jWe="=======",Noe="<<<<<<<";async function UWe(r,e){if(!r.projectCwd)return!1;let t=x.join(r.projectCwd,r.get("lockfileFilename"));if(!await U.existsPromise(t))return!1;let i=await U.readFilePromise(t,"utf8");if(!i.includes(Noe))return!1;if(e)throw new ct(X.AUTOMERGE_IMMUTABLE,"Cannot autofix a lockfile when running an immutable install");let[n,s]=GWe(i),o,a;try{o=Si(n),a=Si(s)}catch(c){throw new ct(X.AUTOMERGE_FAILED_TO_PARSE,"The individual variants of the lockfile failed to parse")}let l=N(N({},o),a);for(let[c,u]of Object.entries(l))typeof u=="string"&&delete l[c];return await U.changeFilePromise(t,Ua(l),{automaticNewlines:!0}),!0}function GWe(r){let e=[[],[]],t=r.split(/\r?\n/g),i=!1;for(;t.length>0;){let n=t.shift();if(typeof n=="undefined")throw new Error("Assertion failed: Some lines should remain");if(n.startsWith(Noe)){for(;t.length>0;){let s=t.shift();if(typeof s=="undefined")throw new Error("Assertion failed: Some lines should remain");if(s===jWe){i=!1;break}else if(i||s.startsWith(KWe)){i=!0;continue}else e[0].push(s)}for(;t.length>0;){let s=t.shift();if(typeof s=="undefined")throw new Error("Assertion failed: Some lines should remain");if(s.startsWith(HWe))break;e[1].push(s)}}else e[0].push(n),e[1].push(n)}return[e[0].join(` +`),e[1].join(` +`)]}var fm=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Link all workspaces belonging to the target project to the current one"});this.private=J.Boolean("-p,--private",!1,{description:"Also link private workspaces belonging to the target project to the current one"});this.relative=J.Boolean("-r,--relative",!1,{description:"Link workspaces using relative paths instead of absolute paths"});this.destination=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState({restoreResolutions:!1});let s=x.resolve(this.context.cwd,H.toPortablePath(this.destination)),o=await ye.find(s,this.context.plugins,{useRc:!1,strict:!1}),{project:a,workspace:l}=await ze.find(o,s);if(t.cwd===a.cwd)throw new Pe("Invalid destination; Can't link the project to itself");if(!l)throw new ht(a.cwd,s);let c=t.topLevelWorkspace,u=[];if(this.all){for(let f of a.workspaces)f.manifest.name&&(!f.manifest.private||this.private)&&u.push(f);if(u.length===0)throw new Pe("No workspace found to be linked in the target project")}else{if(!l.manifest.name)throw new Pe("The target workspace doesn't have a name and thus cannot be linked");if(l.manifest.private&&!this.private)throw new Pe("The target workspace is marked private - use the --private flag to link it anyway");u.push(l)}for(let f of u){let h=P.stringifyIdent(f.locator),p=this.relative?x.relative(t.cwd,f.cwd):f.cwd;c.manifest.resolutions.push({pattern:{descriptor:{fullName:h}},reference:`portal:${p}`})}return(await Je.start({configuration:e,stdout:this.context.stdout},async f=>{await t.install({cache:n,report:f})})).exitCode()}};fm.paths=[["link"]],fm.usage=Re.Usage({description:"connect the local project to another one",details:"\n This command will set a new `resolutions` field in the project-level manifest and point it to the workspace at the specified location (even if part of another project).\n ",examples:[["Register a remote workspace for use in the current project","$0 link ~/ts-loader"],["Register all workspaces from a remote project for use in the current project","$0 link ~/jest --all"]]});var Loe=fm;var hm=class extends Le{constructor(){super(...arguments);this.args=J.Proxy()}async execute(){return this.cli.run(["exec","node",...this.args])}};hm.paths=[["node"]],hm.usage=Re.Usage({description:"run node with the hook already setup",details:` + This command simply runs Node. It also makes sure to call it in a way that's compatible with the current project (for example, on PnP projects the environment will be setup in such a way that PnP will be correctly injected into the environment). + + The Node process will use the exact same version of Node as the one used to run Yarn itself, which might be a good way to ensure that your commands always use a consistent Node version. + `,examples:[["Run a Node script","$0 node ./my-script.js"]]});var Toe=hm;var qoe=ge(require("os"));var Moe=ge(require("os"));var YWe="https://raw.githubusercontent.com/yarnpkg/berry/master/plugins.yml";async function Qu(r){let e=await ir.get(YWe,{configuration:r});return Si(e.toString())}var pm=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins);return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async i=>{let n=await Qu(e);for(let s of Object.entries(n)){let[l,o]=s,a=o,{experimental:c}=a,u=Or(a,["experimental"]);let g=l;c&&(g+=" [experimental]"),i.reportJson(N({name:l,experimental:c},u)),i.reportInfo(null,g)}})).exitCode()}};pm.paths=[["plugin","list"]],pm.usage=Re.Usage({category:"Plugin-related commands",description:"list the available official plugins",details:"\n This command prints the plugins available directly from the Yarn repository. Only those plugins can be referenced by name in `yarn plugin import`.\n ",examples:[["List the official plugins","$0 plugin list"]]});var Ooe=pm;var qWe=/^[0-9]+$/;function Uoe(r){return qWe.test(r)?`pull/${r}/head`:r}var JWe=({repository:r,branch:e},t)=>[["git","init",H.fromPortablePath(t)],["git","remote","add","origin",r],["git","fetch","origin","--depth=1",Uoe(e)],["git","reset","--hard","FETCH_HEAD"]],WWe=({branch:r})=>[["git","fetch","origin","--depth=1",Uoe(r),"--force"],["git","reset","--hard","FETCH_HEAD"],["git","clean","-dfx"]],zWe=({plugins:r,noMinify:e},t)=>[["yarn","build:cli",...new Array().concat(...r.map(i=>["--plugin",x.resolve(t,i)])),...e?["--no-minify"]:[],"|"]],dm=class extends Le{constructor(){super(...arguments);this.installPath=J.String("--path",{description:"The path where the repository should be cloned to"});this.repository=J.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=J.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.plugins=J.Array("--plugin",[],{description:"An array of additional plugins that should be included in the bundle"});this.noMinify=J.Boolean("--no-minify",!1,{description:"Build a bundle for development (debugging) - non-minified and non-mangled"});this.force=J.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.skipPlugins=J.Boolean("--skip-plugins",!1,{description:"Skip updating the contrib plugins"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd),i=typeof this.installPath!="undefined"?x.resolve(this.context.cwd,H.toPortablePath(this.installPath)):x.resolve(H.toPortablePath((0,Moe.tmpdir)()),"yarnpkg-sources",Rn.makeHash(this.repository).slice(0,6));return(await Je.start({configuration:e,stdout:this.context.stdout},async s=>{await kN(this,{configuration:e,report:s,target:i}),s.reportSeparator(),s.reportInfo(X.UNNAMED,"Building a fresh bundle"),s.reportSeparator(),await Cm(zWe(this,i),{configuration:e,context:this.context,target:i}),s.reportSeparator();let o=x.resolve(i,"packages/yarnpkg-cli/bundles/yarn.js"),a=await U.readFilePromise(o);await SN(e,"sources",a,{report:s}),this.skipPlugins||await _We(this,{project:t,report:s,target:i})})).exitCode()}};dm.paths=[["set","version","from","sources"]],dm.usage=Re.Usage({description:"build Yarn from master",details:` + This command will clone the Yarn repository into a temporary folder, then build it. The resulting bundle will then be copied into the local project. + + By default, it also updates all contrib plugins to the same commit the bundle is built from. This behavior can be disabled by using the \`--skip-plugins\` flag. + `,examples:[["Build Yarn from master","$0 set version from sources"]]});var Koe=dm;async function Cm(r,{configuration:e,context:t,target:i}){for(let[n,...s]of r){let o=s[s.length-1]==="|";if(o&&s.pop(),o)await Nr.pipevp(n,s,{cwd:i,stdin:t.stdin,stdout:t.stdout,stderr:t.stderr,strict:!0});else{t.stdout.write(`${ae.pretty(e,` $ ${[n,...s].join(" ")}`,"grey")} +`);try{await Nr.execvp(n,s,{cwd:i,strict:!0})}catch(a){throw t.stdout.write(a.stdout||a.stack),a}}}}async function kN(r,{configuration:e,report:t,target:i}){let n=!1;if(!r.force&&U.existsSync(x.join(i,".git"))){t.reportInfo(X.UNNAMED,"Fetching the latest commits"),t.reportSeparator();try{await Cm(WWe(r),{configuration:e,context:r.context,target:i}),n=!0}catch(s){t.reportSeparator(),t.reportWarning(X.UNNAMED,"Repository update failed; we'll try to regenerate it")}}n||(t.reportInfo(X.UNNAMED,"Cloning the remote repository"),t.reportSeparator(),await U.removePromise(i),await U.mkdirPromise(i,{recursive:!0}),await Cm(JWe(r,i),{configuration:e,context:r.context,target:i}))}async function _We(r,{project:e,report:t,target:i}){let n=await Qu(e.configuration),s=new Set(Object.keys(n));for(let o of e.configuration.plugins.keys())!s.has(o)||await vN(o,r,{project:e,report:t,target:i})}var Hoe=ge(ri()),joe=ge(require("url")),Goe=ge(require("vm"));var mm=class extends Le{constructor(){super(...arguments);this.name=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins);return(await Je.start({configuration:e,stdout:this.context.stdout},async i=>{let{project:n}=await ze.find(e,this.context.cwd),s,o;if(this.name.match(/^\.{0,2}[\\/]/)||H.isAbsolute(this.name)){let a=x.resolve(this.context.cwd,H.toPortablePath(this.name));i.reportInfo(X.UNNAMED,`Reading ${ae.pretty(e,a,ae.Type.PATH)}`),s=x.relative(n.cwd,a),o=await U.readFilePromise(a)}else{let a;if(this.name.match(/^https?:/)){try{new joe.URL(this.name)}catch{throw new ct(X.INVALID_PLUGIN_REFERENCE,`Plugin specifier "${this.name}" is neither a plugin name nor a valid url`)}s=this.name,a=this.name}else{let l=P.parseLocator(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-"));if(l.reference!=="unknown"&&!Hoe.default.valid(l.reference))throw new ct(X.UNNAMED,"Official plugins only accept strict version references. Use an explicit URL if you wish to download them from another location.");let c=P.stringifyIdent(l),u=await Qu(e);if(!Object.prototype.hasOwnProperty.call(u,c))throw new ct(X.PLUGIN_NAME_NOT_FOUND,`Couldn't find a plugin named "${c}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be referenced by their name; any other plugin will have to be referenced through its public url (for example https://github.com/yarnpkg/berry/raw/master/packages/plugin-typescript/bin/%40yarnpkg/plugin-typescript.js).`);s=c,a=u[c].url,l.reference!=="unknown"?a=a.replace(/\/master\//,`/${c}/${l.reference}/`):Kr!==null&&(a=a.replace(/\/master\//,`/@yarnpkg/cli/${Kr}/`))}i.reportInfo(X.UNNAMED,`Downloading ${ae.pretty(e,a,"green")}`),o=await ir.get(a,{configuration:e})}await xN(s,o,{project:n,report:i})})).exitCode()}};mm.paths=[["plugin","import"]],mm.usage=Re.Usage({category:"Plugin-related commands",description:"download a plugin",details:` + This command downloads the specified plugin from its remote location and updates the configuration to reference it in further CLI invocations. + + Three types of plugin references are accepted: + + - If the plugin is stored within the Yarn repository, it can be referenced by name. + - Third-party plugins can be referenced directly through their public urls. + - Local plugins can be referenced by their path on the disk. + + Plugins cannot be downloaded from the npm registry, and aren't allowed to have dependencies (they need to be bundled into a single file, possibly thanks to the \`@yarnpkg/builder\` package). + `,examples:[['Download and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import @yarnpkg/plugin-exec"],['Download and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import exec"],["Download and activate a community plugin","$0 plugin import https://example.org/path/to/plugin.js"],["Activate a local plugin","$0 plugin import ./path/to/plugin.js"]]});var Yoe=mm;async function xN(r,e,{project:t,report:i}){let{configuration:n}=t,s={},o={exports:s};(0,Goe.runInNewContext)(e.toString(),{module:o,exports:s});let a=o.exports.name,l=`.yarn/plugins/${a}.cjs`,c=x.resolve(t.cwd,l);i.reportInfo(X.UNNAMED,`Saving the new plugin in ${ae.pretty(n,l,"magenta")}`),await U.mkdirPromise(x.dirname(c),{recursive:!0}),await U.writeFilePromise(c,e);let u={path:l,spec:r};await ye.updateConfiguration(t.cwd,g=>{let f=[],h=!1;for(let p of g.plugins||[]){let m=typeof p!="string"?p.path:p,y=x.resolve(t.cwd,H.toPortablePath(m)),{name:b}=Se.dynamicRequire(y);b!==a?f.push(p):(f.push(u),h=!0)}return h||f.push(u),te(N({},g),{plugins:f})})}var VWe=({pluginName:r,noMinify:e},t)=>[["yarn",`build:${r}`,...e?["--no-minify"]:[],"|"]],Em=class extends Le{constructor(){super(...arguments);this.installPath=J.String("--path",{description:"The path where the repository should be cloned to"});this.repository=J.String("--repository","https://github.com/yarnpkg/berry.git",{description:"The repository that should be cloned"});this.branch=J.String("--branch","master",{description:"The branch of the repository that should be cloned"});this.noMinify=J.Boolean("--no-minify",!1,{description:"Build a plugin for development (debugging) - non-minified and non-mangled"});this.force=J.Boolean("-f,--force",!1,{description:"Always clone the repository instead of trying to fetch the latest commits"});this.name=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=typeof this.installPath!="undefined"?x.resolve(this.context.cwd,H.toPortablePath(this.installPath)):x.resolve(H.toPortablePath((0,qoe.tmpdir)()),"yarnpkg-sources",Rn.makeHash(this.repository).slice(0,6));return(await Je.start({configuration:e,stdout:this.context.stdout},async n=>{let{project:s}=await ze.find(e,this.context.cwd),o=P.parseIdent(this.name.replace(/^((@yarnpkg\/)?plugin-)?/,"@yarnpkg/plugin-")),a=P.stringifyIdent(o),l=await Qu(e);if(!Object.prototype.hasOwnProperty.call(l,a))throw new ct(X.PLUGIN_NAME_NOT_FOUND,`Couldn't find a plugin named "${a}" on the remote registry. Note that only the plugins referenced on our website (https://github.com/yarnpkg/berry/blob/master/plugins.yml) can be built and imported from sources.`);let c=a;await kN(this,{configuration:e,report:n,target:t}),await vN(c,this,{project:s,report:n,target:t})})).exitCode()}};Em.paths=[["plugin","import","from","sources"]],Em.usage=Re.Usage({category:"Plugin-related commands",description:"build a plugin from sources",details:` + This command clones the Yarn repository into a temporary folder, builds the specified contrib plugin and updates the configuration to reference it in further CLI invocations. + + The plugins can be referenced by their short name if sourced from the official Yarn repository. + `,examples:[['Build and activate the "@yarnpkg/plugin-exec" plugin',"$0 plugin import from sources @yarnpkg/plugin-exec"],['Build and activate the "@yarnpkg/plugin-exec" plugin (shorthand)',"$0 plugin import from sources exec"]]});var Joe=Em;async function vN(r,{context:e,noMinify:t},{project:i,report:n,target:s}){let o=r.replace(/@yarnpkg\//,""),{configuration:a}=i;n.reportSeparator(),n.reportInfo(X.UNNAMED,`Building a fresh ${o}`),n.reportSeparator(),await Cm(VWe({pluginName:o,noMinify:t},s),{configuration:a,context:e,target:s}),n.reportSeparator();let l=x.resolve(s,`packages/${o}/bundles/${r}.js`),c=await U.readFilePromise(l);await xN(r,c,{project:i,report:n})}var Im=class extends Le{constructor(){super(...arguments);this.name=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd);return(await Je.start({configuration:e,stdout:this.context.stdout},async n=>{let s=this.name,o=P.parseIdent(s);if(!e.plugins.has(s))throw new Pe(`${P.prettyIdent(e,o)} isn't referenced by the current configuration`);let a=`.yarn/plugins/${s}.cjs`,l=x.resolve(t.cwd,a);U.existsSync(l)&&(n.reportInfo(X.UNNAMED,`Removing ${ae.pretty(e,a,ae.Type.PATH)}...`),await U.removePromise(l)),n.reportInfo(X.UNNAMED,"Updating the configuration..."),await ye.updateConfiguration(t.cwd,c=>{if(!Array.isArray(c.plugins))return c;let u=c.plugins.filter(g=>g.path!==a);return c.plugins.length===u.length?c:te(N({},c),{plugins:u})})})).exitCode()}};Im.paths=[["plugin","remove"]],Im.usage=Re.Usage({category:"Plugin-related commands",description:"remove a plugin",details:` + This command deletes the specified plugin from the .yarn/plugins folder and removes it from the configuration. + + **Note:** The plugins have to be referenced by their name property, which can be obtained using the \`yarn plugin runtime\` command. Shorthands are not allowed. + `,examples:[["Remove a plugin imported from the Yarn repository","$0 plugin remove @yarnpkg/plugin-typescript"],["Remove a plugin imported from a local file","$0 plugin remove my-local-plugin"]]});var Woe=Im;var ym=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins);return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async i=>{for(let n of e.plugins.keys()){let s=this.context.plugins.plugins.has(n),o=n;s&&(o+=" [builtin]"),i.reportJson({name:n,builtin:s}),i.reportInfo(null,`${o}`)}})).exitCode()}};ym.paths=[["plugin","runtime"]],ym.usage=Re.Usage({category:"Plugin-related commands",description:"list the active plugins",details:` + This command prints the currently active plugins. Will be displayed both builtin plugins and external plugins. + `,examples:[["List the currently active plugins","$0 plugin runtime"]]});var zoe=ym;var wm=class extends Le{constructor(){super(...arguments);this.idents=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);let s=new Set;for(let a of this.idents)s.add(P.parseIdent(a).identHash);if(await t.restoreInstallState({restoreResolutions:!1}),await t.resolveEverything({cache:n,report:new di}),s.size>0)for(let a of t.storedPackages.values())s.has(a.identHash)&&t.storedBuildState.delete(a.locatorHash);else t.storedBuildState.clear();return(await Je.start({configuration:e,stdout:this.context.stdout,includeLogs:!this.context.quiet},async a=>{await t.install({cache:n,report:a})})).exitCode()}};wm.paths=[["rebuild"]],wm.usage=Re.Usage({description:"rebuild the project's native packages",details:` + This command will automatically cause Yarn to forget about previous compilations of the given packages and to run them again. + + Note that while Yarn forgets the compilation, the previous artifacts aren't erased from the filesystem and may affect the next builds (in good or bad). To avoid this, you may remove the .yarn/unplugged folder, or any other relevant location where packages might have been stored (Yarn may offer a way to do that automatically in the future). + + By default all packages will be rebuilt, but you can filter the list by specifying the names of the packages you want to clear from memory. + `,examples:[["Rebuild all packages","$0 rebuild"],["Rebuild fsevents only","$0 rebuild fsevents"]]});var _oe=wm;var PN=ge(ns());ws();var Bm=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Apply the operation to all workspaces from the current project"});this.mode=J.String("--mode",{description:"Change what artifacts installs generate",validator:nn(Ci)});this.patterns=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState({restoreResolutions:!1});let s=this.all?t.workspaces:[i],o=[Hr.REGULAR,Hr.DEVELOPMENT,Hr.PEER],a=[],l=!1,c=[];for(let h of this.patterns){let p=!1,m=P.parseIdent(h);for(let y of s){let b=[...y.manifest.peerDependenciesMeta.keys()];for(let v of(0,PN.default)(b,h))y.manifest.peerDependenciesMeta.delete(v),l=!0,p=!0;for(let v of o){let k=y.manifest.getForScope(v),T=[...k.values()].map(Y=>P.stringifyIdent(Y));for(let Y of(0,PN.default)(T,P.stringifyIdent(m))){let{identHash:q}=P.parseIdent(Y),$=k.get(q);if(typeof $=="undefined")throw new Error("Assertion failed: Expected the descriptor to be registered");y.manifest[v].delete(q),c.push([y,v,$]),l=!0,p=!0}}}p||a.push(h)}let u=a.length>1?"Patterns":"Pattern",g=a.length>1?"don't":"doesn't",f=this.all?"any":"this";if(a.length>0)throw new Pe(`${u} ${ae.prettyList(e,a,Ri.CODE)} ${g} match any packages referenced by ${f} workspace`);return l?(await e.triggerMultipleHooks(p=>p.afterWorkspaceDependencyRemoval,c),(await Je.start({configuration:e,stdout:this.context.stdout},async p=>{await t.install({cache:n,report:p,mode:this.mode})})).exitCode()):0}};Bm.paths=[["remove"]],Bm.usage=Re.Usage({description:"remove dependencies from the project",details:` + This command will remove the packages matching the specified patterns from the current workspace. + + If the \`--mode=\` option is set, Yarn will change which artifacts are generated. The modes currently supported are: + + - \`skip-build\` will not run the build scripts at all. Note that this is different from setting \`enableScripts\` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run. + + - \`update-lockfile\` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost. + + This command accepts glob patterns as arguments (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them. + `,examples:[["Remove a dependency from the current project","$0 remove lodash"],["Remove a dependency from all workspaces at once","$0 remove lodash --all"],["Remove all dependencies starting with `eslint-`","$0 remove 'eslint-*'"],["Remove all dependencies with the `@babel` scope","$0 remove '@babel/*'"],["Remove all dependencies matching `react-dom` or `react-helmet`","$0 remove 'react-{dom,helmet}'"]]});var Voe=Bm;var Xoe=ge(require("util")),X0=class extends Le{async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);return(await Je.start({configuration:e,stdout:this.context.stdout},async s=>{let o=i.manifest.scripts,a=Se.sortMap(o.keys(),u=>u),l={breakLength:Infinity,colors:e.get("enableColors"),maxArrayLength:2},c=a.reduce((u,g)=>Math.max(u,g.length),0);for(let[u,g]of o.entries())s.reportInfo(null,`${u.padEnd(c," ")} ${(0,Xoe.inspect)(g,l)}`)})).exitCode()}};X0.paths=[["run"]];var Zoe=X0;var bm=class extends Le{constructor(){super(...arguments);this.inspect=J.String("--inspect",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.inspectBrk=J.String("--inspect-brk",!1,{tolerateBoolean:!0,description:"Forwarded to the underlying Node process when executing a binary"});this.topLevel=J.Boolean("-T,--top-level",!1,{description:"Check the root workspace for scripts and/or binaries instead of the current one"});this.binariesOnly=J.Boolean("-B,--binaries-only",!1,{description:"Ignore any user defined scripts and only check for binaries"});this.silent=J.Boolean("--silent",{hidden:!0});this.scriptName=J.String();this.args=J.Proxy()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i,locator:n}=await ze.find(e,this.context.cwd);await t.restoreInstallState();let s=this.topLevel?t.topLevelWorkspace.anchoredLocator:n;if(!this.binariesOnly&&await Zt.hasPackageScript(s,this.scriptName,{project:t}))return await Zt.executePackageScript(s,this.scriptName,this.args,{project:t,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr});let o=await Zt.getPackageAccessibleBinaries(s,{project:t});if(o.get(this.scriptName)){let l=[];return this.inspect&&(typeof this.inspect=="string"?l.push(`--inspect=${this.inspect}`):l.push("--inspect")),this.inspectBrk&&(typeof this.inspectBrk=="string"?l.push(`--inspect-brk=${this.inspectBrk}`):l.push("--inspect-brk")),await Zt.executePackageAccessibleBinary(s,this.scriptName,this.args,{cwd:this.context.cwd,project:t,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,nodeArgs:l,packageAccessibleBinaries:o})}if(!this.topLevel&&!this.binariesOnly&&i&&this.scriptName.includes(":")){let c=(await Promise.all(t.workspaces.map(async u=>u.manifest.scripts.has(this.scriptName)?u:null))).filter(u=>u!==null);if(c.length===1)return await Zt.executeWorkspaceScript(c[0],this.scriptName,this.args,{stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})}if(this.topLevel)throw this.scriptName==="node-gyp"?new Pe(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${P.prettyLocator(e,n)}). This typically happens because some package depends on "node-gyp" to build itself, but didn't list it in their dependencies. To fix that, please run "yarn add node-gyp" into your top-level workspace. You also can open an issue on the repository of the specified package to suggest them to use an optional peer dependency.`):new Pe(`Couldn't find a script name "${this.scriptName}" in the top-level (used by ${P.prettyLocator(e,n)}).`);{if(this.scriptName==="global")throw new Pe("The 'yarn global' commands have been removed in 2.x - consider using 'yarn dlx' or a third-party plugin instead");let l=[this.scriptName].concat(this.args);for(let[c,u]of Tf)for(let g of u)if(l.length>=g.length&&JSON.stringify(l.slice(0,g.length))===JSON.stringify(g))throw new Pe(`Couldn't find a script named "${this.scriptName}", but a matching command can be found in the ${c} plugin. You can install it with "yarn plugin import ${c}".`);throw new Pe(`Couldn't find a script named "${this.scriptName}".`)}}};bm.paths=[["run"]],bm.usage=Re.Usage({description:"run a script defined in the package.json",details:` + This command will run a tool. The exact tool that will be executed will depend on the current state of your workspace: + + - If the \`scripts\` field from your local package.json contains a matching script name, its definition will get executed. + + - Otherwise, if one of the local workspace's dependencies exposes a binary with a matching name, this binary will get executed. + + - Otherwise, if the specified name contains a colon character and if one of the workspaces in the project contains exactly one script with a matching name, then this script will get executed. + + Whatever happens, the cwd of the spawned process will be the workspace that declares the script (which makes it possible to call commands cross-workspaces using the third syntax). + `,examples:[["Run the tests from the local workspace","$0 run test"],['Same thing, but without the "run" keyword',"$0 test"],["Inspect Webpack while running","$0 run --inspect-brk webpack"]]});var $oe=bm;var Qm=class extends Le{constructor(){super(...arguments);this.save=J.Boolean("-s,--save",!1,{description:"Persist the resolution inside the top-level manifest"});this.descriptor=J.String();this.resolution=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(await t.restoreInstallState({restoreResolutions:!1}),!i)throw new ht(t.cwd,this.context.cwd);let s=P.parseDescriptor(this.descriptor,!0),o=P.makeDescriptor(s,this.resolution);return t.storedDescriptors.set(s.descriptorHash,s),t.storedDescriptors.set(o.descriptorHash,o),t.resolutionAliases.set(s.descriptorHash,o.descriptorHash),(await Je.start({configuration:e,stdout:this.context.stdout},async l=>{await t.install({cache:n,report:l})})).exitCode()}};Qm.paths=[["set","resolution"]],Qm.usage=Re.Usage({description:"enforce a package resolution",details:'\n This command updates the resolution table so that `descriptor` is resolved by `resolution`.\n\n Note that by default this command only affect the current resolution table - meaning that this "manual override" will disappear if you remove the lockfile, or if the package disappear from the table. If you wish to make the enforced resolution persist whatever happens, add the `-s,--save` flag which will also edit the `resolutions` field from your top-level manifest.\n\n Note that no attempt is made at validating that `resolution` is a valid resolution entry for `descriptor`.\n ',examples:[["Force all instances of lodash@npm:^1.2.3 to resolve to 1.5.0","$0 set resolution lodash@npm:^1.2.3 1.5.0"]]});var eae=Qm;var tae=ge(ns()),Sm=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Unlink all workspaces belonging to the target project from the current one"});this.leadingArguments=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);let s=t.topLevelWorkspace,o=new Set;if(this.leadingArguments.length===0&&this.all)for(let{pattern:l,reference:c}of s.manifest.resolutions)c.startsWith("portal:")&&o.add(l.descriptor.fullName);if(this.leadingArguments.length>0)for(let l of this.leadingArguments){let c=x.resolve(this.context.cwd,H.toPortablePath(l));if(Se.isPathLike(l)){let u=await ye.find(c,this.context.plugins,{useRc:!1,strict:!1}),{project:g,workspace:f}=await ze.find(u,c);if(!f)throw new ht(g.cwd,c);if(this.all){for(let h of g.workspaces)h.manifest.name&&o.add(P.stringifyIdent(h.locator));if(o.size===0)throw new Pe("No workspace found to be unlinked in the target project")}else{if(!f.manifest.name)throw new Pe("The target workspace doesn't have a name and thus cannot be unlinked");o.add(P.stringifyIdent(f.locator))}}else{let u=[...s.manifest.resolutions.map(({pattern:g})=>g.descriptor.fullName)];for(let g of(0,tae.default)(u,l))o.add(g)}}return s.manifest.resolutions=s.manifest.resolutions.filter(({pattern:l})=>!o.has(l.descriptor.fullName)),(await Je.start({configuration:e,stdout:this.context.stdout},async l=>{await t.install({cache:n,report:l})})).exitCode()}};Sm.paths=[["unlink"]],Sm.usage=Re.Usage({description:"disconnect the local project from another one",details:` + This command will remove any resolutions in the project-level manifest that would have been added via a yarn link with similar arguments. + `,examples:[["Unregister a remote workspace in the current project","$0 unlink ~/ts-loader"],["Unregister all workspaces from a remote project in the current project","$0 unlink ~/jest --all"],["Unregister all previously linked workspaces","$0 unlink --all"],["Unregister all workspaces matching a glob","$0 unlink '@babel/*' 'pkg-{a,b}'"]]});var rae=Sm;var iae=ge(zC()),DN=ge(ns());ws();var rh=class extends Le{constructor(){super(...arguments);this.interactive=J.Boolean("-i,--interactive",{description:"Offer various choices, depending on the detected upgrade paths"});this.exact=J.Boolean("-E,--exact",!1,{description:"Don't use any semver modifier on the resolved range"});this.tilde=J.Boolean("-T,--tilde",!1,{description:"Use the `~` semver modifier on the resolved range"});this.caret=J.Boolean("-C,--caret",!1,{description:"Use the `^` semver modifier on the resolved range"});this.recursive=J.Boolean("-R,--recursive",!1,{description:"Resolve again ALL resolutions for those packages"});this.mode=J.String("--mode",{description:"Change what artifacts installs generate",validator:nn(Ci)});this.patterns=J.Rest()}async execute(){return this.recursive?await this.executeUpRecursive():await this.executeUpClassic()}async executeUpRecursive(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState({restoreResolutions:!1});let s=[...t.storedDescriptors.values()],o=s.map(u=>P.stringifyIdent(u)),a=new Set;for(let u of this.patterns){if(P.parseDescriptor(u).range!=="unknown")throw new Pe("Ranges aren't allowed when using --recursive");for(let g of(0,DN.default)(o,u)){let f=P.parseIdent(g);a.add(f.identHash)}}let l=s.filter(u=>a.has(u.identHash));for(let u of l)t.storedDescriptors.delete(u.descriptorHash),t.storedResolutions.delete(u.descriptorHash);return(await Je.start({configuration:e,stdout:this.context.stdout},async u=>{await t.install({cache:n,report:u})})).exitCode()}async executeUpClassic(){var m;let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState({restoreResolutions:!1});let s=(m=this.interactive)!=null?m:e.get("preferInteractive"),o=_C(this,t),a=s?[Vr.KEEP,Vr.REUSE,Vr.PROJECT,Vr.LATEST]:[Vr.PROJECT,Vr.LATEST],l=[],c=[];for(let y of this.patterns){let b=!1,v=P.parseDescriptor(y);for(let k of t.workspaces)for(let T of[Hr.REGULAR,Hr.DEVELOPMENT]){let q=[...k.manifest.getForScope(T).values()].map($=>P.stringifyIdent($));for(let $ of(0,DN.default)(q,P.stringifyIdent(v))){let z=P.parseIdent($),ne=k.manifest[T].get(z.identHash);if(typeof ne=="undefined")throw new Error("Assertion failed: Expected the descriptor to be registered");let ee=P.makeDescriptor(z,v.range);l.push(Promise.resolve().then(async()=>[k,T,ne,await VC(ee,{project:t,workspace:k,cache:n,target:T,modifier:o,strategies:a})])),b=!0}}b||c.push(y)}if(c.length>1)throw new Pe(`Patterns ${ae.prettyList(e,c,Ri.CODE)} don't match any packages referenced by any workspace`);if(c.length>0)throw new Pe(`Pattern ${ae.prettyList(e,c,Ri.CODE)} doesn't match any packages referenced by any workspace`);let u=await Promise.all(l),g=await dA.start({configuration:e,stdout:this.context.stdout,suggestInstall:!1},async y=>{for(let[,,b,{suggestions:v,rejections:k}]of u){let T=v.filter(Y=>Y.descriptor!==null);if(T.length===0){let[Y]=k;if(typeof Y=="undefined")throw new Error("Assertion failed: Expected an error to have been set");let q=this.cli.error(Y);t.configuration.get("enableNetwork")?y.reportError(X.CANT_SUGGEST_RESOLUTIONS,`${P.prettyDescriptor(e,b)} can't be resolved to a satisfying range + +${q}`):y.reportError(X.CANT_SUGGEST_RESOLUTIONS,`${P.prettyDescriptor(e,b)} can't be resolved to a satisfying range (note: network resolution has been disabled) + +${q}`)}else T.length>1&&!s&&y.reportError(X.CANT_SUGGEST_RESOLUTIONS,`${P.prettyDescriptor(e,b)} has multiple possible upgrade strategies; use -i to disambiguate manually`)}});if(g.hasErrors())return g.exitCode();let f=!1,h=[];for(let[y,b,,{suggestions:v}]of u){let k,T=v.filter(z=>z.descriptor!==null),Y=T[0].descriptor,q=T.every(z=>P.areDescriptorsEqual(z.descriptor,Y));T.length===1||q?k=Y:(f=!0,{answer:k}=await(0,iae.prompt)({type:"select",name:"answer",message:`Which range to you want to use in ${P.prettyWorkspace(e,y)} \u276F ${b}?`,choices:v.map(({descriptor:z,name:ne,reason:ee})=>z?{name:ne,hint:ee,descriptor:z}:{name:ne,hint:ee,disabled:!0}),onCancel:()=>process.exit(130),result(z){return this.find(z,"descriptor")},stdin:this.context.stdin,stdout:this.context.stdout}));let $=y.manifest[b].get(k.identHash);if(typeof $=="undefined")throw new Error("Assertion failed: This descriptor should have a matching entry");if($.descriptorHash!==k.descriptorHash)y.manifest[b].set(k.identHash,k),h.push([y,b,$,k]);else{let z=e.makeResolver(),ne={project:t,resolver:z},ee=z.bindDescriptor($,y.anchoredLocator,ne);t.forgetResolution(ee)}}return await e.triggerMultipleHooks(y=>y.afterWorkspaceDependencyReplacement,h),f&&this.context.stdout.write(` +`),(await Je.start({configuration:e,stdout:this.context.stdout},async y=>{await t.install({cache:n,report:y,mode:this.mode})})).exitCode()}};rh.paths=[["up"]],rh.usage=Re.Usage({description:"upgrade dependencies across the project",details:"\n This command upgrades the packages matching the list of specified patterns to their latest available version across the whole project (regardless of whether they're part of `dependencies` or `devDependencies` - `peerDependencies` won't be affected). This is a project-wide command: all workspaces will be upgraded in the process.\n\n If `-R,--recursive` is set the command will change behavior and no other switch will be allowed. When operating under this mode `yarn up` will force all ranges matching the selected packages to be resolved again (often to the highest available versions) before being stored in the lockfile. It however won't touch your manifests anymore, so depending on your needs you might want to run both `yarn up` and `yarn up -R` to cover all bases.\n\n If `-i,--interactive` is set (or if the `preferInteractive` settings is toggled on) the command will offer various choices, depending on the detected upgrade paths. Some upgrades require this flag in order to resolve ambiguities.\n\n The, `-C,--caret`, `-E,--exact` and `-T,--tilde` options have the same meaning as in the `add` command (they change the modifier used when the range is missing or a tag, and are ignored when the range is explicitly set).\n\n If the `--mode=` option is set, Yarn will change which artifacts are generated. The modes currently supported are:\n\n - `skip-build` will not run the build scripts at all. Note that this is different from setting `enableScripts` to false because the later will disable build scripts, and thus affect the content of the artifacts generated on disk, whereas the former will just disable the build step - but not the scripts themselves, which just won't run.\n\n - `update-lockfile` will skip the link step altogether, and only fetch packages that are missing from the lockfile (or that have no associated checksums). This mode is typically used by tools like Renovate or Dependabot to keep a lockfile up-to-date without incurring the full install cost.\n\n Generally you can see `yarn up` as a counterpart to what was `yarn upgrade --latest` in Yarn 1 (ie it ignores the ranges previously listed in your manifests), but unlike `yarn upgrade` which only upgraded dependencies in the current workspace, `yarn up` will upgrade all workspaces at the same time.\n\n This command accepts glob patterns as arguments (if valid Descriptors and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n **Note:** The ranges have to be static, only the package scopes and names can contain glob patterns.\n ",examples:[["Upgrade all instances of lodash to the latest release","$0 up lodash"],["Upgrade all instances of lodash to the latest release, but ask confirmation for each","$0 up lodash -i"],["Upgrade all instances of lodash to 1.2.3","$0 up lodash@1.2.3"],["Upgrade all instances of packages with the `@babel` scope to the latest release","$0 up '@babel/*'"],["Upgrade all instances of packages containing the word `jest` to the latest release","$0 up '*jest*'"],["Upgrade all instances of packages with the `@babel` scope to 7.0.0","$0 up '@babel/*@7.0.0'"]]}),rh.schema=[tS("recursive",bc.Forbids,["interactive","exact","tilde","caret"],{ignore:[void 0,!1]})];var nae=rh;var vm=class extends Le{constructor(){super(...arguments);this.recursive=J.Boolean("-R,--recursive",!1,{description:"List, for each workspace, what are all the paths that lead to the dependency"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.peers=J.Boolean("--peers",!1,{description:"Also print the peer dependencies that match the specified name"});this.package=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState();let n=P.parseIdent(this.package).identHash,s=this.recursive?ZWe(t,n,{configuration:e,peers:this.peers}):XWe(t,n,{configuration:e,peers:this.peers});ls.emitTree(s,{configuration:e,stdout:this.context.stdout,json:this.json,separators:1})}};vm.paths=[["why"]],vm.usage=Re.Usage({description:"display the reason why a package is needed",details:` + This command prints the exact reasons why a package appears in the dependency tree. + + If \`-R,--recursive\` is set, the listing will go in depth and will list, for each workspaces, what are all the paths that lead to the dependency. Note that the display is somewhat optimized in that it will not print the package listing twice for a single package, so if you see a leaf named "Foo" when looking for "Bar", it means that "Foo" already got printed higher in the tree. + `,examples:[["Explain why lodash is used in your project","$0 why lodash"]]});var sae=vm;function XWe(r,e,{configuration:t,peers:i}){let n=Se.sortMap(r.storedPackages.values(),a=>P.stringifyLocator(a)),s={},o={children:s};for(let a of n){let l={},c=null;for(let u of a.dependencies.values()){if(!i&&a.peerDependencies.has(u.identHash))continue;let g=r.storedResolutions.get(u.descriptorHash);if(!g)throw new Error("Assertion failed: The resolution should have been registered");let f=r.storedPackages.get(g);if(!f)throw new Error("Assertion failed: The package should have been registered");if(f.identHash!==e)continue;if(c===null){let p=P.stringifyLocator(a);s[p]={value:[a,ae.Type.LOCATOR],children:l}}let h=P.stringifyLocator(f);l[h]={value:[{descriptor:u,locator:f},ae.Type.DEPENDENT]}}}return o}function ZWe(r,e,{configuration:t,peers:i}){let n=Se.sortMap(r.workspaces,f=>P.stringifyLocator(f.anchoredLocator)),s=new Set,o=new Set,a=f=>{if(s.has(f.locatorHash))return o.has(f.locatorHash);if(s.add(f.locatorHash),f.identHash===e)return o.add(f.locatorHash),!0;let h=!1;f.identHash===e&&(h=!0);for(let p of f.dependencies.values()){if(!i&&f.peerDependencies.has(p.identHash))continue;let m=r.storedResolutions.get(p.descriptorHash);if(!m)throw new Error("Assertion failed: The resolution should have been registered");let y=r.storedPackages.get(m);if(!y)throw new Error("Assertion failed: The package should have been registered");a(y)&&(h=!0)}return h&&o.add(f.locatorHash),h};for(let f of n){let h=r.storedPackages.get(f.anchoredLocator.locatorHash);if(!h)throw new Error("Assertion failed: The package should have been registered");a(h)}let l=new Set,c={},u={children:c},g=(f,h,p)=>{if(!o.has(f.locatorHash))return;let m=p!==null?ae.tuple(ae.Type.DEPENDENT,{locator:f,descriptor:p}):ae.tuple(ae.Type.LOCATOR,f),y={},b={value:m,children:y},v=P.stringifyLocator(f);if(h[v]=b,!l.has(f.locatorHash)&&(l.add(f.locatorHash),!(p!==null&&r.tryWorkspaceByLocator(f))))for(let k of f.dependencies.values()){if(!i&&f.peerDependencies.has(k.identHash))continue;let T=r.storedResolutions.get(k.descriptorHash);if(!T)throw new Error("Assertion failed: The resolution should have been registered");let Y=r.storedPackages.get(T);if(!Y)throw new Error("Assertion failed: The package should have been registered");g(Y,y,k)}};for(let f of n){let h=r.storedPackages.get(f.anchoredLocator.locatorHash);if(!h)throw new Error("Assertion failed: The package should have been registered");g(h,c,null)}return u}var GN={};ft(GN,{default:()=>E3e,gitUtils:()=>Su});var Su={};ft(Su,{TreeishProtocols:()=>Mn,clone:()=>KN,fetchBase:()=>Sae,fetchChangedFiles:()=>vae,fetchChangedWorkspaces:()=>C3e,fetchRoot:()=>Qae,isGitUrl:()=>nh,lsRemote:()=>bae,normalizeLocator:()=>ON,normalizeRepoUrl:()=>km,resolveUrl:()=>UN,splitRepoUrl:()=>xm});var LN=ge(Iae()),yae=ge($w()),ih=ge(require("querystring")),TN=ge(ri()),wae=ge(require("url"));function Bae(){return te(N({},process.env),{GIT_SSH_COMMAND:process.env.GIT_SSH_COMMAND||`${process.env.GIT_SSH||"ssh"} -o BatchMode=yes`})}var d3e=[/^ssh:/,/^git(?:\+[^:]+)?:/,/^(?:git\+)?https?:[^#]+\/[^#]+(?:\.git)(?:#.*)?$/,/^git@[^#]+\/[^#]+\.git(?:#.*)?$/,/^(?:github:|https:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z._0-9-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z._0-9-]+?)(?:\.git)?(?:#.*)?$/,/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/],Mn;(function(n){n.Commit="commit",n.Head="head",n.Tag="tag",n.Semver="semver"})(Mn||(Mn={}));function nh(r){return r?d3e.some(e=>!!r.match(e)):!1}function xm(r){r=km(r);let e=r.indexOf("#");if(e===-1)return{repo:r,treeish:{protocol:Mn.Head,request:"HEAD"},extra:{}};let t=r.slice(0,e),i=r.slice(e+1);if(i.match(/^[a-z]+=/)){let n=ih.default.parse(i);for(let[l,c]of Object.entries(n))if(typeof c!="string")throw new Error(`Assertion failed: The ${l} parameter must be a literal string`);let s=Object.values(Mn).find(l=>Object.prototype.hasOwnProperty.call(n,l)),o,a;typeof s!="undefined"?(o=s,a=n[s]):(o=Mn.Head,a="HEAD");for(let l of Object.values(Mn))delete n[l];return{repo:t,treeish:{protocol:o,request:a},extra:n}}else{let n=i.indexOf(":"),s,o;return n===-1?(s=null,o=i):(s=i.slice(0,n),o=i.slice(n+1)),{repo:t,treeish:{protocol:s,request:o},extra:{}}}}function km(r,{git:e=!1}={}){var t;if(r=r.replace(/^git\+https:/,"https:"),r=r.replace(/^(?:github:|https:\/\/github\.com\/)?(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)(?:\.git)?(#.*)?$/,"https://github.com/$1/$2.git$3"),r=r.replace(/^https:\/\/github\.com\/(?!\.{1,2}\/)([a-zA-Z0-9._-]+)\/(?!\.{1,2}(?:#|$))([a-zA-Z0-9._-]+?)\/tarball\/(.+)?$/,"https://github.com/$1/$2.git#$3"),e){r=r.replace(/^git\+([^:]+):/,"$1:");let i;try{i=wae.default.parse(r)}catch{i=null}i&&i.protocol==="ssh:"&&((t=i.path)==null?void 0:t.startsWith("/:"))&&(r=r.replace(/^ssh:\/\//,""))}return r}function ON(r){return P.makeLocator(r,km(r.reference))}async function bae(r,e){let t=km(r,{git:!0});if(!ir.getNetworkSettings(`https://${(0,LN.default)(t).resource}`,{configuration:e}).enableNetwork)throw new Error(`Request to '${t}' has been blocked because of your configuration settings`);let n=await MN("listing refs",["ls-remote",t],{cwd:e.startingCwd,env:Bae()},{configuration:e,normalizedRepoUrl:t}),s=new Map,o=/^([a-f0-9]{40})\t([^\n]+)/gm,a;for(;(a=o.exec(n.stdout))!==null;)s.set(a[2],a[1]);return s}async function UN(r,e){let{repo:t,treeish:{protocol:i,request:n},extra:s}=xm(r),o=await bae(t,e),a=(c,u)=>{switch(c){case Mn.Commit:{if(!u.match(/^[a-f0-9]{40}$/))throw new Error("Invalid commit hash");return ih.default.stringify(te(N({},s),{commit:u}))}case Mn.Head:{let g=o.get(u==="HEAD"?u:`refs/heads/${u}`);if(typeof g=="undefined")throw new Error(`Unknown head ("${u}")`);return ih.default.stringify(te(N({},s),{commit:g}))}case Mn.Tag:{let g=o.get(`refs/tags/${u}`);if(typeof g=="undefined")throw new Error(`Unknown tag ("${u}")`);return ih.default.stringify(te(N({},s),{commit:g}))}case Mn.Semver:{let g=Wt.validRange(u);if(!g)throw new Error(`Invalid range ("${u}")`);let f=new Map([...o.entries()].filter(([p])=>p.startsWith("refs/tags/")).map(([p,m])=>[TN.default.parse(p.slice(10)),m]).filter(p=>p[0]!==null)),h=TN.default.maxSatisfying([...f.keys()],g);if(h===null)throw new Error(`No matching range ("${u}")`);return ih.default.stringify(te(N({},s),{commit:f.get(h)}))}case null:{let g;if((g=l(Mn.Commit,u))!==null||(g=l(Mn.Tag,u))!==null||(g=l(Mn.Head,u))!==null)return g;throw u.match(/^[a-f0-9]+$/)?new Error(`Couldn't resolve "${u}" as either a commit, a tag, or a head - if a commit, use the 40-characters commit hash`):new Error(`Couldn't resolve "${u}" as either a commit, a tag, or a head`)}default:throw new Error(`Invalid Git resolution protocol ("${c}")`)}},l=(c,u)=>{try{return a(c,u)}catch(g){return null}};return`${t}#${a(i,n)}`}async function KN(r,e){return await e.getLimit("cloneConcurrency")(async()=>{let{repo:t,treeish:{protocol:i,request:n}}=xm(r);if(i!=="commit")throw new Error("Invalid treeish protocol when cloning");let s=km(t,{git:!0});if(ir.getNetworkSettings(`https://${(0,LN.default)(s).resource}`,{configuration:e}).enableNetwork===!1)throw new Error(`Request to '${s}' has been blocked because of your configuration settings`);let o=await U.mktempPromise(),a={cwd:o,env:Bae()};return await MN("cloning the repository",["clone","-c core.autocrlf=false",s,H.fromPortablePath(o)],a,{configuration:e,normalizedRepoUrl:s}),await MN("switching branch",["checkout",`${n}`],a,{configuration:e,normalizedRepoUrl:s}),o})}async function Qae(r){let e=null,t,i=r;do t=i,await U.existsPromise(x.join(t,".git"))&&(e=t),i=x.dirname(t);while(e===null&&i!==t);return e}async function Sae(r,{baseRefs:e}){if(e.length===0)throw new Pe("Can't run this command with zero base refs specified.");let t=[];for(let a of e){let{code:l}=await Nr.execvp("git",["merge-base",a,"HEAD"],{cwd:r});l===0&&t.push(a)}if(t.length===0)throw new Pe(`No ancestor could be found between any of HEAD and ${e.join(", ")}`);let{stdout:i}=await Nr.execvp("git",["merge-base","HEAD",...t],{cwd:r,strict:!0}),n=i.trim(),{stdout:s}=await Nr.execvp("git",["show","--quiet","--pretty=format:%s",n],{cwd:r,strict:!0}),o=s.trim();return{hash:n,title:o}}async function vae(r,{base:e,project:t}){let i=Se.buildIgnorePattern(t.configuration.get("changesetIgnorePatterns")),{stdout:n}=await Nr.execvp("git",["diff","--name-only",`${e}`],{cwd:r,strict:!0}),s=n.split(/\r\n|\r|\n/).filter(c=>c.length>0).map(c=>x.resolve(r,H.toPortablePath(c))),{stdout:o}=await Nr.execvp("git",["ls-files","--others","--exclude-standard"],{cwd:r,strict:!0}),a=o.split(/\r\n|\r|\n/).filter(c=>c.length>0).map(c=>x.resolve(r,H.toPortablePath(c))),l=[...new Set([...s,...a].sort())];return i?l.filter(c=>!x.relative(t.cwd,c).match(i)):l}async function C3e({ref:r,project:e}){if(e.configuration.projectCwd===null)throw new Pe("This command can only be run from within a Yarn project");let t=[x.resolve(e.cwd,e.configuration.get("cacheFolder")),x.resolve(e.cwd,e.configuration.get("installStatePath")),x.resolve(e.cwd,e.configuration.get("lockfileFilename")),x.resolve(e.cwd,e.configuration.get("virtualFolder"))];await e.configuration.triggerHook(o=>o.populateYarnPaths,e,o=>{o!=null&&t.push(o)});let i=await Qae(e.configuration.projectCwd);if(i==null)throw new Pe("This command can only be run on Git repositories");let n=await Sae(i,{baseRefs:typeof r=="string"?[r]:e.configuration.get("changesetBaseRefs")}),s=await vae(i,{base:n.hash,project:e});return new Set(Se.mapAndFilter(s,o=>{let a=e.tryWorkspaceByFilePath(o);return a===null?Se.mapAndFilter.skip:t.some(l=>o.startsWith(l))?Se.mapAndFilter.skip:a}))}async function MN(r,e,t,{configuration:i,normalizedRepoUrl:n}){try{return await Nr.execvp("git",e,te(N({},t),{strict:!0}))}catch(s){if(!(s instanceof Nr.ExecError))throw s;let o=s.reportExtra,a=s.stderr.toString();throw new ct(X.EXCEPTION,`Failed ${r}`,l=>{l.reportError(X.EXCEPTION,` ${ae.prettyField(i,{label:"Repository URL",value:ae.tuple(ae.Type.URL,n)})}`);for(let c of a.matchAll(/^(.+?): (.*)$/gm)){let[,u,g]=c;u=u.toLowerCase();let f=u==="error"?"Error":`${(0,yae.default)(u)} Error`;l.reportError(X.EXCEPTION,` ${ae.prettyField(i,{label:f,value:ae.tuple(ae.Type.NO_HINT,g)})}`)}o==null||o(l)})}}var HN=class{supports(e,t){return nh(e.reference)}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,n=ON(e),s=new Map(t.checksums);s.set(n.locatorHash,i);let o=te(N({},t),{checksums:s}),a=await this.downloadHosted(n,o);if(a!==null)return a;let[l,c,u]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the remote repository`),loader:()=>this.cloneFromRemote(n,o),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:l,releaseFs:c,prefixPath:P.getIdentVendorPath(e),checksum:u}}async downloadHosted(e,t){return t.project.configuration.reduceHook(i=>i.fetchHostedRepository,null,e,t)}async cloneFromRemote(e,t){let i=await KN(e.reference,t.project.configuration),n=xm(e.reference),s=x.join(i,"package.tgz");await Zt.prepareExternalProject(i,s,{configuration:t.project.configuration,report:t.report,workspace:n.extra.workspace,locator:e});let o=await U.readFilePromise(s);return await Se.releaseAfterUseAsync(async()=>await Bi.convertToZip(o,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1}))}};var jN=class{supportsDescriptor(e,t){return nh(e.range)}supportsLocator(e,t){return nh(e.reference)}shouldPersistResolution(e,t){return!0}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=await UN(e.range,i.project.configuration);return[P.makeLocator(e,n)]}async getSatisfying(e,t,i){return null}async resolve(e,t){if(!t.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let i=await t.fetchOptions.fetcher.fetch(e,t.fetchOptions),n=await Se.releaseAfterUseAsync(async()=>await At.find(i.prefixPath,{baseFs:i.packageFs}),i.releaseFs);return te(N({},e),{version:n.version||"0.0.0",languageName:n.languageName||t.project.configuration.get("defaultLanguageName"),linkType:Qt.HARD,conditions:n.getConditions(),dependencies:n.dependencies,peerDependencies:n.peerDependencies,dependenciesMeta:n.dependenciesMeta,peerDependenciesMeta:n.peerDependenciesMeta,bin:n.bin})}};var m3e={configuration:{changesetBaseRefs:{description:"The base git refs that the current HEAD is compared against when detecting changes. Supports git branches, tags, and commits.",type:Ie.STRING,isArray:!0,isNullable:!1,default:["master","origin/master","upstream/master","main","origin/main","upstream/main"]},changesetIgnorePatterns:{description:"Array of glob patterns; files matching them will be ignored when fetching the changed files",type:Ie.STRING,default:[],isArray:!0},cloneConcurrency:{description:"Maximal number of concurrent clones",type:Ie.NUMBER,default:2}},fetchers:[HN],resolvers:[jN]};var E3e=m3e;var Pm=class extends Le{constructor(){super(...arguments);this.since=J.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.recursive=J.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.verbose=J.Boolean("-v,--verbose",!1,{description:"Also return the cross-dependencies between workspaces"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd);return(await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async n=>{let s=this.since?await Su.fetchChangedWorkspaces({ref:this.since,project:t}):t.workspaces,o=new Set(s);if(this.recursive)for(let a of[...s].map(l=>l.getRecursiveWorkspaceDependents()))for(let l of a)o.add(l);for(let a of o){let{manifest:l}=a,c;if(this.verbose){let u=new Set,g=new Set;for(let f of At.hardDependencies)for(let[h,p]of l.getForScope(f)){let m=t.tryWorkspaceByDescriptor(p);m===null?t.workspacesByIdent.has(h)&&g.add(p):u.add(m)}c={workspaceDependencies:Array.from(u).map(f=>f.relativeCwd),mismatchedWorkspaceDependencies:Array.from(g).map(f=>P.stringifyDescriptor(f))}}n.reportInfo(null,`${a.relativeCwd}`),n.reportJson(N({location:a.relativeCwd,name:l.name?P.stringifyIdent(l.name):null},c))}})).exitCode()}};Pm.paths=[["workspaces","list"]],Pm.usage=Re.Usage({category:"Workspace-related commands",description:"list all available workspaces",details:"\n This command will print the list of all workspaces in the project.\n\n - If `--since` is set, Yarn will only list workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If both the `-v,--verbose` and `--json` options are set, Yarn will also return the cross-dependencies between each workspaces (useful when you wish to automatically generate Buck / Bazel rules).\n "});var kae=Pm;var Dm=class extends Le{constructor(){super(...arguments);this.workspaceName=J.String();this.commandName=J.String();this.args=J.Proxy()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);let n=t.workspaces,s=new Map(n.map(a=>{let l=P.convertToIdent(a.locator);return[P.stringifyIdent(l),a]})),o=s.get(this.workspaceName);if(o===void 0){let a=Array.from(s.keys()).sort();throw new Pe(`Workspace '${this.workspaceName}' not found. Did you mean any of the following: + - ${a.join(` + - `)}?`)}return this.cli.run([this.commandName,...this.args],{cwd:o.cwd})}};Dm.paths=[["workspace"]],Dm.usage=Re.Usage({category:"Workspace-related commands",description:"run a command within the specified workspace",details:` + This command will run a given sub-command on a single workspace. + `,examples:[["Add a package to a single workspace","yarn workspace components add -D react"],["Run build script on a single workspace","yarn workspace components run build"]]});var xae=Dm;var I3e={configuration:{enableImmutableInstalls:{description:"If true (the default on CI), prevents the install command from modifying the lockfile",type:Ie.BOOLEAN,default:Pae.isCI},defaultSemverRangePrefix:{description:"The default save prefix: '^', '~' or ''",type:Ie.STRING,values:["^","~",""],default:da.CARET}},commands:[Kne,jne,ioe,poe,eae,Koe,koe,kae,yoe,woe,Boe,boe,Mne,Une,doe,moe,Qoe,Soe,Poe,Roe,Foe,Loe,rae,Toe,Joe,Yoe,Woe,Ooe,zoe,_oe,Voe,Zoe,$oe,nae,sae,xae]},y3e=I3e;var _N={};ft(_N,{default:()=>B3e});var He={optional:!0},qN=[["@tailwindcss/aspect-ratio@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@tailwindcss/line-clamp@<0.2.1",{peerDependencies:{tailwindcss:"^2.0.2"}}],["@fullhuman/postcss-purgecss@3.1.3 || 3.1.3-alpha.0",{peerDependencies:{postcss:"^8.0.0"}}],["@samverschueren/stream-to-observable@<0.3.1",{peerDependenciesMeta:{rxjs:He,zenObservable:He}}],["any-observable@<0.5.1",{peerDependenciesMeta:{rxjs:He,zenObservable:He}}],["@pm2/agent@<1.0.4",{dependencies:{debug:"*"}}],["debug@<4.2.0",{peerDependenciesMeta:{["supports-color"]:He}}],["got@<11",{dependencies:{["@types/responselike"]:"^1.0.0",["@types/keyv"]:"^3.1.1"}}],["cacheable-lookup@<4.1.2",{dependencies:{["@types/keyv"]:"^3.1.1"}}],["http-link-dataloader@*",{peerDependencies:{graphql:"^0.13.1 || ^14.0.0"}}],["typescript-language-server@*",{dependencies:{["vscode-jsonrpc"]:"^5.0.1",["vscode-languageserver-protocol"]:"^3.15.0"}}],["postcss-syntax@*",{peerDependenciesMeta:{["postcss-html"]:He,["postcss-jsx"]:He,["postcss-less"]:He,["postcss-markdown"]:He,["postcss-scss"]:He}}],["jss-plugin-rule-value-function@<=10.1.1",{dependencies:{["tiny-warning"]:"^1.0.2"}}],["ink-select-input@<4.1.0",{peerDependencies:{react:"^16.8.2"}}],["license-webpack-plugin@<2.3.18",{peerDependenciesMeta:{webpack:He}}],["snowpack@>=3.3.0",{dependencies:{["node-gyp"]:"^7.1.0"}}],["promise-inflight@*",{peerDependenciesMeta:{bluebird:He}}],["reactcss@*",{peerDependencies:{react:"*"}}],["react-color@<=2.19.0",{peerDependencies:{react:"*"}}],["gatsby-plugin-i18n@*",{dependencies:{ramda:"^0.24.1"}}],["useragent@^2.0.0",{dependencies:{request:"^2.88.0",yamlparser:"0.0.x",semver:"5.5.x"}}],["@apollographql/apollo-tools@<=0.5.2",{peerDependencies:{graphql:"^14.2.1 || ^15.0.0"}}],["material-table@^2.0.0",{dependencies:{"@babel/runtime":"^7.11.2"}}],["@babel/parser@*",{dependencies:{"@babel/types":"^7.8.3"}}],["fork-ts-checker-webpack-plugin@<=6.3.4",{peerDependencies:{eslint:">= 6",typescript:">= 2.7",webpack:">= 4","vue-template-compiler":"*"},peerDependenciesMeta:{eslint:He,"vue-template-compiler":He}}],["rc-animate@<=3.1.1",{peerDependencies:{react:">=16.9.0","react-dom":">=16.9.0"}}],["react-bootstrap-table2-paginator@*",{dependencies:{classnames:"^2.2.6"}}],["react-draggable@<=4.4.3",{peerDependencies:{react:">= 16.3.0","react-dom":">= 16.3.0"}}],["apollo-upload-client@<14",{peerDependencies:{graphql:"14 - 15"}}],["react-instantsearch-core@<=6.7.0",{peerDependencies:{algoliasearch:">= 3.1 < 5"}}],["react-instantsearch-dom@<=6.7.0",{dependencies:{"react-fast-compare":"^3.0.0"}}],["ws@<7.2.1",{peerDependencies:{bufferutil:"^4.0.1","utf-8-validate":"^5.0.2"},peerDependenciesMeta:{bufferutil:He,"utf-8-validate":He}}],["react-portal@*",{peerDependencies:{"react-dom":"^15.0.0-0 || ^16.0.0-0 || ^17.0.0-0"}}],["react-scripts@<=4.0.1",{peerDependencies:{react:"*"}}],["testcafe@<=1.10.1",{dependencies:{"@babel/plugin-transform-for-of":"^7.12.1","@babel/runtime":"^7.12.5"}}],["testcafe-legacy-api@<=4.2.0",{dependencies:{"testcafe-hammerhead":"^17.0.1","read-file-relative":"^1.2.0"}}],["@google-cloud/firestore@<=4.9.3",{dependencies:{protobufjs:"^6.8.6"}}],["gatsby-source-apiserver@*",{dependencies:{["babel-polyfill"]:"^6.26.0"}}],["@webpack-cli/package-utils@<=1.0.1-alpha.4",{dependencies:{["cross-spawn"]:"^7.0.3"}}],["gatsby-remark-prismjs@<3.3.28",{dependencies:{lodash:"^4"}}],["gatsby-plugin-favicon@*",{peerDependencies:{webpack:"*"}}],["gatsby-plugin-sharp@<=4.6.0-next.3",{dependencies:{debug:"^4.3.1"}}],["gatsby-react-router-scroll@<=5.6.0-next.0",{dependencies:{["prop-types"]:"^15.7.2"}}],["@rebass/forms@*",{dependencies:{["@styled-system/should-forward-prop"]:"^5.0.0"},peerDependencies:{react:"^16.8.6"}}],["rebass@*",{peerDependencies:{react:"^16.8.6"}}],["@ant-design/react-slick@<=0.28.3",{peerDependencies:{react:">=16.0.0"}}],["mqtt@<4.2.7",{dependencies:{duplexify:"^4.1.1"}}],["vue-cli-plugin-vuetify@<=2.0.3",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":He,"vuetify-loader":He}}],["vue-cli-plugin-vuetify@<=2.0.4",{dependencies:{"null-loader":"^3.0.0"}}],["vue-cli-plugin-vuetify@>=2.4.3",{peerDependencies:{vue:"*"}}],["@vuetify/cli-plugin-utils@<=0.0.4",{dependencies:{semver:"^6.3.0"},peerDependenciesMeta:{"sass-loader":He}}],["@vue/cli-plugin-typescript@<=5.0.0-alpha.0",{dependencies:{"babel-loader":"^8.1.0"}}],["@vue/cli-plugin-typescript@<=5.0.0-beta.0",{dependencies:{"@babel/core":"^7.12.16"},peerDependencies:{"vue-template-compiler":"^2.0.0"},peerDependenciesMeta:{"vue-template-compiler":He}}],["cordova-ios@<=6.3.0",{dependencies:{underscore:"^1.9.2"}}],["cordova-lib@<=10.0.1",{dependencies:{underscore:"^1.9.2"}}],["git-node-fs@*",{peerDependencies:{"js-git":"^0.7.8"},peerDependenciesMeta:{"js-git":He}}],["consolidate@<0.16.0",{peerDependencies:{mustache:"^3.0.0"},peerDependenciesMeta:{mustache:He}}],["consolidate@*",{peerDependencies:{velocityjs:"^2.0.1",tinyliquid:"^0.2.34","liquid-node":"^3.0.1",jade:"^1.11.0","then-jade":"*",dust:"^0.3.0","dustjs-helpers":"^1.7.4","dustjs-linkedin":"^2.7.5",swig:"^1.4.2","swig-templates":"^2.0.3","razor-tmpl":"^1.3.1",atpl:">=0.7.6",liquor:"^0.0.5",twig:"^1.15.2",ejs:"^3.1.5",eco:"^1.1.0-rc-3",jazz:"^0.0.18",jqtpl:"~1.1.0",hamljs:"^0.6.2",hamlet:"^0.3.3",whiskers:"^0.4.0","haml-coffee":"^1.14.1","hogan.js":"^3.0.2",templayed:">=0.2.3",handlebars:"^4.7.6",underscore:"^1.11.0",lodash:"^4.17.20",pug:"^3.0.0","then-pug":"*",qejs:"^3.0.5",walrus:"^0.10.1",mustache:"^4.0.1",just:"^0.1.8",ect:"^0.5.9",mote:"^0.2.0",toffee:"^0.3.6",dot:"^1.1.3","bracket-template":"^1.1.5",ractive:"^1.3.12",nunjucks:"^3.2.2",htmling:"^0.0.8","babel-core":"^6.26.3",plates:"~0.4.11","react-dom":"^16.13.1",react:"^16.13.1","arc-templates":"^0.5.3",vash:"^0.13.0",slm:"^2.0.0",marko:"^3.14.4",teacup:"^2.0.0","coffee-script":"^1.12.7",squirrelly:"^5.1.0",twing:"^5.0.2"},peerDependenciesMeta:{velocityjs:He,tinyliquid:He,"liquid-node":He,jade:He,"then-jade":He,dust:He,"dustjs-helpers":He,"dustjs-linkedin":He,swig:He,"swig-templates":He,"razor-tmpl":He,atpl:He,liquor:He,twig:He,ejs:He,eco:He,jazz:He,jqtpl:He,hamljs:He,hamlet:He,whiskers:He,"haml-coffee":He,"hogan.js":He,templayed:He,handlebars:He,underscore:He,lodash:He,pug:He,"then-pug":He,qejs:He,walrus:He,mustache:He,just:He,ect:He,mote:He,toffee:He,dot:He,"bracket-template":He,ractive:He,nunjucks:He,htmling:He,"babel-core":He,plates:He,"react-dom":He,react:He,"arc-templates":He,vash:He,slm:He,marko:He,teacup:He,"coffee-script":He,squirrelly:He,twing:He}}],["vue-loader@<=16.3.3",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",webpack:"^4.1.0 || ^5.0.0-0"},peerDependenciesMeta:{"@vue/compiler-sfc":He}}],["vue-loader@^16.7.0",{peerDependencies:{"@vue/compiler-sfc":"^3.0.8",vue:"^3.2.13"},peerDependenciesMeta:{"@vue/compiler-sfc":He,vue:He}}],["scss-parser@*",{dependencies:{lodash:"^4.17.21"}}],["query-ast@*",{dependencies:{lodash:"^4.17.21"}}],["redux-thunk@<=2.3.0",{peerDependencies:{redux:"^4.0.0"}}],["skypack@<=0.3.2",{dependencies:{tar:"^6.1.0"}}],["@npmcli/metavuln-calculator@<2.0.0",{dependencies:{"json-parse-even-better-errors":"^2.3.1"}}],["bin-links@<2.3.0",{dependencies:{"mkdirp-infer-owner":"^1.0.2"}}],["rollup-plugin-polyfill-node@<=0.8.0",{peerDependencies:{rollup:"^1.20.0 || ^2.0.0"}}],["snowpack@<3.8.6",{dependencies:{"magic-string":"^0.25.7"}}],["elm-webpack-loader@*",{dependencies:{temp:"^0.9.4"}}],["winston-transport@<=4.4.0",{dependencies:{logform:"^2.2.0"}}],["jest-vue-preprocessor@*",{dependencies:{"@babel/core":"7.8.7","@babel/template":"7.8.6"},peerDependencies:{pug:"^2.0.4"},peerDependenciesMeta:{pug:He}}],["redux-persist@*",{peerDependencies:{react:">=16"},peerDependenciesMeta:{react:He}}],["sodium@>=3",{dependencies:{"node-gyp":"^3.8.0"}}],["babel-plugin-graphql-tag@<=3.1.0",{peerDependencies:{graphql:"^14.0.0 || ^15.0.0"}}],["@playwright/test@<=1.14.1",{dependencies:{"jest-matcher-utils":"^26.4.2"}}],...["babel-plugin-remove-graphql-queries@<3.14.0-next.1","babel-preset-gatsby-package@<1.14.0-next.1","create-gatsby@<1.14.0-next.1","gatsby-admin@<0.24.0-next.1","gatsby-cli@<3.14.0-next.1","gatsby-core-utils@<2.14.0-next.1","gatsby-design-tokens@<3.14.0-next.1","gatsby-legacy-polyfills@<1.14.0-next.1","gatsby-plugin-benchmark-reporting@<1.14.0-next.1","gatsby-plugin-graphql-config@<0.23.0-next.1","gatsby-plugin-image@<1.14.0-next.1","gatsby-plugin-mdx@<2.14.0-next.1","gatsby-plugin-netlify-cms@<5.14.0-next.1","gatsby-plugin-no-sourcemaps@<3.14.0-next.1","gatsby-plugin-page-creator@<3.14.0-next.1","gatsby-plugin-preact@<5.14.0-next.1","gatsby-plugin-preload-fonts@<2.14.0-next.1","gatsby-plugin-schema-snapshot@<2.14.0-next.1","gatsby-plugin-styletron@<6.14.0-next.1","gatsby-plugin-subfont@<3.14.0-next.1","gatsby-plugin-utils@<1.14.0-next.1","gatsby-recipes@<0.25.0-next.1","gatsby-source-shopify@<5.6.0-next.1","gatsby-source-wikipedia@<3.14.0-next.1","gatsby-transformer-screenshot@<3.14.0-next.1","gatsby-worker@<0.5.0-next.1"].map(r=>[r,{dependencies:{"@babel/runtime":"^7.14.8"}}]),["gatsby-core-utils@<2.14.0-next.1",{dependencies:{got:"8.3.2"}}],["gatsby-plugin-gatsby-cloud@<=3.1.0-next.0",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["gatsby-plugin-gatsby-cloud@<=3.2.0-next.1",{peerDependencies:{webpack:"*"}}],["babel-plugin-remove-graphql-queries@<=3.14.0-next.1",{dependencies:{"gatsby-core-utils":"^2.8.0-next.1"}}],["gatsby-plugin-netlify@3.13.0-next.1",{dependencies:{"gatsby-core-utils":"^2.13.0-next.0"}}],["clipanion-v3-codemod@<=0.2.0",{peerDependencies:{jscodeshift:"^0.11.0"}}],["react-live@*",{peerDependencies:{"react-dom":"*",react:"*"}}],["webpack@<4.44.1",{peerDependenciesMeta:{"webpack-cli":He,"webpack-command":He}}],["webpack@<5.0.0-beta.23",{peerDependenciesMeta:{"webpack-cli":He}}],["webpack-dev-server@<3.10.2",{peerDependenciesMeta:{"webpack-cli":He}}],["@docusaurus/responsive-loader@<1.5.0",{peerDependenciesMeta:{sharp:He,jimp:He}}],["eslint-module-utils@*",{peerDependenciesMeta:{"eslint-import-resolver-node":He,"eslint-import-resolver-typescript":He,"eslint-import-resolver-webpack":He,"@typescript-eslint/parser":He}}],["eslint-plugin-import@*",{peerDependenciesMeta:{"@typescript-eslint/parser":He}}],["critters-webpack-plugin@<3.0.2",{peerDependenciesMeta:{"html-webpack-plugin":He}}],["terser@<=5.10.0",{dependencies:{acorn:"^8.5.0"}}],["babel-preset-react-app@10.0.x",{dependencies:{"@babel/plugin-proposal-private-property-in-object":"^7.16.0"}}],["eslint-config-react-app@*",{peerDependenciesMeta:{typescript:He}}],["@vue/eslint-config-typescript@<11.0.0",{peerDependenciesMeta:{typescript:He}}],["unplugin-vue2-script-setup@<0.9.1",{peerDependencies:{"@vue/composition-api":"^1.4.3","@vue/runtime-dom":"^3.2.26"}}],["@cypress/snapshot@*",{dependencies:{debug:"^3.2.7"}}],["auto-relay@*",{peerDependencies:{"reflect-metadata":"^0.1.13"}}],["vue-template-babel-compiler@<1.2.0",{peerDependencies:{["vue-template-compiler"]:"^2.6.0"}}],["@parcel/transformer-image@<2.5.0",{peerDependencies:{["@parcel/core"]:"*"}}],["@parcel/transformer-js@<2.5.0",{peerDependencies:{["@parcel/core"]:"*"}}],["parcel@*",{peerDependenciesMeta:{["@parcel/core"]:He}}],["react-scripts@*",{peerDependencies:{eslint:"*"}}],["focus-trap-react@^8.0.0",{dependencies:{tabbable:"^5.3.2"}}],["react-rnd@<10.3.7",{peerDependencies:{react:">=16.3.0","react-dom":">=16.3.0"}}],["connect-mongo@*",{peerDependencies:{"express-session":"^1.17.1"}}],["vue-i18n@<9",{peerDependencies:{vue:"^2"}}],["vue-router@<4",{peerDependencies:{vue:"^2"}}],["unified@<10",{dependencies:{"@types/unist":"^2.0.0"}}],["react-github-btn@<=1.3.0",{peerDependencies:{react:">=16.3.0"}}],["react-dev-utils@*",{peerDependencies:{typescript:">=2.7",webpack:">=4"},peerDependenciesMeta:{typescript:He}}],["@asyncapi/react-component@<=1.0.0-next.39",{peerDependencies:{react:">=16.8.0","react-dom":">=16.8.0"}}],["xo@*",{peerDependencies:{webpack:">=1.11.0"},peerDependenciesMeta:{webpack:He}}],["babel-plugin-remove-graphql-queries@<=4.20.0-next.0",{dependencies:{"@babel/types":"^7.15.4"}}],["gatsby-plugin-page-creator@<=4.20.0-next.1",{dependencies:{"fs-extra":"^10.1.0"}}],["gatsby-plugin-utils@<=3.14.0-next.1",{dependencies:{fastq:"^1.13.0"},peerDependencies:{graphql:"^15.0.0"}}],["gatsby-plugin-mdx@<3.1.0-next.1",{dependencies:{mkdirp:"^1.0.4"}}],["gatsby-plugin-mdx@^2",{peerDependencies:{gatsby:"^3.0.0-next"}}]];var JN;function Dae(){return typeof JN=="undefined"&&(JN=require("zlib").brotliDecompressSync(Buffer.from("G7weAByFTVk3Vs7UfHhq4yykgEM7pbW7TI43SG2S5tvGrwHBAzdz+s/npQ6tgEvobvxisrPIadkXeUAJotBn5bDZ5kAhcRqsIHe3F75Walet5hNalwgFDtxb0BiDUjiUQkjG0yW2hto9HPgiCkm316d6bC0kST72YN7D7rfkhCE9x4J0XwB0yavalxpUu2t9xszHrmtwalOxT7VslsxWcB1qpqZwERUra4psWhTV8BgwWeizurec82Caf1ABL11YMfbf8FJ9JBceZOkgmvrQPbC9DUldX/yMbmX06UQluCEjSwUoyO+EZPIjofr+/oAZUck2enraRD+oWLlnlYnj8xB+gwSo9lmmks4fXv574qSqcWA6z21uYkzMu3EWj+K23RxeQlLqiE35/rC8GcS4CGkKHKKq+zAIQwD9iRDNfiAqueLLpicFFrNsAI4zeTD/eO9MHcnRa5m8UT+M2+V+AkFST4BlKneiAQRSdST8KEAIyFlULt6wa9EBd0Ds28VmpaxquJdVt+nwdEs5xUskI13OVtFyY0UrQIRAlCuvvWivvlSKQfTO+2Q8OyUR1W5RvetaPz4jD27hdtwHFFA1Ptx6Ee/t2cY2rg2G46M1pNDRf2pWhvpy8pqMnuI3++4OF3+7OFIWXGjh+o7Nr2jNvbiYcQdQS1h903/jVFgOpA0yJ78z+x759bFA0rq+6aY5qPB4FzS3oYoLupDUhD9nDz6F6H7hpnlMf18KNKDu4IKjTWwrAnY6MFQw1W6ymOALHlFyCZmQhldg1MQHaMVVQTVgDC60TfaBqG++Y8PEoFhN/PBTZT175KNP/BlHDYGOOBmnBdzqJKplZ/ljiVG0ZBzfqeBRrrUkn6rA54462SgiliKoYVnbeptMdXNfAuaupIEi0bApF10TlgHfmEJAPUVidRVFyDupSem5po5vErPqWKhKbUIp0LozpYsIKK57dM/HKr+nguF+7924IIWMICkQ8JUigs9D+W+c4LnNoRtPPKNRUiCYmP+Jfo2lfKCKw8qpraEeWU3uiNRO6zcyKQoXPR5htmzzLznke7b4YbXW3I1lIRzmgG02Udb58U+7TpwyN7XymCgH+wuPDthZVQvRZuEP+SnLtMicz9m5zASWOBiAcLmkuFlTKuHspSIhCBD0yUPKcxu81A+4YD78rA2vtwsUEday9WNyrShyrl60rWmA+SmbYZkQOwFJWArxRYYc5jGhA5ikxYw1rx3ei4NmeX/lKiwpZ9Ln1tV2Ae7sArvxuVLbJjqJRjW1vFXAyHpvLG+8MJ6T2Ubx5M2KDa2SN6vuIGxJ9WQM9Mk3Q7aCNiZONXllhqq24DmoLbQfW2rYWsOgHWjtOmIQMyMKdiHZDjoyIq5+U700nZ6odJAoYXPQBvFNiQ78d5jaXliBqLTJEqUCwi+LiH2mx92EmNKDsJL74Z613+3lf20pxkV1+erOrjj8pW00vsPaahKUM+05ssd5uwM7K482KWEf3TCwlg/o3e5ngto7qSMz7YteIgCsF1UOcsLk7F7MxWbvrPMY473ew0G+noVL8EPbkmEMftMSeL6HFub/zy+2JQ==","base64")).toString()),JN}var WN;function Rae(){return typeof WN=="undefined"&&(WN=require("zlib").brotliDecompressSync(Buffer.from("G8MSIIzURnVBnObTcvb3XE6v2S9Qgc2K801Oa5otNKEtK8BINZNcaQHy+9/vf/WXBimwutXC33P2DPc64pps5rz7NGGWaOKNSPL4Y2KRE8twut2lFOIN+OXPtRmPMRhMTILib2bEQx43az2I5d3YS8Roa5UZpF/ujHb3Djd3GDvYUfvFYSUQ39vb2cmifp/rgB4J/65JK3wRBTvMBoNBmn3mbXC63/gbBkW/2IRPri0O8bcsRBsmarF328pAln04nyJFkwUAvNu934supAqLtyerZZpJ8I8suJHhf/ocMV+scKwa8NOiDKIPXw6Ex/EEZD6TEGaW8N5zvNHYF10l6Lfooj7D5W2k3dgvQSbp2Wv8TGOayS978gxlOLVjTGXs66ozewbrjwElLtyrYNnWTfzzdEutgROUFPVMhnMoy8EjJLLlWwIEoySxliim9kYW30JUHiPVyjt0iAw/ZpPmCbUCltYPnq6ZNblIKhTNhqS/oqC9iya5sGKZTOVsTEg34n92uZTf2iPpcZih8rPW8CzA+adIGmyCPcKdLMsBLShd+zuEbTrqpwuh+DLmracZcjPC5Sdf5odDAhKpFuOsQS67RT+1VgWWygSv3YwxDnylc04/PYuaMeIzhBkLrvs7e/OUzRTF56MmfY6rI63QtEjEQzq637zQqJ39nNhu3NmoRRhW/086bHGBUtx0PE0j3aEGvkdh9WJC8y8j8mqqke9/dQ5la+Q3ba4RlhvTbnfQhPDDab3tUifkjKuOsp13mXEmO00Mu88F/M67R7LXfoFDFLNtgCSWjWX+3Jn1371pJTK9xPBiMJafvDjtFyAzu8rxeQ0TKMQXNPs5xxiBOd+BRJP8KP88XPtJIbZKh/cdW8KvBUkpqKpGoiIaA32c3/JnQr4efXt85mXvidOvn/eU3Pase1typLYBalJ14mCso9h79nuMOuCa/kZAOkJHmTjP5RM2WNoPasZUAnT1TAE/NH25hUxcQv6hQWR/m1PKk4ooXMcM4SR1iYU3fUohvqk4RY2hbmTVVIXv6TvqO+0doOjgeVFAcom+RlwJQmOVH7pr1Q9LoJT6n1DeQEB+NHygsATbIwTcOKZlJsY8G4+suX1uQLjUWwLjjs0mvSvZcLTpIGAekeR7GCgl8eo3ndAqEe2XCav4huliHjdbIPBsGJuPX7lrO9HX1UbXRH5opOe1x6JsOSgHZR+EaxuXVhpLLxm6jk1LJtZfHSc6BKPun3CpYYVMJGwEUyk8MTGG0XL5MfEwaXpnc9TKnBmlGn6nHiGREc3ysn47XIBDzA+YvFdjZzVIEDcKGpS6PbUJehFRjEne8D0lVU1XuRtlgszq6pTNlQ/3MzNOEgCWPyTct22V2mEi2krizn5VDo9B19/X2DB3hCGRMM7ONbtnAcIx/OWB1u5uPbW1gsH8irXxT/IzG0PoXWYjhbMsH3KTuoOl5o17PulcgvsfTSnKFM354GWI8luqZnrswWjiXy3G+Vbyo1KMopFmmvBwNELgaS8z8dNZchx/Cl/xjddxhMcyqtzFyONb2Zdu90NkI8pAeufe7YlXrp53v8Dj/l8vWeVspRKBGXScBBPI/HinSTGmLDOGGOCIyH0JFdOZx0gWsacNlQLJMIrBhqRxXxHF/5pseWwejlAAvZ3klZSDSYY8mkToaWejXhgNomeGtx1DTLEUFMRkgF5yFB22WYdJnaWN14r1YJj81hGi45+jrADS5nYRhCiSlCJJ1nL8pYX+HDSMhdTEWyRcgHVp/IsUIZYMfT+YYncUQPgcxNGCHfZ88vDdrcUuaGIl6zhAsiaq7R5dfqrqXH/JcBhfjT8D0azayIyEz75Nxp6YkcyDxlJq3EXnJUpqDohJJOysL1t1uNiHESlvsxPb5cpbW0+ICZqJmUZus1BMW0F5IVBODLIo2zHHjA0=","base64")).toString()),WN}var zN;function Fae(){return typeof zN=="undefined"&&(zN=require("zlib").brotliDecompressSync(Buffer.from("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","base64")).toString()),zN}var Nae=new Map([[P.makeIdent(null,"fsevents").identHash,Dae],[P.makeIdent(null,"resolve").identHash,Rae],[P.makeIdent(null,"typescript").identHash,Fae]]),w3e={hooks:{registerPackageExtensions:async(r,e)=>{for(let[t,i]of qN)e(P.parseDescriptor(t,!0),i)},getBuiltinPatch:async(r,e)=>{var s;let t="compat/";if(!e.startsWith(t))return;let i=P.parseIdent(e.slice(t.length)),n=(s=Nae.get(i.identHash))==null?void 0:s();return typeof n!="undefined"?n:null},reduceDependency:async(r,e,t,i)=>typeof Nae.get(r.identHash)=="undefined"?r:P.makeDescriptor(r,P.makeRange({protocol:"patch:",source:P.stringifyDescriptor(r),selector:`~builtin`,params:null}))}},B3e=w3e;var VN={};ft(VN,{default:()=>Q3e});var Z0=class extends Le{constructor(){super(...arguments);this.pkg=J.String("-p,--package",{description:"The package to run the provided command from"});this.quiet=J.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=J.String();this.args=J.Proxy()}async execute(){let e=[];this.pkg&&e.push("--package",this.pkg),this.quiet&&e.push("--quiet");let t=P.parseDescriptor(this.command),i;t.scope?i=P.makeIdent(t.scope,`create-${t.name}`):t.name.startsWith("@")?i=P.makeIdent(t.name.substring(1),"create"):i=P.makeIdent(null,`create-${t.name}`);let n=P.stringifyIdent(i);return t.range!=="unknown"&&(n+=`@${t.range}`),this.cli.run(["dlx",...e,n,...this.args])}};Z0.paths=[["create"]];var Lae=Z0;var Rm=class extends Le{constructor(){super(...arguments);this.packages=J.Array("-p,--package",{description:"The package(s) to install before running the command"});this.quiet=J.Boolean("-q,--quiet",!1,{description:"Only report critical errors instead of printing the full install logs"});this.command=J.String();this.args=J.Proxy()}async execute(){return ye.telemetry=null,await U.mktempPromise(async e=>{var p;let t=x.join(e,`dlx-${process.pid}`);await U.mkdirPromise(t),await U.writeFilePromise(x.join(t,"package.json"),`{} +`),await U.writeFilePromise(x.join(t,"yarn.lock"),"");let i=x.join(t,".yarnrc.yml"),n=await ye.findProjectCwd(this.context.cwd,xt.lockfile),s=!(await ye.find(this.context.cwd,null,{strict:!1})).get("enableGlobalCache"),o=n!==null?x.join(n,".yarnrc.yml"):null;o!==null&&U.existsSync(o)?(await U.copyFilePromise(o,i),await ye.updateConfiguration(t,m=>{let y=te(N({},m),{enableGlobalCache:s,enableTelemetry:!1});return Array.isArray(m.plugins)&&(y.plugins=m.plugins.map(b=>{let v=typeof b=="string"?b:b.path,k=H.isAbsolute(v)?v:H.resolve(H.fromPortablePath(n),v);return typeof b=="string"?k:{path:k,spec:b.spec}})),y})):await U.writeFilePromise(i,`enableGlobalCache: ${s} +enableTelemetry: false +`);let a=(p=this.packages)!=null?p:[this.command],l=P.parseDescriptor(this.command).name,c=await this.cli.run(["add","--",...a],{cwd:t,quiet:this.quiet});if(c!==0)return c;this.quiet||this.context.stdout.write(` +`);let u=await ye.find(t,this.context.plugins),{project:g,workspace:f}=await ze.find(u,t);if(f===null)throw new ht(g.cwd,t);await g.restoreInstallState();let h=await Zt.getWorkspaceAccessibleBinaries(f);return h.has(l)===!1&&h.size===1&&typeof this.packages=="undefined"&&(l=Array.from(h)[0][0]),await Zt.executeWorkspaceAccessibleBinary(f,l,this.args,{packageAccessibleBinaries:h,cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr})})}};Rm.paths=[["dlx"]],Rm.usage=Re.Usage({description:"run a package in a temporary environment",details:"\n This command will install a package within a temporary environment, and run its binary script if it contains any. The binary will run within the current cwd.\n\n By default Yarn will download the package named `command`, but this can be changed through the use of the `-p,--package` flag which will instruct Yarn to still run the same command but from a different package.\n\n Using `yarn dlx` as a replacement of `yarn add` isn't recommended, as it makes your project non-deterministic (Yarn doesn't keep track of the packages installed through `dlx` - neither their name, nor their version).\n ",examples:[["Use create-react-app to create a new React app","yarn dlx create-react-app ./my-app"],["Install multiple packages for a single command",`yarn dlx -p typescript -p ts-node ts-node --transpile-only -e "console.log('hello!')"`]]});var Tae=Rm;var b3e={commands:[Lae,Tae]},Q3e=b3e;var sL={};ft(sL,{default:()=>k3e,fileUtils:()=>XN});var sh=/^(?:[a-zA-Z]:[\\/]|\.{0,2}\/)/,Fm=/^[^?]*\.(?:tar\.gz|tgz)(?:::.*)?$/,Xr="file:";var XN={};ft(XN,{makeArchiveFromLocator:()=>$0,makeBufferFromLocator:()=>eL,makeLocator:()=>$N,makeSpec:()=>Oae,parseSpec:()=>ZN});function ZN(r){let{params:e,selector:t}=P.parseRange(r),i=H.toPortablePath(t);return{parentLocator:e&&typeof e.locator=="string"?P.parseLocator(e.locator):null,path:i}}function Oae({parentLocator:r,path:e,folderHash:t,protocol:i}){let n=r!==null?{locator:P.stringifyLocator(r)}:{},s=typeof t!="undefined"?{hash:t}:{};return P.makeRange({protocol:i,source:e,selector:e,params:N(N({},s),n)})}function $N(r,{parentLocator:e,path:t,folderHash:i,protocol:n}){return P.makeLocator(r,Oae({parentLocator:e,path:t,folderHash:i,protocol:n}))}async function $0(r,{protocol:e,fetchOptions:t,inMemory:i=!1}){let{parentLocator:n,path:s}=P.parseFileStyleRange(r.reference,{protocol:e}),o=x.isAbsolute(s)?{packageFs:new _t(Me.root),prefixPath:Me.dot,localPath:Me.root}:await t.fetcher.fetch(n,t),a=o.localPath?{packageFs:new _t(Me.root),prefixPath:x.relative(Me.root,o.localPath)}:o;o!==a&&o.releaseFs&&o.releaseFs();let l=a.packageFs,c=x.join(a.prefixPath,s);return await Se.releaseAfterUseAsync(async()=>await Bi.makeArchiveFromDirectory(c,{baseFs:l,prefixPath:P.getIdentVendorPath(r),compressionLevel:t.project.configuration.get("compressionLevel"),inMemory:i}),a.releaseFs)}async function eL(r,{protocol:e,fetchOptions:t}){return(await $0(r,{protocol:e,fetchOptions:t,inMemory:!0})).getBufferAndClose()}var tL=class{supports(e,t){return!!e.reference.startsWith(Xr)}getLocalPath(e,t){let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:Xr});if(x.isAbsolute(n))return n;let s=t.fetcher.getLocalPath(i,t);return s===null?null:x.resolve(s,n)}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),localPath:this.getLocalPath(e,t),checksum:o}}async fetchFromDisk(e,t){return $0(e,{protocol:Xr,fetchOptions:t})}};var S3e=2,rL=class{supportsDescriptor(e,t){return e.range.match(sh)?!0:!!e.range.startsWith(Xr)}supportsLocator(e,t){return!!e.reference.startsWith(Xr)}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){return sh.test(e.range)&&(e=P.makeDescriptor(e,`${Xr}${e.range}`)),P.bindDescriptor(e,{locator:P.stringifyLocator(t)})}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){if(!i.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{path:n,parentLocator:s}=ZN(e.range);if(s===null)throw new Error("Assertion failed: The descriptor should have been bound");let o=await eL(P.makeLocator(e,P.makeRange({protocol:Xr,source:n,selector:n,params:{locator:P.stringifyLocator(s)}})),{protocol:Xr,fetchOptions:i.fetchOptions}),a=Rn.makeHash(`${S3e}`,o).slice(0,6);return[$N(e,{parentLocator:s,path:n,folderHash:a,protocol:Xr})]}async getSatisfying(e,t,i){return null}async resolve(e,t){if(!t.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let i=await t.fetchOptions.fetcher.fetch(e,t.fetchOptions),n=await Se.releaseAfterUseAsync(async()=>await At.find(i.prefixPath,{baseFs:i.packageFs}),i.releaseFs);return te(N({},e),{version:n.version||"0.0.0",languageName:n.languageName||t.project.configuration.get("defaultLanguageName"),linkType:Qt.HARD,conditions:n.getConditions(),dependencies:n.dependencies,peerDependencies:n.peerDependencies,dependenciesMeta:n.dependenciesMeta,peerDependenciesMeta:n.peerDependenciesMeta,bin:n.bin})}};var iL=class{supports(e,t){return Fm.test(e.reference)?!!e.reference.startsWith(Xr):!1}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.fetchFromDisk(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),checksum:o}}async fetchFromDisk(e,t){let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:Xr}),s=x.isAbsolute(n)?{packageFs:new _t(Me.root),prefixPath:Me.dot,localPath:Me.root}:await t.fetcher.fetch(i,t),o=s.localPath?{packageFs:new _t(Me.root),prefixPath:x.relative(Me.root,s.localPath)}:s;s!==o&&s.releaseFs&&s.releaseFs();let a=o.packageFs,l=x.join(o.prefixPath,n),c=await a.readFilePromise(l);return await Se.releaseAfterUseAsync(async()=>await Bi.convertToZip(c,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1}),o.releaseFs)}};var nL=class{supportsDescriptor(e,t){return Fm.test(e.range)?!!(e.range.startsWith(Xr)||sh.test(e.range)):!1}supportsLocator(e,t){return Fm.test(e.reference)?!!e.reference.startsWith(Xr):!1}shouldPersistResolution(e,t){return!0}bindDescriptor(e,t,i){return sh.test(e.range)&&(e=P.makeDescriptor(e,`${Xr}${e.range}`)),P.bindDescriptor(e,{locator:P.stringifyLocator(t)})}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=e.range;return n.startsWith(Xr)&&(n=n.slice(Xr.length)),[P.makeLocator(e,`${Xr}${H.toPortablePath(n)}`)]}async getSatisfying(e,t,i){return null}async resolve(e,t){if(!t.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let i=await t.fetchOptions.fetcher.fetch(e,t.fetchOptions),n=await Se.releaseAfterUseAsync(async()=>await At.find(i.prefixPath,{baseFs:i.packageFs}),i.releaseFs);return te(N({},e),{version:n.version||"0.0.0",languageName:n.languageName||t.project.configuration.get("defaultLanguageName"),linkType:Qt.HARD,conditions:n.getConditions(),dependencies:n.dependencies,peerDependencies:n.peerDependencies,dependenciesMeta:n.dependenciesMeta,peerDependenciesMeta:n.peerDependenciesMeta,bin:n.bin})}};var v3e={fetchers:[iL,tL],resolvers:[nL,rL]},k3e=v3e;var aL={};ft(aL,{default:()=>D3e});var Mae=ge(require("querystring")),Uae=[/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+)\/tarball\/([^/#]+)(?:#(.*))?$/,/^https?:\/\/(?:([^/]+?)@)?github.com\/([^/#]+)\/([^/#]+?)(?:\.git)?(?:#(.*))?$/];function Kae(r){return r?Uae.some(e=>!!r.match(e)):!1}function Hae(r){let e;for(let a of Uae)if(e=r.match(a),e)break;if(!e)throw new Error(x3e(r));let[,t,i,n,s="master"]=e,{commit:o}=Mae.default.parse(s);return s=o||s.replace(/[^:]*:/,""),{auth:t,username:i,reponame:n,treeish:s}}function x3e(r){return`Input cannot be parsed as a valid GitHub URL ('${r}').`}var oL=class{supports(e,t){return!!Kae(e.reference)}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from GitHub`),loader:()=>this.fetchFromNetwork(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),checksum:o}}async fetchFromNetwork(e,t){let i=await ir.get(this.getLocatorUrl(e,t),{configuration:t.project.configuration});return await U.mktempPromise(async n=>{let s=new _t(n);await Bi.extractArchiveTo(i,s,{stripComponents:1});let o=Su.splitRepoUrl(e.reference),a=x.join(n,"package.tgz");await Zt.prepareExternalProject(n,a,{configuration:t.project.configuration,report:t.report,workspace:o.extra.workspace,locator:e});let l=await U.readFilePromise(a);return await Bi.convertToZip(l,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1})})}getLocatorUrl(e,t){let{auth:i,username:n,reponame:s,treeish:o}=Hae(e.reference);return`https://${i?`${i}@`:""}github.com/${n}/${s}/archive/${o}.tar.gz`}};var P3e={hooks:{async fetchHostedRepository(r,e,t){if(r!==null)return r;let i=new oL;if(!i.supports(e,t))return null;try{return await i.fetch(e,t)}catch(n){return null}}}},D3e=P3e;var cL={};ft(cL,{default:()=>F3e});var Nm=/^[^?]*\.(?:tar\.gz|tgz)(?:\?.*)?$/,Lm=/^https?:/;var AL=class{supports(e,t){return Nm.test(e.reference)?!!Lm.test(e.reference):!1}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),checksum:o}}async fetchFromNetwork(e,t){let i=await ir.get(e.reference,{configuration:t.project.configuration});return await Bi.convertToZip(i,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1})}};var lL=class{supportsDescriptor(e,t){return Nm.test(e.range)?!!Lm.test(e.range):!1}supportsLocator(e,t){return Nm.test(e.reference)?!!Lm.test(e.reference):!1}shouldPersistResolution(e,t){return!0}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){return[P.convertDescriptorToLocator(e)]}async getSatisfying(e,t,i){return null}async resolve(e,t){if(!t.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let i=await t.fetchOptions.fetcher.fetch(e,t.fetchOptions),n=await Se.releaseAfterUseAsync(async()=>await At.find(i.prefixPath,{baseFs:i.packageFs}),i.releaseFs);return te(N({},e),{version:n.version||"0.0.0",languageName:n.languageName||t.project.configuration.get("defaultLanguageName"),linkType:Qt.HARD,conditions:n.getConditions(),dependencies:n.dependencies,peerDependencies:n.peerDependencies,dependenciesMeta:n.dependenciesMeta,peerDependenciesMeta:n.peerDependenciesMeta,bin:n.bin})}};var R3e={fetchers:[AL],resolvers:[lL]},F3e=R3e;var hL={};ft(hL,{default:()=>N4e});var hAe=ge(fAe()),fL=ge(require("util")),Tm=class extends Le{constructor(){super(...arguments);this.private=J.Boolean("-p,--private",!1,{description:"Initialize a private package"});this.workspace=J.Boolean("-w,--workspace",!1,{description:"Initialize a workspace root with a `packages/` directory"});this.install=J.String("-i,--install",!1,{tolerateBoolean:!0,description:"Initialize a package with a specific bundle that will be locked in the project"});this.usev2=J.Boolean("-2",!1,{hidden:!0});this.yes=J.Boolean("-y,--yes",{hidden:!0});this.assumeFreshProject=J.Boolean("--assume-fresh-project",!1,{hidden:!0})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=typeof this.install=="string"?this.install:this.usev2||this.install===!0?"latest":null;return t!==null?await this.executeProxy(e,t):await this.executeRegular(e)}async executeProxy(e,t){if(e.projectCwd!==null&&e.projectCwd!==this.context.cwd)throw new Pe("Cannot use the --install flag from within a project subdirectory");U.existsSync(this.context.cwd)||await U.mkdirPromise(this.context.cwd,{recursive:!0});let i=x.join(this.context.cwd,e.get("lockfileFilename"));U.existsSync(i)||await U.writeFilePromise(i,"");let n=await this.cli.run(["set","version",t],{quiet:!0});if(n!==0)return n;let s=[];return this.private&&s.push("-p"),this.workspace&&s.push("-w"),this.yes&&s.push("-y"),await U.mktempPromise(async o=>{let{code:a}=await Nr.pipevp("yarn",["init",...s],{cwd:this.context.cwd,stdin:this.context.stdin,stdout:this.context.stdout,stderr:this.context.stderr,env:await Zt.makeScriptEnv({binFolder:o})});return a})}async executeRegular(e){var l;let t=null;try{t=(await ze.find(e,this.context.cwd)).project}catch{t=null}U.existsSync(this.context.cwd)||await U.mkdirPromise(this.context.cwd,{recursive:!0});let i=await At.tryFind(this.context.cwd)||new At,n=Object.fromEntries(e.get("initFields").entries());i.load(n),i.name=(l=i.name)!=null?l:P.makeIdent(e.get("initScope"),x.basename(this.context.cwd)),i.packageManager=Kr&&Se.isTaggedYarnVersion(Kr)?`yarn@${Kr}`:null,typeof i.raw.private=="undefined"&&(this.private||this.workspace&&i.workspaceDefinitions.length===0)&&(i.private=!0),this.workspace&&i.workspaceDefinitions.length===0&&(await U.mkdirPromise(x.join(this.context.cwd,"packages"),{recursive:!0}),i.workspaceDefinitions=[{pattern:"packages/*"}]);let s={};i.exportTo(s),fL.inspect.styles.name="cyan",this.context.stdout.write(`${(0,fL.inspect)(s,{depth:Infinity,colors:!0,compact:!1})} +`);let o=x.join(this.context.cwd,At.fileName);await U.changeFilePromise(o,`${JSON.stringify(s,null,2)} +`,{automaticNewlines:!0});let a=x.join(this.context.cwd,"README.md");if(U.existsSync(a)||await U.writeFilePromise(a,`# ${P.stringifyIdent(i.name)} +`),!t||t.cwd===this.context.cwd){let c=x.join(this.context.cwd,xt.lockfile);U.existsSync(c)||await U.writeFilePromise(c,"");let g=[".yarn/*","!.yarn/patches","!.yarn/plugins","!.yarn/releases","!.yarn/sdks","!.yarn/versions","","# Swap the comments on the following lines if you don't wish to use zero-installs","# Documentation here: https://yarnpkg.com/features/zero-installs","!.yarn/cache","#.pnp.*"].map(y=>`${y} +`).join(""),f=x.join(this.context.cwd,".gitignore");U.existsSync(f)||await U.writeFilePromise(f,g);let h={["*"]:{endOfLine:"lf",insertFinalNewline:!0},["*.{js,json,yml}"]:{charset:"utf-8",indentStyle:"space",indentSize:2}};(0,hAe.default)(h,e.get("initEditorConfig"));let p=`root = true +`;for(let[y,b]of Object.entries(h)){p+=` +[${y}] +`;for(let[v,k]of Object.entries(b))p+=`${v.replace(/[A-Z]/g,Y=>`_${Y.toLowerCase()}`)} = ${k} +`}let m=x.join(this.context.cwd,".editorconfig");U.existsSync(m)||await U.writeFilePromise(m,p),U.existsSync(x.join(this.context.cwd,".git"))||await Nr.execvp("git",["init"],{cwd:this.context.cwd})}}};Tm.paths=[["init"]],Tm.usage=Re.Usage({description:"create a new package",details:"\n This command will setup a new package in your local directory.\n\n If the `-p,--private` or `-w,--workspace` options are set, the package will be private by default.\n\n If the `-w,--workspace` option is set, the package will be configured to accept a set of workspaces in the `packages/` directory.\n\n If the `-i,--install` option is given a value, Yarn will first download it using `yarn set version` and only then forward the init call to the newly downloaded bundle. Without arguments, the downloaded bundle will be `latest`.\n\n The initial settings of the manifest can be changed by using the `initScope` and `initFields` configuration values. Additionally, Yarn will generate an EditorConfig file whose rules can be altered via `initEditorConfig`, and will initialize a Git repository in the current directory.\n ",examples:[["Create a new package in the local directory","yarn init"],["Create a new private package in the local directory","yarn init -p"],["Create a new package and store the Yarn release inside","yarn init -i=latest"],["Create a new private package and defines it as a workspace root","yarn init -w"]]});var pAe=Tm;var F4e={configuration:{initScope:{description:"Scope used when creating packages via the init command",type:Ie.STRING,default:null},initFields:{description:"Additional fields to set when creating packages via the init command",type:Ie.MAP,valueDefinition:{description:"",type:Ie.ANY}},initEditorConfig:{description:"Extra rules to define in the generator editorconfig",type:Ie.MAP,valueDefinition:{description:"",type:Ie.ANY}}},commands:[pAe]},N4e=F4e;var EL={};ft(EL,{default:()=>T4e});var BA="portal:",bA="link:";var pL=class{supports(e,t){return!!e.reference.startsWith(BA)}getLocalPath(e,t){let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:BA});if(x.isAbsolute(n))return n;let s=t.fetcher.getLocalPath(i,t);return s===null?null:x.resolve(s,n)}async fetch(e,t){var c;let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:BA}),s=x.isAbsolute(n)?{packageFs:new _t(Me.root),prefixPath:Me.dot,localPath:Me.root}:await t.fetcher.fetch(i,t),o=s.localPath?{packageFs:new _t(Me.root),prefixPath:x.relative(Me.root,s.localPath),localPath:Me.root}:s;s!==o&&s.releaseFs&&s.releaseFs();let a=o.packageFs,l=x.resolve((c=o.localPath)!=null?c:o.packageFs.getRealPath(),o.prefixPath,n);return s.localPath?{packageFs:new _t(l,{baseFs:a}),releaseFs:o.releaseFs,prefixPath:Me.dot,localPath:l}:{packageFs:new Ta(l,{baseFs:a}),releaseFs:o.releaseFs,prefixPath:Me.dot}}};var dL=class{supportsDescriptor(e,t){return!!e.range.startsWith(BA)}supportsLocator(e,t){return!!e.reference.startsWith(BA)}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){return P.bindDescriptor(e,{locator:P.stringifyLocator(t)})}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=e.range.slice(BA.length);return[P.makeLocator(e,`${BA}${H.toPortablePath(n)}`)]}async getSatisfying(e,t,i){return null}async resolve(e,t){if(!t.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let i=await t.fetchOptions.fetcher.fetch(e,t.fetchOptions),n=await Se.releaseAfterUseAsync(async()=>await At.find(i.prefixPath,{baseFs:i.packageFs}),i.releaseFs);return te(N({},e),{version:n.version||"0.0.0",languageName:n.languageName||t.project.configuration.get("defaultLanguageName"),linkType:Qt.SOFT,conditions:n.getConditions(),dependencies:new Map([...n.dependencies]),peerDependencies:n.peerDependencies,dependenciesMeta:n.dependenciesMeta,peerDependenciesMeta:n.peerDependenciesMeta,bin:n.bin})}};var CL=class{supports(e,t){return!!e.reference.startsWith(bA)}getLocalPath(e,t){let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:bA});if(x.isAbsolute(n))return n;let s=t.fetcher.getLocalPath(i,t);return s===null?null:x.resolve(s,n)}async fetch(e,t){var c;let{parentLocator:i,path:n}=P.parseFileStyleRange(e.reference,{protocol:bA}),s=x.isAbsolute(n)?{packageFs:new _t(Me.root),prefixPath:Me.dot,localPath:Me.root}:await t.fetcher.fetch(i,t),o=s.localPath?{packageFs:new _t(Me.root),prefixPath:x.relative(Me.root,s.localPath),localPath:Me.root}:s;s!==o&&s.releaseFs&&s.releaseFs();let a=o.packageFs,l=x.resolve((c=o.localPath)!=null?c:o.packageFs.getRealPath(),o.prefixPath,n);return s.localPath?{packageFs:new _t(l,{baseFs:a}),releaseFs:o.releaseFs,prefixPath:Me.dot,discardFromLookup:!0,localPath:l}:{packageFs:new Ta(l,{baseFs:a}),releaseFs:o.releaseFs,prefixPath:Me.dot,discardFromLookup:!0}}};var mL=class{supportsDescriptor(e,t){return!!e.range.startsWith(bA)}supportsLocator(e,t){return!!e.reference.startsWith(bA)}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){return P.bindDescriptor(e,{locator:P.stringifyLocator(t)})}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=e.range.slice(bA.length);return[P.makeLocator(e,`${bA}${H.toPortablePath(n)}`)]}async getSatisfying(e,t,i){return null}async resolve(e,t){return te(N({},e),{version:"0.0.0",languageName:t.project.configuration.get("defaultLanguageName"),linkType:Qt.SOFT,conditions:null,dependencies:new Map,peerDependencies:new Map,dependenciesMeta:new Map,peerDependenciesMeta:new Map,bin:new Map})}};var L4e={fetchers:[CL,pL],resolvers:[mL,dL]},T4e=L4e;var WL={};ft(WL,{default:()=>qze});var Un;(function(i){i[i.REGULAR=0]="REGULAR",i[i.WORKSPACE=1]="WORKSPACE",i[i.EXTERNAL_SOFT_LINK=2]="EXTERNAL_SOFT_LINK"})(Un||(Un={}));var QA;(function(i){i[i.YES=0]="YES",i[i.NO=1]="NO",i[i.DEPENDS=2]="DEPENDS"})(QA||(QA={}));var IL=(r,e)=>`${r}@${e}`,dAe=(r,e)=>{let t=e.indexOf("#"),i=t>=0?e.substring(t+1):e;return IL(r,i)},Bo;(function(s){s[s.NONE=-1]="NONE",s[s.PERF=0]="PERF",s[s.CHECK=1]="CHECK",s[s.REASONS=2]="REASONS",s[s.INTENSIVE_CHECK=9]="INTENSIVE_CHECK"})(Bo||(Bo={}));var mAe=(r,e={})=>{let t=e.debugLevel||Number(process.env.NM_DEBUG_LEVEL||-1),i=e.check||t>=9,n=e.hoistingLimits||new Map,s={check:i,debugLevel:t,hoistingLimits:n,fastLookupPossible:!0},o;s.debugLevel>=0&&(o=Date.now());let a=O4e(r,s),l=!1,c=0;do l=yL(a,[a],new Set([a.locator]),new Map,s).anotherRoundNeeded,s.fastLookupPossible=!1,c++;while(l);if(s.debugLevel>=0&&console.log(`hoist time: ${Date.now()-o}ms, rounds: ${c}`),s.debugLevel>=1){let u=Om(a);if(yL(a,[a],new Set([a.locator]),new Map,s).isGraphChanged)throw new Error(`The hoisting result is not terminal, prev tree: +${u}, next tree: +${Om(a)}`);let f=CAe(a);if(f)throw new Error(`${f}, after hoisting finished: +${Om(a)}`)}return s.debugLevel>=2&&console.log(Om(a)),M4e(a)},U4e=r=>{let e=r[r.length-1],t=new Map,i=new Set,n=s=>{if(!i.has(s)){i.add(s);for(let o of s.hoistedDependencies.values())t.set(o.name,o);for(let o of s.dependencies.values())s.peerNames.has(o.name)||n(o)}};return n(e),t},K4e=r=>{let e=r[r.length-1],t=new Map,i=new Set,n=new Set,s=(o,a)=>{if(i.has(o))return;i.add(o);for(let c of o.hoistedDependencies.values())if(!a.has(c.name)){let u;for(let g of r)u=g.dependencies.get(c.name),u&&t.set(u.name,u)}let l=new Set;for(let c of o.dependencies.values())l.add(c.name);for(let c of o.dependencies.values())o.peerNames.has(c.name)||s(c,l)};return s(e,n),t},EAe=(r,e)=>{if(e.decoupled)return e;let{name:t,references:i,ident:n,locator:s,dependencies:o,originalDependencies:a,hoistedDependencies:l,peerNames:c,reasons:u,isHoistBorder:g,hoistPriority:f,dependencyKind:h,hoistedFrom:p,hoistedTo:m}=e,y={name:t,references:new Set(i),ident:n,locator:s,dependencies:new Map(o),originalDependencies:new Map(a),hoistedDependencies:new Map(l),peerNames:new Set(c),reasons:new Map(u),decoupled:!0,isHoistBorder:g,hoistPriority:f,dependencyKind:h,hoistedFrom:new Map(p),hoistedTo:new Map(m)},b=y.dependencies.get(t);return b&&b.ident==y.ident&&y.dependencies.set(t,y),r.dependencies.set(y.name,y),y},H4e=(r,e)=>{let t=new Map([[r.name,[r.ident]]]);for(let n of r.dependencies.values())r.peerNames.has(n.name)||t.set(n.name,[n.ident]);let i=Array.from(e.keys());i.sort((n,s)=>{let o=e.get(n),a=e.get(s);return a.hoistPriority!==o.hoistPriority?a.hoistPriority-o.hoistPriority:a.peerDependents.size!==o.peerDependents.size?a.peerDependents.size-o.peerDependents.size:a.dependents.size-o.dependents.size});for(let n of i){let s=n.substring(0,n.indexOf("@",1)),o=n.substring(s.length+1);if(!r.peerNames.has(s)){let a=t.get(s);a||(a=[],t.set(s,a)),a.indexOf(o)<0&&a.push(o)}}return t},wL=r=>{let e=new Set,t=(i,n=new Set)=>{if(!n.has(i)){n.add(i);for(let s of i.peerNames)if(!r.peerNames.has(s)){let o=r.dependencies.get(s);o&&!e.has(o)&&t(o,n)}e.add(i)}};for(let i of r.dependencies.values())r.peerNames.has(i.name)||t(i);return e},yL=(r,e,t,i,n,s=new Set)=>{let o=e[e.length-1];if(s.has(o))return{anotherRoundNeeded:!1,isGraphChanged:!1};s.add(o);let a=G4e(o),l=H4e(o,a),c=r==o?new Map:n.fastLookupPossible?U4e(e):K4e(e),u,g=!1,f=!1,h=new Map(Array.from(l.entries()).map(([m,y])=>[m,y[0]])),p=new Map;do{let m=j4e(r,e,t,c,h,l,i,p,n);m.isGraphChanged&&(f=!0),m.anotherRoundNeeded&&(g=!0),u=!1;for(let[y,b]of l)b.length>1&&!o.dependencies.has(y)&&(h.delete(y),b.shift(),h.set(y,b[0]),u=!0)}while(u);for(let m of o.dependencies.values())if(!o.peerNames.has(m.name)&&!t.has(m.locator)){t.add(m.locator);let y=yL(r,[...e,m],t,p,n);y.isGraphChanged&&(f=!0),y.anotherRoundNeeded&&(g=!0),t.delete(m.locator)}return{anotherRoundNeeded:g,isGraphChanged:f}},Y4e=r=>{for(let[e,t]of r.dependencies)if(!r.peerNames.has(e)&&t.ident!==r.ident)return!0;return!1},q4e=(r,e,t,i,n,s,o,a,{outputReason:l,fastLookupPossible:c})=>{let u,g=null,f=new Set;l&&(u=`${Array.from(e).map(y=>Li(y)).join("\u2192")}`);let h=t[t.length-1],m=!(i.ident===h.ident);if(l&&!m&&(g="- self-reference"),m&&(m=i.dependencyKind!==1,l&&!m&&(g="- workspace")),m&&i.dependencyKind===2&&(m=!Y4e(i),l&&!m&&(g="- external soft link with unhoisted dependencies")),m&&(m=h.dependencyKind!==1||h.hoistedFrom.has(i.name)||e.size===1,l&&!m&&(g=h.reasons.get(i.name))),m&&(m=!r.peerNames.has(i.name),l&&!m&&(g=`- cannot shadow peer: ${Li(r.originalDependencies.get(i.name).locator)} at ${u}`)),m){let y=!1,b=n.get(i.name);if(y=!b||b.ident===i.ident,l&&!y&&(g=`- filled by: ${Li(b.locator)} at ${u}`),y)for(let v=t.length-1;v>=1;v--){let T=t[v].dependencies.get(i.name);if(T&&T.ident!==i.ident){y=!1;let Y=a.get(h);Y||(Y=new Set,a.set(h,Y)),Y.add(i.name),l&&(g=`- filled by ${Li(T.locator)} at ${t.slice(0,v).map(q=>Li(q.locator)).join("\u2192")}`);break}}m=y}if(m&&(m=s.get(i.name)===i.ident,l&&!m&&(g=`- filled by: ${Li(o.get(i.name)[0])} at ${u}`)),m){let y=!0,b=new Set(i.peerNames);for(let v=t.length-1;v>=1;v--){let k=t[v];for(let T of b){if(k.peerNames.has(T)&&k.originalDependencies.has(T))continue;let Y=k.dependencies.get(T);Y&&r.dependencies.get(T)!==Y&&(v===t.length-1?f.add(Y):(f=null,y=!1,l&&(g=`- peer dependency ${Li(Y.locator)} from parent ${Li(k.locator)} was not hoisted to ${u}`))),b.delete(T)}if(!y)break}m=y}if(m&&!c)for(let y of i.hoistedDependencies.values()){let b=n.get(y.name)||r.dependencies.get(y.name);if(!b||y.ident!==b.ident){m=!1,l&&(g=`- previously hoisted dependency mismatch, needed: ${Li(y.locator)}, available: ${Li(b==null?void 0:b.locator)}`);break}}return f!==null&&f.size>0?{isHoistable:2,dependsOn:f,reason:g}:{isHoistable:m?0:1,reason:g}},eb=r=>`${r.name}@${r.locator}`,j4e=(r,e,t,i,n,s,o,a,l)=>{let c=e[e.length-1],u=new Set,g=!1,f=!1,h=(b,v,k,T,Y)=>{if(u.has(T))return;let q=[...v,eb(T)],$=[...k,eb(T)],z=new Map,ne=new Map;for(let Z of wL(T)){let O=q4e(c,t,[c,...b,T],Z,i,n,s,a,{outputReason:l.debugLevel>=2,fastLookupPossible:l.fastLookupPossible});if(ne.set(Z,O),O.isHoistable===2)for(let L of O.dependsOn){let de=z.get(L.name)||new Set;de.add(Z.name),z.set(L.name,de)}}let ee=new Set,A=(Z,O,L)=>{if(!ee.has(Z)){ee.add(Z),ne.set(Z,{isHoistable:1,reason:L});for(let de of z.get(Z.name)||[])A(T.dependencies.get(de),O,l.debugLevel>=2?`- peer dependency ${Li(Z.locator)} from parent ${Li(T.locator)} was not hoisted`:"")}};for(let[Z,O]of ne)O.isHoistable===1&&A(Z,O,O.reason);let oe=!1;for(let Z of ne.keys())if(!ee.has(Z)){f=!0;let O=o.get(T);O&&O.has(Z.name)&&(g=!0),oe=!0,T.dependencies.delete(Z.name),T.hoistedDependencies.set(Z.name,Z),T.reasons.delete(Z.name);let L=c.dependencies.get(Z.name);if(l.debugLevel>=2){let de=Array.from(v).concat([T.locator]).map(Ge=>Li(Ge)).join("\u2192"),Be=c.hoistedFrom.get(Z.name);Be||(Be=[],c.hoistedFrom.set(Z.name,Be)),Be.push(de),T.hoistedTo.set(Z.name,Array.from(e).map(Ge=>Li(Ge.locator)).join("\u2192"))}if(!L)c.ident!==Z.ident&&(c.dependencies.set(Z.name,Z),Y.add(Z));else for(let de of Z.references)L.references.add(de)}if(T.dependencyKind===2&&oe&&(g=!0),l.check){let Z=CAe(r);if(Z)throw new Error(`${Z}, after hoisting dependencies of ${[c,...b,T].map(O=>Li(O.locator)).join("\u2192")}: +${Om(r)}`)}let ce=wL(T);for(let Z of ce)if(ee.has(Z)){let O=ne.get(Z);if((n.get(Z.name)===Z.ident||!T.reasons.has(Z.name))&&O.isHoistable!==0&&T.reasons.set(Z.name,O.reason),!Z.isHoistBorder&&$.indexOf(eb(Z))<0){u.add(T);let de=EAe(T,Z);h([...b,T],q,$,de,m),u.delete(T)}}},p,m=new Set(wL(c)),y=Array.from(e).map(b=>eb(b));do{p=m,m=new Set;for(let b of p){if(b.locator===c.locator||b.isHoistBorder)continue;let v=EAe(c,b);h([],Array.from(t),y,v,m)}}while(m.size>0);return{anotherRoundNeeded:g,isGraphChanged:f}},CAe=r=>{let e=[],t=new Set,i=new Set,n=(s,o,a)=>{if(t.has(s)||(t.add(s),i.has(s)))return;let l=new Map(o);for(let c of s.dependencies.values())s.peerNames.has(c.name)||l.set(c.name,c);for(let c of s.originalDependencies.values()){let u=l.get(c.name),g=()=>`${Array.from(i).concat([s]).map(f=>Li(f.locator)).join("\u2192")}`;if(s.peerNames.has(c.name)){let f=o.get(c.name);(f!==u||!f||f.ident!==c.ident)&&e.push(`${g()} - broken peer promise: expected ${c.ident} but found ${f&&f.ident}`)}else{let f=a.hoistedFrom.get(s.name),h=s.hoistedTo.get(c.name),p=`${f?` hoisted from ${f.join(", ")}`:""}`,m=`${h?` hoisted to ${h}`:""}`,y=`${g()}${p}`;u?u.ident!==c.ident&&e.push(`${y} - broken require promise for ${c.name}${m}: expected ${c.ident}, but found: ${u.ident}`):e.push(`${y} - broken require promise: no required dependency ${c.name}${m} found`)}}i.add(s);for(let c of s.dependencies.values())s.peerNames.has(c.name)||n(c,l,s);i.delete(s)};return n(r,r.dependencies,r),e.join(` +`)},O4e=(r,e)=>{let{identName:t,name:i,reference:n,peerNames:s}=r,o={name:i,references:new Set([n]),locator:IL(t,n),ident:dAe(t,n),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(s),reasons:new Map,decoupled:!0,isHoistBorder:!0,hoistPriority:0,dependencyKind:1,hoistedFrom:new Map,hoistedTo:new Map},a=new Map([[r,o]]),l=(c,u)=>{let g=a.get(c),f=!!g;if(!g){let{name:h,identName:p,reference:m,peerNames:y,hoistPriority:b,dependencyKind:v}=c,k=e.hoistingLimits.get(u.locator);g={name:h,references:new Set([m]),locator:IL(p,m),ident:dAe(p,m),dependencies:new Map,originalDependencies:new Map,hoistedDependencies:new Map,peerNames:new Set(y),reasons:new Map,decoupled:!0,isHoistBorder:k?k.has(h):!1,hoistPriority:b||0,dependencyKind:v||0,hoistedFrom:new Map,hoistedTo:new Map},a.set(c,g)}if(u.dependencies.set(c.name,g),u.originalDependencies.set(c.name,g),f){let h=new Set,p=m=>{if(!h.has(m)){h.add(m),m.decoupled=!1;for(let y of m.dependencies.values())m.peerNames.has(y.name)||p(y)}};p(g)}else for(let h of c.dependencies)l(h,g)};for(let c of r.dependencies)l(c,o);return o},BL=r=>r.substring(0,r.indexOf("@",1)),M4e=r=>{let e={name:r.name,identName:BL(r.locator),references:new Set(r.references),dependencies:new Set},t=new Set([r]),i=(n,s,o)=>{let a=t.has(n),l;if(s===n)l=o;else{let{name:c,references:u,locator:g}=n;l={name:c,identName:BL(g),references:u,dependencies:new Set}}if(o.dependencies.add(l),!a){t.add(n);for(let c of n.dependencies.values())n.peerNames.has(c.name)||i(c,n,l);t.delete(n)}};for(let n of r.dependencies.values())i(n,r,e);return e},G4e=r=>{let e=new Map,t=new Set([r]),i=o=>`${o.name}@${o.ident}`,n=o=>{let a=i(o),l=e.get(a);return l||(l={dependents:new Set,peerDependents:new Set,hoistPriority:0},e.set(a,l)),l},s=(o,a)=>{let l=!!t.has(a);if(n(a).dependents.add(o.ident),!l){t.add(a);for(let u of a.dependencies.values()){let g=n(u);g.hoistPriority=Math.max(g.hoistPriority,u.hoistPriority),a.peerNames.has(u.name)?g.peerDependents.add(a.ident):s(a,u)}}};for(let o of r.dependencies.values())r.peerNames.has(o.name)||s(r,o);return e},Li=r=>{if(!r)return"none";let e=r.indexOf("@",1),t=r.substring(0,e);t.endsWith("$wsroot$")&&(t=`wh:${t.replace("$wsroot$","")}`);let i=r.substring(e+1);if(i==="workspace:.")return".";if(i){let n=(i.indexOf("#")>0?i.split("#")[1]:i).replace("npm:","");return i.startsWith("virtual")&&(t=`v:${t}`),n.startsWith("workspace")&&(t=`w:${t}`,n=""),`${t}${n?`@${n}`:""}`}else return`${t}`},IAe=5e4,Om=r=>{let e=0,t=(n,s,o="")=>{if(e>IAe||s.has(n))return"";e++;let a=Array.from(n.dependencies.values()).sort((c,u)=>c.name===u.name?0:c.name>u.name?1:-1),l="";s.add(n);for(let c=0;c":"")+(f!==u.name?`a:${u.name}:`:"")+Li(u.locator)+(g?` ${g}`:"")} +`,l+=t(u,s,`${o}${cIAe?` +Tree is too large, part of the tree has been dunped +`:"")};var bo;(function(t){t.HARD="HARD",t.SOFT="SOFT"})(bo||(bo={}));var Kn;(function(i){i.WORKSPACES="workspaces",i.DEPENDENCIES="dependencies",i.NONE="none"})(Kn||(Kn={}));var yAe="node_modules",vu="$wsroot$";var Mm=(r,e)=>{let{packageTree:t,hoistingLimits:i,errors:n,preserveSymlinksRequired:s}=J4e(r,e),o=null;if(n.length===0){let a=mAe(t,{hoistingLimits:i});o=W4e(r,a,e)}return{tree:o,errors:n,preserveSymlinksRequired:s}},Ca=r=>`${r.name}@${r.reference}`,bL=r=>{let e=new Map;for(let[t,i]of r.entries())if(!i.dirList){let n=e.get(i.locator);n||(n={target:i.target,linkType:i.linkType,locations:[],aliases:i.aliases},e.set(i.locator,n)),n.locations.push(t)}for(let t of e.values())t.locations=t.locations.sort((i,n)=>{let s=i.split(x.delimiter).length,o=n.split(x.delimiter).length;return n===i?0:s!==o?o-s:n>i?1:-1});return e},wAe=(r,e)=>{let t=P.isVirtualLocator(r)?P.devirtualizeLocator(r):r,i=P.isVirtualLocator(e)?P.devirtualizeLocator(e):e;return P.areLocatorsEqual(t,i)},QL=(r,e,t,i)=>{if(r.linkType!==bo.SOFT)return!1;let n=H.toPortablePath(t.resolveVirtual&&e.reference&&e.reference.startsWith("virtual:")?t.resolveVirtual(r.packageLocation):r.packageLocation);return x.contains(i,n)===null},z4e=r=>{let e=r.getPackageInformation(r.topLevel);if(e===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");if(r.findPackageLocator(e.packageLocation)===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let i=H.toPortablePath(e.packageLocation.slice(0,-1)),n=new Map,s={children:new Map},o=r.getDependencyTreeRoots(),a=new Map,l=new Set,c=(f,h)=>{let p=Ca(f);if(l.has(p))return;l.add(p);let m=r.getPackageInformation(f);if(m){let y=h?Ca(h):"";if(Ca(f)!==y&&m.linkType===bo.SOFT&&!QL(m,f,r,i)){let b=BAe(m,f,r);(!a.get(b)||f.reference.startsWith("workspace:"))&&a.set(b,f)}for(let[b,v]of m.packageDependencies)v!==null&&(m.packagePeers.has(b)||c(r.getLocator(b,v),f))}};for(let f of o)c(f,null);let u=i.split(x.sep);for(let f of a.values()){let h=r.getPackageInformation(f),m=H.toPortablePath(h.packageLocation.slice(0,-1)).split(x.sep).slice(u.length),y=s;for(let b of m){let v=y.children.get(b);v||(v={children:new Map},y.children.set(b,v)),y=v}y.workspaceLocator=f}let g=(f,h)=>{if(f.workspaceLocator){let p=Ca(h),m=n.get(p);m||(m=new Set,n.set(p,m)),m.add(f.workspaceLocator)}for(let p of f.children.values())g(p,f.workspaceLocator||h)};for(let f of s.children.values())g(f,s.workspaceLocator);return n},J4e=(r,e)=>{let t=[],i=!1,n=new Map,s=z4e(r),o=r.getPackageInformation(r.topLevel);if(o===null)throw new Error("Assertion failed: Expected the top-level package to have been registered");let a=r.findPackageLocator(o.packageLocation);if(a===null)throw new Error("Assertion failed: Expected the top-level package to have a physical locator");let l=H.toPortablePath(o.packageLocation.slice(0,-1)),c={name:a.name,identName:a.name,reference:a.reference,peerNames:o.packagePeers,dependencies:new Set,dependencyKind:Un.WORKSPACE},u=new Map,g=(h,p)=>`${Ca(p)}:${h}`,f=(h,p,m,y,b,v,k,T)=>{var Z,O;let Y=g(h,m),q=u.get(Y),$=!!q;!$&&m.name===a.name&&m.reference===a.reference&&(q=c,u.set(Y,c));let z=QL(p,m,r,l);if(!q){let L=Un.REGULAR;z?L=Un.EXTERNAL_SOFT_LINK:p.linkType===bo.SOFT&&m.name.endsWith(vu)&&(L=Un.WORKSPACE),q={name:h,identName:m.name,reference:m.reference,dependencies:new Set,peerNames:L===Un.WORKSPACE?new Set:p.packagePeers,dependencyKind:L},u.set(Y,q)}let ne;if(z?ne=2:b.linkType===bo.SOFT?ne=1:ne=0,q.hoistPriority=Math.max(q.hoistPriority||0,ne),T&&!z){let L=Ca({name:y.identName,reference:y.reference}),de=n.get(L)||new Set;n.set(L,de),de.add(q.name)}let ee=new Map(p.packageDependencies);if(e.project){let L=e.project.workspacesByCwd.get(H.toPortablePath(p.packageLocation.slice(0,-1)));if(L){let de=new Set([...Array.from(L.manifest.peerDependencies.values(),Be=>P.stringifyIdent(Be)),...Array.from(L.manifest.peerDependenciesMeta.keys())]);for(let Be of de)ee.has(Be)||(ee.set(Be,v.get(Be)||null),q.peerNames.add(Be))}}let A=Ca({name:m.name.replace(vu,""),reference:m.reference}),oe=s.get(A);if(oe)for(let L of oe)ee.set(`${L.name}${vu}`,L.reference);(p!==b||p.linkType!==bo.SOFT||!z&&(!e.selfReferencesByCwd||e.selfReferencesByCwd.get(k)))&&y.dependencies.add(q);let ce=m!==a&&p.linkType===bo.SOFT&&!m.name.endsWith(vu)&&!z;if(!$&&!ce){let L=new Map;for(let[de,Be]of ee)if(Be!==null){let Ge=r.getLocator(de,Be),re=r.getLocator(de.replace(vu,""),Be),se=r.getPackageInformation(re);if(se===null)throw new Error("Assertion failed: Expected the package to have been registered");let be=QL(se,Ge,r,l);if(e.validateExternalSoftLinks&&e.project&&be){se.packageDependencies.size>0&&(i=!0);for(let[ve,pe]of se.packageDependencies)if(pe!==null){let V=P.parseLocator(Array.isArray(pe)?`${pe[0]}@${pe[1]}`:`${ve}@${pe}`);if(Ca(V)!==Ca(Ge)){let Qe=ee.get(ve);if(Qe){let le=P.parseLocator(Array.isArray(Qe)?`${Qe[0]}@${Qe[1]}`:`${ve}@${Qe}`);wAe(le,V)||t.push({messageName:X.NM_CANT_INSTALL_EXTERNAL_SOFT_LINK,text:`Cannot link ${P.prettyIdent(e.project.configuration,P.parseIdent(Ge.name))} into ${P.prettyLocator(e.project.configuration,P.parseLocator(`${m.name}@${m.reference}`))} dependency ${P.prettyLocator(e.project.configuration,V)} conflicts with parent dependency ${P.prettyLocator(e.project.configuration,le)}`})}else{let le=L.get(ve);if(le){let fe=le.target,gt=P.parseLocator(Array.isArray(fe)?`${fe[0]}@${fe[1]}`:`${ve}@${fe}`);wAe(gt,V)||t.push({messageName:X.NM_CANT_INSTALL_EXTERNAL_SOFT_LINK,text:`Cannot link ${P.prettyIdent(e.project.configuration,P.parseIdent(Ge.name))} into ${P.prettyLocator(e.project.configuration,P.parseLocator(`${m.name}@${m.reference}`))} dependency ${P.prettyLocator(e.project.configuration,V)} conflicts with dependency ${P.prettyLocator(e.project.configuration,gt)} from sibling portal ${P.prettyIdent(e.project.configuration,P.parseIdent(le.portal.name))}`})}else L.set(ve,{target:V.reference,portal:Ge})}}}}let he=(Z=e.hoistingLimitsByCwd)==null?void 0:Z.get(k),Fe=be?k:x.relative(l,H.toPortablePath(se.packageLocation))||Me.dot,Ue=(O=e.hoistingLimitsByCwd)==null?void 0:O.get(Fe),xe=he===Kn.DEPENDENCIES||Ue===Kn.DEPENDENCIES||Ue===Kn.WORKSPACES;f(de,se,Ge,q,p,ee,Fe,xe)}}};return f(a.name,o,a,c,o,o.packageDependencies,Me.dot,!1),{packageTree:c,hoistingLimits:n,errors:t,preserveSymlinksRequired:i}};function BAe(r,e,t){let i=t.resolveVirtual&&e.reference&&e.reference.startsWith("virtual:")?t.resolveVirtual(r.packageLocation):r.packageLocation;return H.toPortablePath(i||r.packageLocation)}function _4e(r,e,t){let i=e.getLocator(r.name.replace(vu,""),r.reference),n=e.getPackageInformation(i);if(n===null)throw new Error("Assertion failed: Expected the package to be registered");let s,o;return t.pnpifyFs?(o=H.toPortablePath(n.packageLocation),s=bo.SOFT):(o=BAe(n,r,e),s=n.linkType),{linkType:s,target:o}}var W4e=(r,e,t)=>{let i=new Map,n=(u,g,f)=>{let{linkType:h,target:p}=_4e(u,r,t);return{locator:Ca(u),nodePath:g,target:p,linkType:h,aliases:f}},s=u=>{let[g,f]=u.split("/");return f?{scope:Jr(g),name:Jr(f)}:{scope:null,name:Jr(g)}},o=new Set,a=(u,g,f)=>{if(!o.has(u)){o.add(u);for(let h of u.dependencies){if(h===u)continue;let p=Array.from(h.references).sort(),m={name:h.identName,reference:p[0]},{name:y,scope:b}=s(h.name),v=b?[b,y]:[y],k=x.join(g,yAe),T=x.join(k,...v),Y=`${f}/${m.name}`,q=n(m,f,p.slice(1)),$=!1;if(q.linkType===bo.SOFT&&t.project){let z=t.project.workspacesByCwd.get(q.target.slice(0,-1));$=!!(z&&!z.manifest.name)}if(!h.name.endsWith(vu)&&!$){let z=i.get(T);if(z){if(z.dirList)throw new Error(`Assertion failed: ${T} cannot merge dir node with leaf node`);{let oe=P.parseLocator(z.locator),ce=P.parseLocator(q.locator);if(z.linkType!==q.linkType)throw new Error(`Assertion failed: ${T} cannot merge nodes with different link types ${z.nodePath}/${P.stringifyLocator(oe)} and ${f}/${P.stringifyLocator(ce)}`);if(oe.identHash!==ce.identHash)throw new Error(`Assertion failed: ${T} cannot merge nodes with different idents ${z.nodePath}/${P.stringifyLocator(oe)} and ${f}/s${P.stringifyLocator(ce)}`);q.aliases=[...q.aliases,...z.aliases,P.parseLocator(z.locator).reference]}}i.set(T,q);let ne=T.split("/"),ee=ne.indexOf(yAe),A=ne.length-1;for(;ee>=0&&A>ee;){let oe=H.toPortablePath(ne.slice(0,A).join(x.sep)),ce=Jr(ne[A]),Z=i.get(oe);if(!Z)i.set(oe,{dirList:new Set([ce])});else if(Z.dirList){if(Z.dirList.has(ce))break;Z.dirList.add(ce)}A--}}a(h,q.linkType===bo.SOFT?q.target:T,Y)}}},l=n({name:e.name,reference:Array.from(e.references)[0]},"",[]),c=l.target;return i.set(c,l),a(e,c,""),i};var TL={};ft(TL,{PnpInstaller:()=>ah,PnpLinker:()=>xu,default:()=>Eze,getPnpPath:()=>Ol,jsInstallUtils:()=>ma,pnpUtils:()=>NL,quotePathIfNeeded:()=>WAe});var qAe=ge(ri()),JAe=ge(require("url"));var bAe;(function(t){t.HARD="HARD",t.SOFT="SOFT"})(bAe||(bAe={}));var er;(function(f){f.DEFAULT="DEFAULT",f.TOP_LEVEL="TOP_LEVEL",f.FALLBACK_EXCLUSION_LIST="FALLBACK_EXCLUSION_LIST",f.FALLBACK_EXCLUSION_ENTRIES="FALLBACK_EXCLUSION_ENTRIES",f.FALLBACK_EXCLUSION_DATA="FALLBACK_EXCLUSION_DATA",f.PACKAGE_REGISTRY_DATA="PACKAGE_REGISTRY_DATA",f.PACKAGE_REGISTRY_ENTRIES="PACKAGE_REGISTRY_ENTRIES",f.PACKAGE_STORE_DATA="PACKAGE_STORE_DATA",f.PACKAGE_STORE_ENTRIES="PACKAGE_STORE_ENTRIES",f.PACKAGE_INFORMATION_DATA="PACKAGE_INFORMATION_DATA",f.PACKAGE_DEPENDENCIES="PACKAGE_DEPENDENCIES",f.PACKAGE_DEPENDENCY="PACKAGE_DEPENDENCY"})(er||(er={}));var QAe={[er.DEFAULT]:{collapsed:!1,next:{["*"]:er.DEFAULT}},[er.TOP_LEVEL]:{collapsed:!1,next:{fallbackExclusionList:er.FALLBACK_EXCLUSION_LIST,packageRegistryData:er.PACKAGE_REGISTRY_DATA,["*"]:er.DEFAULT}},[er.FALLBACK_EXCLUSION_LIST]:{collapsed:!1,next:{["*"]:er.FALLBACK_EXCLUSION_ENTRIES}},[er.FALLBACK_EXCLUSION_ENTRIES]:{collapsed:!0,next:{["*"]:er.FALLBACK_EXCLUSION_DATA}},[er.FALLBACK_EXCLUSION_DATA]:{collapsed:!0,next:{["*"]:er.DEFAULT}},[er.PACKAGE_REGISTRY_DATA]:{collapsed:!1,next:{["*"]:er.PACKAGE_REGISTRY_ENTRIES}},[er.PACKAGE_REGISTRY_ENTRIES]:{collapsed:!0,next:{["*"]:er.PACKAGE_STORE_DATA}},[er.PACKAGE_STORE_DATA]:{collapsed:!1,next:{["*"]:er.PACKAGE_STORE_ENTRIES}},[er.PACKAGE_STORE_ENTRIES]:{collapsed:!0,next:{["*"]:er.PACKAGE_INFORMATION_DATA}},[er.PACKAGE_INFORMATION_DATA]:{collapsed:!1,next:{packageDependencies:er.PACKAGE_DEPENDENCIES,["*"]:er.DEFAULT}},[er.PACKAGE_DEPENDENCIES]:{collapsed:!1,next:{["*"]:er.PACKAGE_DEPENDENCY}},[er.PACKAGE_DEPENDENCY]:{collapsed:!0,next:{["*"]:er.DEFAULT}}};function V4e(r,e,t){let i="";i+="[";for(let n=0,s=r.length;ns(o)));let n=t.map((s,o)=>o);return n.sort((s,o)=>{for(let a of i){let l=a[s]a[o]?1:0;if(l!==0)return l}return 0}),n.map(s=>t[s])}function eze(r){let e=new Map,t=Um(r.fallbackExclusionList||[],[({name:i,reference:n})=>i,({name:i,reference:n})=>n]);for(let{name:i,reference:n}of t){let s=e.get(i);typeof s=="undefined"&&e.set(i,s=new Set),s.add(n)}return Array.from(e).map(([i,n])=>[i,Array.from(n)])}function tze(r){return Um(r.fallbackPool||[],([e])=>e)}function rze(r){let e=[];for(let[t,i]of Um(r.packageRegistry,([n])=>n===null?"0":`1${n}`)){let n=[];e.push([t,n]);for(let[s,{packageLocation:o,packageDependencies:a,packagePeers:l,linkType:c,discardFromLookup:u}]of Um(i,([g])=>g===null?"0":`1${g}`)){let g=[];t!==null&&s!==null&&!a.has(t)&&g.push([t,s]);for(let[p,m]of Um(a.entries(),([y])=>y))g.push([p,m]);let f=l&&l.size>0?Array.from(l):void 0,h=u||void 0;n.push([s,{packageLocation:o,packageDependencies:g,packagePeers:f,linkType:c,discardFromLookup:h}])}}return e}function Km(r){return{__info:["This file is automatically generated. Do not touch it, or risk","your modifications being lost. We also recommend you not to read","it either without using the @yarnpkg/pnp package, as the data layout","is entirely unspecified and WILL change from a version to another."],dependencyTreeRoots:r.dependencyTreeRoots,enableTopLevelFallback:r.enableTopLevelFallback||!1,ignorePatternData:r.ignorePattern||null,fallbackExclusionList:eze(r),fallbackPool:tze(r),packageRegistryData:rze(r)}}var PAe=ge(xAe());function DAe(r,e){return[r?`${r} +`:"",`/* eslint-disable */ + +`,`try { +`,` Object.freeze({}).detectStrictMode = true; +`,`} catch (error) { +`," throw new Error(`The whole PnP file got strict-mode-ified, which is known to break (Emscripten libraries aren't strict mode). This usually happens when the file goes through Babel.`);\n",`} +`,` +`,`function $$SETUP_STATE(hydrateRuntimeState, basePath) { +`,e.replace(/^/gm," "),`} +`,` +`,(0,PAe.default)()].join("")}function ize(r){return JSON.stringify(r,null,2)}function nze(r){return`'${r.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/\n/g,`\\ +`)}'`}function sze(r){return[`return hydrateRuntimeState(JSON.parse(${nze(vAe(r))}), {basePath: basePath || __dirname}); +`].join("")}function oze(r){return[`var path = require('path'); +`,`var dataLocation = path.resolve(__dirname, ${JSON.stringify(r)}); +`,`return hydrateRuntimeState(require(dataLocation), {basePath: basePath || path.dirname(dataLocation)}); +`].join("")}function RAe(r){let e=Km(r),t=sze(e);return DAe(r.shebang,t)}function FAe(r){let e=Km(r),t=oze(r.dataLocation),i=DAe(r.shebang,t);return{dataFile:ize(e),loaderFile:i}}var TAe=ge(require("fs")),gze=ge(require("path")),OAe=ge(require("util"));function vL(r,{basePath:e}){let t=H.toPortablePath(e),i=x.resolve(t),n=r.ignorePatternData!==null?new RegExp(r.ignorePatternData):null,s=new Map,o=new Map(r.packageRegistryData.map(([g,f])=>[g,new Map(f.map(([h,p])=>{var k;if(g===null!=(h===null))throw new Error("Assertion failed: The name and reference should be null, or neither should");let m=(k=p.discardFromLookup)!=null?k:!1,y={name:g,reference:h},b=s.get(p.packageLocation);b?(b.discardFromLookup=b.discardFromLookup&&m,m||(b.locator=y)):s.set(p.packageLocation,{locator:y,discardFromLookup:m});let v=null;return[h,{packageDependencies:new Map(p.packageDependencies),packagePeers:new Set(p.packagePeers),linkType:p.linkType,discardFromLookup:m,get packageLocation(){return v||(v=x.join(i,p.packageLocation))}}]}))])),a=new Map(r.fallbackExclusionList.map(([g,f])=>[g,new Set(f)])),l=new Map(r.fallbackPool),c=r.dependencyTreeRoots,u=r.enableTopLevelFallback;return{basePath:t,dependencyTreeRoots:c,enableTopLevelFallback:u,fallbackExclusionList:a,fallbackPool:l,ignorePattern:n,packageLocatorsByLocations:s,packageRegistry:o}}var Hm=ge(require("module"));function oh(r,e){if(typeof r=="string")return r;if(r){let t,i;if(Array.isArray(r)){for(t=0;t0)return(f=oh(n[g],u))?f.replace("*",c.substring(g.length-1)):ku(i,c,1)}return ku(i,c)}}var kL=ge(require("util"));var ur;(function(c){c.API_ERROR="API_ERROR",c.BUILTIN_NODE_RESOLUTION_FAILED="BUILTIN_NODE_RESOLUTION_FAILED",c.EXPORTS_RESOLUTION_FAILED="EXPORTS_RESOLUTION_FAILED",c.MISSING_DEPENDENCY="MISSING_DEPENDENCY",c.MISSING_PEER_DEPENDENCY="MISSING_PEER_DEPENDENCY",c.QUALIFIED_PATH_RESOLUTION_FAILED="QUALIFIED_PATH_RESOLUTION_FAILED",c.INTERNAL="INTERNAL",c.UNDECLARED_DEPENDENCY="UNDECLARED_DEPENDENCY",c.UNSUPPORTED="UNSUPPORTED"})(ur||(ur={}));var Aze=new Set([ur.BUILTIN_NODE_RESOLUTION_FAILED,ur.MISSING_DEPENDENCY,ur.MISSING_PEER_DEPENDENCY,ur.QUALIFIED_PATH_RESOLUTION_FAILED,ur.UNDECLARED_DEPENDENCY]);function ai(r,e,t={},i){i!=null||(i=Aze.has(r)?"MODULE_NOT_FOUND":r);let n={configurable:!0,writable:!0,enumerable:!1};return Object.defineProperties(new Error(e),{code:te(N({},n),{value:i}),pnpCode:te(N({},n),{value:r}),data:te(N({},n),{value:t})})}function Qo(r){return H.normalize(H.fromPortablePath(r))}var lze=ge(require("fs")),LAe=ge(require("module")),cze=ge(require("path")),uze=new Set(LAe.Module.builtinModules||Object.keys(process.binding("natives"))),rb=r=>r.startsWith("node:")||uze.has(r);function xL(r,e){let t=Number(process.env.PNP_ALWAYS_WARN_ON_FALLBACK)>0,i=Number(process.env.PNP_DEBUG_LEVEL),n=/^(?![a-zA-Z]:[\\/]|\\\\|\.{0,2}(?:\/|$))((?:node:)?(?:@[^/]+\/)?[^/]+)\/*(.*|)$/,s=/^(\/|\.{1,2}(\/|$))/,o=/\/$/,a=/^\.{0,2}\//,l={name:null,reference:null},c=[],u=new Set;if(r.enableTopLevelFallback===!0&&c.push(l),e.compatibilityMode!==!1)for(let re of["react-scripts","gatsby"]){let se=r.packageRegistry.get(re);if(se)for(let be of se.keys()){if(be===null)throw new Error("Assertion failed: This reference shouldn't be null");c.push({name:re,reference:be})}}let{ignorePattern:g,packageRegistry:f,packageLocatorsByLocations:h}=r;function p(re,se){return{fn:re,args:se,error:null,result:null}}function m(re){var Ue,xe,ve,pe,V,Qe;let se=(ve=(xe=(Ue=process.stderr)==null?void 0:Ue.hasColors)==null?void 0:xe.call(Ue))!=null?ve:process.stdout.isTTY,be=(le,fe)=>`[${le}m${fe}`,he=re.error;console.error(he?be("31;1",`\u2716 ${(pe=re.error)==null?void 0:pe.message.replace(/\n.*/s,"")}`):be("33;1","\u203C Resolution")),re.args.length>0&&console.error();for(let le of re.args)console.error(` ${be("37;1","In \u2190")} ${(0,kL.inspect)(le,{colors:se,compact:!0})}`);re.result&&(console.error(),console.error(` ${be("37;1","Out \u2192")} ${(0,kL.inspect)(re.result,{colors:se,compact:!0})}`));let Fe=(Qe=(V=new Error().stack.match(/(?<=^ +)at.*/gm))==null?void 0:V.slice(2))!=null?Qe:[];if(Fe.length>0){console.error();for(let le of Fe)console.error(` ${be("38;5;244",le)}`)}console.error()}function y(re,se){if(e.allowDebug===!1)return se;if(Number.isFinite(i)){if(i>=2)return(...be)=>{let he=p(re,be);try{return he.result=se(...be)}catch(Fe){throw he.error=Fe}finally{m(he)}};if(i>=1)return(...be)=>{try{return se(...be)}catch(he){let Fe=p(re,be);throw Fe.error=he,m(Fe),he}}}return se}function b(re){let se=A(re);if(!se)throw ai(ur.INTERNAL,"Couldn't find a matching entry in the dependency tree for the specified parent (this is probably an internal error)");return se}function v(re){if(re.name===null)return!0;for(let se of r.dependencyTreeRoots)if(se.name===re.name&&se.reference===re.reference)return!0;return!1}let k=new Set(["default","node","require"]);function T(re,se=k){let be=Z(x.join(re,"internal.js"),{resolveIgnored:!0,includeDiscardFromLookup:!0});if(be===null)throw ai(ur.INTERNAL,`The locator that owns the "${re}" path can't be found inside the dependency tree (this is probably an internal error)`);let{packageLocation:he}=b(be),Fe=x.join(he,xt.manifest);if(!e.fakeFs.existsSync(Fe))return null;let Ue=JSON.parse(e.fakeFs.readFileSync(Fe,"utf8")),xe=x.contains(he,re);if(xe===null)throw ai(ur.INTERNAL,"unqualifiedPath doesn't contain the packageLocation (this is probably an internal error)");a.test(xe)||(xe=`./${xe}`);let ve;try{ve=NAe(Ue,x.normalize(xe),{conditions:se,unsafe:!0})}catch(pe){throw ai(ur.EXPORTS_RESOLUTION_FAILED,pe.message,{unqualifiedPath:Qo(re),locator:be,pkgJson:Ue,subpath:Qo(xe),conditions:se},"ERR_PACKAGE_PATH_NOT_EXPORTED")}return typeof ve=="string"?x.join(he,ve):null}function Y(re,se,{extensions:be}){let he;try{se.push(re),he=e.fakeFs.statSync(re)}catch(Fe){}if(he&&!he.isDirectory())return e.fakeFs.realpathSync(re);if(he&&he.isDirectory()){let Fe;try{Fe=JSON.parse(e.fakeFs.readFileSync(x.join(re,xt.manifest),"utf8"))}catch(xe){}let Ue;if(Fe&&Fe.main&&(Ue=x.resolve(re,Fe.main)),Ue&&Ue!==re){let xe=Y(Ue,se,{extensions:be});if(xe!==null)return xe}}for(let Fe=0,Ue=be.length;Fe{let ve=JSON.stringify(xe.name);if(he.has(ve))return;he.add(ve);let pe=oe(xe);for(let V of pe)if(b(V).packagePeers.has(re))Fe(V);else{let le=be.get(V.name);typeof le=="undefined"&&be.set(V.name,le=new Set),le.add(V.reference)}};Fe(se);let Ue=[];for(let xe of[...be.keys()].sort())for(let ve of[...be.get(xe)].sort())Ue.push({name:xe,reference:ve});return Ue}function Z(re,{resolveIgnored:se=!1,includeDiscardFromLookup:be=!1}={}){if(z(re)&&!se)return null;let he=x.relative(r.basePath,re);he.match(s)||(he=`./${he}`),he.endsWith("/")||(he=`${he}/`);do{let Fe=h.get(he);if(typeof Fe=="undefined"||Fe.discardFromLookup&&!be){he=he.substring(0,he.lastIndexOf("/",he.length-2)+1);continue}return Fe.locator}while(he!=="");return null}function O(re,se,{considerBuiltins:be=!0}={}){if(re==="pnpapi")return H.toPortablePath(e.pnpapiResolution);if(be&&rb(re))return null;let he=Qo(re),Fe=se&&Qo(se);if(se&&z(se)&&(!x.isAbsolute(re)||Z(re)===null)){let ve=$(re,se);if(ve===!1)throw ai(ur.BUILTIN_NODE_RESOLUTION_FAILED,`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer was explicitely ignored by the regexp) + +Require request: "${he}" +Required by: ${Fe} +`,{request:he,issuer:Fe});return H.toPortablePath(ve)}let Ue,xe=re.match(n);if(xe){if(!se)throw ai(ur.API_ERROR,"The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:he,issuer:Fe});let[,ve,pe]=xe,V=Z(se);if(!V){let jt=$(re,se);if(jt===!1)throw ai(ur.BUILTIN_NODE_RESOLUTION_FAILED,`The builtin node resolution algorithm was unable to resolve the requested module (it didn't go through the pnp resolver because the issuer doesn't seem to be part of the Yarn-managed dependency tree). + +Require path: "${he}" +Required by: ${Fe} +`,{request:he,issuer:Fe});return H.toPortablePath(jt)}let le=b(V).packageDependencies.get(ve),fe=null;if(le==null&&V.name!==null){let jt=r.fallbackExclusionList.get(V.name);if(!jt||!jt.has(V.reference)){for(let Oi=0,$s=c.length;Oi<$s;++Oi){let jn=b(c[Oi]).packageDependencies.get(ve);if(jn!=null){t?fe=jn:le=jn;break}}if(r.enableTopLevelFallback&&le==null&&fe===null){let Oi=r.fallbackPool.get(ve);Oi!=null&&(fe=Oi)}}}let gt=null;if(le===null)if(v(V))gt=ai(ur.MISSING_PEER_DEPENDENCY,`Your application tried to access ${ve} (a peer dependency); this isn't allowed as there is no ancestor to satisfy the requirement. Use a devDependency if needed. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${Fe} +`,{request:he,issuer:Fe,dependencyName:ve});else{let jt=ce(ve,V);jt.every(Qr=>v(Qr))?gt=ai(ur.MISSING_PEER_DEPENDENCY,`${V.name} tried to access ${ve} (a peer dependency) but it isn't provided by your application; this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${V.name}@${V.reference} (via ${Fe}) +${jt.map(Qr=>`Ancestor breaking the chain: ${Qr.name}@${Qr.reference} +`).join("")} +`,{request:he,issuer:Fe,issuerLocator:Object.assign({},V),dependencyName:ve,brokenAncestors:jt}):gt=ai(ur.MISSING_PEER_DEPENDENCY,`${V.name} tried to access ${ve} (a peer dependency) but it isn't provided by its ancestors; this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${V.name}@${V.reference} (via ${Fe}) + +${jt.map(Qr=>`Ancestor breaking the chain: ${Qr.name}@${Qr.reference} +`).join("")} +`,{request:he,issuer:Fe,issuerLocator:Object.assign({},V),dependencyName:ve,brokenAncestors:jt})}else le===void 0&&(!be&&rb(re)?v(V)?gt=ai(ur.UNDECLARED_DEPENDENCY,`Your application tried to access ${ve}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${ve} isn't otherwise declared in your dependencies, this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${Fe} +`,{request:he,issuer:Fe,dependencyName:ve}):gt=ai(ur.UNDECLARED_DEPENDENCY,`${V.name} tried to access ${ve}. While this module is usually interpreted as a Node builtin, your resolver is running inside a non-Node resolution context where such builtins are ignored. Since ${ve} isn't otherwise declared in ${V.name}'s dependencies, this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${Fe} +`,{request:he,issuer:Fe,issuerLocator:Object.assign({},V),dependencyName:ve}):v(V)?gt=ai(ur.UNDECLARED_DEPENDENCY,`Your application tried to access ${ve}, but it isn't declared in your dependencies; this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${Fe} +`,{request:he,issuer:Fe,dependencyName:ve}):gt=ai(ur.UNDECLARED_DEPENDENCY,`${V.name} tried to access ${ve}, but it isn't declared in its dependencies; this makes the require call ambiguous and unsound. + +Required package: ${ve}${ve!==he?` (via "${he}")`:""} +Required by: ${V.name}@${V.reference} (via ${Fe}) +`,{request:he,issuer:Fe,issuerLocator:Object.assign({},V),dependencyName:ve}));if(le==null){if(fe===null||gt===null)throw gt||new Error("Assertion failed: Expected an error to have been set");le=fe;let jt=gt.message.replace(/\n.*/g,"");gt.message=jt,!u.has(jt)&&i!==0&&(u.add(jt),process.emitWarning(gt))}let Ht=Array.isArray(le)?{name:le[0],reference:le[1]}:{name:ve,reference:le},Mt=b(Ht);if(!Mt.packageLocation)throw ai(ur.MISSING_DEPENDENCY,`A dependency seems valid but didn't get installed for some reason. This might be caused by a partial install, such as dev vs prod. + +Required package: ${Ht.name}@${Ht.reference}${Ht.name!==he?` (via "${he}")`:""} +Required by: ${V.name}@${V.reference} (via ${Fe}) +`,{request:he,issuer:Fe,dependencyLocator:Object.assign({},Ht)});let Ei=Mt.packageLocation;pe?Ue=x.join(Ei,pe):Ue=Ei}else if(x.isAbsolute(re))Ue=x.normalize(re);else{if(!se)throw ai(ur.API_ERROR,"The resolveToUnqualified function must be called with a valid issuer when the path isn't a builtin nor absolute",{request:he,issuer:Fe});let ve=x.resolve(se);se.match(o)?Ue=x.normalize(x.join(ve,re)):Ue=x.normalize(x.join(x.dirname(ve),re))}return x.normalize(Ue)}function L(re,se,be=k){if(s.test(re))return se;let he=T(se,be);return he?x.normalize(he):se}function de(re,{extensions:se=Object.keys(Hm.Module._extensions)}={}){var Fe,Ue;let be=[],he=Y(re,be,{extensions:se});if(he)return x.normalize(he);{let xe=Qo(re),ve=Z(re);if(ve){let{packageLocation:pe}=b(ve),V=!0;try{e.fakeFs.accessSync(pe)}catch(Qe){if((Qe==null?void 0:Qe.code)==="ENOENT")V=!1;else{let le=((Ue=(Fe=Qe==null?void 0:Qe.message)!=null?Fe:Qe)!=null?Ue:"empty exception thrown").replace(/^[A-Z]/,fe=>fe.toLowerCase());throw ai(ur.QUALIFIED_PATH_RESOLUTION_FAILED,`Required package exists but could not be accessed (${le}). + +Missing package: ${ve.name}@${ve.reference} +Expected package location: ${Qo(pe)} +`,{unqualifiedPath:xe,extensions:se})}}if(!V){let Qe=pe.includes("/unplugged/")?"Required unplugged package missing from disk. This may happen when switching branches without running installs (unplugged packages must be fully materialized on disk to work).":"Required package missing from disk. If you keep your packages inside your repository then restarting the Node process may be enough. Otherwise, try to run an install first.";throw ai(ur.QUALIFIED_PATH_RESOLUTION_FAILED,`${Qe} + +Missing package: ${ve.name}@${ve.reference} +Expected package location: ${Qo(pe)} +`,{unqualifiedPath:xe,extensions:se})}}throw ai(ur.QUALIFIED_PATH_RESOLUTION_FAILED,`Qualified path resolution failed: we looked for the following paths, but none could be accessed. + +Source path: ${xe} +${be.map(pe=>`Not found: ${Qo(pe)} +`).join("")}`,{unqualifiedPath:xe,extensions:se})}}function Be(re,se,{considerBuiltins:be,extensions:he,conditions:Fe}={}){try{let Ue=O(re,se,{considerBuiltins:be});if(re==="pnpapi")return Ue;if(Ue===null)return null;let xe=()=>se!==null?z(se):!1,ve=(!be||!rb(re))&&!xe()?L(re,Ue,Fe):Ue;return de(ve,{extensions:he})}catch(Ue){throw Object.prototype.hasOwnProperty.call(Ue,"pnpCode")&&Object.assign(Ue.data,{request:Qo(re),issuer:se&&Qo(se)}),Ue}}function Ge(re){let se=x.normalize(re),be=Wr.resolveVirtual(se);return be!==se?be:null}return{VERSIONS:ne,topLevel:ee,getLocator:(re,se)=>Array.isArray(se)?{name:se[0],reference:se[1]}:{name:re,reference:se},getDependencyTreeRoots:()=>[...r.dependencyTreeRoots],getAllLocators(){let re=[];for(let[se,be]of f)for(let he of be.keys())se!==null&&he!==null&&re.push({name:se,reference:he});return re},getPackageInformation:re=>{let se=A(re);if(se===null)return null;let be=H.fromPortablePath(se.packageLocation);return te(N({},se),{packageLocation:be})},findPackageLocator:re=>Z(H.toPortablePath(re)),resolveToUnqualified:y("resolveToUnqualified",(re,se,be)=>{let he=se!==null?H.toPortablePath(se):null,Fe=O(H.toPortablePath(re),he,be);return Fe===null?null:H.fromPortablePath(Fe)}),resolveUnqualified:y("resolveUnqualified",(re,se)=>H.fromPortablePath(de(H.toPortablePath(re),se))),resolveRequest:y("resolveRequest",(re,se,be)=>{let he=se!==null?H.toPortablePath(se):null,Fe=Be(H.toPortablePath(re),he,be);return Fe===null?null:H.fromPortablePath(Fe)}),resolveVirtual:y("resolveVirtual",re=>{let se=Ge(H.toPortablePath(re));return se!==null?H.fromPortablePath(se):null})}}var U0t=(0,OAe.promisify)(TAe.readFile);var MAe=(r,e,t)=>{let i=Km(r),n=vL(i,{basePath:e}),s=H.join(e,xt.pnpCjs);return xL(n,{fakeFs:t,pnpapiResolution:s})};var DL=ge(KAe());var ma={};ft(ma,{checkAndReportManifestCompatibility:()=>jAe,checkManifestCompatibility:()=>HAe,extractBuildScripts:()=>ib,getExtractHint:()=>RL,hasBindingGyp:()=>FL});function HAe(r){return P.isPackageCompatible(r,Xg.getArchitectureSet())}function jAe(r,e,{configuration:t,report:i}){return HAe(r)?!0:(i==null||i.reportWarningOnce(X.INCOMPATIBLE_ARCHITECTURE,`${P.prettyLocator(t,r)} The ${Xg.getArchitectureName()} architecture is incompatible with this package, ${e} skipped.`),!1)}function ib(r,e,t,{configuration:i,report:n}){let s=[];for(let a of["preinstall","install","postinstall"])e.manifest.scripts.has(a)&&s.push([cs.SCRIPT,a]);return!e.manifest.scripts.has("install")&&e.misc.hasBindingGyp&&s.push([cs.SHELLCODE,"node-gyp rebuild"]),s.length===0?[]:r.linkType!==Qt.HARD?(n==null||n.reportWarningOnce(X.SOFT_LINK_BUILD,`${P.prettyLocator(i,r)} lists build scripts, but is referenced through a soft link. Soft links don't support build scripts, so they'll be ignored.`),[]):t&&t.built===!1?(n==null||n.reportInfoOnce(X.BUILD_DISABLED,`${P.prettyLocator(i,r)} lists build scripts, but its build has been explicitly disabled through configuration.`),[]):!i.get("enableScripts")&&!t.built?(n==null||n.reportWarningOnce(X.DISABLED_BUILD_SCRIPTS,`${P.prettyLocator(i,r)} lists build scripts, but all build scripts have been disabled.`),[]):jAe(r,"build",{configuration:i,report:n})?s:[]}var fze=new Set([".exe",".h",".hh",".hpp",".c",".cc",".cpp",".java",".jar",".node"]);function RL(r){return r.packageFs.getExtractHint({relevantExtensions:fze})}function FL(r){let e=x.join(r.prefixPath,"binding.gyp");return r.packageFs.existsSync(e)}var NL={};ft(NL,{getUnpluggedPath:()=>jm});function jm(r,{configuration:e}){return x.resolve(e.get("pnpUnpluggedFolder"),P.slugifyLocator(r))}var hze=new Set([P.makeIdent(null,"nan").identHash,P.makeIdent(null,"node-gyp").identHash,P.makeIdent(null,"node-pre-gyp").identHash,P.makeIdent(null,"node-addon-api").identHash,P.makeIdent(null,"fsevents").identHash,P.makeIdent(null,"open").identHash,P.makeIdent(null,"opn").identHash]),xu=class{constructor(){this.mode="strict";this.pnpCache=new Map}supportsPackage(e,t){return this.isEnabled(t)}async findPackageLocation(e,t){if(!this.isEnabled(t))throw new Error("Assertion failed: Expected the PnP linker to be enabled");let i=Ol(t.project).cjs;if(!U.existsSync(i))throw new Pe(`The project in ${ae.pretty(t.project.configuration,`${t.project.cwd}/package.json`,ae.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let n=Se.getFactoryWithDefault(this.pnpCache,i,()=>Se.dynamicRequire(i,{cachingStrategy:Se.CachingStrategy.FsTime})),s={name:P.stringifyIdent(e),reference:e.reference},o=n.getPackageInformation(s);if(!o)throw new Pe(`Couldn't find ${P.prettyLocator(t.project.configuration,e)} in the currently installed PnP map - running an install might help`);return H.toPortablePath(o.packageLocation)}async findPackageLocator(e,t){if(!this.isEnabled(t))return null;let i=Ol(t.project).cjs;if(!U.existsSync(i))return null;let s=Se.getFactoryWithDefault(this.pnpCache,i,()=>Se.dynamicRequire(i,{cachingStrategy:Se.CachingStrategy.FsTime})).findPackageLocator(H.fromPortablePath(e));return s?P.makeLocator(P.parseIdent(s.name),s.reference):null}makeInstaller(e){return new ah(e)}isEnabled(e){return!(e.project.configuration.get("nodeLinker")!=="pnp"||e.project.configuration.get("pnpMode")!==this.mode)}},ah=class{constructor(e){this.opts=e;this.mode="strict";this.asyncActions=new Se.AsyncActions(10);this.packageRegistry=new Map;this.virtualTemplates=new Map;this.isESMLoaderRequired=!1;this.customData={store:new Map};this.unpluggedPaths=new Set;this.opts=e}getCustomDataKey(){return JSON.stringify({name:"PnpInstaller",version:2})}attachCustomData(e){this.customData=e}async installPackage(e,t,i){let n=P.stringifyIdent(e),s=e.reference,o=!!this.opts.project.tryWorkspaceByLocator(e),a=P.isVirtualLocator(e),l=e.peerDependencies.size>0&&!a,c=!l&&!o,u=!l&&e.linkType!==Qt.SOFT,g,f;if(c||u){let k=a?P.devirtualizeLocator(e):e;g=this.customData.store.get(k.locatorHash),typeof g=="undefined"&&(g=await pze(t),e.linkType===Qt.HARD&&this.customData.store.set(k.locatorHash,g)),g.manifest.type==="module"&&(this.isESMLoaderRequired=!0),f=this.opts.project.getDependencyMeta(k,e.version)}let h=c?ib(e,g,f,{configuration:this.opts.project.configuration,report:this.opts.report}):[],p=u?await this.unplugPackageIfNeeded(e,g,t,f,i):t.packageFs;if(x.isAbsolute(t.prefixPath))throw new Error(`Assertion failed: Expected the prefix path (${t.prefixPath}) to be relative to the parent`);let m=x.resolve(p.getRealPath(),t.prefixPath),y=LL(this.opts.project.cwd,m),b=new Map,v=new Set;if(a){for(let k of e.peerDependencies.values())b.set(P.stringifyIdent(k),null),v.add(P.stringifyIdent(k));if(!o){let k=P.devirtualizeLocator(e);this.virtualTemplates.set(k.locatorHash,{location:LL(this.opts.project.cwd,Wr.resolveVirtual(m)),locator:k})}}return Se.getMapWithDefault(this.packageRegistry,n).set(s,{packageLocation:y,packageDependencies:b,packagePeers:v,linkType:e.linkType,discardFromLookup:t.discardFromLookup||!1}),{packageLocation:m,buildDirective:h.length>0?h:null}}async attachInternalDependencies(e,t){let i=this.getPackageInformation(e);for(let[n,s]of t){let o=P.areIdentsEqual(n,s)?s.reference:[P.stringifyIdent(s),s.reference];i.packageDependencies.set(P.stringifyIdent(n),o)}}async attachExternalDependents(e,t){for(let i of t)this.getDiskInformation(i).packageDependencies.set(P.stringifyIdent(e),e.reference)}async finalizeInstall(){if(this.opts.project.configuration.get("pnpMode")!==this.mode)return;let e=Ol(this.opts.project);if(U.existsSync(e.cjsLegacy)&&(this.opts.report.reportWarning(X.UNNAMED,`Removing the old ${ae.pretty(this.opts.project.configuration,xt.pnpJs,ae.Type.PATH)} file. You might need to manually update existing references to reference the new ${ae.pretty(this.opts.project.configuration,xt.pnpCjs,ae.Type.PATH)} file. If you use Editor SDKs, you'll have to rerun ${ae.pretty(this.opts.project.configuration,"yarn sdks",ae.Type.CODE)}.`),await U.removePromise(e.cjsLegacy)),this.isEsmEnabled()||await U.removePromise(e.esmLoader),this.opts.project.configuration.get("nodeLinker")!=="pnp"){await U.removePromise(e.cjs),await U.removePromise(this.opts.project.configuration.get("pnpDataPath")),await U.removePromise(e.esmLoader);return}for(let{locator:u,location:g}of this.virtualTemplates.values())Se.getMapWithDefault(this.packageRegistry,P.stringifyIdent(u)).set(u.reference,{packageLocation:g,packageDependencies:new Map,packagePeers:new Set,linkType:Qt.SOFT,discardFromLookup:!1});this.packageRegistry.set(null,new Map([[null,this.getPackageInformation(this.opts.project.topLevelWorkspace.anchoredLocator)]]));let t=this.opts.project.configuration.get("pnpFallbackMode"),i=this.opts.project.workspaces.map(({anchoredLocator:u})=>({name:P.stringifyIdent(u),reference:u.reference})),n=t!=="none",s=[],o=new Map,a=Se.buildIgnorePattern([".yarn/sdks/**",...this.opts.project.configuration.get("pnpIgnorePatterns")]),l=this.packageRegistry,c=this.opts.project.configuration.get("pnpShebang");if(t==="dependencies-only")for(let u of this.opts.project.storedPackages.values())this.opts.project.tryWorkspaceByLocator(u)&&s.push({name:P.stringifyIdent(u),reference:u.reference});return await this.asyncActions.wait(),await this.finalizeInstallWithPnp({dependencyTreeRoots:i,enableTopLevelFallback:n,fallbackExclusionList:s,fallbackPool:o,ignorePattern:a,packageRegistry:l,shebang:c}),{customData:this.customData}}async transformPnpSettings(e){}isEsmEnabled(){if(this.opts.project.configuration.sources.has("pnpEnableEsmLoader"))return this.opts.project.configuration.get("pnpEnableEsmLoader");if(this.isESMLoaderRequired)return!0;for(let e of this.opts.project.workspaces)if(e.manifest.type==="module")return!0;return!1}async finalizeInstallWithPnp(e){let t=Ol(this.opts.project),i=this.opts.project.configuration.get("pnpDataPath"),n=await this.locateNodeModules(e.ignorePattern);if(n.length>0){this.opts.report.reportWarning(X.DANGEROUS_NODE_MODULES,"One or more node_modules have been detected and will be removed. This operation may take some time.");for(let o of n)await U.removePromise(o)}if(await this.transformPnpSettings(e),this.opts.project.configuration.get("pnpEnableInlining")){let o=RAe(e);await U.changeFilePromise(t.cjs,o,{automaticNewlines:!0,mode:493}),await U.removePromise(i)}else{let o=x.relative(x.dirname(t.cjs),i),{dataFile:a,loaderFile:l}=FAe(te(N({},e),{dataLocation:o}));await U.changeFilePromise(t.cjs,l,{automaticNewlines:!0,mode:493}),await U.changeFilePromise(i,a,{automaticNewlines:!0,mode:420})}this.isEsmEnabled()&&(this.opts.report.reportWarning(X.UNNAMED,"ESM support for PnP uses the experimental loader API and is therefore experimental"),await U.changeFilePromise(t.esmLoader,(0,DL.default)(),{automaticNewlines:!0,mode:420}));let s=this.opts.project.configuration.get("pnpUnpluggedFolder");if(this.unpluggedPaths.size===0)await U.removePromise(s);else for(let o of await U.readdirPromise(s)){let a=x.resolve(s,o);this.unpluggedPaths.has(a)||await U.removePromise(a)}}async locateNodeModules(e){let t=[],i=e?new RegExp(e):null;for(let n of this.opts.project.workspaces){let s=x.join(n.cwd,"node_modules");if(i&&i.test(x.relative(this.opts.project.cwd,n.cwd))||!U.existsSync(s))continue;let o=await U.readdirPromise(s,{withFileTypes:!0}),a=o.filter(l=>!l.isDirectory()||l.name===".bin"||!l.name.startsWith("."));if(a.length===o.length)t.push(s);else for(let l of a)t.push(x.join(s,l.name))}return t}async unplugPackageIfNeeded(e,t,i,n,s){return this.shouldBeUnplugged(e,t,n)?this.unplugPackage(e,i,s):i.packageFs}shouldBeUnplugged(e,t,i){return typeof i.unplugged!="undefined"?i.unplugged:hze.has(e.identHash)||e.conditions!=null?!0:t.manifest.preferUnplugged!==null?t.manifest.preferUnplugged:!!(ib(e,t,i,{configuration:this.opts.project.configuration}).length>0||t.misc.extractHint)}async unplugPackage(e,t,i){let n=jm(e,{configuration:this.opts.project.configuration});return this.opts.project.disabledLocators.has(e.locatorHash)?new La(n,{baseFs:t.packageFs,pathUtils:x}):(this.unpluggedPaths.add(n),i.holdFetchResult(this.asyncActions.set(e.locatorHash,async()=>{let s=x.join(n,t.prefixPath,".ready");await U.existsPromise(s)||(this.opts.project.storedBuildState.delete(e.locatorHash),await U.mkdirPromise(n,{recursive:!0}),await U.copyPromise(n,Me.dot,{baseFs:t.packageFs,overwrite:!1}),await U.writeFilePromise(s,""))})),new _t(n))}getPackageInformation(e){let t=P.stringifyIdent(e),i=e.reference,n=this.packageRegistry.get(t);if(!n)throw new Error(`Assertion failed: The package information store should have been available (for ${P.prettyIdent(this.opts.project.configuration,e)})`);let s=n.get(i);if(!s)throw new Error(`Assertion failed: The package information should have been available (for ${P.prettyLocator(this.opts.project.configuration,e)})`);return s}getDiskInformation(e){let t=Se.getMapWithDefault(this.packageRegistry,"@@disk"),i=LL(this.opts.project.cwd,e);return Se.getFactoryWithDefault(t,i,()=>({packageLocation:i,packageDependencies:new Map,packagePeers:new Set,linkType:Qt.SOFT,discardFromLookup:!1}))}};function LL(r,e){let t=x.relative(r,e);return t.match(/^\.{0,2}\//)||(t=`./${t}`),t.replace(/\/?$/,"/")}async function pze(r){var i;let e=(i=await At.tryFind(r.prefixPath,{baseFs:r.packageFs}))!=null?i:new At,t=new Set(["preinstall","install","postinstall"]);for(let n of e.scripts.keys())t.has(n)||e.scripts.delete(n);return{manifest:{scripts:e.scripts,preferUnplugged:e.preferUnplugged,type:e.type},misc:{extractHint:RL(r),hasBindingGyp:FL(r)}}}var GAe=ge(ns());var Gm=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Unplug direct dependencies from the entire project"});this.recursive=J.Boolean("-R,--recursive",!1,{description:"Unplug both direct and transitive dependencies"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.patterns=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);if(e.get("nodeLinker")!=="pnp")throw new Pe("This command can only be used if the `nodeLinker` option is set to `pnp`");await t.restoreInstallState();let s=new Set(this.patterns),o=this.patterns.map(f=>{let h=P.parseDescriptor(f),p=h.range!=="unknown"?h:P.makeDescriptor(h,"*");if(!Wt.validRange(p.range))throw new Pe(`The range of the descriptor patterns must be a valid semver range (${P.prettyDescriptor(e,p)})`);return m=>{let y=P.stringifyIdent(m);return!GAe.default.isMatch(y,P.stringifyIdent(p))||m.version&&!Wt.satisfiesWithPrereleases(m.version,p.range)?!1:(s.delete(f),!0)}}),a=()=>{let f=[];for(let h of t.storedPackages.values())!t.tryWorkspaceByLocator(h)&&!P.isVirtualLocator(h)&&o.some(p=>p(h))&&f.push(h);return f},l=f=>{let h=new Set,p=[],m=(y,b)=>{if(!h.has(y.locatorHash)&&(h.add(y.locatorHash),!t.tryWorkspaceByLocator(y)&&o.some(v=>v(y))&&p.push(y),!(b>0&&!this.recursive)))for(let v of y.dependencies.values()){let k=t.storedResolutions.get(v.descriptorHash);if(!k)throw new Error("Assertion failed: The resolution should have been registered");let T=t.storedPackages.get(k);if(!T)throw new Error("Assertion failed: The package should have been registered");m(T,b+1)}};for(let y of f){let b=t.storedPackages.get(y.anchoredLocator.locatorHash);if(!b)throw new Error("Assertion failed: The package should have been registered");m(b,0)}return p},c,u;if(this.all&&this.recursive?(c=a(),u="the project"):this.all?(c=l(t.workspaces),u="any workspace"):(c=l([i]),u="this workspace"),s.size>1)throw new Pe(`Patterns ${ae.prettyList(e,s,ae.Type.CODE)} don't match any packages referenced by ${u}`);if(s.size>0)throw new Pe(`Pattern ${ae.prettyList(e,s,ae.Type.CODE)} doesn't match any packages referenced by ${u}`);return c=Se.sortMap(c,f=>P.stringifyLocator(f)),(await Je.start({configuration:e,stdout:this.context.stdout,json:this.json},async f=>{var h;for(let p of c){let m=(h=p.version)!=null?h:"unknown",y=t.topLevelWorkspace.manifest.ensureDependencyMeta(P.makeDescriptor(p,m));y.unplugged=!0,f.reportInfo(X.UNNAMED,`Will unpack ${P.prettyLocator(e,p)} to ${ae.pretty(e,jm(p,{configuration:e}),ae.Type.PATH)}`),f.reportJson({locator:P.stringifyLocator(p),version:m})}await t.topLevelWorkspace.persistManifest(),f.reportSeparator(),await t.install({cache:n,report:f})})).exitCode()}};Gm.paths=[["unplug"]],Gm.usage=Re.Usage({description:"force the unpacking of a list of packages",details:"\n This command will add the selectors matching the specified patterns to the list of packages that must be unplugged when installed.\n\n A package being unplugged means that instead of being referenced directly through its archive, it will be unpacked at install time in the directory configured via `pnpUnpluggedFolder`. Note that unpacking packages this way is generally not recommended because it'll make it harder to store your packages within the repository. However, it's a good approach to quickly and safely debug some packages, and can even sometimes be required depending on the context (for example when the package contains shellscripts).\n\n Running the command will set a persistent flag inside your top-level `package.json`, in the `dependenciesMeta` field. As such, to undo its effects, you'll need to revert the changes made to the manifest and run `yarn install` to apply the modification.\n\n By default, only direct dependencies from the current workspace are affected. If `-A,--all` is set, direct dependencies from the entire project are affected. Using the `-R,--recursive` flag will affect transitive dependencies as well as direct ones.\n\n This command accepts glob patterns inside the scope and name components (not the range). Make sure to escape the patterns to prevent your own shell from trying to expand them.\n ",examples:[["Unplug the lodash dependency from the active workspace","yarn unplug lodash"],["Unplug all instances of lodash referenced by any workspace","yarn unplug lodash -A"],["Unplug all instances of lodash referenced by the active workspace and its dependencies","yarn unplug lodash -R"],["Unplug all instances of lodash, anywhere","yarn unplug lodash -AR"],["Unplug one specific version of lodash","yarn unplug lodash@1.2.3"],["Unplug all packages with the `@babel` scope","yarn unplug '@babel/*'"],["Unplug all packages (only for testing, not recommended)","yarn unplug -R '*'"]]});var YAe=Gm;var Ol=r=>({cjs:x.join(r.cwd,xt.pnpCjs),cjsLegacy:x.join(r.cwd,xt.pnpJs),esmLoader:x.join(r.cwd,".pnp.loader.mjs")}),WAe=r=>/\s/.test(r)?JSON.stringify(r):r;async function dze(r,e,t){let i=Ol(r),n=`--require ${WAe(H.fromPortablePath(i.cjs))}`;if(U.existsSync(i.esmLoader)&&(n=`${n} --experimental-loader ${(0,JAe.pathToFileURL)(H.fromPortablePath(i.esmLoader)).href}`),i.cjs.includes(" ")&&qAe.default.lt(process.versions.node,"12.0.0"))throw new Error(`Expected the build location to not include spaces when using Node < 12.0.0 (${process.versions.node})`);if(U.existsSync(i.cjs)){let s=e.NODE_OPTIONS||"",o=/\s*--require\s+\S*\.pnp\.c?js\s*/g,a=/\s*--experimental-loader\s+\S*\.pnp\.loader\.mjs\s*/;s=s.replace(o," ").replace(a," ").trim(),s=s?`${n} ${s}`:n,e.NODE_OPTIONS=s}}async function Cze(r,e){let t=Ol(r);e(t.cjs),e(t.esmLoader),e(r.configuration.get("pnpDataPath")),e(r.configuration.get("pnpUnpluggedFolder"))}var mze={hooks:{populateYarnPaths:Cze,setupScriptEnvironment:dze},configuration:{nodeLinker:{description:'The linker used for installing Node packages, one of: "pnp", "node-modules"',type:Ie.STRING,default:"pnp"},pnpMode:{description:"If 'strict', generates standard PnP maps. If 'loose', merges them with the n_m resolution.",type:Ie.STRING,default:"strict"},pnpShebang:{description:"String to prepend to the generated PnP script",type:Ie.STRING,default:"#!/usr/bin/env node"},pnpIgnorePatterns:{description:"Array of glob patterns; files matching them will use the classic resolution",type:Ie.STRING,default:[],isArray:!0},pnpEnableEsmLoader:{description:"If true, Yarn will generate an ESM loader (`.pnp.loader.mjs`). If this is not explicitly set Yarn tries to automatically detect whether ESM support is required.",type:Ie.BOOLEAN,default:!1},pnpEnableInlining:{description:"If true, the PnP data will be inlined along with the generated loader",type:Ie.BOOLEAN,default:!0},pnpFallbackMode:{description:"If true, the generated PnP loader will follow the top-level fallback rule",type:Ie.STRING,default:"dependencies-only"},pnpUnpluggedFolder:{description:"Folder where the unplugged packages must be stored",type:Ie.ABSOLUTE_PATH,default:"./.yarn/unplugged"},pnpDataPath:{description:"Path of the file where the PnP data (used by the loader) must be written",type:Ie.ABSOLUTE_PATH,default:"./.pnp.data.json"}},linkers:[xu],commands:[YAe]},Eze=mze;var $Ae=ge(ZAe());var HL=ge(require("crypto")),ele=ge(require("fs")),tle=1,jr="node_modules",nb=".bin",rle=".yarn-state.yml",Ti;(function(i){i.CLASSIC="classic",i.HARDLINKS_LOCAL="hardlinks-local",i.HARDLINKS_GLOBAL="hardlinks-global"})(Ti||(Ti={}));var jL=class{constructor(){this.installStateCache=new Map}supportsPackage(e,t){return this.isEnabled(t)}async findPackageLocation(e,t){if(!this.isEnabled(t))throw new Error("Assertion failed: Expected the node-modules linker to be enabled");let i=t.project.tryWorkspaceByLocator(e);if(i)return i.cwd;let n=await Se.getFactoryWithDefault(this.installStateCache,t.project.cwd,async()=>await GL(t.project,{unrollAliases:!0}));if(n===null)throw new Pe("Couldn't find the node_modules state file - running an install might help (findPackageLocation)");let s=n.locatorMap.get(P.stringifyLocator(e));if(!s){let a=new Pe(`Couldn't find ${P.prettyLocator(t.project.configuration,e)} in the currently installed node_modules map - running an install might help`);throw a.code="LOCATOR_NOT_INSTALLED",a}let o=t.project.configuration.startingCwd;return s.locations.find(a=>x.contains(o,a))||s.locations[0]}async findPackageLocator(e,t){if(!this.isEnabled(t))return null;let i=await Se.getFactoryWithDefault(this.installStateCache,t.project.cwd,async()=>await GL(t.project,{unrollAliases:!0}));if(i===null)return null;let{locationRoot:n,segments:s}=sb(x.resolve(e),{skipPrefix:t.project.cwd}),o=i.locationTree.get(n);if(!o)return null;let a=o.locator;for(let l of s){if(o=o.children.get(l),!o)break;a=o.locator||a}return P.parseLocator(a)}makeInstaller(e){return new ile(e)}isEnabled(e){return e.project.configuration.get("nodeLinker")==="node-modules"}},ile=class{constructor(e){this.opts=e;this.localStore=new Map;this.realLocatorChecksums=new Map;this.customData={store:new Map}}getCustomDataKey(){return JSON.stringify({name:"NodeModulesInstaller",version:2})}attachCustomData(e){this.customData=e}async installPackage(e,t){var u;let i=x.resolve(t.packageFs.getRealPath(),t.prefixPath),n=this.customData.store.get(e.locatorHash);if(typeof n=="undefined"&&(n=await Tze(e,t),e.linkType===Qt.HARD&&this.customData.store.set(e.locatorHash,n)),!P.isPackageCompatible(e,this.opts.project.configuration.getSupportedArchitectures()))return{packageLocation:null,buildDirective:null};let s=new Map,o=new Set;s.has(P.stringifyIdent(e))||s.set(P.stringifyIdent(e),e.reference);let a=e;if(P.isVirtualLocator(e)){a=P.devirtualizeLocator(e);for(let g of e.peerDependencies.values())s.set(P.stringifyIdent(g),null),o.add(P.stringifyIdent(g))}let l={packageLocation:`${H.fromPortablePath(i)}/`,packageDependencies:s,packagePeers:o,linkType:e.linkType,discardFromLookup:(u=t.discardFromLookup)!=null?u:!1};this.localStore.set(e.locatorHash,{pkg:e,customPackageData:n,dependencyMeta:this.opts.project.getDependencyMeta(e,e.version),pnpNode:l});let c=t.checksum?t.checksum.substring(t.checksum.indexOf("/")+1):null;return this.realLocatorChecksums.set(a.locatorHash,c),{packageLocation:i,buildDirective:null}}async attachInternalDependencies(e,t){let i=this.localStore.get(e.locatorHash);if(typeof i=="undefined")throw new Error("Assertion failed: Expected information object to have been registered");for(let[n,s]of t){let o=P.areIdentsEqual(n,s)?s.reference:[P.stringifyIdent(s),s.reference];i.pnpNode.packageDependencies.set(P.stringifyIdent(n),o)}}async attachExternalDependents(e,t){throw new Error("External dependencies haven't been implemented for the node-modules linker")}async finalizeInstall(){if(this.opts.project.configuration.get("nodeLinker")!=="node-modules")return;let e=new Wr({baseFs:new ys({libzip:await fn(),maxOpenFiles:80,readOnlyArchives:!0})}),t=await GL(this.opts.project),i=this.opts.project.configuration.get("nmMode");(t===null||i!==t.nmMode)&&(this.opts.project.storedBuildState.clear(),t={locatorMap:new Map,binSymlinks:new Map,locationTree:new Map,nmMode:i,mtimeMs:0});let n=new Map(this.opts.project.workspaces.map(f=>{var p,m;let h=this.opts.project.configuration.get("nmHoistingLimits");try{h=Se.validateEnum(Kn,(m=(p=f.manifest.installConfig)==null?void 0:p.hoistingLimits)!=null?m:h)}catch(y){let b=P.prettyWorkspace(this.opts.project.configuration,f);this.opts.report.reportWarning(X.INVALID_MANIFEST,`${b}: Invalid 'installConfig.hoistingLimits' value. Expected one of ${Object.values(Kn).join(", ")}, using default: "${h}"`)}return[f.relativeCwd,h]})),s=new Map(this.opts.project.workspaces.map(f=>{var p,m;let h=this.opts.project.configuration.get("nmSelfReferences");return h=(m=(p=f.manifest.installConfig)==null?void 0:p.selfReferences)!=null?m:h,[f.relativeCwd,h]})),o={VERSIONS:{std:1},topLevel:{name:null,reference:null},getLocator:(f,h)=>Array.isArray(h)?{name:h[0],reference:h[1]}:{name:f,reference:h},getDependencyTreeRoots:()=>this.opts.project.workspaces.map(f=>{let h=f.anchoredLocator;return{name:P.stringifyIdent(f.locator),reference:h.reference}}),getPackageInformation:f=>{let h=f.reference===null?this.opts.project.topLevelWorkspace.anchoredLocator:P.makeLocator(P.parseIdent(f.name),f.reference),p=this.localStore.get(h.locatorHash);if(typeof p=="undefined")throw new Error("Assertion failed: Expected the package reference to have been registered");return p.pnpNode},findPackageLocator:f=>{let h=this.opts.project.tryWorkspaceByCwd(H.toPortablePath(f));if(h!==null){let p=h.anchoredLocator;return{name:P.stringifyIdent(p),reference:p.reference}}throw new Error("Assertion failed: Unimplemented")},resolveToUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveUnqualified:()=>{throw new Error("Assertion failed: Unimplemented")},resolveRequest:()=>{throw new Error("Assertion failed: Unimplemented")},resolveVirtual:f=>H.fromPortablePath(Wr.resolveVirtual(H.toPortablePath(f)))},{tree:a,errors:l,preserveSymlinksRequired:c}=Mm(o,{pnpifyFs:!1,validateExternalSoftLinks:!0,hoistingLimitsByCwd:n,project:this.opts.project,selfReferencesByCwd:s});if(!a){for(let{messageName:f,text:h}of l)this.opts.report.reportError(f,h);return}let u=bL(a);await Oze(t,u,{baseFs:e,project:this.opts.project,report:this.opts.report,realLocatorChecksums:this.realLocatorChecksums,loadManifest:async f=>{let h=P.parseLocator(f),p=this.localStore.get(h.locatorHash);if(typeof p=="undefined")throw new Error("Assertion failed: Expected the slot to exist");return p.customPackageData.manifest}});let g=[];for(let[f,h]of u.entries()){if(nle(f))continue;let p=P.parseLocator(f),m=this.localStore.get(p.locatorHash);if(typeof m=="undefined")throw new Error("Assertion failed: Expected the slot to exist");if(this.opts.project.tryWorkspaceByLocator(m.pkg))continue;let y=ma.extractBuildScripts(m.pkg,m.customPackageData,m.dependencyMeta,{configuration:this.opts.project.configuration,report:this.opts.report});y.length!==0&&g.push({buildLocations:h.locations,locatorHash:p.locatorHash,buildDirective:y})}return c&&this.opts.report.reportWarning(X.NM_PRESERVE_SYMLINKS_REQUIRED,`The application uses portals and that's why ${ae.pretty(this.opts.project.configuration,"--preserve-symlinks",ae.Type.CODE)} Node option is required for launching it`),{customData:this.customData,records:g}}};async function Tze(r,e){var n;let t=(n=await At.tryFind(e.prefixPath,{baseFs:e.packageFs}))!=null?n:new At,i=new Set(["preinstall","install","postinstall"]);for(let s of t.scripts.keys())i.has(s)||t.scripts.delete(s);return{manifest:{bin:t.bin,scripts:t.scripts},misc:{extractHint:ma.getExtractHint(e),hasBindingGyp:ma.hasBindingGyp(e)}}}async function Mze(r,e,t,i,{installChangedByUser:n}){let s="";s+=`# Warning: This file is automatically generated. Removing it is fine, but will +`,s+=`# cause your node_modules installation to become invalidated. +`,s+=` +`,s+=`__metadata: +`,s+=` version: ${tle} +`,s+=` nmMode: ${i.value} +`;let o=Array.from(e.keys()).sort(),a=P.stringifyLocator(r.topLevelWorkspace.anchoredLocator);for(let u of o){let g=e.get(u);s+=` +`,s+=`${JSON.stringify(u)}: +`,s+=` locations: +`;for(let f of g.locations){let h=x.contains(r.cwd,f);if(h===null)throw new Error(`Assertion failed: Expected the path to be within the project (${f})`);s+=` - ${JSON.stringify(h)} +`}if(g.aliases.length>0){s+=` aliases: +`;for(let f of g.aliases)s+=` - ${JSON.stringify(f)} +`}if(u===a&&t.size>0){s+=` bin: +`;for(let[f,h]of t){let p=x.contains(r.cwd,f);if(p===null)throw new Error(`Assertion failed: Expected the path to be within the project (${f})`);s+=` ${JSON.stringify(p)}: +`;for(let[m,y]of h){let b=x.relative(x.join(f,jr),y);s+=` ${JSON.stringify(m)}: ${JSON.stringify(b)} +`}}}}let l=r.cwd,c=x.join(l,jr,rle);n&&await U.removePromise(c),await U.changeFilePromise(c,s,{automaticNewlines:!0})}async function GL(r,{unrollAliases:e=!1}={}){let t=r.cwd,i=x.join(t,jr,rle),n;try{n=await U.statPromise(i)}catch(c){}if(!n)return null;let s=Si(await U.readFilePromise(i,"utf8"));if(s.__metadata.version>tle)return null;let o=s.__metadata.nmMode||Ti.CLASSIC,a=new Map,l=new Map;delete s.__metadata;for(let[c,u]of Object.entries(s)){let g=u.locations.map(h=>x.join(t,h)),f=u.bin;if(f)for(let[h,p]of Object.entries(f)){let m=x.join(t,H.toPortablePath(h)),y=Se.getMapWithDefault(l,m);for(let[b,v]of Object.entries(p))y.set(Jr(b),H.toPortablePath([m,jr,v].join(x.sep)))}if(a.set(c,{target:Me.dot,linkType:Qt.HARD,locations:g,aliases:u.aliases||[]}),e&&u.aliases)for(let h of u.aliases){let{scope:p,name:m}=P.parseLocator(c),y=P.makeLocator(P.makeIdent(p,m),h),b=P.stringifyLocator(y);a.set(b,{target:Me.dot,linkType:Qt.HARD,locations:g,aliases:[]})}}return{locatorMap:a,binSymlinks:l,locationTree:sle(a,{skipPrefix:r.cwd}),nmMode:o,mtimeMs:n.mtimeMs}}var lh=async(r,e)=>{if(r.split(x.sep).indexOf(jr)<0)throw new Error(`Assertion failed: trying to remove dir that doesn't contain node_modules: ${r}`);try{if(!e.innerLoop){let i=e.allowSymlink?await U.statPromise(r):await U.lstatPromise(r);if(e.allowSymlink&&!i.isDirectory()||!e.allowSymlink&&i.isSymbolicLink()){await U.unlinkPromise(r);return}}let t=await U.readdirPromise(r,{withFileTypes:!0});for(let i of t){let n=x.join(r,Jr(i.name));i.isDirectory()?(i.name!==jr||e&&e.innerLoop)&&await lh(n,{innerLoop:!0,contentsOnly:!1}):await U.unlinkPromise(n)}e.contentsOnly||await U.rmdirPromise(r)}catch(t){if(t.code!=="ENOENT"&&t.code!=="ENOTEMPTY")throw t}},ole=4,sb=(r,{skipPrefix:e})=>{let t=x.contains(e,r);if(t===null)throw new Error(`Assertion failed: Writing attempt prevented to ${r} which is outside project root: ${e}`);let i=t.split(x.sep).filter(l=>l!==""),n=i.indexOf(jr),s=i.slice(0,n).join(x.sep),o=x.join(e,s),a=i.slice(n);return{locationRoot:o,segments:a}},sle=(r,{skipPrefix:e})=>{let t=new Map;if(r===null)return t;let i=()=>({children:new Map,linkType:Qt.HARD});for(let[n,s]of r.entries()){if(s.linkType===Qt.SOFT&&x.contains(e,s.target)!==null){let a=Se.getFactoryWithDefault(t,s.target,i);a.locator=n,a.linkType=s.linkType}for(let o of s.locations){let{locationRoot:a,segments:l}=sb(o,{skipPrefix:e}),c=Se.getFactoryWithDefault(t,a,i);for(let u=0;u{let t;try{process.platform==="win32"&&(t=await U.lstatPromise(r))}catch(i){}process.platform=="win32"&&(!t||t.isDirectory())?await U.symlinkPromise(r,e,"junction"):await U.symlinkPromise(x.relative(x.dirname(e),r),e)};async function ale(r,e,t){let i=x.join(r,Jr(`${HL.default.randomBytes(16).toString("hex")}.tmp`));try{await U.writeFilePromise(i,t);try{await U.linkPromise(i,e)}catch(n){}}finally{await U.unlinkPromise(i)}}async function Uze({srcPath:r,dstPath:e,srcMode:t,globalHardlinksStore:i,baseFs:n,nmMode:s,digest:o}){if(s.value===Ti.HARDLINKS_GLOBAL&&i&&o){let l=x.join(i,o.substring(0,2),`${o.substring(2)}.dat`),c;try{if(await Rn.checksumFile(l,{baseFs:U,algorithm:"sha1"})!==o){let g=x.join(i,Jr(`${HL.default.randomBytes(16).toString("hex")}.tmp`));await U.renamePromise(l,g);let f=await n.readFilePromise(r);await U.writeFilePromise(g,f);try{await U.linkPromise(g,l),await U.unlinkPromise(g)}catch(h){}}await U.linkPromise(l,e),c=!0}catch(u){c=!1}if(!c){let u=await n.readFilePromise(r);await ale(i,l,u);try{await U.linkPromise(l,e)}catch(g){g&&g.code&&g.code=="EXDEV"&&(s.value=Ti.HARDLINKS_LOCAL,await n.copyFilePromise(r,e))}}}else await n.copyFilePromise(r,e);let a=t&511;a!==420&&await U.chmodPromise(e,a)}var Ml;(function(i){i.FILE="file",i.DIRECTORY="directory",i.SYMLINK="symlink"})(Ml||(Ml={}));var Kze=async(r,e,{baseFs:t,globalHardlinksStore:i,nmMode:n,packageChecksum:s})=>{await U.mkdirPromise(r,{recursive:!0});let o=async(l=Me.dot)=>{let c=x.join(e,l),u=await t.readdirPromise(c,{withFileTypes:!0}),g=new Map;for(let f of u){let h=x.join(l,f.name),p,m=x.join(c,f.name);if(f.isFile()){if(p={kind:Ml.FILE,mode:(await t.lstatPromise(m)).mode},n.value===Ti.HARDLINKS_GLOBAL){let y=await Rn.checksumFile(m,{baseFs:t,algorithm:"sha1"});p.digest=y}}else if(f.isDirectory())p={kind:Ml.DIRECTORY};else if(f.isSymbolicLink())p={kind:Ml.SYMLINK,symlinkTo:await t.readlinkPromise(m)};else throw new Error(`Unsupported file type (file: ${m}, mode: 0o${await t.statSync(m).mode.toString(8).padStart(6,"0")})`);if(g.set(h,p),f.isDirectory()&&h!==jr){let y=await o(h);for(let[b,v]of y)g.set(b,v)}}return g},a;if(n.value===Ti.HARDLINKS_GLOBAL&&i&&s){let l=x.join(i,s.substring(0,2),`${s.substring(2)}.json`);try{a=new Map(Object.entries(JSON.parse(await U.readFilePromise(l,"utf8"))))}catch(c){a=await o(),await ale(i,l,Buffer.from(JSON.stringify(Object.fromEntries(a))))}}else a=await o();for(let[l,c]of a){let u=x.join(e,l),g=x.join(r,l);c.kind===Ml.DIRECTORY?await U.mkdirPromise(g,{recursive:!0}):c.kind===Ml.FILE?await Uze({srcPath:u,dstPath:g,srcMode:c.mode,digest:c.digest,nmMode:n,baseFs:t,globalHardlinksStore:i}):c.kind===Ml.SYMLINK&&await YL(x.resolve(x.dirname(g),c.symlinkTo),g)}};function Hze(r,e,t,i){let n=new Map,s=new Map,o=new Map,a=!1,l=(c,u,g,f,h)=>{let p=!0,m=x.join(c,u),y=new Set;if(u===jr||u.startsWith("@")){let v;try{v=U.statSync(m)}catch(T){}p=!!v,v?v.mtimeMs>t?(a=!0,y=new Set(U.readdirSync(m))):y=new Set(g.children.get(u).children.keys()):a=!0;let k=e.get(c);if(k){let T=x.join(c,jr,nb),Y;try{Y=U.statSync(T)}catch(q){}if(!Y)a=!0;else if(Y.mtimeMs>t){a=!0;let q=new Set(U.readdirSync(T)),$=new Map;s.set(c,$);for(let[z,ne]of k)q.has(z)&&$.set(z,ne)}else s.set(c,k)}}else p=h.has(u);let b=g.children.get(u);if(p){let{linkType:v,locator:k}=b,T={children:new Map,linkType:v,locator:k};if(f.children.set(u,T),k){let Y=Se.getSetWithDefault(o,k);Y.add(m),o.set(k,Y)}for(let Y of b.children.keys())l(m,Y,b,T,y)}else b.locator&&i.storedBuildState.delete(P.parseLocator(b.locator).locatorHash)};for(let[c,u]of r){let{linkType:g,locator:f}=u,h={children:new Map,linkType:g,locator:f};if(n.set(c,h),f){let p=Se.getSetWithDefault(o,u.locator);p.add(c),o.set(u.locator,p)}u.children.has(jr)&&l(c,jr,u,h,new Set)}return{locationTree:n,binSymlinks:s,locatorLocations:o,installChangedByUser:a}}function nle(r){let e=P.parseDescriptor(r);return P.isVirtualDescriptor(e)&&(e=P.devirtualizeDescriptor(e)),e.range.startsWith("link:")}async function jze(r,e,t,{loadManifest:i}){let n=new Map;for(let[a,{locations:l}]of r){let c=nle(a)?null:await i(a,l[0]),u=new Map;if(c)for(let[g,f]of c.bin){let h=x.join(l[0],f);f!==""&&U.existsSync(h)&&u.set(g,f)}n.set(a,u)}let s=new Map,o=(a,l,c)=>{let u=new Map,g=x.contains(t,a);if(c.locator&&g!==null){let f=n.get(c.locator);for(let[h,p]of f){let m=x.join(a,H.toPortablePath(p));u.set(Jr(h),m)}for(let[h,p]of c.children){let m=x.join(a,h),y=o(m,m,p);y.size>0&&s.set(a,new Map([...s.get(a)||new Map,...y]))}}else for(let[f,h]of c.children){let p=o(x.join(a,f),l,h);for(let[m,y]of p)u.set(m,y)}return u};for(let[a,l]of e){let c=o(a,a,l);c.size>0&&s.set(a,new Map([...s.get(a)||new Map,...c]))}return s}var Ale=(r,e)=>{if(!r||!e)return r===e;let t=P.parseLocator(r);P.isVirtualLocator(t)&&(t=P.devirtualizeLocator(t));let i=P.parseLocator(e);return P.isVirtualLocator(i)&&(i=P.devirtualizeLocator(i)),P.areLocatorsEqual(t,i)};function qL(r){return x.join(r.get("globalFolder"),"store")}async function Oze(r,e,{baseFs:t,project:i,report:n,loadManifest:s,realLocatorChecksums:o}){let a=x.join(i.cwd,jr),{locationTree:l,binSymlinks:c,locatorLocations:u,installChangedByUser:g}=Hze(r.locationTree,r.binSymlinks,r.mtimeMs,i),f=sle(e,{skipPrefix:i.cwd}),h=[],p=async({srcDir:z,dstDir:ne,linkType:ee,globalHardlinksStore:A,nmMode:oe,packageChecksum:ce})=>{let Z=(async()=>{try{ee===Qt.SOFT?(await U.mkdirPromise(x.dirname(ne),{recursive:!0}),await YL(x.resolve(z),ne)):await Kze(ne,z,{baseFs:t,globalHardlinksStore:A,nmMode:oe,packageChecksum:ce})}catch(O){throw O.message=`While persisting ${z} -> ${ne} ${O.message}`,O}finally{T.tick()}})().then(()=>h.splice(h.indexOf(Z),1));h.push(Z),h.length>ole&&await Promise.race(h)},m=async(z,ne,ee)=>{let A=(async()=>{let oe=async(ce,Z,O)=>{try{O.innerLoop||await U.mkdirPromise(Z,{recursive:!0});let L=await U.readdirPromise(ce,{withFileTypes:!0});for(let de of L){if(!O.innerLoop&&de.name===nb)continue;let Be=x.join(ce,de.name),Ge=x.join(Z,de.name);de.isDirectory()?(de.name!==jr||O&&O.innerLoop)&&(await U.mkdirPromise(Ge,{recursive:!0}),await oe(Be,Ge,te(N({},O),{innerLoop:!0}))):$.value===Ti.HARDLINKS_LOCAL||$.value===Ti.HARDLINKS_GLOBAL?await U.linkPromise(Be,Ge):await U.copyFilePromise(Be,Ge,ele.default.constants.COPYFILE_FICLONE)}}catch(L){throw O.innerLoop||(L.message=`While cloning ${ce} -> ${Z} ${L.message}`),L}finally{O.innerLoop||T.tick()}};await oe(z,ne,ee)})().then(()=>h.splice(h.indexOf(A),1));h.push(A),h.length>ole&&await Promise.race(h)},y=async(z,ne,ee)=>{if(ee)for(let[A,oe]of ne.children){let ce=ee.children.get(A);await y(x.join(z,A),oe,ce)}else{ne.children.has(jr)&&await lh(x.join(z,jr),{contentsOnly:!1});let A=x.basename(z)===jr&&f.has(x.join(x.dirname(z),x.sep));await lh(z,{contentsOnly:z===a,allowSymlink:A})}};for(let[z,ne]of l){let ee=f.get(z);for(let[A,oe]of ne.children){if(A===".")continue;let ce=ee&&ee.children.get(A),Z=x.join(z,A);await y(Z,oe,ce)}}let b=async(z,ne,ee)=>{if(ee){Ale(ne.locator,ee.locator)||await lh(z,{contentsOnly:ne.linkType===Qt.HARD});for(let[A,oe]of ne.children){let ce=ee.children.get(A);await b(x.join(z,A),oe,ce)}}else{ne.children.has(jr)&&await lh(x.join(z,jr),{contentsOnly:!0});let A=x.basename(z)===jr&&f.has(x.join(x.dirname(z),x.sep));await lh(z,{contentsOnly:ne.linkType===Qt.HARD,allowSymlink:A})}};for(let[z,ne]of f){let ee=l.get(z);for(let[A,oe]of ne.children){if(A===".")continue;let ce=ee&&ee.children.get(A);await b(x.join(z,A),oe,ce)}}let v=new Map,k=[];for(let[z,ne]of u)for(let ee of ne){let{locationRoot:A,segments:oe}=sb(ee,{skipPrefix:i.cwd}),ce=f.get(A),Z=A;if(ce){for(let O of oe)if(Z=x.join(Z,O),ce=ce.children.get(O),!ce)break;if(ce){let O=Ale(ce.locator,z),L=e.get(ce.locator),de=L.target,Be=Z,Ge=L.linkType;if(O)v.has(de)||v.set(de,Be);else if(de!==Be){let re=P.parseLocator(ce.locator);P.isVirtualLocator(re)&&(re=P.devirtualizeLocator(re)),k.push({srcDir:de,dstDir:Be,linkType:Ge,realLocatorHash:re.locatorHash})}}}}for(let[z,{locations:ne}]of e.entries())for(let ee of ne){let{locationRoot:A,segments:oe}=sb(ee,{skipPrefix:i.cwd}),ce=l.get(A),Z=f.get(A),O=A,L=e.get(z),de=P.parseLocator(z);P.isVirtualLocator(de)&&(de=P.devirtualizeLocator(de));let Be=de.locatorHash,Ge=L.target,re=ee;if(Ge===re)continue;let se=L.linkType;for(let be of oe)Z=Z.children.get(be);if(!ce)k.push({srcDir:Ge,dstDir:re,linkType:se,realLocatorHash:Be});else for(let be of oe)if(O=x.join(O,be),ce=ce.children.get(be),!ce){k.push({srcDir:Ge,dstDir:re,linkType:se,realLocatorHash:Be});break}}let T=Ji.progressViaCounter(k.length),Y=n.reportProgress(T),q=i.configuration.get("nmMode"),$={value:q};try{let z=$.value===Ti.HARDLINKS_GLOBAL?`${qL(i.configuration)}/v1`:null;if(z&&!await U.existsPromise(z)){await U.mkdirpPromise(z);for(let ee=0;ee<256;ee++)await U.mkdirPromise(x.join(z,ee.toString(16).padStart(2,"0")))}for(let ee of k)(ee.linkType===Qt.SOFT||!v.has(ee.srcDir))&&(v.set(ee.srcDir,ee.dstDir),await p(te(N({},ee),{globalHardlinksStore:z,nmMode:$,packageChecksum:o.get(ee.realLocatorHash)||null})));await Promise.all(h),h.length=0;for(let ee of k){let A=v.get(ee.srcDir);ee.linkType!==Qt.SOFT&&ee.dstDir!==A&&await m(A,ee.dstDir,{nmMode:$})}await Promise.all(h),await U.mkdirPromise(a,{recursive:!0});let ne=await jze(e,f,i.cwd,{loadManifest:s});await Gze(c,ne,i.cwd),await Mze(i,e,ne,$,{installChangedByUser:g}),q==Ti.HARDLINKS_GLOBAL&&$.value==Ti.HARDLINKS_LOCAL&&n.reportWarningOnce(X.NM_HARDLINKS_MODE_DOWNGRADED,"'nmMode' has been downgraded to 'hardlinks-local' due to global cache and install folder being on different devices")}finally{Y.stop()}}async function Gze(r,e,t){for(let i of r.keys()){if(x.contains(t,i)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${i}`);if(!e.has(i)){let n=x.join(i,jr,nb);await U.removePromise(n)}}for(let[i,n]of e){if(x.contains(t,i)===null)throw new Error(`Assertion failed. Excepted bin symlink location to be inside project dir, instead it was at ${i}`);let s=x.join(i,jr,nb),o=r.get(i)||new Map;await U.mkdirPromise(s,{recursive:!0});for(let a of o.keys())n.has(a)||(await U.removePromise(x.join(s,a)),process.platform==="win32"&&await U.removePromise(x.join(s,Jr(`${a}.cmd`))));for(let[a,l]of n){let c=o.get(a),u=x.join(s,a);c!==l&&(process.platform==="win32"?await(0,$Ae.default)(H.fromPortablePath(l),H.fromPortablePath(u),{createPwshFile:!1}):(await U.removePromise(u),await YL(l,u),x.contains(t,await U.realpathPromise(l))!==null&&await U.chmodPromise(l,493)))}}}var JL=class extends xu{constructor(){super(...arguments);this.mode="loose"}makeInstaller(e){return new lle(e)}},lle=class extends ah{constructor(){super(...arguments);this.mode="loose"}async transformPnpSettings(e){let t=new Wr({baseFs:new ys({libzip:await fn(),maxOpenFiles:80,readOnlyArchives:!0})}),i=MAe(e,this.opts.project.cwd,t),{tree:n,errors:s}=Mm(i,{pnpifyFs:!1,project:this.opts.project});if(!n){for(let{messageName:u,text:g}of s)this.opts.report.reportError(u,g);return}let o=new Map;e.fallbackPool=o;let a=(u,g)=>{let f=P.parseLocator(g.locator),h=P.stringifyIdent(f);h===u?o.set(u,f.reference):o.set(u,[h,f.reference])},l=x.join(this.opts.project.cwd,xt.nodeModules),c=n.get(l);if(typeof c!="undefined"){if("target"in c)throw new Error("Assertion failed: Expected the root junction point to be a directory");for(let u of c.dirList){let g=x.join(l,u),f=n.get(g);if(typeof f=="undefined")throw new Error("Assertion failed: Expected the child to have been registered");if("target"in f)a(u,f);else for(let h of f.dirList){let p=x.join(g,h),m=n.get(p);if(typeof m=="undefined")throw new Error("Assertion failed: Expected the subchild to have been registered");if("target"in m)a(`${u}/${h}`,m);else throw new Error("Assertion failed: Expected the leaf junction to be a package")}}}}};var Yze={hooks:{cleanGlobalArtifacts:async r=>{let e=qL(r);await U.removePromise(e)}},configuration:{nmHoistingLimits:{description:"Prevent packages to be hoisted past specific levels",type:Ie.STRING,values:[Kn.WORKSPACES,Kn.DEPENDENCIES,Kn.NONE],default:Kn.NONE},nmMode:{description:'If set to "hardlinks-local" Yarn will utilize hardlinks to reduce disk space consumption inside "node_modules" directories. With "hardlinks-global" Yarn will use global content addressable storage to reduce "node_modules" size across all the projects using this option.',type:Ie.STRING,values:[Ti.CLASSIC,Ti.HARDLINKS_LOCAL,Ti.HARDLINKS_GLOBAL],default:Ti.CLASSIC},nmSelfReferences:{description:"If set to 'false' the workspace will not be allowed to require itself and corresponding self-referencing symlink will not be created",type:Ie.BOOLEAN,default:!0}},linkers:[jL,JL]},qze=Yze;var JT={};ft(JT,{default:()=>$9e,npmConfigUtils:()=>br,npmHttpUtils:()=>zt,npmPublishUtils:()=>Bh});var hle=ge(ri());var Cr="npm:";var zt={};ft(zt,{AuthType:()=>us,customPackageError:()=>zze,del:()=>Xze,get:()=>So,getIdentUrl:()=>Kl,handleInvalidAuthenticationError:()=>Ul,post:()=>_ze,put:()=>Vze});var gle=ge(zC()),fle=ge(require("url"));var br={};ft(br,{RegistryType:()=>SA,getAuditRegistry:()=>Jze,getAuthConfiguration:()=>_L,getDefaultRegistry:()=>ob,getPublishRegistry:()=>cle,getRegistryConfiguration:()=>ule,getScopeConfiguration:()=>zL,getScopeRegistry:()=>vA,normalizeRegistry:()=>Ea});var SA;(function(i){i.AUDIT_REGISTRY="npmAuditRegistry",i.FETCH_REGISTRY="npmRegistryServer",i.PUBLISH_REGISTRY="npmPublishRegistry"})(SA||(SA={}));function Ea(r){return r.replace(/\/$/,"")}function Jze(r,{configuration:e}){let t=e.get(SA.AUDIT_REGISTRY);return t!==null?Ea(t):cle(r,{configuration:e})}function cle(r,{configuration:e}){var t;return((t=r.publishConfig)==null?void 0:t.registry)?Ea(r.publishConfig.registry):r.name?vA(r.name.scope,{configuration:e,type:SA.PUBLISH_REGISTRY}):ob({configuration:e,type:SA.PUBLISH_REGISTRY})}function vA(r,{configuration:e,type:t=SA.FETCH_REGISTRY}){let i=zL(r,{configuration:e});if(i===null)return ob({configuration:e,type:t});let n=i.get(t);return n===null?ob({configuration:e,type:t}):Ea(n)}function ob({configuration:r,type:e=SA.FETCH_REGISTRY}){let t=r.get(e);return Ea(t!==null?t:r.get(SA.FETCH_REGISTRY))}function ule(r,{configuration:e}){let t=e.get("npmRegistries"),i=Ea(r),n=t.get(i);if(typeof n!="undefined")return n;let s=t.get(i.replace(/^[a-z]+:/,""));return typeof s!="undefined"?s:null}function zL(r,{configuration:e}){if(r===null)return null;let i=e.get("npmScopes").get(r);return i||null}function _L(r,{configuration:e,ident:t}){let i=t&&zL(t.scope,{configuration:e});return(i==null?void 0:i.get("npmAuthIdent"))||(i==null?void 0:i.get("npmAuthToken"))?i:ule(r,{configuration:e})||e}var us;(function(n){n[n.NO_AUTH=0]="NO_AUTH",n[n.BEST_EFFORT=1]="BEST_EFFORT",n[n.CONFIGURATION=2]="CONFIGURATION",n[n.ALWAYS_AUTH=3]="ALWAYS_AUTH"})(us||(us={}));async function Ul(r,{attemptedAs:e,registry:t,headers:i,configuration:n}){var s,o;if(ab(r))throw new ct(X.AUTHENTICATION_INVALID,"Invalid OTP token");if(((s=r.originalError)==null?void 0:s.name)==="HTTPError"&&((o=r.originalError)==null?void 0:o.response.statusCode)===401)throw new ct(X.AUTHENTICATION_INVALID,`Invalid authentication (${typeof e!="string"?`as ${await Wze(t,i,{configuration:n})}`:`attempted as ${e}`})`)}function zze(r){var e;return((e=r.response)==null?void 0:e.statusCode)===404?"Package not found":null}function Kl(r){return r.scope?`/@${r.scope}%2f${r.name}`:`/${r.name}`}async function So(r,a){var l=a,{configuration:e,headers:t,ident:i,authType:n,registry:s}=l,o=Or(l,["configuration","headers","ident","authType","registry"]);if(i&&typeof s=="undefined"&&(s=vA(i.scope,{configuration:e})),i&&i.scope&&typeof n=="undefined"&&(n=1),typeof s!="string")throw new Error("Assertion failed: The registry should be a string");let c=await Ab(s,{authType:n,configuration:e,ident:i});c&&(t=te(N({},t),{authorization:c}));try{return await ir.get(r.charAt(0)==="/"?`${s}${r}`:r,N({configuration:e,headers:t},o))}catch(u){throw await Ul(u,{registry:s,configuration:e,headers:t}),u}}async function _ze(r,e,u){var g=u,{attemptedAs:t,configuration:i,headers:n,ident:s,authType:o=3,registry:a,otp:l}=g,c=Or(g,["attemptedAs","configuration","headers","ident","authType","registry","otp"]);if(s&&typeof a=="undefined"&&(a=vA(s.scope,{configuration:i})),typeof a!="string")throw new Error("Assertion failed: The registry should be a string");let f=await Ab(a,{authType:o,configuration:i,ident:s});f&&(n=te(N({},n),{authorization:f})),l&&(n=N(N({},n),ch(l)));try{return await ir.post(a+r,e,N({configuration:i,headers:n},c))}catch(h){if(!ab(h)||l)throw await Ul(h,{attemptedAs:t,registry:a,configuration:i,headers:n}),h;l=await VL();let p=N(N({},n),ch(l));try{return await ir.post(`${a}${r}`,e,N({configuration:i,headers:p},c))}catch(m){throw await Ul(m,{attemptedAs:t,registry:a,configuration:i,headers:n}),m}}}async function Vze(r,e,u){var g=u,{attemptedAs:t,configuration:i,headers:n,ident:s,authType:o=3,registry:a,otp:l}=g,c=Or(g,["attemptedAs","configuration","headers","ident","authType","registry","otp"]);if(s&&typeof a=="undefined"&&(a=vA(s.scope,{configuration:i})),typeof a!="string")throw new Error("Assertion failed: The registry should be a string");let f=await Ab(a,{authType:o,configuration:i,ident:s});f&&(n=te(N({},n),{authorization:f})),l&&(n=N(N({},n),ch(l)));try{return await ir.put(a+r,e,N({configuration:i,headers:n},c))}catch(h){if(!ab(h))throw await Ul(h,{attemptedAs:t,registry:a,configuration:i,headers:n}),h;l=await VL();let p=N(N({},n),ch(l));try{return await ir.put(`${a}${r}`,e,N({configuration:i,headers:p},c))}catch(m){throw await Ul(m,{attemptedAs:t,registry:a,configuration:i,headers:n}),m}}}async function Xze(r,c){var u=c,{attemptedAs:e,configuration:t,headers:i,ident:n,authType:s=3,registry:o,otp:a}=u,l=Or(u,["attemptedAs","configuration","headers","ident","authType","registry","otp"]);if(n&&typeof o=="undefined"&&(o=vA(n.scope,{configuration:t})),typeof o!="string")throw new Error("Assertion failed: The registry should be a string");let g=await Ab(o,{authType:s,configuration:t,ident:n});g&&(i=te(N({},i),{authorization:g})),a&&(i=N(N({},i),ch(a)));try{return await ir.del(o+r,N({configuration:t,headers:i},l))}catch(f){if(!ab(f)||a)throw await Ul(f,{attemptedAs:e,registry:o,configuration:t,headers:i}),f;a=await VL();let h=N(N({},i),ch(a));try{return await ir.del(`${o}${r}`,N({configuration:t,headers:h},l))}catch(p){throw await Ul(p,{attemptedAs:e,registry:o,configuration:t,headers:i}),p}}}async function Ab(r,{authType:e=2,configuration:t,ident:i}){let n=_L(r,{configuration:t,ident:i}),s=Zze(n,e);if(!s)return null;let o=await t.reduceHook(a=>a.getNpmAuthenticationHeader,void 0,r,{configuration:t,ident:i});if(o)return o;if(n.get("npmAuthToken"))return`Bearer ${n.get("npmAuthToken")}`;if(n.get("npmAuthIdent")){let a=n.get("npmAuthIdent");return a.includes(":")?`Basic ${Buffer.from(a).toString("base64")}`:`Basic ${a}`}if(s&&e!==1)throw new ct(X.AUTHENTICATION_NOT_FOUND,"No authentication configured for request");return null}function Zze(r,e){switch(e){case 2:return r.get("npmAlwaysAuth");case 1:case 3:return!0;case 0:return!1;default:throw new Error("Unreachable")}}async function Wze(r,e,{configuration:t}){var i;if(typeof e=="undefined"||typeof e.authorization=="undefined")return"an anonymous user";try{return(i=(await ir.get(new fle.URL(`${r}/-/whoami`).href,{configuration:t,headers:e,jsonResponse:!0})).username)!=null?i:"an unknown user"}catch{return"an unknown user"}}async function VL(){if(process.env.TEST_ENV)return process.env.TEST_NPM_2FA_TOKEN||"";let{otp:r}=await(0,gle.prompt)({type:"password",name:"otp",message:"One-time password:",required:!0,onCancel:()=>process.exit(130)});return r}function ab(r){var e,t;if(((e=r.originalError)==null?void 0:e.name)!=="HTTPError")return!1;try{return((t=r.originalError)==null?void 0:t.response.headers["www-authenticate"].split(/,\s*/).map(n=>n.toLowerCase())).includes("otp")}catch(i){return!1}}function ch(r){return{["npm-otp"]:r}}var XL=class{supports(e,t){if(!e.reference.startsWith(Cr))return!1;let{selector:i,params:n}=P.parseRange(e.reference);return!(!hle.default.valid(i)||n===null||typeof n.__archiveUrl!="string")}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the remote server`),loader:()=>this.fetchFromNetwork(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),checksum:o}}async fetchFromNetwork(e,t){let{params:i}=P.parseRange(e.reference);if(i===null||typeof i.__archiveUrl!="string")throw new Error("Assertion failed: The archiveUrl querystring parameter should have been available");let n=await So(i.__archiveUrl,{configuration:t.project.configuration,ident:e});return await Bi.convertToZip(n,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1})}};var ZL=class{supportsDescriptor(e,t){return!(!e.range.startsWith(Cr)||!P.tryParseDescriptor(e.range.slice(Cr.length),!0))}supportsLocator(e,t){return!1}shouldPersistResolution(e,t){throw new Error("Unreachable")}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){let i=P.parseDescriptor(e.range.slice(Cr.length),!0);return t.resolver.getResolutionDependencies(i,t)}async getCandidates(e,t,i){let n=P.parseDescriptor(e.range.slice(Cr.length),!0);return await i.resolver.getCandidates(n,t,i)}async getSatisfying(e,t,i){let n=P.parseDescriptor(e.range.slice(Cr.length),!0);return i.resolver.getSatisfying(n,t,i)}resolve(e,t){throw new Error("Unreachable")}};var ple=ge(ri()),dle=ge(require("url"));var vo=class{supports(e,t){if(!e.reference.startsWith(Cr))return!1;let i=new dle.URL(e.reference);return!(!ple.default.valid(i.pathname)||i.searchParams.has("__archiveUrl"))}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the remote registry`),loader:()=>this.fetchFromNetwork(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),checksum:o}}async fetchFromNetwork(e,t){let i;try{i=await So(vo.getLocatorUrl(e),{configuration:t.project.configuration,ident:e})}catch(n){i=await So(vo.getLocatorUrl(e).replace(/%2f/g,"/"),{configuration:t.project.configuration,ident:e})}return await Bi.convertToZip(i,{compressionLevel:t.project.configuration.get("compressionLevel"),prefixPath:P.getIdentVendorPath(e),stripComponents:1})}static isConventionalTarballUrl(e,t,{configuration:i}){let n=vA(e.scope,{configuration:i}),s=vo.getLocatorUrl(e);return t=t.replace(/^https?:(\/\/(?:[^/]+\.)?npmjs.org(?:$|\/))/,"https:$1"),n=n.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),t=t.replace(/^https:\/\/registry\.npmjs\.org($|\/)/,"https://registry.yarnpkg.com$1"),t===n+s||t===n+s.replace(/%2f/g,"/")}static getLocatorUrl(e){let t=Wt.clean(e.reference.slice(Cr.length));if(t===null)throw new ct(X.RESOLVER_NOT_FOUND,"The npm semver resolver got selected, but the version isn't semver");return`${Kl(e)}/-/${e.name}-${t}.tgz`}};var Cle=ge(ri());var lb=P.makeIdent(null,"node-gyp"),$ze=/\b(node-gyp|prebuild-install)\b/,$L=class{supportsDescriptor(e,t){return e.range.startsWith(Cr)?!!Wt.validRange(e.range.slice(Cr.length)):!1}supportsLocator(e,t){if(!e.reference.startsWith(Cr))return!1;let{selector:i}=P.parseRange(e.reference);return!!Cle.default.valid(i)}shouldPersistResolution(e,t){return!0}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=Wt.validRange(e.range.slice(Cr.length));if(n===null)throw new Error(`Expected a valid range, got ${e.range.slice(Cr.length)}`);let s=await So(Kl(e),{configuration:i.project.configuration,ident:e,jsonResponse:!0}),o=Se.mapAndFilter(Object.keys(s.versions),c=>{try{let u=new Wt.SemVer(c);if(n.test(u))return u}catch{}return Se.mapAndFilter.skip}),a=o.filter(c=>!s.versions[c.raw].deprecated),l=a.length>0?a:o;return l.sort((c,u)=>-c.compare(u)),l.map(c=>{let u=P.makeLocator(e,`${Cr}${c.raw}`),g=s.versions[c.raw].dist.tarball;return vo.isConventionalTarballUrl(u,g,{configuration:i.project.configuration})?u:P.bindLocator(u,{__archiveUrl:g})})}async getSatisfying(e,t,i){let n=Wt.validRange(e.range.slice(Cr.length));if(n===null)throw new Error(`Expected a valid range, got ${e.range.slice(Cr.length)}`);return Se.mapAndFilter(t,s=>{try{let{selector:o}=P.parseRange(s,{requireProtocol:Cr}),a=new Wt.SemVer(o);if(n.test(a))return{reference:s,version:a}}catch{}return Se.mapAndFilter.skip}).sort((s,o)=>-s.version.compare(o.version)).map(({reference:s})=>P.makeLocator(e,s))}async resolve(e,t){let{selector:i}=P.parseRange(e.reference),n=Wt.clean(i);if(n===null)throw new ct(X.RESOLVER_NOT_FOUND,"The npm semver resolver got selected, but the version isn't semver");let s=await So(Kl(e),{configuration:t.project.configuration,ident:e,jsonResponse:!0});if(!Object.prototype.hasOwnProperty.call(s,"versions"))throw new ct(X.REMOTE_INVALID,'Registry returned invalid data for - missing "versions" field');if(!Object.prototype.hasOwnProperty.call(s.versions,n))throw new ct(X.REMOTE_NOT_FOUND,`Registry failed to return reference "${n}"`);let o=new At;if(o.load(s.versions[n]),!o.dependencies.has(lb.identHash)&&!o.peerDependencies.has(lb.identHash)){for(let a of o.scripts.values())if(a.match($ze)){o.dependencies.set(lb.identHash,P.makeDescriptor(lb,"latest")),t.report.reportWarningOnce(X.NODE_GYP_INJECTED,`${P.prettyLocator(t.project.configuration,e)}: Implicit dependencies on node-gyp are discouraged`);break}}if(typeof o.raw.deprecated=="string"&&o.raw.deprecated!==""){let a=P.prettyLocator(t.project.configuration,e),l=o.raw.deprecated.match(/\S/)?`${a} is deprecated: ${o.raw.deprecated}`:`${a} is deprecated`;t.report.reportWarningOnce(X.DEPRECATED_PACKAGE,l)}return te(N({},e),{version:n,languageName:"node",linkType:Qt.HARD,conditions:o.getConditions(),dependencies:o.dependencies,peerDependencies:o.peerDependencies,dependenciesMeta:o.dependenciesMeta,peerDependenciesMeta:o.peerDependenciesMeta,bin:o.bin})}};var eT=class{supportsDescriptor(e,t){return!(!e.range.startsWith(Cr)||!_g.test(e.range.slice(Cr.length)))}supportsLocator(e,t){return!1}shouldPersistResolution(e,t){throw new Error("Unreachable")}bindDescriptor(e,t,i){return e}getResolutionDependencies(e,t){return[]}async getCandidates(e,t,i){let n=e.range.slice(Cr.length),s=await So(Kl(e),{configuration:i.project.configuration,ident:e,jsonResponse:!0});if(!Object.prototype.hasOwnProperty.call(s,"dist-tags"))throw new ct(X.REMOTE_INVALID,'Registry returned invalid data - missing "dist-tags" field');let o=s["dist-tags"];if(!Object.prototype.hasOwnProperty.call(o,n))throw new ct(X.REMOTE_NOT_FOUND,`Registry failed to return tag "${n}"`);let a=o[n],l=P.makeLocator(e,`${Cr}${a}`),c=s.versions[a].dist.tarball;return vo.isConventionalTarballUrl(l,c,{configuration:i.project.configuration})?[l]:[P.bindLocator(l,{__archiveUrl:c})]}async getSatisfying(e,t,i){return null}async resolve(e,t){throw new Error("Unreachable")}};var Bh={};ft(Bh,{getGitHead:()=>X9e,makePublishBody:()=>V9e});var jT={};ft(jT,{default:()=>R9e,packUtils:()=>DA});var DA={};ft(DA,{genPackList:()=>Db,genPackStream:()=>HT,genPackageManifest:()=>qce,hasPackScripts:()=>UT,prepareForPack:()=>KT});var MT=ge(ns()),Gce=ge(jce()),Yce=ge(require("zlib")),y9e=["/package.json","/readme","/readme.*","/license","/license.*","/licence","/licence.*","/changelog","/changelog.*"],w9e=["/package.tgz",".github",".git",".hg","node_modules",".npmignore",".gitignore",".#*",".DS_Store"];async function UT(r){return!!(Zt.hasWorkspaceScript(r,"prepack")||Zt.hasWorkspaceScript(r,"postpack"))}async function KT(r,{report:e},t){await Zt.maybeExecuteWorkspaceLifecycleScript(r,"prepack",{report:e});try{let i=x.join(r.cwd,At.fileName);await U.existsPromise(i)&&await r.manifest.loadFile(i,{baseFs:U}),await t()}finally{await Zt.maybeExecuteWorkspaceLifecycleScript(r,"postpack",{report:e})}}async function HT(r,e){var s,o;typeof e=="undefined"&&(e=await Db(r));let t=new Set;for(let a of(o=(s=r.manifest.publishConfig)==null?void 0:s.executableFiles)!=null?o:new Set)t.add(x.normalize(a));for(let a of r.manifest.bin.values())t.add(x.normalize(a));let i=Gce.default.pack();process.nextTick(async()=>{for(let a of e){let l=x.normalize(a),c=x.resolve(r.cwd,l),u=x.join("package",l),g=await U.lstatPromise(c),f={name:u,mtime:new Date(Rr.SAFE_TIME*1e3)},h=t.has(l)?493:420,p,m,y=new Promise((v,k)=>{p=v,m=k}),b=v=>{v?m(v):p()};if(g.isFile()){let v;l==="package.json"?v=Buffer.from(JSON.stringify(await qce(r),null,2)):v=await U.readFilePromise(c),i.entry(te(N({},f),{mode:h,type:"file"}),v,b)}else g.isSymbolicLink()?i.entry(te(N({},f),{mode:h,type:"symlink",linkname:await U.readlinkPromise(c)}),b):b(new Error(`Unsupported file type ${g.mode} for ${H.fromPortablePath(l)}`));await y}i.finalize()});let n=(0,Yce.createGzip)();return i.pipe(n),n}async function qce(r){let e=JSON.parse(JSON.stringify(r.manifest.raw));return await r.project.configuration.triggerHook(t=>t.beforeWorkspacePacking,r,e),e}async function Db(r){var g,f,h,p,m,y,b,v;let e=r.project,t=e.configuration,i={accept:[],reject:[]};for(let k of w9e)i.reject.push(k);for(let k of y9e)i.accept.push(k);i.reject.push(t.get("rcFilename"));let n=k=>{if(k===null||!k.startsWith(`${r.cwd}/`))return;let T=x.relative(r.cwd,k),Y=x.resolve(Me.root,T);i.reject.push(Y)};n(x.resolve(e.cwd,t.get("lockfileFilename"))),n(t.get("cacheFolder")),n(t.get("globalFolder")),n(t.get("installStatePath")),n(t.get("virtualFolder")),n(t.get("yarnPath")),await t.triggerHook(k=>k.populateYarnPaths,e,k=>{n(k)});for(let k of e.workspaces){let T=x.relative(r.cwd,k.cwd);T!==""&&!T.match(/^(\.\.)?\//)&&i.reject.push(`/${T}`)}let s={accept:[],reject:[]},o=(f=(g=r.manifest.publishConfig)==null?void 0:g.main)!=null?f:r.manifest.main,a=(p=(h=r.manifest.publishConfig)==null?void 0:h.module)!=null?p:r.manifest.module,l=(y=(m=r.manifest.publishConfig)==null?void 0:m.browser)!=null?y:r.manifest.browser,c=(v=(b=r.manifest.publishConfig)==null?void 0:b.bin)!=null?v:r.manifest.bin;o!=null&&s.accept.push(x.resolve(Me.root,o)),a!=null&&s.accept.push(x.resolve(Me.root,a)),typeof l=="string"&&s.accept.push(x.resolve(Me.root,l));for(let k of c.values())s.accept.push(x.resolve(Me.root,k));if(l instanceof Map)for(let[k,T]of l.entries())s.accept.push(x.resolve(Me.root,k)),typeof T=="string"&&s.accept.push(x.resolve(Me.root,T));let u=r.manifest.files!==null;if(u){s.reject.push("/*");for(let k of r.manifest.files)Jce(s.accept,k,{cwd:Me.root})}return await B9e(r.cwd,{hasExplicitFileList:u,globalList:i,ignoreList:s})}async function B9e(r,{hasExplicitFileList:e,globalList:t,ignoreList:i}){let n=[],s=new Ta(r),o=[[Me.root,[i]]];for(;o.length>0;){let[a,l]=o.pop(),c=await s.lstatPromise(a);if(!zce(a,{globalList:t,ignoreLists:c.isDirectory()?null:l}))if(c.isDirectory()){let u=await s.readdirPromise(a),g=!1,f=!1;if(!e||a!==Me.root)for(let m of u)g=g||m===".gitignore",f=f||m===".npmignore";let h=f?await Wce(s,a,".npmignore"):g?await Wce(s,a,".gitignore"):null,p=h!==null?[h].concat(l):l;zce(a,{globalList:t,ignoreLists:l})&&(p=[...l,{accept:[],reject:["**/*"]}]);for(let m of u)o.push([x.resolve(a,m),p])}else(c.isFile()||c.isSymbolicLink())&&n.push(x.relative(Me.root,a))}return n.sort()}async function Wce(r,e,t){let i={accept:[],reject:[]},n=await r.readFilePromise(x.join(e,t),"utf8");for(let s of n.split(/\n/g))Jce(i.reject,s,{cwd:e});return i}function b9e(r,{cwd:e}){let t=r[0]==="!";return t&&(r=r.slice(1)),r.match(/\.{0,1}\//)&&(r=x.resolve(e,r)),t&&(r=`!${r}`),r}function Jce(r,e,{cwd:t}){let i=e.trim();i===""||i[0]==="#"||r.push(b9e(i,{cwd:t}))}var gs;(function(i){i[i.None=0]="None",i[i.Match=1]="Match",i[i.NegatedMatch=2]="NegatedMatch"})(gs||(gs={}));function zce(r,{globalList:e,ignoreLists:t}){let i=Rb(r,e.accept);if(i!==0)return i===2;let n=Rb(r,e.reject);if(n!==0)return n===1;if(t!==null)for(let s of t){let o=Rb(r,s.accept);if(o!==0)return o===2;let a=Rb(r,s.reject);if(a!==0)return a===1}return!1}function Rb(r,e){let t=e,i=[];for(let n=0;n{await KT(i,{report:l},async()=>{l.reportJson({base:H.fromPortablePath(i.cwd)});let c=await Db(i);for(let u of c)l.reportInfo(null,H.fromPortablePath(u)),l.reportJson({location:H.fromPortablePath(u)});if(!this.dryRun){let u=await HT(i,c),g=U.createWriteStream(s);u.pipe(g),await new Promise(f=>{g.on("finish",f)})}}),this.dryRun||(l.reportInfo(X.UNNAMED,`Package archive generated in ${ae.pretty(e,s,ae.Type.PATH)}`),l.reportJson({output:H.fromPortablePath(s)}))})).exitCode()}};iE.paths=[["pack"]],iE.usage=Re.Usage({description:"generate a tarball from the active workspace",details:"\n This command will turn the active workspace into a compressed archive suitable for publishing. The archive will by default be stored at the root of the workspace (`package.tgz`).\n\n If the `-o,---out` is set the archive will be created at the specified path. The `%s` and `%v` variables can be used within the path and will be respectively replaced by the package name and version.\n ",examples:[["Create an archive from the active workspace","yarn pack"],["List the files that would be made part of the workspace's archive","yarn pack --dry-run"],["Name and output the archive in a dedicated folder","yarn pack --out /artifacts/%s-%v.tgz"]]});var Vce=iE;function Q9e(r,{workspace:e}){let t=r.replace("%s",S9e(e)).replace("%v",v9e(e));return H.toPortablePath(t)}function S9e(r){return r.manifest.name!==null?P.slugifyIdent(r.manifest.name):"package"}function v9e(r){return r.manifest.version!==null?r.manifest.version:"unknown"}var k9e=["dependencies","devDependencies","peerDependencies"],x9e="workspace:",P9e=(r,e)=>{var i,n;e.publishConfig&&(e.publishConfig.main&&(e.main=e.publishConfig.main),e.publishConfig.browser&&(e.browser=e.publishConfig.browser),e.publishConfig.module&&(e.module=e.publishConfig.module),e.publishConfig.browser&&(e.browser=e.publishConfig.browser),e.publishConfig.exports&&(e.exports=e.publishConfig.exports),e.publishConfig.bin&&(e.bin=e.publishConfig.bin));let t=r.project;for(let s of k9e)for(let o of r.manifest.getForScope(s).values()){let a=t.tryWorkspaceByDescriptor(o),l=P.parseRange(o.range);if(l.protocol===x9e)if(a===null){if(t.tryWorkspaceByIdent(o)===null)throw new ct(X.WORKSPACE_NOT_FOUND,`${P.prettyDescriptor(t.configuration,o)}: No local workspace found for this range`)}else{let c;P.areDescriptorsEqual(o,a.anchoredDescriptor)||l.selector==="*"?c=(i=a.manifest.version)!=null?i:"0.0.0":l.selector==="~"||l.selector==="^"?c=`${l.selector}${(n=a.manifest.version)!=null?n:"0.0.0"}`:c=l.selector;let u=s==="dependencies"?P.makeDescriptor(o,"unknown"):null,g=u!==null&&r.manifest.ensureDependencyMeta(u).optional?"optionalDependencies":s;e[g][P.stringifyIdent(o)]=c}}},D9e={hooks:{beforeWorkspacePacking:P9e},commands:[Vce]},R9e=D9e;var sue=ge(require("crypto")),oue=ge(nue()),aue=ge(require("url"));async function V9e(r,e,{access:t,tag:i,registry:n,gitHead:s}){let o=r.project.configuration,a=r.manifest.name,l=r.manifest.version,c=P.stringifyIdent(a),u=(0,sue.createHash)("sha1").update(e).digest("hex"),g=oue.default.fromData(e).toString();typeof t=="undefined"&&(r.manifest.publishConfig&&typeof r.manifest.publishConfig.access=="string"?t=r.manifest.publishConfig.access:o.get("npmPublishAccess")!==null?t=o.get("npmPublishAccess"):a.scope?t="restricted":t="public");let f=await DA.genPackageManifest(r),h=`${c}-${l}.tgz`,p=new aue.URL(`${Ea(n)}/${c}/-/${h}`);return{_id:c,_attachments:{[h]:{content_type:"application/octet-stream",data:e.toString("base64"),length:e.length}},name:c,access:t,["dist-tags"]:{[i]:l},versions:{[l]:te(N({},f),{_id:`${c}@${l}`,name:c,version:l,gitHead:s,dist:{shasum:u,integrity:g,tarball:p.toString()}})}}}async function X9e(r){try{let{stdout:e}=await Nr.execvp("git",["rev-parse","--revs-only","HEAD"],{cwd:r});return e.trim()===""?void 0:e.trim()}catch{return}}var WT={npmAlwaysAuth:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:Ie.BOOLEAN,default:!1},npmAuthIdent:{description:"Authentication identity for the npm registry (_auth in npm and yarn v1)",type:Ie.SECRET,default:null},npmAuthToken:{description:"Authentication token for the npm registry (_authToken in npm and yarn v1)",type:Ie.SECRET,default:null}},Aue={npmAuditRegistry:{description:"Registry to query for audit reports",type:Ie.STRING,default:null},npmPublishRegistry:{description:"Registry to push packages to",type:Ie.STRING,default:null},npmRegistryServer:{description:"URL of the selected npm registry (note: npm enterprise isn't supported)",type:Ie.STRING,default:"https://registry.yarnpkg.com"}},Z9e={configuration:te(N(N({},WT),Aue),{npmScopes:{description:"Settings per package scope",type:Ie.MAP,valueDefinition:{description:"",type:Ie.SHAPE,properties:N(N({},WT),Aue)}},npmRegistries:{description:"Settings per registry",type:Ie.MAP,normalizeKeys:Ea,valueDefinition:{description:"",type:Ie.SHAPE,properties:N({},WT)}}}),fetchers:[XL,vo],resolvers:[ZL,$L,eT]},$9e=Z9e;var XT={};ft(XT,{default:()=>A_e});ws();var ba;(function(i){i.All="all",i.Production="production",i.Development="development"})(ba||(ba={}));var xo;(function(s){s.Info="info",s.Low="low",s.Moderate="moderate",s.High="high",s.Critical="critical"})(xo||(xo={}));var Fb=[xo.Info,xo.Low,xo.Moderate,xo.High,xo.Critical];function lue(r,e){let t=[],i=new Set,n=o=>{i.has(o)||(i.add(o),t.push(o))};for(let o of e)n(o);let s=new Set;for(;t.length>0;){let o=t.shift(),a=r.storedResolutions.get(o);if(typeof a=="undefined")throw new Error("Assertion failed: Expected the resolution to have been registered");let l=r.storedPackages.get(a);if(!!l){s.add(o);for(let c of l.dependencies.values())n(c.descriptorHash)}}return s}function e_e(r,e){return new Set([...r].filter(t=>!e.has(t)))}function t_e(r,e,{all:t}){let i=t?r.workspaces:[e],n=i.map(f=>f.manifest),s=new Set(n.map(f=>[...f.dependencies].map(([h,p])=>h)).flat()),o=new Set(n.map(f=>[...f.devDependencies].map(([h,p])=>h)).flat()),a=i.map(f=>[...f.dependencies.values()]).flat(),l=a.filter(f=>s.has(f.identHash)).map(f=>f.descriptorHash),c=a.filter(f=>o.has(f.identHash)).map(f=>f.descriptorHash),u=lue(r,l),g=lue(r,c);return e_e(g,u)}function cue(r){let e={};for(let t of r)e[P.stringifyIdent(t)]=P.parseRange(t.range).selector;return e}function uue(r){if(typeof r=="undefined")return new Set;let e=Fb.indexOf(r),t=Fb.slice(e);return new Set(t)}function r_e(r,e){let t=uue(e),i={};for(let n of t)i[n]=r[n];return i}function gue(r,e){var i;let t=r_e(r,e);for(let n of Object.keys(t))if((i=t[n])!=null?i:0>0)return!0;return!1}function fue(r,e){var s;let t={},i={children:t},n=Object.values(r.advisories);if(e!=null){let o=uue(e);n=n.filter(a=>o.has(a.severity))}for(let o of Se.sortMap(n,a=>a.module_name))t[o.module_name]={label:o.module_name,value:ae.tuple(ae.Type.RANGE,o.findings.map(a=>a.version).join(", ")),children:{Issue:{label:"Issue",value:ae.tuple(ae.Type.NO_HINT,o.title)},URL:{label:"URL",value:ae.tuple(ae.Type.URL,o.url)},Severity:{label:"Severity",value:ae.tuple(ae.Type.NO_HINT,o.severity)},["Vulnerable Versions"]:{label:"Vulnerable Versions",value:ae.tuple(ae.Type.RANGE,o.vulnerable_versions)},["Patched Versions"]:{label:"Patched Versions",value:ae.tuple(ae.Type.RANGE,o.patched_versions)},Via:{label:"Via",value:ae.tuple(ae.Type.NO_HINT,Array.from(new Set(o.findings.map(a=>a.paths).flat().map(a=>a.split(">")[0]))).join(", "))},Recommendation:{label:"Recommendation",value:ae.tuple(ae.Type.NO_HINT,(s=o.recommendation)==null?void 0:s.replace(/\n/g," "))}}};return i}function hue(r,e,{all:t,environment:i}){let n=t?r.workspaces:[e],s=[ba.All,ba.Production].includes(i),o=[];if(s)for(let c of n)for(let u of c.manifest.dependencies.values())o.push(u);let a=[ba.All,ba.Development].includes(i),l=[];if(a)for(let c of n)for(let u of c.manifest.devDependencies.values())l.push(u);return cue([...o,...l].filter(c=>P.parseRange(c.range).protocol===null))}function pue(r,e,{all:t}){var s;let i=t_e(r,e,{all:t}),n={};for(let o of r.storedPackages.values())n[P.stringifyIdent(o)]={version:(s=o.version)!=null?s:"0.0.0",integrity:o.identHash,requires:cue(o.dependencies.values()),dev:i.has(P.convertLocatorToDescriptor(o).descriptorHash)};return n}var oE=class extends Le{constructor(){super(...arguments);this.all=J.Boolean("-A,--all",!1,{description:"Audit dependencies from all workspaces"});this.recursive=J.Boolean("-R,--recursive",!1,{description:"Audit transitive dependencies as well"});this.environment=J.String("--environment",ba.All,{description:"Which environments to cover",validator:nn(ba)});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.severity=J.String("--severity",xo.Info,{description:"Minimal severity requested for packages to be displayed",validator:nn(xo)})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState();let n=hue(t,i,{all:this.all,environment:this.environment}),s=pue(t,i,{all:this.all});if(!this.recursive)for(let f of Object.keys(s))Object.prototype.hasOwnProperty.call(n,f)?s[f].requires={}:delete s[f];let o={requires:n,dependencies:s},a=br.getAuditRegistry(i.manifest,{configuration:e}),l,c=await dA.start({configuration:e,stdout:this.context.stdout},async()=>{l=await zt.post("/-/npm/v1/security/audits/quick",o,{authType:zt.AuthType.BEST_EFFORT,configuration:e,jsonResponse:!0,registry:a})});if(c.hasErrors())return c.exitCode();let u=gue(l.metadata.vulnerabilities,this.severity);return!this.json&&u?(ls.emitTree(fue(l,this.severity),{configuration:e,json:this.json,stdout:this.context.stdout,separators:2}),1):(await Je.start({configuration:e,includeFooter:!1,json:this.json,stdout:this.context.stdout},async f=>{f.reportJson(l),u||f.reportInfo(X.EXCEPTION,"No audit suggestions")})).exitCode()}};oE.paths=[["npm","audit"]],oE.usage=Re.Usage({description:"perform a vulnerability audit against the installed packages",details:` + This command checks for known security reports on the packages you use. The reports are by default extracted from the npm registry, and may or may not be relevant to your actual program (not all vulnerabilities affect all code paths). + + For consistency with our other commands the default is to only check the direct dependencies for the active workspace. To extend this search to all workspaces, use \`-A,--all\`. To extend this search to both direct and transitive dependencies, use \`-R,--recursive\`. + + Applying the \`--severity\` flag will limit the audit table to vulnerabilities of the corresponding severity and above. Valid values are ${Fb.map(e=>`\`${e}\``).join(", ")}. + + If the \`--json\` flag is set, Yarn will print the output exactly as received from the registry. Regardless of this flag, the process will exit with a non-zero exit code if a report is found for the selected packages. + + To understand the dependency tree requiring vulnerable packages, check the raw report with the \`--json\` flag or use \`yarn why \` to get more information as to who depends on them. + `,examples:[["Checks for known security issues with the installed packages. The output is a list of known issues.","yarn npm audit"],["Audit dependencies in all workspaces","yarn npm audit --all"],["Limit auditing to `dependencies` (excludes `devDependencies`)","yarn npm audit --environment production"],["Show audit report as valid JSON","yarn npm audit --json"],["Audit all direct and transitive dependencies","yarn npm audit --recursive"],["Output moderate (or more severe) vulnerabilities","yarn npm audit --severity moderate"]]});var due=oE;var zT=ge(ri()),_T=ge(require("util")),aE=class extends Le{constructor(){super(...arguments);this.fields=J.String("-f,--fields",{description:"A comma-separated list of manifest fields that should be displayed"});this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.packages=J.Rest()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t}=await ze.find(e,this.context.cwd),i=typeof this.fields!="undefined"?new Set(["name",...this.fields.split(/\s*,\s*/)]):null,n=[],s=!1,o=await Je.start({configuration:e,includeFooter:!1,json:this.json,stdout:this.context.stdout},async a=>{for(let l of this.packages){let c;if(l==="."){let k=t.topLevelWorkspace;if(!k.manifest.name)throw new Pe(`Missing ${ae.pretty(e,"name",ae.Type.CODE)} field in ${H.fromPortablePath(x.join(k.cwd,xt.manifest))}`);c=P.makeDescriptor(k.manifest.name,"unknown")}else c=P.parseDescriptor(l);let u=zt.getIdentUrl(c),g=VT(await zt.get(u,{configuration:e,ident:c,jsonResponse:!0,customErrorMessage:zt.customPackageError})),f=Object.keys(g.versions).sort(zT.default.compareLoose),p=g["dist-tags"].latest||f[f.length-1],m=Wt.validRange(c.range);if(m){let k=zT.default.maxSatisfying(f,m);k!==null?p=k:(a.reportWarning(X.UNNAMED,`Unmet range ${P.prettyRange(e,c.range)}; falling back to the latest version`),s=!0)}else Object.prototype.hasOwnProperty.call(g["dist-tags"],c.range)?p=g["dist-tags"][c.range]:c.range!=="unknown"&&(a.reportWarning(X.UNNAMED,`Unknown tag ${P.prettyRange(e,c.range)}; falling back to the latest version`),s=!0);let y=g.versions[p],b=te(N(N({},g),y),{version:p,versions:f}),v;if(i!==null){v={};for(let k of i){let T=b[k];if(typeof T!="undefined")v[k]=T;else{a.reportWarning(X.EXCEPTION,`The ${ae.pretty(e,k,ae.Type.CODE)} field doesn't exist inside ${P.prettyIdent(e,c)}'s information`),s=!0;continue}}}else this.json||(delete b.dist,delete b.readme,delete b.users),v=b;a.reportJson(v),this.json||n.push(v)}});_T.inspect.styles.name="cyan";for(let a of n)(a!==n[0]||s)&&this.context.stdout.write(` +`),this.context.stdout.write(`${(0,_T.inspect)(a,{depth:Infinity,colors:!0,compact:!1})} +`);return o.exitCode()}};aE.paths=[["npm","info"]],aE.usage=Re.Usage({category:"Npm-related commands",description:"show information about a package",details:"\n This command fetches information about a package from the npm registry and prints it in a tree format.\n\n The package does not have to be installed locally, but needs to have been published (in particular, local changes will be ignored even for workspaces).\n\n Append `@` to the package argument to provide information specific to the latest version that satisfies the range or to the corresponding tagged version. If the range is invalid or if there is no version satisfying the range, the command will print a warning and fall back to the latest version.\n\n If the `-f,--fields` option is set, it's a comma-separated list of fields which will be used to only display part of the package information.\n\n By default, this command won't return the `dist`, `readme`, and `users` fields, since they are often very long. To explicitly request those fields, explicitly list them with the `--fields` flag or request the output in JSON mode.\n ",examples:[["Show all available information about react (except the `dist`, `readme`, and `users` fields)","yarn npm info react"],["Show all available information about react as valid JSON (including the `dist`, `readme`, and `users` fields)","yarn npm info react --json"],["Show all available information about react@16.12.0","yarn npm info react@16.12.0"],["Show all available information about react@next","yarn npm info react@next"],["Show the description of react","yarn npm info react --fields description"],["Show all available versions of react","yarn npm info react --fields versions"],["Show the readme of react","yarn npm info react --fields readme"],["Show a few fields of react","yarn npm info react --fields homepage,repository"]]});var Cue=aE;function VT(r){if(Array.isArray(r)){let e=[];for(let t of r)t=VT(t),t&&e.push(t);return e}else if(typeof r=="object"&&r!==null){let e={};for(let t of Object.keys(r)){if(t.startsWith("_"))continue;let i=VT(r[t]);i&&(e[t]=i)}return e}else return r||null}var mue=ge(zC()),AE=class extends Le{constructor(){super(...arguments);this.scope=J.String("-s,--scope",{description:"Login to the registry configured for a given scope"});this.publish=J.Boolean("--publish",!1,{description:"Login to the publish registry"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=await Nb({configuration:e,cwd:this.context.cwd,publish:this.publish,scope:this.scope});return(await Je.start({configuration:e,stdout:this.context.stdout},async n=>{let s=await n_e({registry:t,report:n,stdin:this.context.stdin,stdout:this.context.stdout}),o=`/-/user/org.couchdb.user:${encodeURIComponent(s.name)}`,a=await zt.put(o,s,{attemptedAs:s.name,configuration:e,registry:t,jsonResponse:!0,authType:zt.AuthType.NO_AUTH});return await i_e(t,a.token,{configuration:e,scope:this.scope}),n.reportInfo(X.UNNAMED,"Successfully logged in")})).exitCode()}};AE.paths=[["npm","login"]],AE.usage=Re.Usage({category:"Npm-related commands",description:"store new login info to access the npm registry",details:"\n This command will ask you for your username, password, and 2FA One-Time-Password (when it applies). It will then modify your local configuration (in your home folder, never in the project itself) to reference the new tokens thus generated.\n\n Adding the `-s,--scope` flag will cause the authentication to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the authentication to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n ",examples:[["Login to the default registry","yarn npm login"],["Login to the registry linked to the @my-scope registry","yarn npm login --scope my-scope"],["Login to the publish registry for the current package","yarn npm login --publish"]]});var Eue=AE;async function Nb({scope:r,publish:e,configuration:t,cwd:i}){return r&&e?br.getScopeRegistry(r,{configuration:t,type:br.RegistryType.PUBLISH_REGISTRY}):r?br.getScopeRegistry(r,{configuration:t}):e?br.getPublishRegistry((await zf(t,i)).manifest,{configuration:t}):br.getDefaultRegistry({configuration:t})}async function i_e(r,e,{configuration:t,scope:i}){let n=o=>a=>{let l=Se.isIndexableObject(a)?a:{},c=l[o],u=Se.isIndexableObject(c)?c:{};return te(N({},l),{[o]:te(N({},u),{npmAuthToken:e})})},s=i?{npmScopes:n(i)}:{npmRegistries:n(r)};return await ye.updateHomeConfiguration(s)}async function n_e({registry:r,report:e,stdin:t,stdout:i}){if(process.env.TEST_ENV)return{name:process.env.TEST_NPM_USER||"",password:process.env.TEST_NPM_PASSWORD||""};e.reportInfo(X.UNNAMED,`Logging in to ${r}`);let n=!1;r.match(/^https:\/\/npm\.pkg\.github\.com(\/|$)/)&&(e.reportInfo(X.UNNAMED,"You seem to be using the GitHub Package Registry. Tokens must be generated with the 'repo', 'write:packages', and 'read:packages' permissions."),n=!0),e.reportSeparator();let{username:s,password:o}=await(0,mue.prompt)([{type:"input",name:"username",message:"Username:",required:!0,onCancel:()=>process.exit(130),stdin:t,stdout:i},{type:"password",name:"password",message:n?"Token:":"Password:",required:!0,onCancel:()=>process.exit(130),stdin:t,stdout:i}]);return e.reportSeparator(),{name:s,password:o}}var bh=new Set(["npmAuthIdent","npmAuthToken"]),lE=class extends Le{constructor(){super(...arguments);this.scope=J.String("-s,--scope",{description:"Logout of the registry configured for a given scope"});this.publish=J.Boolean("--publish",!1,{description:"Logout of the publish registry"});this.all=J.Boolean("-A,--all",!1,{description:"Logout of all registries"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t=async()=>{var l;let n=await Nb({configuration:e,cwd:this.context.cwd,publish:this.publish,scope:this.scope}),s=await ye.find(this.context.cwd,this.context.plugins),o=P.makeIdent((l=this.scope)!=null?l:null,"pkg");return!br.getAuthConfiguration(n,{configuration:s,ident:o}).get("npmAuthToken")};return(await Je.start({configuration:e,stdout:this.context.stdout},async n=>{if(this.all&&(await s_e(),n.reportInfo(X.UNNAMED,"Successfully logged out from everything")),this.scope){await Iue("npmScopes",this.scope),await t()?n.reportInfo(X.UNNAMED,`Successfully logged out from ${this.scope}`):n.reportWarning(X.UNNAMED,"Scope authentication settings removed, but some other ones settings still apply to it");return}let s=await Nb({configuration:e,cwd:this.context.cwd,publish:this.publish});await Iue("npmRegistries",s),await t()?n.reportInfo(X.UNNAMED,`Successfully logged out from ${s}`):n.reportWarning(X.UNNAMED,"Registry authentication settings removed, but some other ones settings still apply to it")})).exitCode()}};lE.paths=[["npm","logout"]],lE.usage=Re.Usage({category:"Npm-related commands",description:"logout of the npm registry",details:"\n This command will log you out by modifying your local configuration (in your home folder, never in the project itself) to delete all credentials linked to a registry.\n\n Adding the `-s,--scope` flag will cause the deletion to be done against whatever registry is configured for the associated scope (see also `npmScopes`).\n\n Adding the `--publish` flag will cause the deletion to be done against the registry used when publishing the package (see also `publishConfig.registry` and `npmPublishRegistry`).\n\n Adding the `-A,--all` flag will cause the deletion to be done against all registries and scopes.\n ",examples:[["Logout of the default registry","yarn npm logout"],["Logout of the @my-scope scope","yarn npm logout --scope my-scope"],["Logout of the publish registry for the current package","yarn npm logout --publish"],["Logout of all registries","yarn npm logout --all"]]});var yue=lE;function o_e(r,e){let t=r[e];if(!Se.isIndexableObject(t))return!1;let i=new Set(Object.keys(t));if([...bh].every(s=>!i.has(s)))return!1;for(let s of bh)i.delete(s);if(i.size===0)return r[e]=void 0,!0;let n=N({},t);for(let s of bh)delete n[s];return r[e]=n,!0}async function s_e(){let r=e=>{let t=!1,i=Se.isIndexableObject(e)?N({},e):{};i.npmAuthToken&&(delete i.npmAuthToken,t=!0);for(let n of Object.keys(i))o_e(i,n)&&(t=!0);if(Object.keys(i).length!==0)return t?i:e};return await ye.updateHomeConfiguration({npmRegistries:r,npmScopes:r})}async function Iue(r,e){return await ye.updateHomeConfiguration({[r]:t=>{let i=Se.isIndexableObject(t)?t:{};if(!Object.prototype.hasOwnProperty.call(i,e))return t;let n=i[e],s=Se.isIndexableObject(n)?n:{},o=new Set(Object.keys(s));if([...bh].every(l=>!o.has(l)))return t;for(let l of bh)o.delete(l);if(o.size===0)return Object.keys(i).length===1?void 0:te(N({},i),{[e]:void 0});let a={};for(let l of bh)a[l]=void 0;return te(N({},i),{[e]:N(N({},s),a)})}})}var cE=class extends Le{constructor(){super(...arguments);this.access=J.String("--access",{description:"The access for the published package (public or restricted)"});this.tag=J.String("--tag","latest",{description:"The tag on the registry that the package should be attached to"});this.tolerateRepublish=J.Boolean("--tolerate-republish",!1,{description:"Warn and exit when republishing an already existing version of a package"});this.otp=J.String("--otp",{description:"The OTP token to use with the command"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);if(i.manifest.private)throw new Pe("Private workspaces cannot be published");if(i.manifest.name===null||i.manifest.version===null)throw new Pe("Workspaces must have valid names and versions to be published on an external registry");await t.restoreInstallState();let n=i.manifest.name,s=i.manifest.version,o=br.getPublishRegistry(i.manifest,{configuration:e});return(await Je.start({configuration:e,stdout:this.context.stdout},async l=>{var c,u;if(this.tolerateRepublish)try{let g=await zt.get(zt.getIdentUrl(n),{configuration:e,registry:o,ident:n,jsonResponse:!0});if(!Object.prototype.hasOwnProperty.call(g,"versions"))throw new ct(X.REMOTE_INVALID,'Registry returned invalid data for - missing "versions" field');if(Object.prototype.hasOwnProperty.call(g.versions,s)){l.reportWarning(X.UNNAMED,`Registry already knows about version ${s}; skipping.`);return}}catch(g){if(((u=(c=g.originalError)==null?void 0:c.response)==null?void 0:u.statusCode)!==404)throw g}await Zt.maybeExecuteWorkspaceLifecycleScript(i,"prepublish",{report:l}),await DA.prepareForPack(i,{report:l},async()=>{let g=await DA.genPackList(i);for(let y of g)l.reportInfo(null,y);let f=await DA.genPackStream(i,g),h=await Se.bufferStream(f),p=await Bh.getGitHead(i.cwd),m=await Bh.makePublishBody(i,h,{access:this.access,tag:this.tag,registry:o,gitHead:p});await zt.put(zt.getIdentUrl(n),m,{configuration:e,registry:o,ident:n,otp:this.otp,jsonResponse:!0})}),l.reportInfo(X.UNNAMED,"Package archive published")})).exitCode()}};cE.paths=[["npm","publish"]],cE.usage=Re.Usage({category:"Npm-related commands",description:"publish the active workspace to the npm registry",details:'\n This command will pack the active workspace into a fresh archive and upload it to the npm registry.\n\n The package will by default be attached to the `latest` tag on the registry, but this behavior can be overriden by using the `--tag` option.\n\n Note that for legacy reasons scoped packages are by default published with an access set to `restricted` (aka "private packages"). This requires you to register for a paid npm plan. In case you simply wish to publish a public scoped package to the registry (for free), just add the `--access public` flag. This behavior can be enabled by default through the `npmPublishAccess` settings.\n ',examples:[["Publish the active workspace","yarn npm publish"]]});var wue=cE;var bue=ge(ri());var uE=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=J.String({required:!1})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n;if(typeof this.package!="undefined")n=P.parseIdent(this.package);else{if(!i)throw new ht(t.cwd,this.context.cwd);if(!i.manifest.name)throw new Pe(`Missing 'name' field in ${H.fromPortablePath(x.join(i.cwd,xt.manifest))}`);n=i.manifest.name}let s=await gE(n,e),a={children:Se.sortMap(Object.entries(s),([l])=>l).map(([l,c])=>({value:ae.tuple(ae.Type.RESOLUTION,{descriptor:P.makeDescriptor(n,l),locator:P.makeLocator(n,c)})}))};return ls.emitTree(a,{configuration:e,json:this.json,stdout:this.context.stdout})}};uE.paths=[["npm","tag","list"]],uE.usage=Re.Usage({category:"Npm-related commands",description:"list all dist-tags of a package",details:` + This command will list all tags of a package from the npm registry. + + If the package is not specified, Yarn will default to the current workspace. + `,examples:[["List all tags of package `my-pkg`","yarn npm tag list my-pkg"]]});var Bue=uE;async function gE(r,e){let t=`/-/package${zt.getIdentUrl(r)}/dist-tags`;return zt.get(t,{configuration:e,ident:r,jsonResponse:!0,customErrorMessage:zt.customPackageError})}var fE=class extends Le{constructor(){super(...arguments);this.package=J.String();this.tag=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);let n=P.parseDescriptor(this.package,!0),s=n.range;if(!bue.default.valid(s))throw new Pe(`The range ${ae.pretty(e,n.range,ae.Type.RANGE)} must be a valid semver version`);let o=br.getPublishRegistry(i.manifest,{configuration:e}),a=ae.pretty(e,n,ae.Type.IDENT),l=ae.pretty(e,s,ae.Type.RANGE),c=ae.pretty(e,this.tag,ae.Type.CODE);return(await Je.start({configuration:e,stdout:this.context.stdout},async g=>{let f=await gE(n,e);Object.prototype.hasOwnProperty.call(f,this.tag)&&f[this.tag]===s&&g.reportWarning(X.UNNAMED,`Tag ${c} is already set to version ${l}`);let h=`/-/package${zt.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await zt.put(h,s,{configuration:e,registry:o,ident:n,jsonRequest:!0,jsonResponse:!0}),g.reportInfo(X.UNNAMED,`Tag ${c} added to version ${l} of package ${a}`)})).exitCode()}};fE.paths=[["npm","tag","add"]],fE.usage=Re.Usage({category:"Npm-related commands",description:"add a tag for a specific version of a package",details:` + This command will add a tag to the npm registry for a specific version of a package. If the tag already exists, it will be overwritten. + `,examples:[["Add a `beta` tag for version `2.3.4-beta.4` of package `my-pkg`","yarn npm tag add my-pkg@2.3.4-beta.4 beta"]]});var Que=fE;var hE=class extends Le{constructor(){super(...arguments);this.package=J.String();this.tag=J.String()}async execute(){if(this.tag==="latest")throw new Pe("The 'latest' tag cannot be removed.");let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);let n=P.parseIdent(this.package),s=br.getPublishRegistry(i.manifest,{configuration:e}),o=ae.pretty(e,this.tag,ae.Type.CODE),a=ae.pretty(e,n,ae.Type.IDENT),l=await gE(n,e);if(!Object.prototype.hasOwnProperty.call(l,this.tag))throw new Pe(`${o} is not a tag of package ${a}`);return(await Je.start({configuration:e,stdout:this.context.stdout},async u=>{let g=`/-/package${zt.getIdentUrl(n)}/dist-tags/${encodeURIComponent(this.tag)}`;await zt.del(g,{configuration:e,registry:s,ident:n,jsonResponse:!0}),u.reportInfo(X.UNNAMED,`Tag ${o} removed from package ${a}`)})).exitCode()}};hE.paths=[["npm","tag","remove"]],hE.usage=Re.Usage({category:"Npm-related commands",description:"remove a tag from a package",details:` + This command will remove a tag from a package from the npm registry. + `,examples:[["Remove the `beta` tag from package `my-pkg`","yarn npm tag remove my-pkg beta"]]});var Sue=hE;var pE=class extends Le{constructor(){super(...arguments);this.scope=J.String("-s,--scope",{description:"Print username for the registry configured for a given scope"});this.publish=J.Boolean("--publish",!1,{description:"Print username for the publish registry"})}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),t;return this.scope&&this.publish?t=br.getScopeRegistry(this.scope,{configuration:e,type:br.RegistryType.PUBLISH_REGISTRY}):this.scope?t=br.getScopeRegistry(this.scope,{configuration:e}):this.publish?t=br.getPublishRegistry((await zf(e,this.context.cwd)).manifest,{configuration:e}):t=br.getDefaultRegistry({configuration:e}),(await Je.start({configuration:e,stdout:this.context.stdout},async n=>{var o,a;let s;try{s=await zt.get("/-/whoami",{configuration:e,registry:t,authType:zt.AuthType.ALWAYS_AUTH,jsonResponse:!0,ident:this.scope?P.makeIdent(this.scope,""):void 0})}catch(l){if(((o=l.response)==null?void 0:o.statusCode)===401||((a=l.response)==null?void 0:a.statusCode)===403){n.reportError(X.AUTHENTICATION_INVALID,"Authentication failed - your credentials may have expired");return}else throw l}n.reportInfo(X.UNNAMED,s.username)})).exitCode()}};pE.paths=[["npm","whoami"]],pE.usage=Re.Usage({category:"Npm-related commands",description:"display the name of the authenticated user",details:"\n Print the username associated with the current authentication settings to the standard output.\n\n When using `-s,--scope`, the username printed will be the one that matches the authentication settings of the registry associated with the given scope (those settings can be overriden using the `npmRegistries` map, and the registry associated with the scope is configured via the `npmScopes` map).\n\n When using `--publish`, the registry we'll select will by default be the one used when publishing packages (`publishConfig.registry` or `npmPublishRegistry` if available, otherwise we'll fallback to the regular `npmRegistryServer`).\n ",examples:[["Print username for the default registry","yarn npm whoami"],["Print username for the registry on a given scope","yarn npm whoami --scope company"]]});var vue=pE;var a_e={configuration:{npmPublishAccess:{description:"Default access of the published packages",type:Ie.STRING,default:null}},commands:[due,Cue,Eue,yue,wue,Que,Bue,Sue,vue]},A_e=a_e;var AO={};ft(AO,{default:()=>B_e,patchUtils:()=>ZT});var ZT={};ft(ZT,{applyPatchFile:()=>Ob,diffFolders:()=>sO,ensureUnpatchedDescriptor:()=>eO,extractPackageToDisk:()=>nO,extractPatchFlags:()=>Nue,isParentRequired:()=>iO,loadPatchFiles:()=>EE,makeDescriptor:()=>tO,makeLocator:()=>rO,parseDescriptor:()=>CE,parseLocator:()=>mE,parsePatchFile:()=>Tb});var dE=class extends Error{constructor(e,t){super(`Cannot apply hunk #${e+1}`);this.hunk=t}};var l_e=/^@@ -(\d+)(,(\d+))? \+(\d+)(,(\d+))? @@.*/;function Qh(r){return x.relative(Me.root,x.resolve(Me.root,H.toPortablePath(r)))}function c_e(r){let e=r.trim().match(l_e);if(!e)throw new Error(`Bad header line: '${r}'`);return{original:{start:Math.max(Number(e[1]),1),length:Number(e[3]||1)},patched:{start:Math.max(Number(e[4]),1),length:Number(e[6]||1)}}}var u_e=420,g_e=493,Zr;(function(i){i.Context="context",i.Insertion="insertion",i.Deletion="deletion"})(Zr||(Zr={}));var kue=()=>({semverExclusivity:null,diffLineFromPath:null,diffLineToPath:null,oldMode:null,newMode:null,deletedFileMode:null,newFileMode:null,renameFrom:null,renameTo:null,beforeHash:null,afterHash:null,fromPath:null,toPath:null,hunks:null}),f_e=r=>({header:c_e(r),parts:[]}),h_e={["@"]:"header",["-"]:Zr.Deletion,["+"]:Zr.Insertion,[" "]:Zr.Context,["\\"]:"pragma",undefined:Zr.Context};function d_e(r){let e=[],t=kue(),i="parsing header",n=null,s=null;function o(){n&&(s&&(n.parts.push(s),s=null),t.hunks.push(n),n=null)}function a(){o(),e.push(t),t=kue()}for(let l=0;l0?"patch":"mode change",v=null;switch(b){case"rename":{if(!u||!g)throw new Error("Bad parser state: rename from & to not given");e.push({type:"rename",semverExclusivity:i,fromPath:Qh(u),toPath:Qh(g)}),v=g}break;case"file deletion":{let k=n||p;if(!k)throw new Error("Bad parse state: no path given for file deletion");e.push({type:"file deletion",semverExclusivity:i,hunk:y&&y[0]||null,path:Qh(k),mode:Lb(l),hash:f})}break;case"file creation":{let k=s||m;if(!k)throw new Error("Bad parse state: no path given for file creation");e.push({type:"file creation",semverExclusivity:i,hunk:y&&y[0]||null,path:Qh(k),mode:Lb(c),hash:h})}break;case"patch":case"mode change":v=m||s;break;default:Se.assertNever(b);break}v&&o&&a&&o!==a&&e.push({type:"mode change",semverExclusivity:i,path:Qh(v),oldMode:Lb(o),newMode:Lb(a)}),v&&y&&y.length&&e.push({type:"patch",semverExclusivity:i,path:Qh(v),hunks:y,beforeHash:f,afterHash:h})}if(e.length===0)throw new Error("Unable to parse patch file: No changes found. Make sure the patch is a valid UTF8 encoded string");return e}function Lb(r){let e=parseInt(r,8)&511;if(e!==u_e&&e!==g_e)throw new Error(`Unexpected file mode string: ${r}`);return e}function Tb(r){let e=r.split(/\n/g);return e[e.length-1]===""&&e.pop(),C_e(d_e(e))}function p_e(r){let e=0,t=0;for(let{type:i,lines:n}of r.parts)switch(i){case Zr.Context:t+=n.length,e+=n.length;break;case Zr.Deletion:e+=n.length;break;case Zr.Insertion:t+=n.length;break;default:Se.assertNever(i);break}if(e!==r.header.original.length||t!==r.header.patched.length){let i=n=>n<0?n:`+${n}`;throw new Error(`hunk header integrity check failed (expected @@ ${i(r.header.original.length)} ${i(r.header.patched.length)} @@, got @@ ${i(e)} ${i(t)} @@)`)}}async function Sh(r,e,t){let i=await r.lstatPromise(e),n=await t();if(typeof n!="undefined"&&(e=n),r.lutimesPromise)await r.lutimesPromise(e,i.atime,i.mtime);else if(!i.isSymbolicLink())await r.utimesPromise(e,i.atime,i.mtime);else throw new Error("Cannot preserve the time values of a symlink")}async function Ob(r,{baseFs:e=new ar,dryRun:t=!1,version:i=null}={}){for(let n of r)if(!(n.semverExclusivity!==null&&i!==null&&!Wt.satisfiesWithPrereleases(i,n.semverExclusivity)))switch(n.type){case"file deletion":if(t){if(!e.existsSync(n.path))throw new Error(`Trying to delete a file that doesn't exist: ${n.path}`)}else await Sh(e,x.dirname(n.path),async()=>{await e.unlinkPromise(n.path)});break;case"rename":if(t){if(!e.existsSync(n.fromPath))throw new Error(`Trying to move a file that doesn't exist: ${n.fromPath}`)}else await Sh(e,x.dirname(n.fromPath),async()=>{await Sh(e,x.dirname(n.toPath),async()=>{await Sh(e,n.fromPath,async()=>(await e.movePromise(n.fromPath,n.toPath),n.toPath))})});break;case"file creation":if(t){if(e.existsSync(n.path))throw new Error(`Trying to create a file that already exists: ${n.path}`)}else{let s=n.hunk?n.hunk.parts[0].lines.join(` +`)+(n.hunk.parts[0].noNewlineAtEndOfFile?"":` +`):"";await e.mkdirpPromise(x.dirname(n.path),{chmod:493,utimes:[Rr.SAFE_TIME,Rr.SAFE_TIME]}),await e.writeFilePromise(n.path,s,{mode:n.mode}),await e.utimesPromise(n.path,Rr.SAFE_TIME,Rr.SAFE_TIME)}break;case"patch":await Sh(e,n.path,async()=>{await m_e(n,{baseFs:e,dryRun:t})});break;case"mode change":{let o=(await e.statPromise(n.path)).mode;if(xue(n.newMode)!==xue(o))continue;await Sh(e,n.path,async()=>{await e.chmodPromise(n.path,n.newMode)})}break;default:Se.assertNever(n);break}}function xue(r){return(r&64)>0}function Pue(r){return r.replace(/\s+$/,"")}function E_e(r,e){return Pue(r)===Pue(e)}async function m_e({hunks:r,path:e},{baseFs:t,dryRun:i=!1}){let n=await t.statSync(e).mode,o=(await t.readFileSync(e,"utf8")).split(/\n/),a=[],l=0,c=0;for(let g of r){let f=Math.max(c,g.header.patched.start+l),h=Math.max(0,f-c),p=Math.max(0,o.length-f-g.header.original.length),m=Math.max(h,p),y=0,b=0,v=null;for(;y<=m;){if(y<=h&&(b=f-y,v=Due(g,o,b),v!==null)){y=-y;break}if(y<=p&&(b=f+y,v=Due(g,o,b),v!==null))break;y+=1}if(v===null)throw new dE(r.indexOf(g),g);a.push(v),l+=y,c=b+g.header.original.length}if(i)return;let u=0;for(let g of a)for(let f of g)switch(f.type){case"splice":{let h=f.index+u;o.splice(h,f.numToDelete,...f.linesToInsert),u+=f.linesToInsert.length-f.numToDelete}break;case"pop":o.pop();break;case"push":o.push(f.line);break;default:Se.assertNever(f);break}await t.writeFilePromise(e,o.join(` +`),{mode:n})}function Due(r,e,t){let i=[];for(let n of r.parts)switch(n.type){case Zr.Context:case Zr.Deletion:{for(let s of n.lines){let o=e[t];if(o==null||!E_e(o,s))return null;t+=1}n.type===Zr.Deletion&&(i.push({type:"splice",index:t-n.lines.length,numToDelete:n.lines.length,linesToInsert:[]}),n.noNewlineAtEndOfFile&&i.push({type:"push",line:""}))}break;case Zr.Insertion:i.push({type:"splice",index:t,numToDelete:0,linesToInsert:n.lines}),n.noNewlineAtEndOfFile&&i.push({type:"pop"});break;default:Se.assertNever(n.type);break}return i}var I_e=/^builtin<([^>]+)>$/;function $T(r,e){let{source:t,selector:i,params:n}=P.parseRange(r);if(t===null)throw new Error("Patch locators must explicitly define their source");let s=i?i.split(/&/).map(c=>H.toPortablePath(c)):[],o=n&&typeof n.locator=="string"?P.parseLocator(n.locator):null,a=n&&typeof n.version=="string"?n.version:null,l=e(t);return{parentLocator:o,sourceItem:l,patchPaths:s,sourceVersion:a}}function CE(r){let i=$T(r.range,P.parseDescriptor),{sourceItem:e}=i,t=Or(i,["sourceItem"]);return te(N({},t),{sourceDescriptor:e})}function mE(r){let i=$T(r.reference,P.parseLocator),{sourceItem:e}=i,t=Or(i,["sourceItem"]);return te(N({},t),{sourceLocator:e})}function eO(r){if(!r.range.startsWith("patch:"))return r;let{sourceItem:e}=$T(r.range,P.parseDescriptor);return e}function Rue({parentLocator:r,sourceItem:e,patchPaths:t,sourceVersion:i,patchHash:n},s){let o=r!==null?{locator:P.stringifyLocator(r)}:{},a=typeof i!="undefined"?{version:i}:{},l=typeof n!="undefined"?{hash:n}:{};return P.makeRange({protocol:"patch:",source:s(e),selector:t.join("&"),params:N(N(N({},a),l),o)})}function tO(r,{parentLocator:e,sourceDescriptor:t,patchPaths:i}){return P.makeDescriptor(r,Rue({parentLocator:e,sourceItem:t,patchPaths:i},P.stringifyDescriptor))}function rO(r,{parentLocator:e,sourcePackage:t,patchPaths:i,patchHash:n}){return P.makeLocator(r,Rue({parentLocator:e,sourceItem:t,sourceVersion:t.version,patchPaths:i,patchHash:n},P.stringifyLocator))}function Fue({onAbsolute:r,onRelative:e,onBuiltin:t},i){i.startsWith("~")&&(i=i.slice(1));let s=i.match(I_e);return s!==null?t(s[1]):x.isAbsolute(i)?r(i):e(i)}function Nue(r){let e=r.startsWith("~");return e&&(r=r.slice(1)),{optional:e}}function iO(r){return Fue({onAbsolute:()=>!1,onRelative:()=>!0,onBuiltin:()=>!1},r)}async function EE(r,e,t){let i=r!==null?await t.fetcher.fetch(r,t):null,n=i&&i.localPath?{packageFs:new _t(Me.root),prefixPath:x.relative(Me.root,i.localPath)}:i;i&&i!==n&&i.releaseFs&&i.releaseFs();let s=await Se.releaseAfterUseAsync(async()=>await Promise.all(e.map(async o=>{let a=Nue(o),l=await Fue({onAbsolute:async()=>await U.readFilePromise(o,"utf8"),onRelative:async()=>{if(n===null)throw new Error("Assertion failed: The parent locator should have been fetched");return await n.packageFs.readFilePromise(x.join(n.prefixPath,o),"utf8")},onBuiltin:async c=>await t.project.configuration.firstHook(u=>u.getBuiltinPatch,t.project,c)},o);return te(N({},a),{source:l})})));for(let o of s)typeof o.source=="string"&&(o.source=o.source.replace(/\r\n?/g,` +`));return s}async function nO(r,{cache:e,project:t}){let i=t.storedPackages.get(r.locatorHash);if(typeof i=="undefined")throw new Error("Assertion failed: Expected the package to be registered");let n=t.storedChecksums,s=new di,o=t.configuration.makeFetcher(),a=await o.fetch(r,{cache:e,project:t,fetcher:o,checksums:n,report:s}),l=await U.mktempPromise(),c=x.join(l,"source"),u=x.join(l,"user"),g=x.join(l,".yarn-patch.json");return await Promise.all([U.copyPromise(c,a.prefixPath,{baseFs:a.packageFs}),U.copyPromise(u,a.prefixPath,{baseFs:a.packageFs}),U.writeJsonPromise(g,{locator:P.stringifyLocator(r),version:i.version})]),U.detachTemp(l),u}async function sO(r,e){let t=H.fromPortablePath(r).replace(/\\/g,"/"),i=H.fromPortablePath(e).replace(/\\/g,"/"),{stdout:n,stderr:s}=await Nr.execvp("git",["-c","core.safecrlf=false","diff","--src-prefix=a/","--dst-prefix=b/","--ignore-cr-at-eol","--full-index","--no-index","--no-renames","--text",t,i],{cwd:H.toPortablePath(process.cwd()),env:te(N({},process.env),{GIT_CONFIG_NOSYSTEM:"1",HOME:"",XDG_CONFIG_HOME:"",USERPROFILE:""})});if(s.length>0)throw new Error(`Unable to diff directories. Make sure you have a recent version of 'git' available in PATH. +The following error was reported by 'git': +${s}`);let o=t.startsWith("/")?a=>a.slice(1):a=>a;return n.replace(new RegExp(`(a|b)(${Se.escapeRegExp(`/${o(t)}/`)})`,"g"),"$1/").replace(new RegExp(`(a|b)${Se.escapeRegExp(`/${o(i)}/`)}`,"g"),"$1/").replace(new RegExp(Se.escapeRegExp(`${t}/`),"g"),"").replace(new RegExp(Se.escapeRegExp(`${i}/`),"g"),"")}function Lue(r,{configuration:e,report:t}){for(let i of r.parts)for(let n of i.lines)switch(i.type){case Zr.Context:t.reportInfo(null,` ${ae.pretty(e,n,"grey")}`);break;case Zr.Deletion:t.reportError(X.FROZEN_LOCKFILE_EXCEPTION,`- ${ae.pretty(e,n,ae.Type.REMOVED)}`);break;case Zr.Insertion:t.reportError(X.FROZEN_LOCKFILE_EXCEPTION,`+ ${ae.pretty(e,n,ae.Type.ADDED)}`);break;default:Se.assertNever(i.type)}}var oO=class{supports(e,t){return!!e.reference.startsWith("patch:")}getLocalPath(e,t){return null}async fetch(e,t){let i=t.checksums.get(e.locatorHash)||null,[n,s,o]=await t.cache.fetchPackageFromCache(e,i,N({onHit:()=>t.report.reportCacheHit(e),onMiss:()=>t.report.reportCacheMiss(e,`${P.prettyLocator(t.project.configuration,e)} can't be found in the cache and will be fetched from the disk`),loader:()=>this.patchPackage(e,t),skipIntegrityCheck:t.skipIntegrityCheck},t.cacheOptions));return{packageFs:n,releaseFs:s,prefixPath:P.getIdentVendorPath(e),localPath:this.getLocalPath(e,t),checksum:o}}async patchPackage(e,t){let{parentLocator:i,sourceLocator:n,sourceVersion:s,patchPaths:o}=mE(e),a=await EE(i,o,t),l=await U.mktempPromise(),c=x.join(l,"current.zip"),u=await t.fetcher.fetch(n,t),g=P.getIdentVendorPath(e),f=await fn(),h=new li(c,{libzip:f,create:!0,level:t.project.configuration.get("compressionLevel")});await Se.releaseAfterUseAsync(async()=>{await h.copyPromise(g,u.prefixPath,{baseFs:u.packageFs,stableSort:!0})},u.releaseFs),h.saveAndClose();for(let{source:p,optional:m}of a){if(p===null)continue;let y=new li(c,{libzip:f,level:t.project.configuration.get("compressionLevel")}),b=new _t(x.resolve(Me.root,g),{baseFs:y});try{await Ob(Tb(p),{baseFs:b,version:s})}catch(v){if(!(v instanceof dE))throw v;let k=t.project.configuration.get("enableInlineHunks"),T=!k&&!m?" (set enableInlineHunks for details)":"",Y=`${P.prettyLocator(t.project.configuration,e)}: ${v.message}${T}`,q=$=>{!k||Lue(v.hunk,{configuration:t.project.configuration,report:$})};if(y.discardAndClose(),m){t.report.reportWarningOnce(X.PATCH_HUNK_FAILED,Y,{reportExtra:q});continue}else throw new ct(X.PATCH_HUNK_FAILED,Y,q)}y.saveAndClose()}return new li(c,{libzip:f,level:t.project.configuration.get("compressionLevel")})}};var y_e=3,aO=class{supportsDescriptor(e,t){return!!e.range.startsWith("patch:")}supportsLocator(e,t){return!!e.reference.startsWith("patch:")}shouldPersistResolution(e,t){return!1}bindDescriptor(e,t,i){let{patchPaths:n}=CE(e);return n.every(s=>!iO(s))?e:P.bindDescriptor(e,{locator:P.stringifyLocator(t)})}getResolutionDependencies(e,t){let{sourceDescriptor:i}=CE(e);return[i]}async getCandidates(e,t,i){if(!i.fetchOptions)throw new Error("Assertion failed: This resolver cannot be used unless a fetcher is configured");let{parentLocator:n,sourceDescriptor:s,patchPaths:o}=CE(e),a=await EE(n,o,i.fetchOptions),l=t.get(s.descriptorHash);if(typeof l=="undefined")throw new Error("Assertion failed: The dependency should have been resolved");let c=Rn.makeHash(`${y_e}`,...a.map(u=>JSON.stringify(u))).slice(0,6);return[rO(e,{parentLocator:n,sourcePackage:l,patchPaths:o,patchHash:c})]}async getSatisfying(e,t,i){return null}async resolve(e,t){let{sourceLocator:i}=mE(e),n=await t.resolver.resolve(i,t);return N(N({},n),e)}};var IE=class extends Le{constructor(){super(...arguments);this.save=J.Boolean("-s,--save",!1,{description:"Add the patch to your resolution entries"});this.patchFolder=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState();let n=x.resolve(this.context.cwd,H.toPortablePath(this.patchFolder)),s=x.join(n,"../source"),o=x.join(n,"../.yarn-patch.json");if(!U.existsSync(s))throw new Pe("The argument folder didn't get created by 'yarn patch'");let a=await sO(s,n),l=await U.readJsonPromise(o),c=P.parseLocator(l.locator,!0);if(!t.storedPackages.has(c.locatorHash))throw new Pe("No package found in the project for the given locator");if(!this.save){this.context.stdout.write(a);return}let u=e.get("patchFolder"),g=x.join(u,`${P.slugifyLocator(c)}.patch`);await U.mkdirPromise(u,{recursive:!0}),await U.writeFilePromise(g,a);let f=new Map;for(let h of t.storedPackages.values()){if(P.isVirtualLocator(h))continue;let p=h.dependencies.get(c.identHash);if(!p)continue;let m=P.isVirtualDescriptor(p)?P.devirtualizeDescriptor(p):p,y=eO(m),b=t.storedResolutions.get(y.descriptorHash);if(!b)throw new Error("Assertion failed: Expected the resolution to have been registered");if(!t.storedPackages.get(b))throw new Error("Assertion failed: Expected the package to have been registered");let k=t.originalPackages.get(h.locatorHash);if(!k)throw new Error("Assertion failed: Expected the original package to have been registered");let T=k.dependencies.get(p.identHash);if(!T)throw new Error("Assertion failed: Expected the original dependency to have been registered");f.set(T.descriptorHash,T)}for(let h of f.values()){let p=tO(h,{parentLocator:null,sourceDescriptor:P.convertLocatorToDescriptor(c),sourceVersion:null,patchPaths:[`./${x.relative(t.cwd,g)}`]});t.topLevelWorkspace.manifest.resolutions.push({pattern:{descriptor:{fullName:P.stringifyIdent(p),description:h.range}},reference:p.range})}await t.persist()}};IE.paths=[["patch-commit"]],IE.usage=Re.Usage({description:"generate a patch out of a directory",details:"\n By default, this will print a patchfile on stdout based on the diff between the folder passed in and the original version of the package. Such file is suitable for consumption with the `patch:` protocol.\n\n With the `-s,--save` option set, the patchfile won't be printed on stdout anymore and will instead be stored within a local file (by default kept within `.yarn/patches`, but configurable via the `patchFolder` setting). A `resolutions` entry will also be added to your top-level manifest, referencing the patched package via the `patch:` protocol.\n\n Note that only folders generated by `yarn patch` are accepted as valid input for `yarn patch-commit`.\n "});var Tue=IE;var yE=class extends Le{constructor(){super(...arguments);this.json=J.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.package=J.String()}async execute(){let e=await ye.find(this.context.cwd,this.context.plugins),{project:t,workspace:i}=await ze.find(e,this.context.cwd),n=await Nt.find(e);if(!i)throw new ht(t.cwd,this.context.cwd);await t.restoreInstallState();let s=P.parseLocator(this.package);if(s.reference==="unknown"){let o=Se.mapAndFilter([...t.storedPackages.values()],a=>a.identHash!==s.identHash?Se.mapAndFilter.skip:P.isVirtualLocator(a)?Se.mapAndFilter.skip:a);if(o.length===0)throw new Pe("No package found in the project for the given locator");if(o.length>1)throw new Pe(`Multiple candidate packages found; explicitly choose one of them (use \`yarn why \` to get more information as to who depends on them): +${o.map(a=>` +- ${P.prettyLocator(e,a)}`).join("")}`);s=o[0]}if(!t.storedPackages.has(s.locatorHash))throw new Pe("No package found in the project for the given locator");await Je.start({configuration:e,json:this.json,stdout:this.context.stdout},async o=>{let a=await nO(s,{cache:n,project:t});o.reportJson({locator:P.stringifyLocator(s),path:H.fromPortablePath(a)}),o.reportInfo(X.UNNAMED,`Package ${P.prettyLocator(e,s)} got extracted with success!`),o.reportInfo(X.UNNAMED,`You can now edit the following folder: ${ae.pretty(e,H.fromPortablePath(a),"magenta")}`),o.reportInfo(X.UNNAMED,`Once you are done run ${ae.pretty(e,`yarn patch-commit -s ${process.platform==="win32"?'"':""}${H.fromPortablePath(a)}${process.platform==="win32"?'"':""}`,"cyan")} and Yarn will store a patchfile based on your changes.`)})}};yE.paths=[["patch"]],yE.usage=Re.Usage({description:"prepare a package for patching",details:"\n This command will cause a package to be extracted in a temporary directory intended to be editable at will.\n \n Once you're done with your changes, run `yarn patch-commit -s ` (with `` being the temporary directory you received) to generate a patchfile and register it into your top-level manifest via the `patch:` protocol. Run `yarn patch-commit -h` for more details.\n "});var Oue=yE;var w_e={configuration:{enableInlineHunks:{description:"If true, the installs will print unmatched patch hunks",type:Ie.BOOLEAN,default:!1},patchFolder:{description:"Folder where the patch files must be written",type:Ie.ABSOLUTE_PATH,default:"./.yarn/patches"}},commands:[Tue,Oue],fetchers:[oO],resolvers:[aO]},B_e=w_e;var gO={};ft(gO,{default:()=>S_e});var lO=class{supportsPackage(e,t){return this.isEnabled(t)}async findPackageLocation(e,t){if(!this.isEnabled(t))throw new Error("Assertion failed: Expected the pnpm linker to be enabled");let i=cO(),n=t.project.installersCustomData.get(i);if(!n)throw new Pe(`The project in ${ae.pretty(t.project.configuration,`${t.project.cwd}/package.json`,ae.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let s=n.pathByLocator.get(e.locatorHash);if(typeof s=="undefined")throw new Pe(`Couldn't find ${P.prettyLocator(t.project.configuration,e)} in the currently installed pnpm map - running an install might help`);return s}async findPackageLocator(e,t){if(!this.isEnabled(t))return null;let i=cO(),n=t.project.installersCustomData.get(i);if(!n)throw new Pe(`The project in ${ae.pretty(t.project.configuration,`${t.project.cwd}/package.json`,ae.Type.PATH)} doesn't seem to have been installed - running an install there might help`);let s=e.match(/(^.*\/node_modules\/(@[^/]*\/)?[^/]+)(\/.*$)/);if(s){let l=n.locatorByPath.get(s[1]);if(l)return l}let o=e,a=e;do{a=o,o=x.dirname(a);let l=n.locatorByPath.get(a);if(l)return l}while(o!==a);return null}makeInstaller(e){return new Mue(e)}isEnabled(e){return e.project.configuration.get("nodeLinker")==="pnpm"}},Mue=class{constructor(e){this.opts=e;this.asyncActions=new Se.AsyncActions(10);this.customData={pathByLocator:new Map,locatorByPath:new Map}}getCustomDataKey(){return cO()}attachCustomData(e){}async installPackage(e,t,i){switch(e.linkType){case Qt.SOFT:return this.installPackageSoft(e,t,i);case Qt.HARD:return this.installPackageHard(e,t,i)}throw new Error("Assertion failed: Unsupported package link type")}async installPackageSoft(e,t,i){let n=x.resolve(t.packageFs.getRealPath(),t.prefixPath);return this.customData.pathByLocator.set(e.locatorHash,n),{packageLocation:n,buildDirective:null}}async installPackageHard(e,t,i){var u;let n=b_e(e,{project:this.opts.project});this.customData.locatorByPath.set(n,P.stringifyLocator(e)),this.customData.pathByLocator.set(e.locatorHash,n),i.holdFetchResult(this.asyncActions.set(e.locatorHash,async()=>{await U.mkdirPromise(n,{recursive:!0}),await U.copyPromise(n,t.prefixPath,{baseFs:t.packageFs,overwrite:!1})}));let o=P.isVirtualLocator(e)?P.devirtualizeLocator(e):e,a={manifest:(u=await At.tryFind(t.prefixPath,{baseFs:t.packageFs}))!=null?u:new At,misc:{hasBindingGyp:ma.hasBindingGyp(t)}},l=this.opts.project.getDependencyMeta(o,e.version),c=ma.extractBuildScripts(e,a,l,{configuration:this.opts.project.configuration,report:this.opts.report});return{packageLocation:n,buildDirective:c}}async attachInternalDependencies(e,t){this.opts.project.configuration.get("nodeLinker")==="pnpm"&&(!Hue(e,{project:this.opts.project})||this.asyncActions.reduce(e.locatorHash,async i=>{await i;let n=this.customData.pathByLocator.get(e.locatorHash);if(typeof n=="undefined")throw new Error(`Assertion failed: Expected the package to have been registered (${P.stringifyLocator(e)})`);let s=x.join(n,xt.nodeModules),o=[],a=await jue(s);for(let[l,c]of t){let u=c;Hue(c,{project:this.opts.project})||(this.opts.report.reportWarning(X.UNNAMED,"The pnpm linker doesn't support providing different versions to workspaces' peer dependencies"),u=P.devirtualizeLocator(c));let g=this.customData.pathByLocator.get(u.locatorHash);if(typeof g=="undefined")throw new Error(`Assertion failed: Expected the package to have been registered (${P.stringifyLocator(c)})`);let f=P.stringifyIdent(l),h=x.join(s,f),p=x.relative(x.dirname(h),g),m=a.get(f);a.delete(f),o.push(Promise.resolve().then(async()=>{if(m){if(m.isSymbolicLink()&&await U.readlinkPromise(h)===p)return;await U.removePromise(h)}await U.mkdirpPromise(x.dirname(h)),process.platform=="win32"?await U.symlinkPromise(g,h,"junction"):await U.symlinkPromise(p,h)}))}o.push(Gue(s,a)),await Promise.all(o)}))}async attachExternalDependents(e,t){throw new Error("External dependencies haven't been implemented for the pnpm linker")}async finalizeInstall(){let e=Kue(this.opts.project);if(this.opts.project.configuration.get("nodeLinker")!=="pnpm")await U.removePromise(e);else{let t=[],i=new Set;for(let s of this.customData.pathByLocator.values()){let o=x.contains(e,s);if(o!==null){let[a,,...l]=o.split(x.sep);i.add(a);let c=x.join(e,a);t.push(U.readdirPromise(c).then(u=>Promise.all(u.map(async g=>{let f=x.join(c,g);if(g===xt.nodeModules){let h=await jue(f);return h.delete(l.join(x.sep)),Gue(f,h)}else return U.removePromise(f)}))).catch(u=>{if(u.code!=="ENOENT")throw u}))}}let n;try{n=await U.readdirPromise(e)}catch{n=[]}for(let s of n)i.has(s)||t.push(U.removePromise(x.join(e,s)));await Promise.all(t)}return await this.asyncActions.wait(),await uO(e),this.opts.project.configuration.get("nodeLinker")!=="node-modules"&&await uO(Uue(this.opts.project)),{customData:this.customData}}};function cO(){return JSON.stringify({name:"PnpmInstaller",version:2})}function Uue(r){return x.join(r.cwd,xt.nodeModules)}function Kue(r){return x.join(Uue(r),".store")}function b_e(r,{project:e}){let t=P.slugifyLocator(r),i=P.getIdentVendorPath(r);return x.join(Kue(e),t,i)}function Hue(r,{project:e}){return!P.isVirtualLocator(r)||!e.tryWorkspaceByLocator(r)}async function jue(r){let e=new Map,t=[];try{t=await U.readdirPromise(r,{withFileTypes:!0})}catch(i){if(i.code!=="ENOENT")throw i}try{for(let i of t)if(!i.name.startsWith("."))if(i.name.startsWith("@")){let n=await U.readdirPromise(x.join(r,i.name),{withFileTypes:!0});if(n.length===0)e.set(i.name,i);else for(let s of n)e.set(`${i.name}/${s.name}`,s)}else e.set(i.name,i)}catch(i){if(i.code!=="ENOENT")throw i}return e}async function Gue(r,e){var n;let t=[],i=new Set;for(let s of e.keys()){t.push(U.removePromise(x.join(r,s)));let o=(n=P.tryParseIdent(s))==null?void 0:n.scope;o&&i.add(`@${o}`)}return Promise.all(t).then(()=>Promise.all([...i].map(s=>uO(x.join(r,s)))))}async function uO(r){try{await U.rmdirPromise(r)}catch(e){if(e.code!=="ENOENT"&&e.code!=="ENOTEMPTY")throw e}}var Q_e={linkers:[lO]},S_e=Q_e;var T0=()=>({modules:new Map([["@yarnpkg/cli",YC],["@yarnpkg/core",IC],["@yarnpkg/fslib",$h],["@yarnpkg/libzip",Ud],["@yarnpkg/parsers",ap],["@yarnpkg/shell",Hd],["clipanion",hZ(mp)],["semver",v_e],["typanion",ug],["yup",k_e],["@yarnpkg/plugin-essentials",YN],["@yarnpkg/plugin-compat",_N],["@yarnpkg/plugin-dlx",VN],["@yarnpkg/plugin-file",sL],["@yarnpkg/plugin-git",GN],["@yarnpkg/plugin-github",aL],["@yarnpkg/plugin-http",cL],["@yarnpkg/plugin-init",hL],["@yarnpkg/plugin-link",EL],["@yarnpkg/plugin-nm",WL],["@yarnpkg/plugin-npm",JT],["@yarnpkg/plugin-npm-cli",XT],["@yarnpkg/plugin-pack",jT],["@yarnpkg/plugin-patch",AO],["@yarnpkg/plugin-pnp",TL],["@yarnpkg/plugin-pnpm",gO]]),plugins:new Set(["@yarnpkg/plugin-essentials","@yarnpkg/plugin-compat","@yarnpkg/plugin-dlx","@yarnpkg/plugin-file","@yarnpkg/plugin-git","@yarnpkg/plugin-github","@yarnpkg/plugin-http","@yarnpkg/plugin-init","@yarnpkg/plugin-link","@yarnpkg/plugin-nm","@yarnpkg/plugin-npm","@yarnpkg/plugin-npm-cli","@yarnpkg/plugin-pack","@yarnpkg/plugin-patch","@yarnpkg/plugin-pnp","@yarnpkg/plugin-pnpm"])});o0({binaryVersion:Kr||"",pluginConfiguration:T0()});})(); +/*! + * buildToken + * Builds OAuth token prefix (helper function) + * + * @name buildToken + * @function + * @param {GitUrl} obj The parsed Git url object. + * @return {String} token prefix + */ +/*! + * fill-range + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-extglob + * + * Copyright (c) 2014-2016, Jon Schlinkert. + * Licensed under the MIT License. + */ +/*! + * is-glob + * + * Copyright (c) 2014-2017, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * is-number + * + * Copyright (c) 2014-present, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * is-windows + * + * Copyright © 2015-2018, Jon Schlinkert. + * Released under the MIT License. + */ +/*! + * to-regex-range + * + * Copyright (c) 2015-present, Jon Schlinkert. + * Released under the MIT License. + */ diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000..94627bf --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1,39 @@ +enableTelemetry: false + +changesetBaseRefs: + - main + - origin/main + - upstream/main + +changesetIgnorePatterns: + - '**/*.test.{js,jsx,ts,tsx}' + +defaultSemverRangePrefix: '' + +enableGlobalCache: false + +nmMode: hardlinks-local + +logFilters: + - code: YN0002 + level: discard + - code: YN0060 + level: discard + - code: YN0006 + level: discard + - code: YN0076 + level: discard + - code: YN0013 + level: discard + +nodeLinker: node-modules + +plugins: + - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs + spec: '@yarnpkg/plugin-workspace-tools' + - path: .yarn/plugins/@yarnpkg/plugin-version.cjs + spec: '@yarnpkg/plugin-version' + - path: .yarn/plugins/@yarnpkg/plugin-constraints.cjs + spec: '@yarnpkg/plugin-constraints' + +yarnPath: .yarn/releases/yarn-3.2.3.cjs diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..f707736 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @uniswap/web-admins diff --git a/README.md b/README.md new file mode 100644 index 0000000..200db06 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# sepolia测试网部署uniswap + +## 1.环境版本 + +``` +node v18.20.5 +npm 10.7.0 +yarn 3.2.3 + +``` + +## 2.安装依赖 + +``` +yarn + +``` + +## 3.执行 + +``` +yarn web start + +``` + +## 4.sepolia部署MyToken.sol合约 + +``` +使用remix部署MyToken.sol合约 +``` + +## 5.创建池子 + +## 6.添加流动性 + +## 详细链接 +https://www.coderspace.site/2024/11/30/uniswap-01/ \ No newline at end of file diff --git a/RELEASE b/RELEASE new file mode 100644 index 0000000..35b8606 --- /dev/null +++ b/RELEASE @@ -0,0 +1,24 @@ +IPFS hash of the deployment: +- CIDv0: `QmU9Yoq9SE7sqBXEwUhAYbfnJFwQp2BRb17J9ohrqWVnRK` +- CIDv1: `bafybeicwjyhv4k3yykxinpfnpipkhvqit2aaoqu4nzgodsghwzzf4wrciy` + +The latest release is always mirrored at [app.uniswap.org](https://app.uniswap.org). + +You can also access the Uniswap Interface from an IPFS gateway. +**BEWARE**: The Uniswap interface uses [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) to remember your settings, such as which tokens you have imported. +**You should always use an IPFS gateway that enforces origin separation**, or our hosted deployment of the latest release at [app.uniswap.org](https://app.uniswap.org). +Your Uniswap settings are never remembered across different URLs. + +IPFS gateways: +- https://bafybeicwjyhv4k3yykxinpfnpipkhvqit2aaoqu4nzgodsghwzzf4wrciy.ipfs.dweb.link/ +- https://bafybeicwjyhv4k3yykxinpfnpipkhvqit2aaoqu4nzgodsghwzzf4wrciy.ipfs.cf-ipfs.com/ +- [ipfs://QmU9Yoq9SE7sqBXEwUhAYbfnJFwQp2BRb17J9ohrqWVnRK/](ipfs://QmU9Yoq9SE7sqBXEwUhAYbfnJFwQp2BRb17J9ohrqWVnRK/) + +### 5.19.3 (2024-03-23) + + +### Bug Fixes + +* **web:** prod hotfix - dont show table error when only one query fails (#7075) aa14544 + + diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..5889a3a --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +web/5.19.3 \ No newline at end of file diff --git a/apps/mobile/.depcheckrc b/apps/mobile/.depcheckrc new file mode 100644 index 0000000..60522dc --- /dev/null +++ b/apps/mobile/.depcheckrc @@ -0,0 +1,29 @@ +ignores: [ + # Dependencies that depcheck thinks are unused but are actually used + "@uniswap/ethers-rs-mobile", + "babel-loader", + "babel-jest", + "babel-plugin-react-native-web", + "babel-plugin-transform-remove-console", + "cross-fetch", + "expo-localization", + "expo-linking", + "madge", + "postinstall-postinstall", + ## React Native Usage + "@amplitude/analytics-react-native", + "@react-native-masked-view/masked-view", + "@react-native-firebase/app-check", + "react-native-image-colors", + # Dependencies that depcheck thinks are missing but are actually present or never used + ## Internal packages / workspaces + "e2e", + "src", + "ui", + "tsconfig", + ## Subpackages of installed packages + "@redux-saga/core", + "@ethersproject/constants", + "@react-navigation/elements", + "metro-config", + ] diff --git a/apps/mobile/.detoxrc.js b/apps/mobile/.detoxrc.js new file mode 100644 index 0000000..c186e98 --- /dev/null +++ b/apps/mobile/.detoxrc.js @@ -0,0 +1,78 @@ +/** @type {Detox.DetoxConfig} */ +module.exports = { + testRunner: { + args: { + '$0': 'jest', + config: 'e2e/jest.config.js' + }, + jest: { + setupTimeout: 300000 + } + }, + apps: { + 'ios.debug': { + type: "ios.app", + binaryPath: "ios/build/Build/Products/Debug-iphonesimulator/Uniswap.app", + build: "RN_SRC_EXT=e2e.js,e2e.ts xcodebuild -workspace ios/Uniswap.xcworkspace -scheme Uniswap -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -arch x86_64" + }, + 'ios.release': { + type: 'ios.app', + binaryPath: "ios/build/Build/Products/Dev-iphonesimulator/Uniswap.app", + build: "RN_SRC_EXT=e2e.js,e2e.ts xcodebuild -workspace ios/Uniswap.xcworkspace -scheme Uniswap -configuration Dev -sdk iphonesimulator -derivedDataPath ios/build -UseModernBuildSystem=YES -arch x86_64" + }, + 'android.debug': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/dev/debug/app-dev-debug.apk', + testBinaryPath: "android/app/build/outputs/apk/androidTest/dev/debug/app-dev-debug-androidTest.apk", + build: 'cd android && ./gradlew assembleDevDebug assembleAndroidTest -DtestBuildType=debug', + reversePorts: [ + 8081 + ] + }, + 'android.release': { + type: 'android.apk', + binaryPath: 'android/app/build/outputs/apk/dev/release/app-dev-release.apk', + testBinaryPath: "android/app/build/outputs/apk/androidTest/dev/release/app-dev-release-androidTest.apk", + build: 'cd android && ./gradlew assembleDevRelease assembleAndroidTest -DtestBuildType=release' + } + }, + devices: { + simulator: { + type: 'ios.simulator', + device: { + type: "iPhone 15" + } + }, + attached: { + type: 'android.attached', + device: { + adbName: '.*' + } + }, + emulator: { + type: 'android.emulator', + device: { + avdName: 'Pixel_6_API_34' + } + } + }, + + configurations: { + "ios.sim.debug": { + device: "simulator", + app: "ios.debug" + }, + "ios.sim.release": { + device: "simulator", + app: "ios.release" + }, + "android.emu.debug": { + device: "emulator", + app: "android.debug" + }, + "android.emu.release": { + device: "emulator", + app: "android.release" + } + } +}; diff --git a/apps/mobile/.eslintignore b/apps/mobile/.eslintignore new file mode 100644 index 0000000..f7fff17 --- /dev/null +++ b/apps/mobile/.eslintignore @@ -0,0 +1,9 @@ +.eslintrc.js +babel.config.js +jest.config.js +metro.config.js +node_modules + +storybook-static + +coverage diff --git a/apps/mobile/.eslintrc.js b/apps/mobile/.eslintrc.js new file mode 100644 index 0000000..a8f1fdf --- /dev/null +++ b/apps/mobile/.eslintrc.js @@ -0,0 +1,13 @@ +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/native'], + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 2018, + sourceType: 'module', + }, +} diff --git a/apps/mobile/.gitignore b/apps/mobile/.gitignore new file mode 100644 index 0000000..35d8ec7 --- /dev/null +++ b/apps/mobile/.gitignore @@ -0,0 +1,117 @@ +# OSX +# +.DS_Store + +.tamagui + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +ios/.xcode.env.local +ios/GoogleService-Info.plist + +# Android/IntelliJ +# +build/ +.idea +.gradle +local.properties +*.iml +*.hprof +*.jks +keystore.properties +*.aab + +# node.js +# +node_modules/ +npm-debug.log +yarn-error.log + +# BUCK +buck-out/ +\.buckd/ +*.keystore +!debug.keystore + +# Yarn +.yarn/cache +.yarn/versions +.yarn/install-state.gz + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/ + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/.env +fastlane/.env.* +fastlane/builds + +# firebase +firebase-debug.log +firestore-debug.log +ui-debug.log + +# Bundle artifact +*.jsbundle +*.jsbundle.map + +# Detox artifacts +artifacts/ + +# CocoaPods +/ios/Pods/ + +# ccache +.ccache + +# hardhat network fork +/cache/ + +# Storybook +build-storybook.log +storybook-static/* + +# Private keys +.env.local + +# Built Items +ios/assets/ +./lib/ + +# Jest +coverage/ + +# Swift GraphQL codegen +# Ignores everything inside the schema folder except the README.md +!ios/WidgetsCore/MobileSchema/ +ios/WidgetsCore/MobileSchema/* +!ios/WidgetsCore/MobileSchema/README.md + +# Swift env +ios/WidgetsCore/Env.swift + +# Sentry +ios/sentry.properties +android/sentry.properties diff --git a/apps/mobile/.prettierignore b/apps/mobile/.prettierignore new file mode 100644 index 0000000..2875313 --- /dev/null +++ b/apps/mobile/.prettierignore @@ -0,0 +1,3 @@ +ios +android +.eslintrc.js diff --git a/apps/mobile/.watchmanconfig b/apps/mobile/.watchmanconfig new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/apps/mobile/.watchmanconfig @@ -0,0 +1 @@ +{} diff --git a/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch b/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch new file mode 100644 index 0000000..5187d9c --- /dev/null +++ b/apps/mobile/.yarn/patches/react-native-fast-image-npm-8.6.3-03ee2d23c0.patch @@ -0,0 +1,12 @@ +diff --git a/RNFastImage.podspec b/RNFastImage.podspec +index db0fada63fc06191f8620d336d244edde6c3dba3..286fa816e47996fdff9f25261644d612c682ae0b 100644 +--- a/RNFastImage.podspec ++++ b/RNFastImage.podspec +@@ -16,6 +16,6 @@ Pod::Spec.new do |s| + s.source_files = "ios/**/*.{h,m}" + + s.dependency 'React-Core' +- s.dependency 'SDWebImage', '~> 5.11.1' ++ s.dependency 'SDWebImage', '~> 5.15.5' + s.dependency 'SDWebImageWebPCoder', '~> 0.8.4' + end diff --git a/apps/mobile/Gemfile b/apps/mobile/Gemfile new file mode 100644 index 0000000..b5f8cfb --- /dev/null +++ b/apps/mobile/Gemfile @@ -0,0 +1,7 @@ +source "https://rubygems.org" + +gem 'fastlane', '2.214.0' +gem 'cocoapods', '1.14.3' + +plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile') +eval_gemfile(plugins_path) if File.exist?(plugins_path) diff --git a/apps/mobile/Gemfile.lock b/apps/mobile/Gemfile.lock new file mode 100644 index 0000000..1e55d89 --- /dev/null +++ b/apps/mobile/Gemfile.lock @@ -0,0 +1,292 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.6) + rexml + activesupport (7.1.2) + base64 + bigdecimal + concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb + i18n (>= 1.6, < 2) + minitest (>= 5.1) + mutex_m + tzinfo (~> 2.0) + addressable (2.8.6) + public_suffix (>= 2.0.2, < 6.0) + algoliasearch (1.27.5) + httpclient (~> 2.8, >= 2.8.3) + json (>= 1.5.1) + artifactory (3.0.15) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.877.0) + aws-sdk-core (3.190.1) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.651.0) + aws-sigv4 (~> 1.8) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.75.0) + aws-sdk-core (~> 3, >= 3.188.0) + aws-sigv4 (~> 1.1) + aws-sdk-s3 (1.142.0) + aws-sdk-core (~> 3, >= 3.189.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + bigdecimal (3.1.5) + claide (1.1.0) + cocoapods (1.14.3) + addressable (~> 2.8) + claide (>= 1.0.2, < 2.0) + cocoapods-core (= 1.14.3) + cocoapods-deintegrate (>= 1.0.3, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) + cocoapods-plugins (>= 1.0.0, < 2.0) + cocoapods-search (>= 1.0.0, < 2.0) + cocoapods-trunk (>= 1.6.0, < 2.0) + cocoapods-try (>= 1.1.0, < 2.0) + colored2 (~> 3.1) + escape (~> 0.0.4) + fourflusher (>= 2.3.0, < 3.0) + gh_inspector (~> 1.0) + molinillo (~> 0.8.0) + nap (~> 1.0) + ruby-macho (>= 2.3.0, < 3.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.14.3) + activesupport (>= 5.0, < 8) + addressable (~> 2.8) + algoliasearch (~> 1.0) + concurrent-ruby (~> 1.1) + fuzzy_match (~> 2.0.4) + nap (~> 1.0) + netrc (~> 0.11) + public_suffix (~> 4.0) + typhoeus (~> 1.0) + cocoapods-deintegrate (1.0.5) + cocoapods-downloader (2.1) + cocoapods-plugins (1.0.0) + nap + cocoapods-search (1.0.1) + cocoapods-trunk (1.6.0) + nap (>= 0.8, < 2.0) + netrc (~> 0.11) + cocoapods-try (1.2.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + concurrent-ruby (1.2.2) + connection_pool (2.4.1) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20231109) + dotenv (2.8.1) + drb (2.2.0) + ruby2_keywords + emoji_regex (3.2.3) + escape (0.0.4) + ethon (0.16.0) + ffi (>= 1.15.0) + excon (0.109.0) + faraday (1.10.3) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.1) + faraday-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.214.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 + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (~> 0.1.1) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.3) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (>= 1.4.5, < 2.0.0) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3) + fastlane-plugin-get_version_name (0.2.2) + fastlane-plugin-versioning_android (0.1.1) + ffi (1.16.3) + fourflusher (2.3.1) + fuzzy_match (2.0.4) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.2) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + webrick + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.29.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (2.1.0) + faraday (>= 1.0, < 3.a) + google-cloud-errors (1.3.1) + google-cloud-storage (1.45.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.29.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.9.1) + faraday (>= 1.0, < 3.a) + google-cloud-env (~> 2.1) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.5) + 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) + mini_mime (1.1.5) + minitest (5.20.0) + molinillo (0.8.0) + multi_json (1.15.0) + multipart-post (2.3.0) + mutex_m (0.2.0) + nanaimo (0.3.0) + nap (1.1.0) + naturally (2.2.1) + netrc (0.11.0) + optparse (0.1.1) + os (1.1.4) + plist (3.7.1) + public_suffix (4.0.7) + rake (13.1.0) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.2.6) + 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) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + terminal-notifier (2.0.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + typhoeus (1.4.1) + ethon (>= 0.9.0) + tzinfo (2.0.6) + concurrent-ruby (~> 1.0) + uber (0.1.0) + unicode-display_width (1.8.0) + webrick (1.8.1) + word_wrap (1.0.0) + xcodeproj (1.23.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) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + ruby + +DEPENDENCIES + cocoapods (= 1.14.3) + fastlane (= 2.214.0) + fastlane-plugin-get_version_name + fastlane-plugin-versioning_android + +BUNDLED WITH + 2.4.10 diff --git a/apps/mobile/LICENSE b/apps/mobile/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/apps/mobile/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/apps/mobile/README.md b/apps/mobile/README.md new file mode 100644 index 0000000..19ef49b --- /dev/null +++ b/apps/mobile/README.md @@ -0,0 +1,232 @@ +# Uniswap Wallet + +[Uniswap Wallet](https://wallet.uniswap.org/) is the simplest, safest, and most powerful self-custodial crypto wallet. It is developed by the Uniswap Labs team, inventors of the Uniswap Protocol. + +If you have suggestions on how we can improve the app, or would like to report a bug or a problem, check out the [Uniswap Help Center](https://support.uniswap.org/). + +## Setup + +### Requirements + +This guide assumes that: + +- You are using a Mac (you will need a Mac computer in order to run the Xcode iOS Simulator) +- You are using an Apple Silicon Mac (if you’re not sure, go to  → About this Mac and check if the chip name starts with "Apple") + +Note: if you are indeed using an Apple Silicon Mac, we recommend setting up your environment _without_ using Rosetta. Some instructions on how to do that can be found [here](https://medium.com/@davidjasonharding/developing-a-react-native-app-on-an-m1-mac-without-rosetta-29fcc7314d70). + +### Packages and Software + +#### Xcode + +You should start with downloading Xcode if you don't already have it installed, since the file is so large. You can find it here: [developer.apple.com/xcode](https://developer.apple.com/xcode/) + +You must use **XCode 15** to compile the app. + +#### Homebrew + +We’ll be using Homebrew to install many of the other required tools through the command line. + +1. Open a terminal +2. Copy and paste the command from [brew.sh](https://brew.sh/) into your terminal and run it + +#### nvm + +`nvm` is the Node Version Manager. While not required, it makes it easy to install Node and switch between different versions. A minimum Node version of 18 is required to use this repository. + +Copy the curl command listed under _Install & Update Script_ on [this page](https://github.com/nvm-sh/nvm#install--update-script) and run it in your terminal. + +To make sure nvm installed correctly, try running `nvm -v` (you may need to quit and re-open the terminal window). It should return a version number. If it returns something like `zsh: command not found: nvm`, it hasn’t been installed correctly. + +#### node + +Now we want to use nvm to install a specific version of node. + +Run the following command in your terminal: +`nvm install 18` + +and then when it’s finished, run: +`nvm use 18` + +Quit and re-open the terminal, and then run: +`node -v` + +to make sure you get a version number that starts with `v18.`. + +#### yarn + +We use yarn as our package manager and to run scripts. + +Run the following command to install it: +`npm install --global yarn` +(npm comes with node, so it should work if the above step has been completed correctly) + +Then run: +`yarn -v` +to see if it installed correctly. + +#### Ruby + +Use `rbenv` to install a specific version of `ruby`: +`brew install rbenv ruby-build` + +Run `rbenv init` and follow the instructions to complete the installation. + +After following the instructions, make sure you `source` your `.zshrc` or `.bash_profile`, or start a new terminal session. + +Install a version of `ruby`: +`rbenv install 3.2.2` + +Set this as your default version: +`rbenv global 3.2.2` + +Install cocoapods and fastlane using bundler (make sure to run in `mobile`) +`bundle install` + +#### Add Xcode Command Line Tools + +Open Xcode and go to: + +`Preferences → Locations → Command Line Tools` + +And select the version that pops up. + +#### JDK (Android) + +Taken from [RN instructions](https://reactnative.dev/docs/environment-setup?guide=native&platform=android) + +``` +brew tap homebrew/cask-versions +brew install --cask zulu17 + +# Get path to where cask was installed to double-click installer +brew info --cask zulu17 +``` + +Add the following to your .rc file +`export JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-17.jdk/Contents/Home` + +#### Android Studio + +Install [Android Studio](https://developer.android.com/studio) + +Add the following to your .rc file +``` +export ANDROID_HOME=$HOME/Library/Android/sdk +export PATH=$PATH:$ANDROID_HOME/emulator +export PATH=$PATH:$ANDROID_HOME/platform-tools +``` + +Android Studio should have an emulator already, but if not: +Open the project at `universe/apps/mobile/android` +Tools -> Device Manager to create a new emulator + +## Development + +Once all the setup steps above are completed, you're ready to try running the app locally! + +### Environment variables + +Note: The app will likely have limited functionality when running it locally with the default environment variables. + +Use the environment variables defined in the `.env.defaults.local` file to run the app locally. + +### Compile contract ABI types + +This is done in bootstrap but good to know about. Before the code will compile you need to generate types for the smart contracts the wallet interacts with. Run `yarn g:prepare` at the top level. Re-run this if the ABIs are ever changed. + +### Run the app + +In the root directory, run `yarn` to install all the necessary npm packages. + +Then run `yarn mobile pod` to install all the necessary pods. (You may need to updated source repos with `pod repo update` if this fails.) + +Finally, run `yarn mobile ios` to boot up the iOS Simulator and run the app inside it. The JS bundler (metro) should automatically open in a new terminal window. If it does not, start it manually with `yarn start`. + +Or you can use one command to run them all one after the other: `yarn && yarn pod && yarn ios` + +You can also run the app from Xcode, which is necessary for any Swift related changes. Xcode will automatically start the metro bundler. + +Hopefully you now (after a few minutes) see the Uniswap Wallet running in the iOS Simulator! + +### Enabling Flipper + +We do not check Flipper into source. To prevent `pod install` from adding Flipper, set an environment variable in your `.bash_profile` or `.zshrc` or `.zprofile`: + +```bash +# To enable flipper inclusion (optional) +export USE_FLIPPER=1 +``` + +Note: To disable Flipper, the whole line should be commented out, as setting this value to 0 will not disable Flipper. + +## Important Libraries and Tools + +These are some tools you might want to familiarize yourself with to understand the codebase better and how different aspects of it work. + +- [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/): state management +- [redux-saga](https://redux-saga.js.org/) & [typed-redux-saga](https://github.com/agiledigital/typed-redux-saga): Redux side effect manager -- used for complex/stateful network calls +- [ethers](https://docs.ethers.io/v5/) +- [Tamagui](https://tamagui.dev): UI framework +- [React navigation](https://reactnavigation.org/): routing and navigation with animations and gestures +- [react-i18next](https://react.i18next.com/): i18n + +## Migrations + +We use `redux-persist` to persist Redux state between user sessions. When the Redux state schema is altered, a migration may be needed to transfer the existing persisted state to the new Redux schema. Failing to define a migration results in the app defaulting to the persisted schema, which will very likely cause `undefined` errors because the code has references to Redux state properties that were dropped in favor the the persisted schema. + +### When to define a migration + +Anytime a required property is added or any property is renamed or deleted to/from Redux state. Migrations are not necessary when optional properties are added to an existing slice. Make sure to always add new required properties to the `schema.ts` file as well. + +### How to migrate + +1. Increment the `version` of `persistConfig` defined within `store.ts` +2. Create a migration function within `migrations.ts`. The migration key should be the same as the `version` defined in the previous step +3. Write a test for your migration within `migrations.test.ts` +4. Create a new schema within `schema.ts` and ensure it is being exported by the `getSchema` function at the bottom of the file + +## Troubleshooting + +### Common issues + +- `zsh: command not found: [package name]` +This means whichever package you're trying to run (`[package name]`) wasn’t correctly installed, or your Terminal can’t figure out how to run it. If you just installed it, try quitting terminal and re-opening it. Otherwise try reinstalling the package. + +- `unable to open file (in target "OneSignalNotificationServiceExtension" in project "Uniswap")`. +Resolve this issue by navigating to the `ios/` directory and running `pod update`. + +### Common fixes + +If something isn’t working the way it should or you’re getting a weird error when trying to run the app, try the following: + +1. Quit the terminal +2. Quit Metro terminal +3. Open Finder and navigate to the `mobile` directory +4. Delete the `node_modules` folder +5. Navigate into the `ios` folder +6. Delete the `Pods` folder +7. Open XCode +8. Go to Product → Clean Build Folder +9. Open your terminal again +10. Navigate to the `mobile` directory in the terminal +11. Run `yarn && yarn pod` again +12. Run `yarn ios` + +### Shell profile setup + +Your shell profile file is most likely one of: `.bash_profile`, `.zshrc`, or `.zprofile`, and will be located in `/Users/[username]/`. You can reveal hidden files in Finder by pressing `⌘` + `Shift` + `.`. + +If issues with your terminal or shell seem to be the cause of some of your problems, here is an example of what that file may look like in order for your terminal to be able to run the app locally: + +```zsh +eval "$(/opt/homebrew/bin/brew shellenv)" + +export NVM_DIR="$HOME/.nvm" +[ -s "/opt/homebrew/opt/nvm/nvm.sh" ] && \. "/opt/homebrew/opt/nvm/nvm.sh" # This loads nvm +[ -s "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" ] && \. "/opt/homebrew/opt/nvm/etc/bash_completion.d/nvm" # This loads nvm bash_completion + +# To enable flipper inclusion (optional) +export USE_FLIPPER=1 +export PATH="/opt/homebrew/opt/ruby/bin:$PATH" +``` diff --git a/apps/mobile/__mocks__/@react-native-firebase/app.ts b/apps/mobile/__mocks__/@react-native-firebase/app.ts new file mode 100644 index 0000000..6d394e0 --- /dev/null +++ b/apps/mobile/__mocks__/@react-native-firebase/app.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +export default { + app: () => ({ + auth: () => ({ + signInAnonymously: () => undefined, + }), + }), +} diff --git a/apps/mobile/__mocks__/@react-native-firebase/firestore.ts b/apps/mobile/__mocks__/@react-native-firebase/firestore.ts new file mode 100644 index 0000000..b1c6ea4 --- /dev/null +++ b/apps/mobile/__mocks__/@react-native-firebase/firestore.ts @@ -0,0 +1 @@ +export default {} diff --git a/apps/mobile/__mocks__/@react-native-firebase/remote-config.ts b/apps/mobile/__mocks__/@react-native-firebase/remote-config.ts new file mode 100644 index 0000000..b1c6ea4 --- /dev/null +++ b/apps/mobile/__mocks__/@react-native-firebase/remote-config.ts @@ -0,0 +1 @@ +export default {} diff --git a/apps/mobile/__mocks__/@react-native-masked-view/masked-view.ts b/apps/mobile/__mocks__/@react-native-masked-view/masked-view.ts new file mode 100644 index 0000000..66e67ac --- /dev/null +++ b/apps/mobile/__mocks__/@react-native-masked-view/masked-view.ts @@ -0,0 +1,13 @@ +import React, { PropsWithChildren, ReactNode } from 'react' +import { View, ViewProps } from 'react-native' + +// react-native-masked-view for Storybook web +// https://github.com/react-native-masked-view/masked-view/issues/70#issuecomment-1171801526 +function MaskedViewWeb({ + maskElement, + ...props +}: PropsWithChildren<{ maskElement: ReactNode }>): React.CElement { + return React.createElement(View, props, maskElement) +} + +export default MaskedViewWeb diff --git a/apps/mobile/__mocks__/@shopify/react-native-skia.ts b/apps/mobile/__mocks__/@shopify/react-native-skia.ts new file mode 100644 index 0000000..1a3bab1 --- /dev/null +++ b/apps/mobile/__mocks__/@shopify/react-native-skia.ts @@ -0,0 +1,22 @@ +import React, { PropsWithChildren } from 'react' +import { View, ViewProps } from 'react-native' + +// Source: https://github.com/Shopify/react-native-skia/issues/548#issuecomment-1157609472 + +const PlainView = ({ + children, + ...props +}: PropsWithChildren): React.CElement => { + return React.createElement(View, props, children) +} +const noop = (): null => null + +export const BlurMask = PlainView +export const Canvas = PlainView +export const Circle = PlainView +export const Group = PlainView +export const LinearGradient = PlainView +export const Mask = PlainView +export const Path = PlainView +export const Rect = PlainView +export const vec = noop diff --git a/apps/mobile/__mocks__/react-native-context-menu-view.ts b/apps/mobile/__mocks__/react-native-context-menu-view.ts new file mode 100644 index 0000000..e105414 --- /dev/null +++ b/apps/mobile/__mocks__/react-native-context-menu-view.ts @@ -0,0 +1,11 @@ +import React, { PropsWithChildren } from 'react' +import { View, ViewProps } from 'react-native' + +const PlainView = ({ + children, + ...props +}: PropsWithChildren): React.CElement => { + return React.createElement(View, props, children) +} + +export default PlainView diff --git a/apps/mobile/__mocks__/react-native-fast-image.ts b/apps/mobile/__mocks__/react-native-fast-image.ts new file mode 100644 index 0000000..92f6067 --- /dev/null +++ b/apps/mobile/__mocks__/react-native-fast-image.ts @@ -0,0 +1,13 @@ +import React, { PropsWithChildren } from 'react' +import { Image, ImageProps } from 'react-native' + +const PlainImage = ({ + children, + ...props +}: PropsWithChildren): React.CElement => { + return React.createElement(Image, props, children) +} + +PlainImage.resizeMode = {} + +export default PlainImage diff --git a/apps/mobile/__mocks__/react-native-permissions.ts b/apps/mobile/__mocks__/react-native-permissions.ts new file mode 100644 index 0000000..b1c6ea4 --- /dev/null +++ b/apps/mobile/__mocks__/react-native-permissions.ts @@ -0,0 +1 @@ +export default {} diff --git a/apps/mobile/android/.env.template b/apps/mobile/android/.env.template new file mode 100644 index 0000000..3bd6ee6 --- /dev/null +++ b/apps/mobile/android/.env.template @@ -0,0 +1,4 @@ +KEYSTORE_FILE=op://Android/keystore-properties-$APP_ENV/storeFile +STORE_PASSWORD=op://Android/keystore-properties-$APP_ENV/storePassword +KEYSTORE_ALIAS=op://Android/keystore-properties-$APP_ENV/keyAlias +KEY_PASSWORD=op://Android/keystore-properties-$APP_ENV/keyPassword \ No newline at end of file diff --git a/apps/mobile/android/README.md b/apps/mobile/android/README.md new file mode 100644 index 0000000..42328a4 --- /dev/null +++ b/apps/mobile/android/README.md @@ -0,0 +1,4 @@ +# Known Issues + +Android 14 (API level 34) features cannot be used until we upgrade to React Native 0.71.13. That means that the sdk versions cannot be upgraded to 34 until React Native is also upgraded. +https://github.com/facebook/react-native/issues/37769 diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle new file mode 100644 index 0000000..0454bfa --- /dev/null +++ b/apps/mobile/android/app/build.gradle @@ -0,0 +1,268 @@ +import com.android.build.OutputFile + +plugins { + id 'com.android.application' + id 'com.facebook.react' + id 'com.google.gms.google-services' + id 'maven-publish' + id 'kotlin-android' +} + +def nodeModulesPath = "../../../../node_modules" +def rnRoot = "../.." + + +def keystorePropertiesFile = rootProject.file("keystore.properties"); +def keystoreProperties = new Properties() +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +react { + root = file("$rnRoot/") + reactNativeDir = file("$nodeModulesPath/react-native") + codegenDir = file("$nodeModulesPath/react-native-codegen") + cliFile = file("$nodeModulesPath/react-native/cli.js") + debuggableVariants = ["devDebug", "betaDebug", "prodDebug"] + hermesCommand = "../../node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc" // This is relative to the project root. +} +/** + * Set this to true to create four separate APKs instead of one, + * one for each native architecture. This is useful if you don't + * use App Bundles (https://developer.android.com/guide/app-bundle/) + * and want to have separate APKs to upload to the Play Store. + */ +def enableSeparateBuildPerCPUArchitecture = false + +/** + * Set this to true to Run Proguard on Release builds to minify the Java bytecode. + */ +def enableProguardInReleaseBuilds = false + +/** + * The preferred build flavor of JavaScriptCore (JSC) + * + * For example, to use the international variant, you can use: + * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * + * The international variant includes ICU i18n library and necessary data + * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that + * give correct results when using with locales other than en-US. Note that + * this variant is about 6MiB larger per architecture than default. + */ +def jscFlavor = 'org.webkit:android-jsc-intl:+' + +/** + * Private function to get the list of Native Architectures you want to build. + * This reads the value from reactNativeArchitectures in your gradle.properties + * file and works together with the --active-arch-only flag of react-native run-android. + */ +def reactNativeArchitectures() { + def value = project.getProperties().get("reactNativeArchitectures") + return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"] +} + +boolean isCI = System.getenv('CI') != null +boolean isDetox = System.getenv('DETOX_MODE') != null +boolean sentryPropertiesAvailable = System.getenv('SENTRY_AUTH_TOKEN') != null && System.getenv('SENTRY_PROJECT') != null && System.getenv('SENTRY_ORG') != null + +if (isCI && sentryPropertiesAvailable && !isDetox) { + project.ext.sentryCli = [ + logLevel: "info", + ] + + apply from: "../../../../node_modules/@sentry/react-native/sentry.gradle" + apply plugin: "io.sentry.android.gradle" + + sentry { + uploadNativeSymbols = true + includeNativeSources = true + + autoInstallation { + enabled = false + } + } +} + +android { + ndkVersion rootProject.ext.ndkVersion + + namespace "com.uniswap" + defaultConfig { + applicationId "com.uniswap.mobile" + compileSdk rootProject.ext.compileSdkVersion + minSdkVersion rootProject.ext.minSdkVersion + targetSdkVersion rootProject.ext.targetSdkVersion + versionCode 1 + versionName "1.0" + multiDexEnabled true + testBuildType System.getProperty('testBuildType', 'debug') + testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + } + splits { + abi { + reset() + enable enableSeparateBuildPerCPUArchitecture + universalApk false // If true, also generate a universal APK + include (*reactNativeArchitectures()) + } + } + lintOptions { + abortOnError false + } + signingConfigs { + debug { + storeFile file('debug.keystore') + storePassword 'android' + keyAlias 'androiddebugkey' + keyPassword 'android' + } + release { + storeFile file(System.getenv("ANDROID_KEYSTORE_FILE") ?: 'keystore.jks') + storePassword System.getenv("ANDROID_STORE_PASSWORD") ?: keystoreProperties.getProperty("STORE_PASSWORD") + keyAlias System.getenv("ANDROID_KEYSTORE_ALIAS") ?: keystoreProperties.getProperty("KEYSTORE_ALIAS") + keyPassword System.getenv("ANDROID_KEY_PASSWORD") ?: keystoreProperties.getProperty("KEY_PASSWORD") + } + } + + flavorDimensions += "variant" + + productFlavors { + dev { + isDefault(true) + applicationIdSuffix ".dev" + versionName "1.24" + dimension "variant" + } + beta { + applicationIdSuffix ".beta" + versionName "1.24" + dimension "variant" + } + prod { + dimension "variant" + versionName "1.24" + } + } + + buildTypes { + debug { + signingConfig signingConfigs.debug + } + release { + signingConfig signingConfigs.release + minifyEnabled enableProguardInReleaseBuilds + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } + + // applicationVariants are e.g. debug, release + applicationVariants.all { variant -> + variant.outputs.each { output -> + // For each separate APK per architecture, set a unique version code as described here: + // https://developer.android.com/studio/build/configure-apk-splits.html + // Example: versionCode 1 will generate 1001 for armeabi-v7a, 1002 for x86, etc. + def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4] + def abi = output.getFilter(OutputFile.ABI) + if (abi != null) { // null for the universal-debug, universal-release variants + output.versionCodeOverride = + defaultConfig.versionCode * 1000 + versionCodes.get(abi) + } + + } + } + + packagingOptions { + resources.excludes.add("META-INF/*") + pickFirst 'lib/x86/libc++_shared.so' + pickFirst 'lib/x86_64/libc++_shared.so' + pickFirst 'lib/armeabi-v7a/libc++_shared.so' + pickFirst 'lib/arm64-v8a/libc++_shared.so' + } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.4.6" + } + + sourceSets { + main { + jniLibs { + srcDir '../../../../node_modules/@uniswap/ethers-rs-mobile/android/jniLibs' + } + } + } +} + +dependencies { + // The version of react-native is set by the React Native Gradle Plugin + implementation "com.facebook.react:react-android" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + + // Used to deal with Flipper/OkHttp issues with DevSupportManager + implementation "androidx.multidex:multidex:$multidexVersion" + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlinVersion" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerialization" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle" + + implementation 'com.google.android.play:integrity:1.2.0' + + // Firebase App Check: Import the BoM for the Firebase platform + implementation(platform("com.google.firebase:firebase-bom:32.7.2")) + implementation("com.google.firebase:firebase-appcheck-playintegrity") + + // Guava + implementation "com.google.guava:guava:24.1-jre" + // Guava fix + implementation "com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" + + //TODO: Revisit dependencies during security audit + //Drive + implementation('com.google.api-client:google-api-client-android:2.1.0') { + exclude group: 'org.apache.httpcomponents' + exclude module: 'guava-jdk5' + } + implementation('com.google.apis:google-api-services-drive:v3-rev20221023-2.0.0') { + exclude group: 'org.apache.httpcomponents' + exclude module: 'guava-jdk5' + } + implementation 'com.google.android.gms:play-services-auth:20.4.0' + + implementation 'com.google.api-client:google-api-client-jackson2:1.31.1' + implementation 'com.google.auth:google-auth-library-oauth2-http:1.11.0' + + implementation "androidx.compose.foundation:foundation:$compose" + implementation "androidx.compose.material:material:$compose" + implementation "androidx.compose.ui:ui:$compose" + + implementation "androidx.security:security-crypto:1.0.0" + implementation 'com.lambdapioneer.argon2kt:argon2kt:1.3.0' + + implementation "com.google.accompanist:accompanist-flowlayout:$flowlayout" + + implementation project(':@sentry_react-native') + // Used for device-reported performance class. + implementation("androidx.core:core-performance:$corePerf") + implementation("androidx.core:core-performance-play-services:$corePerf") + + debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") { + exclude group:'com.facebook.fbjni' + } + debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") { + exclude group:'com.squareup.okhttp3', module:'okhttp' + } + debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") + + if (hermesEnabled.toBoolean()) { + implementation("com.facebook.react:hermes-android") + } else { + implementation jscFlavor + } + androidTestImplementation('com.wix:detox:+') { + exclude module: "protobuf-lite" + } +} + +apply from: file("${nodeModulesPath}/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/apps/mobile/android/app/debug.keystore b/apps/mobile/android/app/debug.keystore new file mode 100644 index 0000000..364e105 Binary files /dev/null and b/apps/mobile/android/app/debug.keystore differ diff --git a/apps/mobile/android/app/proguard-rules.pro b/apps/mobile/android/app/proguard-rules.pro new file mode 100644 index 0000000..11b0257 --- /dev/null +++ b/apps/mobile/android/app/proguard-rules.pro @@ -0,0 +1,10 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: diff --git a/apps/mobile/android/app/src/androidTest/java/com/uniswap/DetoxTest.java b/apps/mobile/android/app/src/androidTest/java/com/uniswap/DetoxTest.java new file mode 100644 index 0000000..87753be --- /dev/null +++ b/apps/mobile/android/app/src/androidTest/java/com/uniswap/DetoxTest.java @@ -0,0 +1,29 @@ +package com.uniswap; + +import com.wix.detox.Detox; +import com.wix.detox.config.DetoxConfig; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.LargeTest; +import androidx.test.rule.ActivityTestRule; + +@RunWith(AndroidJUnit4.class) +@LargeTest +public class DetoxTest { + @Rule + public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); + + @Test + public void runDetoxTests() { + DetoxConfig detoxConfig = new DetoxConfig(); + detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; + detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; + detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); + + Detox.runTests(mActivityRule, detoxConfig); + } +} diff --git a/apps/mobile/android/app/src/beta/AndroidManifest.xml b/apps/mobile/android/app/src/beta/AndroidManifest.xml new file mode 100644 index 0000000..34bd376 --- /dev/null +++ b/apps/mobile/android/app/src/beta/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/mobile/android/app/src/beta/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/android/app/src/beta/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..ec59d6e --- /dev/null +++ b/apps/mobile/android/app/src/beta/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..fa62883 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_background.png b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..1966948 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..fa62883 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..42b7123 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..b02e661 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_background.png b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..75025cf Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b02e661 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..2fb8e79 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..14c3662 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..9784f16 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..14c3662 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..0660b13 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..b170256 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..04ef206 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b170256 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..6a71cfa Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..65a7df3 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..66a5487 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..65a7df3 Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8bf7f8d Binary files /dev/null and b/apps/mobile/android/app/src/beta/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/beta/res/values/strings.xml b/apps/mobile/android/app/src/beta/res/values/strings.xml new file mode 100644 index 0000000..8df98a8 --- /dev/null +++ b/apps/mobile/android/app/src/beta/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Uniswap Beta + diff --git a/apps/mobile/android/app/src/debug/AndroidManifest.xml b/apps/mobile/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..1d469e5 --- /dev/null +++ b/apps/mobile/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/apps/mobile/android/app/src/debug/java/com/uniswap/ReactNativeFlipper.java b/apps/mobile/android/app/src/debug/java/com/uniswap/ReactNativeFlipper.java new file mode 100644 index 0000000..a52b966 --- /dev/null +++ b/apps/mobile/android/app/src/debug/java/com/uniswap/ReactNativeFlipper.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.uniswap; + +import android.content.Context; +import com.facebook.flipper.android.AndroidFlipperClient; +import com.facebook.flipper.android.utils.FlipperUtils; +import com.facebook.flipper.core.FlipperClient; +import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin; +import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin; +import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin; +import com.facebook.flipper.plugins.inspector.DescriptorMapping; +import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin; +import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor; +import com.facebook.flipper.plugins.network.NetworkFlipperPlugin; +import com.facebook.flipper.plugins.react.ReactFlipperPlugin; +import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.modules.network.NetworkingModule; +import okhttp3.OkHttpClient; + +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + if (FlipperUtils.shouldEnableFlipper(context)) { + final FlipperClient client = AndroidFlipperClient.getInstance(context); + + client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults())); + client.addPlugin(new DatabasesFlipperPlugin(context)); + client.addPlugin(new SharedPreferencesFlipperPlugin(context)); + client.addPlugin(CrashReporterPlugin.getInstance()); + + NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin(); + NetworkingModule.setCustomClientBuilder( + new NetworkingModule.CustomClientBuilder() { + @Override + public void apply(OkHttpClient.Builder builder) { + builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin)); + } + }); + client.addPlugin(networkFlipperPlugin); + client.start(); + + // Fresco Plugin needs to ensure that ImagePipelineFactory is initialized + // Hence we run if after all native modules have been initialized + ReactContext reactContext = reactInstanceManager.getCurrentReactContext(); + if (reactContext == null) { + reactInstanceManager.addReactInstanceEventListener( + new ReactInstanceManager.ReactInstanceEventListener() { + @Override + public void onReactContextInitialized(ReactContext reactContext) { + reactInstanceManager.removeReactInstanceEventListener(this); + reactContext.runOnNativeModulesQueueThread( + new Runnable() { + @Override + public void run() { + client.addPlugin(new FrescoFlipperPlugin()); + } + }); + } + }); + } else { + client.addPlugin(new FrescoFlipperPlugin()); + } + } + } +} diff --git a/apps/mobile/android/app/src/dev/AndroidManifest.xml b/apps/mobile/android/app/src/dev/AndroidManifest.xml new file mode 100644 index 0000000..d304a60 --- /dev/null +++ b/apps/mobile/android/app/src/dev/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..ec59d6e --- /dev/null +++ b/apps/mobile/android/app/src/dev/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..fa99c2e Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..5def7fe Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..fa99c2e Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..1a0de42 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..63046df Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..b2b5d15 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..63046df Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..67713a1 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..1744144 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..43dca89 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1744144 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c225832 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..2d45be7 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..e843b87 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2d45be7 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..990b30f Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..c37dc96 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..c44b383 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..c37dc96 Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..2b8eeaa Binary files /dev/null and b/apps/mobile/android/app/src/dev/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/dev/res/values/strings.xml b/apps/mobile/android/app/src/dev/res/values/strings.xml new file mode 100644 index 0000000..fea264d --- /dev/null +++ b/apps/mobile/android/app/src/dev/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Uniswap Dev + diff --git a/apps/mobile/android/app/src/dev/res/xml/network_security_config.xml b/apps/mobile/android/app/src/dev/res/xml/network_security_config.xml new file mode 100644 index 0000000..fe0064a --- /dev/null +++ b/apps/mobile/android/app/src/dev/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + 10.0.2.2 + localhost + + diff --git a/apps/mobile/android/app/src/main/AndroidManifest.xml b/apps/mobile/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..98a145c --- /dev/null +++ b/apps/mobile/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/assets/OnboardingDark.riv b/apps/mobile/android/app/src/main/assets/OnboardingDark.riv new file mode 100644 index 0000000..64d5092 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/OnboardingDark.riv differ diff --git a/apps/mobile/android/app/src/main/assets/OnboardingLight.riv b/apps/mobile/android/app/src/main/assets/OnboardingLight.riv new file mode 100644 index 0000000..5a6da1d Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/OnboardingLight.riv differ diff --git a/apps/mobile/android/app/src/main/assets/PendingSend.riv b/apps/mobile/android/app/src/main/assets/PendingSend.riv new file mode 100644 index 0000000..f4e4bd4 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/PendingSend.riv differ diff --git a/apps/mobile/android/app/src/main/assets/PendingSwap.riv b/apps/mobile/android/app/src/main/assets/PendingSwap.riv new file mode 100644 index 0000000..fa2f5ff Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/PendingSwap.riv differ diff --git a/apps/mobile/android/app/src/main/assets/fonts/Basel-Book.ttf b/apps/mobile/android/app/src/main/assets/fonts/Basel-Book.ttf new file mode 100644 index 0000000..136d64d Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/fonts/Basel-Book.ttf differ diff --git a/apps/mobile/android/app/src/main/assets/fonts/Basel-Medium.ttf b/apps/mobile/android/app/src/main/assets/fonts/Basel-Medium.ttf new file mode 100644 index 0000000..6e5bf68 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/fonts/Basel-Medium.ttf differ diff --git a/apps/mobile/android/app/src/main/assets/fonts/InputMono-Regular.ttf b/apps/mobile/android/app/src/main/assets/fonts/InputMono-Regular.ttf new file mode 100644 index 0000000..1da5604 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/fonts/InputMono-Regular.ttf differ diff --git a/apps/mobile/android/app/src/main/assets/fonts/monospace.ttf b/apps/mobile/android/app/src/main/assets/fonts/monospace.ttf new file mode 100644 index 0000000..9fcf9b4 Binary files /dev/null and b/apps/mobile/android/app/src/main/assets/fonts/monospace.ttf differ diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/AndroidDeviceModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/AndroidDeviceModule.kt new file mode 100644 index 0000000..4a722b8 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/AndroidDeviceModule.kt @@ -0,0 +1,24 @@ +package com.uniswap + +import androidx.core.performance.play.services.PlayServicesDevicePerformance +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod + + +/** + * React module to provide device level information particular to Android + */ +class AndroidDeviceModule(reactContext: ReactApplicationContext): ReactContextBaseJavaModule(reactContext) { + override fun getName() = REACT_CLASS + + @ReactMethod + fun getPerformanceClass(promise: Promise) { + val devicePerformance = PlayServicesDevicePerformance(reactApplicationContext) + promise.resolve(devicePerformance.mediaPerformanceClass) + } + companion object { + private const val REACT_CLASS = "AndroidDeviceModule" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/EncryptionHelper.kt b/apps/mobile/android/app/src/main/java/com/uniswap/EncryptionHelper.kt new file mode 100644 index 0000000..6159ba9 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/EncryptionHelper.kt @@ -0,0 +1,139 @@ +import com.lambdapioneer.argon2kt.Argon2Kt +import com.lambdapioneer.argon2kt.Argon2KtResult +import com.lambdapioneer.argon2kt.Argon2Mode +import javax.crypto.spec.SecretKeySpec +import java.security.SecureRandom +import android.util.Base64; +import javax.crypto.Cipher +import javax.crypto.spec.IvParameterSpec + +/** + * Encrypts a string using an AES/GCM cipher and a key derived from a password and salt. + * + * This function creates a key from a password and salt with the function [keyFromPassword]. + * It then creates a Cipher instance for AES/GCM/NoPadding with a given random nonce and + * encrypts the original string. The result of this operation is encoded as Base64 and + * then returned. + * + * @param secret The original plaintext string to be encrypted. + * @param password The user-supplied password used for generating the encryption key with the salt. + * @param salt The unique salt used in conjunction with the password for key generation. + * @param nonce The unique byte array nonce (IV) used for AES-GCM cipher + * + * @return A string representing the Base64 encoded encrypted string. + * + * @throws IllegalArgumentException If secret, password, salt, or nonce is blank. + * + * @see Cipher + * @see SecretKeySpec + * @see IvParameterSpec + * @see Base64 + */ +fun encrypt(secret: String, password: String, salt: String, nonce: ByteArray): String { + val key = keyFromPassword(password, salt) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(nonce)) + val encrypted = cipher.doFinal(secret.toByteArray(Charsets.UTF_8)) + return Base64.encodeToString(encrypted, Base64.DEFAULT) +} + +/** + * Decrypts an AES/GCM encrypted string using a password, salt, and nonce to provide the decryption key. + * + * This function generates a key derived from a password and salt, along with the nonce used for AES-GCM cipher + * This key is then used in an AES/GCM cipher to decrypt the provided encrypted secret. Returns the decrypted + * original string. + * + * @param encryptedSecret The encrypted string (in Base64 format) to be decrypted. + * @param password User-supplied password used along with salt for deriving the decryption key. + * @param salt A unique string (in Base64 format) used to diversify derived encryption keys + * and to protect from rainbow table attacks. + * @param nonce A unique byte array used as an nonce/IV for the AES-GCM cipher. + * + * @return A UTF-8 encoded string that was decrypted from the encryptedSecret. + * + * @throws IllegalArgumentException If password or salt or nonce is blank or encryptedSecret is not a valid Base64 string. + * + * @see Cipher + * @see SecretKeySpec + * @see IvParameterSpec + * @see Base64 + * @see Charsets.UTF_8 + */ +fun decrypt(encryptedSecret: String, password: String, salt: String, nonce: ByteArray): String { + val key = keyFromPassword(password, salt) + val cipher = Cipher.getInstance("AES/GCM/NoPadding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), IvParameterSpec(nonce)) + val original = cipher.doFinal(Base64.decode(encryptedSecret, Base64.DEFAULT)) + return String(original, Charsets.UTF_8) +} + +/** + * Generates a cryptographic key from a user password and salt using Argon2Kt. + * + * This function uses Argon2Kt to generate a hash from a user password and a given salt. + * It then returns the raw hash as a byte array. + * This is useful for secure password storage or key derivation. + * Parameters where picked based on the security audit and recommendations + * + * @param password The plaintext password provided by the user. + * @param salt The unique salt generated for hashing. + * + * @return A byte array representing the raw hash of the password. + * + * @throws IllegalArgumentException If password or salt is blank. + * + * @see Argon2Kt + * @see Charsets + */ +fun keyFromPassword(password: String, salt: String): ByteArray { + val hash: Argon2KtResult = Argon2Kt().hash( + mode = Argon2Mode.ARGON2_ID, + password = password.toByteArray(Charsets.UTF_8), + salt = salt.toByteArray(Charsets.UTF_8), + tCostInIterations = 3, + mCostInKibibyte = 65536, + parallelism = 4 + ) + return hash.rawHashAsByteArray() +} + +/** + * Generates a Base64-encoded string to be used as a security salt. + * + * This function creates a ByteArray of a given length and populates it + * with securely random bytes. These bytes are then encoded to Base64 string. + * + * @param length The length of the byte array to be generated which further determines the length of the salt. + * + * @return A Base64 encoded string representing the generated salt. + * + * @see SecureRandom + * @see Base64 + */ +fun generateSalt(length: Int): String { + val bytes = ByteArray(length) + val secureRandom = SecureRandom() + secureRandom.nextBytes(bytes) + return Base64.encodeToString(bytes, Base64.DEFAULT) +} + +/** + * Generates a byte array to be used as a nonce/initialization vector. + * + * This function creates a ByteArray of a given length and populates it + * with securely random bytes. + * + * @param length The length of the byte array to be generated. + * + * @return A random array of bytes representing the generated nonce. + * + * @see SecureRandom + * @see Base64 + */ +fun generateNonce(length: Int): ByteArray { + val bytes = ByteArray(length) + val secureRandom = SecureRandom() + secureRandom.nextBytes(bytes) + return bytes +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/EthersRs.kt b/apps/mobile/android/app/src/main/java/com/uniswap/EthersRs.kt new file mode 100644 index 0000000..233c191 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/EthersRs.kt @@ -0,0 +1,106 @@ +package com.uniswap + +/** + * These functions are defined from an object to be used from a static context. + * The Rust implementation contains JNI bindings that are generated from the definition here. + */ +object EthersRs { + + /** + * Validates a mnemonic string to check that each word exists in the BIP 39 wordlist. + * @param mnemonic - the mnemonic string + * @return The first invalid word. If there are none, an invalid string. + */ + external fun findInvalidWord(mnemonic: String): String + + /** + * General validation for a mnemonic string, including entropy. + * @param mnemonic - the mnemonic string + * @return True if valid and false if not. + */ + external fun validateMnemonic(mnemonic: String): Boolean + + /** + * Generates a mnemonic and its associated address. + * @return A CMnemonicAndAddress object containing the generated mnemonic and its associated address. + */ + external fun generateMnemonic(): MnemonicAndAddress + + /** + * Generates a private key from a given mnemonic. + * @param mnemonic The mnemonic to generate the private key from. + * @param index The index of the private key to generate. + * @return A CPrivateKey object containing the generated private key. + */ + external fun privateKeyFromMnemonic(mnemonic: String?, index: Int): PrivateKeyAndAddress + + /** + * Creates a wallet from a given private key. + * @param privateKey The private key to create the wallet from. + * @return A long representing the pointer to the created wallet. + */ + external fun walletFromPrivateKey(privateKey: String?): Long + + /** + * Frees the memory allocated for the wallet. + * @param walletPtr The pointer to the wallet to be freed. + */ + external fun walletFree(walletPtr: Long) + + /** + * Signs a transaction with a wallet. + * @param localWallet The wallet to sign the transaction with. + * @param txHash The transaction hash to sign. + * @param chainId The id of the blockchain network. + * @return A signed transaction hash. + */ + external fun signTxWithWallet( + localWallet: Long, + txHash: String, + chainId: Long + ): String + + /** + * Signs a message with a wallet. + * @param localWallet The wallet to sign the message with. + * @param message The message to sign. + * @return The signed message. + */ + external fun signMessageWithWallet( + localWallet: Long, + message: String + ): String + + /** + * Signs a hash with a wallet. + * @param localWallet The wallet to sign the hash with. + * @param hash The hash to sign. + * @param chainId The id of the blockchain network. + * @return The signed hash. + */ + external fun signHashWithWallet( + localWallet: Long, + hash: String, + chainId: Long + ): String +} + +/** + * Represents a private key and its associated address. + * @property privateKey The private key. + * @property address The address associated with the private key. + */ +class PrivateKeyAndAddress( + var privateKey: String, + var address: String, +) + +/** + * Represents a mnemonic and its associated address. + * @property mnemonic The mnemonic phrase. + * @property address The address associated with the mnemonic. + */ +class MnemonicAndAddress( + var mnemonic: String, + var address: String, +) diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/GoogleDriveApiHelper.kt b/apps/mobile/android/app/src/main/java/com/uniswap/GoogleDriveApiHelper.kt new file mode 100644 index 0000000..93b0c48 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/GoogleDriveApiHelper.kt @@ -0,0 +1,235 @@ +package com.uniswap + +import android.app.Activity +import android.content.Intent +import android.util.Log +import com.facebook.react.bridge.ActivityEventListener +import com.facebook.react.bridge.ReactApplicationContext +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.auth.api.signin.GoogleSignInClient +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.common.api.Scope +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.http.ByteArrayContent +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.gson.GsonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.FileList +import com.google.gson.Gson +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.nio.charset.StandardCharsets + +object GDriveParams { + const val SPACES = "appDataFolder" + const val FIELDS = "nextPageToken, files(id, name)" + const val PAGE_SIZE_NORMAL = 30 + const val PAGE_SIZE_SINGLE = 1 +} + +/** + * Helper class for Google Drive operations such as fetching and storing backups. + */ +class GoogleDriveApiHelper { + companion object { + + private val gson = Gson() + + /** + * Returns a GoogleSignInClient object, which is needed to access Google Drive. + * + * @param reactContext The react application context. + * @return GoogleSignInClient object + * @throws IllegalStateException if the activity context is null. + */ + private fun getGoogleSignInClient(reactContext: ReactApplicationContext): GoogleSignInClient { + val activity = reactContext.currentActivity + if (activity != null) { + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestScopes(Scope(DriveScopes.DRIVE_APPDATA)) + .build() + return GoogleSignIn.getClient(activity, signInOptions) + } else { + throw IllegalStateException("Activity cannot be null") + } + } + + /** + * Determines if the user has permission to Google Drive. + * + * @param reactContext The react application context. + * @return A Boolean value indicating whether the user has permission. + */ + private fun hasPermissionToGoogleDrive(reactContext: ReactApplicationContext): Boolean { + val acc = GoogleSignIn.getLastSignedInAccount(reactContext) + val hasPermissions = + acc?.let { GoogleSignIn.hasPermissions(acc, Scope(DriveScopes.DRIVE_APPDATA)) } + return hasPermissions == true + } + + /** + * Fetches cloud backups from Google Drive and triggers corresponding events. + * + * @param drive The authenticated Drive object of Google Drive. + */ + suspend fun fetchCloudBackupFiles( + drive: Drive + ): FileList { + return withContext(Dispatchers.IO) { + val files: FileList = drive.files().list() + .setSpaces(GDriveParams.SPACES) + .setFields(GDriveParams.FIELDS) + .setPageSize(GDriveParams.PAGE_SIZE_NORMAL) + .execute() + + files + } + } + + /** + * Asynchronously retrieves an authenticated GoogleDrive object by gaining Google Drive permissions. + * + * @param reactContext The react application context. + * @return An authenticated GoogleSignInAccount object. + */ + private suspend fun getGoogleDrivePermissions(reactContext: ReactApplicationContext): GoogleSignInAccount? = + suspendCancellableCoroutine { continuation -> + try { + val googleSignInClient = getGoogleSignInClient(reactContext) + googleSignInClient.signOut() // Force a sign out so that we can reselect account + val signInIntent = googleSignInClient.signInIntent + reactContext.currentActivity?.startActivityForResult( + signInIntent, + Request.GOOGLE_SIGN_IN.value + ) + val listener = object : ActivityEventListener { + override fun onActivityResult( + activity: Activity?, + requestCode: Int, + resultCode: Int, + intent: Intent? + ) { + // Remove the listener after using it + reactContext.removeActivityEventListener(this) + if (requestCode == Request.GOOGLE_SIGN_IN.value && resultCode == Activity.RESULT_OK) { + + val signInTask = GoogleSignIn.getSignedInAccountFromIntent(intent) + val account: GoogleSignInAccount? = + signInTask.getResult(ApiException::class.java) + continuation.resumeWith(Result.success(account)) + + } else { + continuation.resumeWith(Result.failure(Exception("Oauth process has been interrupted"))) + Log.d("Activity intent", "Indent null") + } + } + + override fun onNewIntent(p0: Intent?) {} + } + reactContext.addActivityEventListener(listener) + } catch (e: Exception) { + Log.e("EXCEPTION", "${e.message}") + continuation.resumeWith( + Result.failure( + Exception("Failed to get google drive account") + ) + ) + } + } + + /** + * Asynchronously retrieves an authenticated Drive object. + * + * @param reactContext The react application context. + * @return Google Drive object if user has permissions, `null` otherwise. + */ + suspend fun getGoogleDrive( + reactContext: ReactApplicationContext, + useRecentAccount: Boolean = false + ): Pair { + return withContext(Dispatchers.IO) { + val canUseRecentAccount = useRecentAccount && hasPermissionToGoogleDrive(reactContext) + val account = + if (canUseRecentAccount) + GoogleSignIn.getLastSignedInAccount(reactContext) + else + getGoogleDrivePermissions(reactContext) + + val drive = account?.let { + val credential = + GoogleAccountCredential.usingOAuth2(reactContext, listOf(DriveScopes.DRIVE_APPDATA)) + credential.selectedAccount = account.account!! + + Drive.Builder( + NetHttpTransport(), + GsonFactory.getDefaultInstance(), + credential + ) + .setApplicationName(reactContext.getString(R.string.app_name)) + .build() + } + + Pair(drive, account) + } + } + + /** + * Fetches the fileId of a file in Google Drive by its file name. + * Assuming there is no bug in code, should always be only one file with the given name + * even though google drive allows to store multiple files with the same name + * + * @param drive The authenticated Drive object of Google Drive. + * @param name Name of the file. + * @return String fileId if file exists, `null` otherwise. + */ + fun getFileIdByFileName(drive: Drive, name: String): String? { + try { + val files: FileList = drive.files().list() + .setSpaces(GDriveParams.SPACES) + .setFields(GDriveParams.FIELDS) + .setPageSize(GDriveParams.PAGE_SIZE_SINGLE) + .setQ("name = '$name.json'") + .execute() + return files.files.firstOrNull()?.id + } catch (e: Exception) { + e.printStackTrace() + } + return null + } + + /** + * Uploads mnemonic backup to google drive in json formant + * + * @param drive The authenticated Drive object of Google Drive. + * @param mnemonicId Id of saved mnemonic. + * @param backup Instance of [CloudStorageMnemonicBackup] object representing mnemonic backup. + * @return String fileId if file exists, `null` otherwise. + */ + fun saveMnemonicToGoogleDrive( + drive: Drive, + mnemonicId: String, + backup: CloudStorageMnemonicBackup + ) { + val fileMetadata = File() + fileMetadata.name = "$mnemonicId.json" + fileMetadata.parents = listOf("appDataFolder") + + val jsonData = gson.toJson(backup) + + val jsonByteArray = jsonData.toByteArray(StandardCharsets.UTF_8) + val inputContent = ByteArrayContent("application/json", jsonByteArray) + val fileId = getFileIdByFileName(drive, mnemonicId) + if (fileId != null) { + drive.files().delete(fileId).execute() + } + drive.files().create(fileMetadata, inputContent) + .execute() + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt new file mode 100644 index 0000000..013d8f2 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainActivity.kt @@ -0,0 +1,51 @@ +package com.uniswap + +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.view.View +import com.facebook.react.ReactActivity +import com.facebook.react.ReactActivityDelegate +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.concurrentReactEnabled +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled +import com.facebook.react.defaults.DefaultReactActivityDelegate +import com.facebook.react.modules.i18nmanager.I18nUtil + + +class MainActivity : ReactActivity() { + + /** + * Returns the name of the main component registered from JavaScript. This is used to schedule + * rendering of the component. + */ + override fun getMainComponentName(): String { + return "Uniswap" + } + + // Required for react-navigation to work on Android + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(null); + + window.navigationBarColor = Color.TRANSPARENT + + if (Build.VERSION_CODES.Q <= Build.VERSION.SDK_INT) { + window.isNavigationBarContrastEnforced = false + } + val sharedI18nUtilInstance = I18nUtil.getInstance() + sharedI18nUtilInstance.allowRTL(applicationContext, false) + } + + /** + * Returns the instance of the [ReactActivityDelegate]. Here we use a util class [ ] which allows you to easily enable Fabric and Concurrent React + * (aka React 18) with two boolean flags. + */ + override fun createReactActivityDelegate(): ReactActivityDelegate? { + return DefaultReactActivityDelegate( + this, + mainComponentName, // If you opted-in for the New Architecture, we enable the Fabric Renderer. + fabricEnabled, // fabricEnabled + // If you opted-in for the New Architecture, we enable Concurrent React (i.e. React 18). + concurrentReactEnabled // concurrentRootEnabled + ) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt new file mode 100644 index 0000000..7ef4498 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/MainApplication.kt @@ -0,0 +1,55 @@ +package com.uniswap + +import com.facebook.react.PackageList +import com.facebook.react.ReactApplication +import com.facebook.react.ReactNativeHost +import com.facebook.react.ReactPackage +import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint; +import com.facebook.react.defaults.DefaultReactNativeHost; +import com.facebook.soloader.SoLoader +import androidx.multidex.MultiDexApplication; +import com.shopify.reactnativeperformance.ReactNativePerformance +import com.uniswap.onboarding.scantastic.ScantasticEncryptionModule + +class MainApplication : MultiDexApplication(), ReactApplication { + private val mReactNativeHost: ReactNativeHost = + object : DefaultReactNativeHost(this) { + override fun getUseDeveloperSupport(): Boolean { + return BuildConfig.DEBUG + } + + override fun getPackages(): List = + PackageList(this).packages.apply { + // Packages that cannot be autolinked yet can be added manually here, for example: + // packages.add(new MyReactNativePackage()); + add(UniswapPackage()) + add(RNCloudStorageBackupsManagerModule()) + add(ScantasticEncryptionModule()) + } + + override fun getJSMainModuleName(): String { + return "index" + } + + override val isNewArchEnabled: Boolean + get() = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED + + override val isHermesEnabled: Boolean + get() = BuildConfig.IS_HERMES_ENABLED + } + + override fun getReactNativeHost(): ReactNativeHost { + return mReactNativeHost + } + + override fun onCreate() { + ReactNativePerformance.onAppStarted() + super.onCreate() + SoLoader.init(this,false) + if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) { + // If you opted-in for the New Architecture, we load the native entry point for this app. + DefaultNewArchitectureEntryPoint.load(); + } + ReactNativeFlipper.initializeFlipper(this, reactNativeHost.reactInstanceManager); + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManager.kt b/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManager.kt new file mode 100644 index 0000000..8faa6ec --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManager.kt @@ -0,0 +1,330 @@ +package com.uniswap + +import android.util.Log +import com.facebook.react.bridge.Arguments +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.bridge.WritableMap +import com.facebook.react.modules.core.DeviceEventManagerModule +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.gson.Gson +import decrypt +import encrypt +import generateSalt +import generateNonce +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.io.FileNotFoundException +import java.util.Date +import javax.crypto.BadPaddingException +import javax.crypto.IllegalBlockSizeException + +/** + * Data class representing a mnemonic backup in cloud storage. + * + * @property mnemonicId The ID of the mnemonic string. + * @property encryptedMnemonic The encrypted mnemonic string. + * @property encryptionSalt The salt used for generating the encryption key from password. + * @property encryptionNonce The nonce used for encryption. + * @property createdAt The time the backup was created, in seconds since the epoch. + */ +data class CloudStorageMnemonicBackup( + val mnemonicId: String, + val encryptedMnemonic: String, + val encryptionSalt: String, + val encryptionNonce: ByteArray, + val createdAt: Double, + val googleDriveEmail: String? = null +) + +/** + * Enum representing various types of cloud backup errors. + */ +enum class CloudBackupError(val value: String) { + BACKUP_NOT_FOUND_ERROR("backupNotFoundError"), + BACKUP_ENCRYPTION_ERROR("backupEncryptionError"), + BACKUP_DECRYPTION_ERROR("backupDecryptionError"), + BACKUP_INCORRECT_PASSWORD_ERROR("backupIncorrectPasswordError"), + DELETE_BACKUP_ERROR("deleteBackupError"), + CLOUD_ERROR("cloudError") +} + +/** + * Enum representing various types of requests. + */ +enum class Request(val value: Int) { + GOOGLE_SIGN_IN(122) +} + +/** + * Enum representing various types of events in iCloud manager. + */ + +enum class ICloudManagerEventType(val value: String) { + FOUND_CLOUD_BACKUP("FoundCloudBackup"), +} + +/** + * Class for managing cloud storage backups on android for React Native. + * + * @property reactContext The react application context. + */ +class RNCloudStorageBackupsManager(private val reactContext: ReactApplicationContext) : + ReactContextBaseJavaModule(reactContext) { + + override fun getName() = "RNCloudStorageBackupsManager" + + private val rnEthersRS = RnEthersRs(reactContext) + + private val gson = Gson() + + /** + * Sends FOUND_CLOUD_BACKUP event to react-native app. + * + * @param mnemonicId Id of backup mnemonic. + * @param createdAt Date of backup creation. + * @param googleDriveEmail Email address associated with the backup. + */ + private fun sendFoundBackupEvent(mnemonicId: String, createdAt: String, googleDriveEmail: String?) { + val body: WritableMap = Arguments.createMap() + body.putString("mnemonicId", mnemonicId) + body.putString("createdAt", createdAt) + body.putString("googleDriveEmail", googleDriveEmail) + sendEvent(ICloudManagerEventType.FOUND_CLOUD_BACKUP.value, body) + } + + /** + * Sends an event to react-native app with the given name and parameters. + * + * @param eventName Name of the event. + * @param params Parameters for the event, encapsulated into a WritableMap. + */ + private fun sendEvent(eventName: String, params: WritableMap?) { + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(eventName, params) + } + + /** + * Checks if cloud storage services (like Google Play Services) are available. + * There is no way to check if just google drive api is available + * + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun isCloudStorageAvailable(promise: Promise) { + val googleApiAvailability = GoogleApiAvailability.getInstance() + val resultCode = googleApiAvailability.isGooglePlayServicesAvailable(reactContext) + promise.resolve(resultCode == ConnectionResult.SUCCESS) + } + + /** + * Starts fetching cloud storage backups. Data is send back using to React-Native using [sendFoundBackupEvent] method. + * + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun startFetchingCloudStorageBackups(promise: Promise) { + CoroutineScope(Dispatchers.Main).launch { + try { + GoogleDriveApiHelper.getGoogleDrive(reactContext).let { (drive) -> + if (drive == null) return@launch + GoogleDriveApiHelper.fetchCloudBackupFiles(drive).let { files -> + withContext(Dispatchers.IO) { + files.files.forEach { file -> + launch { + val outputStream = ByteArrayOutputStream() + drive.files()[file.id] + .executeMediaAndDownloadTo(outputStream) + val mnemonicsBackup: CloudStorageMnemonicBackup = gson.fromJson(outputStream.toString(), CloudStorageMnemonicBackup::class.java) + sendFoundBackupEvent(mnemonicsBackup.mnemonicId, mnemonicsBackup.createdAt.toString(),mnemonicsBackup.googleDriveEmail) + } + } + } + } + } + promise.resolve(true) + } catch (e: Exception) { + promise.reject(CloudBackupError.CLOUD_ERROR.value, "Failed to fetch cloud backups") + } + } + } + + /** + * Stops fetching cloud storage backups. It's empty because there is not need to stopping + * cloud fetch on android. Added just to meet native module interface + * + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun stopFetchingCloudStorageBackups(promise: Promise) { + } + + /** + * Backs up a mnemonic string as a json file with mnemonicId as a name to google drive api. + * Backup is stored in app specific folder which is not accessible to other apps. + * + * @param mnemonicId The ID of the mnemonic string. + * @param password The password used for encryption. + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun backupMnemonicToCloudStorage(mnemonicId: String, password: String, promise: Promise) { + CoroutineScope(Dispatchers.Default).launch { + try { + val mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId) + ?: throw Exception("rnEthersRs module retrieve mnemonic null") + val encryptionSalt = generateSalt(16) + val encryptionNonce = generateNonce(12) + val encryptedMnemonic = + withContext(Dispatchers.IO) { encrypt(mnemonic, password, encryptionSalt, encryptionNonce) } + GoogleDriveApiHelper.getGoogleDrive(reactContext).let { (drive, acc) -> + if (drive == null) return@launch + val createdAt = Date().time / 1000.0 + val backup = CloudStorageMnemonicBackup( + mnemonicId, + encryptedMnemonic, + encryptionSalt, + encryptionNonce, + createdAt, + acc?.email + ) + withContext(Dispatchers.IO) { + GoogleDriveApiHelper.saveMnemonicToGoogleDrive(drive, mnemonicId, backup) + } + sendFoundBackupEvent(mnemonicId, createdAt.toString(), acc?.email) + } + promise.resolve(true) + } catch (e: Exception) { + promise.reject( + CloudBackupError.BACKUP_ENCRYPTION_ERROR.value, + "Failed to encrypt mnemonics: ${e.message}", + e, + ) + } + } + } + + /** + * Restores a mnemonic string from a backup in google drive. + * + * @param mnemonicId The ID of the mnemonic string. + * @param password The password used for decryption. + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun restoreMnemonicFromCloudStorage(mnemonicId: String, password: String, promise: Promise) { + CoroutineScope(Dispatchers.Main).launch { + try { + GoogleDriveApiHelper.getGoogleDrive(reactContext, true).let { (drive) -> + if (drive == null) return@launch + val fileId = withContext(Dispatchers.IO) { + GoogleDriveApiHelper.getFileIdByFileName( + drive, + mnemonicId + ) + } + if (fileId == null) { + promise.reject( + CloudBackupError.BACKUP_NOT_FOUND_ERROR.value, + "The file $mnemonicId is not found in Google Drive" + ) + } + val outputStream = ByteArrayOutputStream() + withContext(Dispatchers.IO) { + drive.files().get(fileId).executeMediaAndDownloadTo(outputStream) + } + var mnemonicsBackup: CloudStorageMnemonicBackup? + var decryptedMnemonics: String? = null + + withContext(Dispatchers.IO) { + mnemonicsBackup = + gson.fromJson(outputStream.toString(), CloudStorageMnemonicBackup::class.java) + } + + val encryptedMnemonic = mnemonicsBackup?.encryptedMnemonic + val encryptionSalt = mnemonicsBackup?.encryptionSalt + val encryptionNonce = mnemonicsBackup?.encryptionNonce + + if (encryptedMnemonic == null || encryptionSalt == null || encryptionNonce == null) throw Exception("Failed to read mnemonics backup") + + try { + decryptedMnemonics = withContext(Dispatchers.IO) { + decrypt(encryptedMnemonic, password, encryptionSalt, encryptionNonce) + } + } catch (e: BadPaddingException) { + Log.e("EXCEPTION", "${e.message}") + promise.reject( + CloudBackupError.BACKUP_INCORRECT_PASSWORD_ERROR.value, + "Incorrect decryption password" + ) + } catch (e: IllegalBlockSizeException) { + Log.e("EXCEPTION", "${e.message}") + promise.reject( + CloudBackupError.BACKUP_DECRYPTION_ERROR.value, + "Incorrect decryption password" + ) + } catch (e: Exception) { + Log.e("EXCEPTION", "${e.message}") + promise.reject( + CloudBackupError.BACKUP_DECRYPTION_ERROR.value, + "Failed to decrypt mnemonics" + ) + } + + rnEthersRS.storeNewMnemonic(mnemonic = decryptedMnemonics, address = mnemonicId) + promise.resolve(true) + } + } catch (e: Exception) { + Log.e("EXCEPTION", "${e.message}") + promise.reject( + CloudBackupError.BACKUP_NOT_FOUND_ERROR.value, + "Backup file not found in local storage" + ) + } + } + } + + /** + * Deletes a mnemonic backup from google drive. + * + * @param mnemonicId The ID of the mnemonic backup. + * @param promise A promise to return the result of the operation. + */ + @ReactMethod + fun deleteCloudStorageMnemonicBackup(mnemonicId: String, promise: Promise) { + CoroutineScope(Dispatchers.Main).launch { + try { + GoogleDriveApiHelper.getGoogleDrive(reactContext, true).let { (drive) -> + if (drive == null) return@launch + withContext(Dispatchers.IO) { + val fileId = GoogleDriveApiHelper.getFileIdByFileName(drive, mnemonicId) + if (fileId == null) { + GoogleDriveApiHelper.getGoogleDrive(reactContext).let { (drive) -> + if (drive == null) return@let + val fileId = GoogleDriveApiHelper.getFileIdByFileName(drive, mnemonicId) + ?: throw FileNotFoundException("Failed to locate backup") + drive.files().delete(fileId).execute() + } + } else { + drive.files().delete(fileId).execute() + } + } + } + promise.resolve(true) + } catch (e: FileNotFoundException) { + Log.e("EXCEPTION", "${e.message}") + promise.reject(CloudBackupError.DELETE_BACKUP_ERROR.value, "Failed to locate backup") + } catch (e: Exception) { + Log.e("EXCEPTION", "${e.message}") + promise.reject(CloudBackupError.DELETE_BACKUP_ERROR.value, "Failed to delete backup") + } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManagerModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManagerModule.kt new file mode 100644 index 0000000..0c1f140 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/RNCloudStorageBackupsManagerModule.kt @@ -0,0 +1,19 @@ +package com.uniswap + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class RNCloudStorageBackupsManagerModule : ReactPackage { + + override fun createViewManagers( + reactContext: ReactApplicationContext + ): MutableList>> = mutableListOf() + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): MutableList = listOf(RNCloudStorageBackupsManager(reactContext)).toMutableList() +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/RNEthersRSModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/RNEthersRSModule.kt new file mode 100644 index 0000000..1e55349 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/RNEthersRSModule.kt @@ -0,0 +1,75 @@ +package com.uniswap; +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.bridge.WritableArray +import com.facebook.react.bridge.WritableNativeArray +import com.facebook.react.module.annotations.ReactModule +import com.facebook.soloader.SoLoader + +/** + * Bridge between the React Native JavaScript code and the native Android code. + * It provides several methods that can be called from JavaScript, using the @ReactMethod annotation. + * The module uses the RnEthersRs class, which is initialized with the application context. + * The native library "ethers_ffi" is loaded when the module is initialized (`libethers_ffi.so`). + */ +@ReactModule(name = "RNEthersRS") +class RNEthersRSModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + private val ethersRs: RnEthersRs = RnEthersRs(reactContext.applicationContext) + + // Needs to be initialized form a static context + companion object { + init { + SoLoader.loadLibrary("ethers_ffi") + } + } + + override fun getName() = "RNEthersRS" + + @ReactMethod fun getMnemonicIds(promise: Promise) { + promise.resolve(ethersRs.mnemonicIds) + } + + @ReactMethod fun importMnemonic(mnemonic: String, promise: Promise) { + promise.resolve(ethersRs.importMnemonic(mnemonic)) + } + + + @ReactMethod fun generateAndStoreMnemonic(promise: Promise) { + promise.resolve(ethersRs.generateAndStoreMnemonic()) + } + + @ReactMethod fun getAddressesForStoredPrivateKeys(promise: Promise) { + val addresses = ethersRs.addressesForStoredPrivateKeys + + // Convert the List to a WritableArray for passing over the bridge + val writableArray: WritableArray = WritableNativeArray() + for (address in addresses) { + writableArray.pushString(address) + } + + promise.resolve(writableArray) + } + + @ReactMethod fun generateAddressForMnemonic(mnemonic: String, derivationIndex: Int, promise: Promise) { + promise.resolve(ethersRs.generateAddressForMnemonic(mnemonic, derivationIndex)) + } + + @ReactMethod fun generateAndStorePrivateKey(mnemonicId: String, derivationIndex: Int, promise: Promise) { + promise.resolve(ethersRs.generateAndStorePrivateKey(mnemonicId, derivationIndex)) + } + + @ReactMethod fun signTransactionHashForAddress(address: String, hash: String, chainId: Int, promise: Promise) { + promise.resolve(ethersRs.signTransactionHashForAddress(address, hash, chainId.toLong())) + } + + @ReactMethod fun signMessageForAddress(address: String, message: String, promise: Promise) { + promise.resolve(ethersRs.signMessageForAddress(address, message)) + } + + @ReactMethod fun signHashForAddress(address: String, hash: String, chainId: Int, promise: Promise) { + promise.resolve(ethersRs.signHashForAddress(address, hash, chainId.toLong())) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/RnEthersRs.kt b/apps/mobile/android/app/src/main/java/com/uniswap/RnEthersRs.kt new file mode 100644 index 0000000..70214db --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/RnEthersRs.kt @@ -0,0 +1,192 @@ +package com.uniswap + +import android.content.Context +import android.content.SharedPreferences +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKeys +import com.uniswap.EthersRs.generateMnemonic +import com.uniswap.EthersRs.privateKeyFromMnemonic +import com.uniswap.EthersRs.signHashWithWallet +import com.uniswap.EthersRs.signMessageWithWallet +import com.uniswap.EthersRs.signTxWithWallet +import com.uniswap.EthersRs.walletFromPrivateKey + +class RnEthersRs(applicationContext: Context) { + + // Long represents the opaque pointer to the Rust LocalWallet struct. + private val walletCache: MutableMap = mutableMapOf() + private val keychain: SharedPreferences + + init { + val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC) + keychain = EncryptedSharedPreferences.create( + "preferences", + masterKeyAlias, + applicationContext, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + } + + val mnemonicIds: Array + get() = keychain.all.keys.map { + key -> key.replace(ENTIRE_MNEMONIC_PREFIX, "") + }.toTypedArray() + + /** + * Imports a mnemonic and returns the associated address. + * @param mnemonic The mnemonic to import. + * @return The address associated with the mnemonic. + */ + fun importMnemonic(mnemonic: String): String { + val privateKey = privateKeyFromMnemonic(mnemonic, 0) + val address = privateKey.address + return storeNewMnemonic(mnemonic, address) + } + + /** + * Generates a new mnemonic, stores it, and returns the associated address. + * @return The address associated with the new mnemonic. + */ + fun generateAndStoreMnemonic(): String { + val mnemonic = generateMnemonic() + val mnemonicStr = mnemonic.mnemonic + val addressStr = mnemonic.address + return storeNewMnemonic(mnemonicStr, addressStr) + } + + /** + * Stores a new mnemonic and its associated address. + * @param mnemonic The mnemonic to store. + * @param address The address associated with the mnemonic. + * @return The address. + */ + fun storeNewMnemonic(mnemonic: String?, address: String): String { + val checkStored = retrieveMnemonic(address) + if (checkStored == null) { + val newMnemonicKey = keychainKeyForMnemonicId(address) + keychain.edit().putString(newMnemonicKey, mnemonic).apply() + } + return address + } + + + private fun keychainKeyForMnemonicId(mnemonicId: String): String { + return MNEMONIC_PREFIX + mnemonicId + } + + fun retrieveMnemonic(mnemonicId: String): String? { + return keychain.getString(keychainKeyForMnemonicId(mnemonicId), null) + } + + val addressesForStoredPrivateKeys: List + get() = keychain.all.keys + .filter { key -> key.contains(PRIVATE_KEY_PREFIX) } + .map { key -> key.replace(PRIVATE_KEY_PREFIX, "") } + + private fun storeNewPrivateKey(address: String, privateKey: String?) { + val newKey = keychainKeyForPrivateKey(address) + keychain.edit().putString(newKey, privateKey).apply() + } + + /** + * Generates public address for a given mnemonic and returns the associated address. + * @param mnemonic Mmnemonic to generate the public address from. + * @param derivationIndex The index of the private key to generate. + * @return The address associated with the new private key. + */ + fun generateAddressForMnemonic(mnemonic: String, derivationIndex: Int): String { + val privateKey = privateKeyFromMnemonic(mnemonic, derivationIndex) + return privateKey.address + } + + /** + * Generates and stores a new private key for a given mnemonic and returns the associated address. + * @param mnemonicId The id of the mnemonic to generate the private key from. + * @param derivationIndex The index of the private key to generate. + * @return The address associated with the new private key. + */ + fun generateAndStorePrivateKey(mnemonicId: String, derivationIndex: Int): String { + val mnemonic = retrieveMnemonic(mnemonicId) + val privateKey = privateKeyFromMnemonic(mnemonic, derivationIndex) + val xprv = privateKey.privateKey + val address = privateKey.address + storeNewPrivateKey(address, xprv) + return address + } + + /** + * Signs a transaction for a given address. + * @param address The address to sign the transaction for. + * @param hash The transaction hash to sign. + * @param chainId The id of the blockchain network. + * @return The signed transaction hash. + */ + fun signTransactionHashForAddress(address: String, hash: String, chainId: Long): String { + val wallet = retrieveOrCreateWalletForAddress(address) + return signTxWithWallet(wallet, hash, chainId) + } + + /** + * Signs a message for a given address. + * @param address The address to sign the message for. + * @param message The message to sign. + * @return The signed message. + */ + fun signMessageForAddress(address: String, message: String): String { + val wallet = retrieveOrCreateWalletForAddress(address) + return signMessageWithWallet(wallet, message) + } + + /** + * Signs a hash for a given address. + * @param address The address to sign the hash for. + * @param hash The hash to sign. + * @param chainId The id of the blockchain network. + * @return The signed hash. + */ + fun signHashForAddress(address: String, hash: String, chainId: Long): String { + val wallet = retrieveOrCreateWalletForAddress(address) + return signHashWithWallet(wallet, hash, chainId) + } + + /** + * Retrieves an existing wallet for a given address or creates a new one if it doesn't exist. + * @param address The address of the wallet. + * @return A long representing the pointer to the wallet. + */ + private fun retrieveOrCreateWalletForAddress(address: String): Long { + val wallet = walletCache[address] + if (wallet != null) { + return wallet + } + val privateKey = retrievePrivateKey(address) + val newWallet = walletFromPrivateKey(privateKey) + walletCache[address] = newWallet + return newWallet + } + + /** + * Retrieves the private key for a given address. + * @param address The address to retrieve the private key for. + * @return The private key, or null if it doesn't exist. + */ + private fun retrievePrivateKey(address: String): String? { + return keychain.getString(keychainKeyForPrivateKey(address), null) + } + /** + * Generates the keychain key for a given address. + * @param address The address to generate the key for. + * @return The keychain key. + */ + private fun keychainKeyForPrivateKey(address: String): String { + return PRIVATE_KEY_PREFIX + address + } + + companion object { + private const val PREFIX = "com.uniswap" + private const val MNEMONIC_PREFIX = ".mnemonic." + private const val PRIVATE_KEY_PREFIX = ".privateKey." + private const val ENTIRE_MNEMONIC_PREFIX = PREFIX + MNEMONIC_PREFIX + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/ThemeModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/ThemeModule.kt new file mode 100644 index 0000000..053bca7 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/ThemeModule.kt @@ -0,0 +1,58 @@ +package com.uniswap +import android.app.Activity +import android.os.Build +import android.view.View +import android.view.WindowInsetsController +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import android.content.Context +import android.content.res.Configuration + +import androidx.appcompat.app.AppCompatDelegate + +class ThemeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + + override fun getName() = "ThemeModule" + + @ReactMethod fun setColorScheme(style: String) { + val activity = currentActivity + when (style) { + "dark" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + "light" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + "system" -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM); + } + val isLightTheme = style == "light" || (style == "system" && !isSystemInDarkTheme(reactContext)) + if (activity != null) setBottomNavigationTheme(activity, isLightTheme) + } + companion object { + fun setBottomNavigationTheme(activity: Activity, isLightTheme: Boolean) { + val window = activity.window + activity.runOnUiThread { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + window.setDecorFitsSystemWindows(false) + window.insetsController?.let { + it.setSystemBarsAppearance( + if (isLightTheme) WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS else 0, + WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS + ) + } + } else { + //TODO: Deprecated method won't allow for dynamic switch so it's safer to hardcoded dark buttons layout + @Suppress("DEPRECATION") + window.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + ) + } + } + } + fun isSystemInDarkTheme(context: Context): Boolean { + return when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_YES -> true + Configuration.UI_MODE_NIGHT_NO -> false + else -> false + } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt new file mode 100644 index 0000000..7ac59f6 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/UniswapPackage.kt @@ -0,0 +1,29 @@ +package com.uniswap + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager +import com.uniswap.onboarding.backup.MnemonicDisplayViewManager +import com.uniswap.onboarding.backup.MnemonicConfirmationViewManager +import com.uniswap.onboarding.import.SeedPhraseInputViewManager + +class UniswapPackage : ReactPackage { + override fun createViewManagers( + reactContext: ReactApplicationContext + ): List>> = listOf( + MnemonicConfirmationViewManager(), + MnemonicDisplayViewManager(), + SeedPhraseInputViewManager(), + ) + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): List = listOf( + AndroidDeviceModule(reactContext), + RNEthersRSModule(reactContext), + ThemeModule(reactContext), + ) +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/extensions/ScrollFadeExtensions.kt b/apps/mobile/android/app/src/main/java/com/uniswap/extensions/ScrollFadeExtensions.kt new file mode 100644 index 0000000..aec34e6 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/extensions/ScrollFadeExtensions.kt @@ -0,0 +1,66 @@ +package com.uniswap.extensions + +import android.annotation.SuppressLint +import android.os.Build +import androidx.compose.foundation.ScrollState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.uniswap.theme.UniswapTheme +import java.lang.Float.min + +@SuppressLint("ComposableModifierFactory") +@Composable +fun Modifier.fadingEdges( + scrollState: ScrollState, + topEdgeHeight: Dp = 0.dp, + bottomEdgeHeight: Dp = UniswapTheme.spacing.spacing48, +): Modifier = this.then(Modifier + // adding layer fixes issue with blending gradient and content + .graphicsLayer { alpha = 0.99F } + .drawWithContent { + drawContent() + + val topColors = listOf(Color.Transparent, Color.Black) + val topStartY = scrollState.value.toFloat() + val topGradientHeight = min(topEdgeHeight.toPx(), topStartY) + val topGradientBrush = Brush.verticalGradient( + colors = topColors, startY = topStartY, endY = topStartY + topGradientHeight + ) + + val bottomColors = listOf(Color.Black, Color.Transparent) + val bottomEndY = size.height - scrollState.maxValue + scrollState.value + val bottomGradientHeight = + min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value) + val bottomGradientBrush = Brush.verticalGradient( + colors = bottomColors, startY = bottomEndY - bottomGradientHeight, endY = bottomEndY + ) + + // Render gradient with blend mode on Android Q and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + drawRect( + brush = topGradientBrush, blendMode = BlendMode.DstIn + ) + if (bottomGradientHeight != 0f) { + drawRect( + brush = bottomGradientBrush, blendMode = BlendMode.DstIn + ) + } + // Otherwise, render gradient without blend mode if the blend mode is not supported + } else { + drawRect( + brush = topGradientBrush, + ) + if (bottomGradientHeight != 0f) { + drawRect( + brush = bottomGradientBrush + ) + } + } + }) diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicConfirmationViewManager.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicConfirmationViewManager.kt new file mode 100644 index 0000000..a987ea0 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicConfirmationViewManager.kt @@ -0,0 +1,77 @@ +package com.uniswap.onboarding.backup + +import android.view.View +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.events.RCTEventEmitter +import com.uniswap.R +import com.uniswap.RnEthersRs +import com.uniswap.onboarding.backup.ui.MnemonicConfirmation +import com.uniswap.onboarding.backup.ui.MnemonicConfirmationViewModel +import com.uniswap.theme.UniswapComponent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +/** + * View manager used to import native component into React Native code + * for the MnemonicTest component used to test if user has saved their + * seed phrase + */ +class MnemonicConfirmationViewManager : ViewGroupManager() { + + override fun getName(): String = REACT_CLASS + + private val mnemonicIdFlow = MutableStateFlow("") + + override fun createViewInstance(reactContext: ThemedReactContext): ComposeView { + val ethersRs = RnEthersRs(reactContext) + val viewModel = MnemonicConfirmationViewModel(ethersRs) + return ComposeView(reactContext).apply { + id = R.id.mnemonic_confirmation_compose_id // Needed for RN event emitter + + setContent { + val mnemonicId by mnemonicIdFlow.collectAsState() + + UniswapComponent { + MnemonicConfirmation(mnemonicId = mnemonicId, viewModel = viewModel) { + val reactContext = context as ReactContext + reactContext + .getJSModule(RCTEventEmitter::class.java) + .receiveEvent(id, EVENT_COMPLETED, null) // Sends event to RN bridge + } + } + } + } + } + + /** + * Maps local event name to expected RN prop. See RN [ViewManager] docs on github for schema. + * Using bubbling instead of direct events because overriding + * getExportedCustomDirectEventTypeConstants leads to a component not found error for some reason. + * Direct events will try find callback prop on native component, and bubbled events will bubble + * up until it finds component with the right callback prop. + */ + override fun getExportedCustomBubblingEventTypeConstants(): Map { + return mapOf( + EVENT_COMPLETED to mapOf( + "phasedRegistrationNames" to mapOf("bubbled" to EVENT_COMPLETED) + ) + ) + } + + @ReactProp(name = "mnemonicId") + fun setMnemonicId(view: View, mnemonicId: String) { + mnemonicIdFlow.update { mnemonicId } + } + + companion object { + private const val REACT_CLASS = "MnemonicConfirmation" + private const val EVENT_COMPLETED = "onConfirmComplete" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicDisplayViewManager.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicDisplayViewManager.kt new file mode 100644 index 0000000..8ee4f22 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/MnemonicDisplayViewManager.kt @@ -0,0 +1,50 @@ +package com.uniswap.onboarding.backup + +import android.view.View +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.uniswap.RnEthersRs +import com.uniswap.onboarding.backup.ui.MnemonicDisplay +import com.uniswap.onboarding.backup.ui.MnemonicDisplayViewModel +import com.uniswap.theme.UniswapComponent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +/** + * View manager used to import native component into React Native code + * for the MnemonicDisplay component used to show the seed phrases + */ +class MnemonicDisplayViewManager : ViewGroupManager() { + + override fun getName(): String = REACT_CLASS + + private val mnemonicIdFlow = MutableStateFlow("") + + override fun createViewInstance(reactContext: ThemedReactContext): ComposeView { + val ethersRs = RnEthersRs(reactContext) + val viewModel = MnemonicDisplayViewModel(ethersRs) + + return ComposeView(reactContext).apply { + setContent { + val mnemonicId by mnemonicIdFlow.collectAsState() + + UniswapComponent { + MnemonicDisplay(mnemonicId = mnemonicId, viewModel = viewModel) + } + } + } + } + + @ReactProp(name = "mnemonicId") + fun setMnemonicId(view: View, mnemonicId: String) { + mnemonicIdFlow.update { mnemonicId } + } + + companion object { + private const val REACT_CLASS = "MnemonicDisplay" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt new file mode 100644 index 0000000..749c31f --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmation.kt @@ -0,0 +1,68 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.uniswap.theme.UniswapTheme + +/** + * Renders screen for confirming that a user wrote down their seed phrase + * by testing they can select it in order + */ +@Composable +fun MnemonicConfirmation( + viewModel: MnemonicConfirmationViewModel, + mnemonicId: String, + onCompleted: () -> Unit, +) { + + val displayedWords by viewModel.displayWords.collectAsState() + val wordBankList by viewModel.wordBankList.collectAsState() + val completed by viewModel.completed.collectAsState() + + LaunchedEffect(mnemonicId) { + viewModel.setup(mnemonicId) + } + + LaunchedEffect(completed) { + if (completed) { + onCompleted() + } + } + + + BoxWithConstraints( + modifier = Modifier.padding(horizontal = UniswapTheme.spacing.spacing16) + ) { + val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + MnemonicWordsGroup(words = displayedWords, showCompact = showCompact) { + viewModel.handleWordRowClick(it) + } + Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing24)) + MnemonicWordBank(words = wordBankList, showCompact = showCompact) { + viewModel.handleWordBankClick(it) + } + } + } +} + +private const val SCREEN_HEIGHT_BREAKPOINT = 500 + + diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt new file mode 100644 index 0000000..bf9bd62 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicConfirmationViewModel.kt @@ -0,0 +1,105 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.uniswap.RnEthersRs +import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState +import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update + +class MnemonicConfirmationViewModel( + private val ethersRs: RnEthersRs, // Move to repository layer if app gets more complex +) : ViewModel() { + + private var sourceWords = emptyList() + private var shuffledWords = emptyList() + private val focusedIndex = MutableStateFlow(0) + private val selectedWords = MutableStateFlow>(emptyList()) + + val displayWords: StateFlow> = + combine(focusedIndex, selectedWords) { focusedIndexValue, words -> + words.mapIndexed { index, word -> + MnemonicWordUiState( + num = index + 1, + text = word ?: "", + focused = index == focusedIndexValue, + hasError = word != null && word != sourceWords[index], + ) + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + val wordBankList: StateFlow> = + combine(focusedIndex, selectedWords) { focusedIndexValue, words -> + val counter = words.groupingBy { it }.eachCount().toMutableMap() + shuffledWords.map { shuffledWord -> + counter[shuffledWord] = counter.getOrDefault(shuffledWord, 0) - 1 + MnemonicWordBankCellUiState( + text = shuffledWord, + used = counter.getOrDefault(shuffledWord, -1) >= 0, + ) + } + }.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList()) + + private val _completed = MutableStateFlow(false) + val completed = _completed.asStateFlow() + + private var currentMnemonicId = "" + + fun setup(mnemonicId: String) { + if (mnemonicId.isNotEmpty() && mnemonicId != currentMnemonicId) { + currentMnemonicId = mnemonicId + reset() + + ethersRs.retrieveMnemonic(mnemonicId)?.let { mnemonic -> + val words = mnemonic.split(" ") + sourceWords = words + shuffledWords = words.shuffled() + selectedWords.update { List(words.size) { null } } + } + } + } + + private fun reset() { + sourceWords = emptyList() + shuffledWords = emptyList() + focusedIndex.update { 0 } + selectedWords.update { emptyList() } + _completed.update { false } + } + + fun handleWordRowClick(word: MnemonicWordUiState) { + val index = displayWords.value.indexOf(word) + focusedIndex.update { index } + } + + fun handleWordBankClick(state: MnemonicWordBankCellUiState) { + val focusedIndexValue = focusedIndex.value + selectedWords.update { words -> + val updated = words.mapIndexed { index, word -> + if (index == focusedIndexValue) { + if (state.text == sourceWords[focusedIndexValue]) { + focusedIndex.update { focusedIndexValue + 1 } + } + state.text + } else { + word + } + } + + checkIfCompleted(updated) + updated + } + } + + private fun checkIfCompleted(words: List) { + if (sourceWords == words) { + _completed.update { true } + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplay.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplay.kt new file mode 100644 index 0000000..c24c833 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplay.kt @@ -0,0 +1,52 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.uniswap.extensions.fadingEdges +import com.uniswap.theme.UniswapTheme + +@Composable +fun MnemonicDisplay( + viewModel: MnemonicDisplayViewModel, + mnemonicId: String, +) { + + val words by viewModel.words.collectAsState() + + LaunchedEffect(mnemonicId) { + viewModel.setup(mnemonicId) + } + + BoxWithConstraints { + val showCompact = maxHeight < SCREEN_HEIGHT_BREAKPOINT.dp + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(horizontal = UniswapTheme.spacing.spacing16) + .fadingEdges(scrollState) + ) { + Box( + modifier = Modifier + .padding(bottom = UniswapTheme.spacing.spacing16) + ) { + MnemonicWordsGroup(words = words, showCompact = showCompact) + } + } + } +} + +private const val SCREEN_HEIGHT_BREAKPOINT = 347 diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplayViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplayViewModel.kt new file mode 100644 index 0000000..efb45ab --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicDisplayViewModel.kt @@ -0,0 +1,41 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.lifecycle.ViewModel +import com.uniswap.RnEthersRs +import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +class MnemonicDisplayViewModel( + private val ethersRs: RnEthersRs // Move to repository layer if app gets more complex +) : ViewModel() { + + private val _words = MutableStateFlow>(emptyList()) + val words = _words.asStateFlow() + + private var currentMnemonicId = "" + + fun setup(mnemonicId: String) { + if (mnemonicId.isNotEmpty() && mnemonicId != currentMnemonicId) { + currentMnemonicId = mnemonicId + reset() + + ethersRs.retrieveMnemonic(mnemonicId)?.let { mnemonic -> + val phraseList = mnemonic.split(" ") + _words.update { + phraseList.mapIndexed { index, phrase -> + MnemonicWordUiState( + num = index + 1, + text = phrase, + ) + } + } + } + } + } + + private fun reset() { + _words.update { emptyList() } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt new file mode 100644 index 0000000..8687341 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordBank.kt @@ -0,0 +1,71 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import com.google.accompanist.flowlayout.FlowRow +import com.google.accompanist.flowlayout.MainAxisAlignment +import com.uniswap.onboarding.backup.ui.model.MnemonicWordBankCellUiState +import com.uniswap.theme.UniswapTheme + +/** + * Used to render a set of clickable mnemonic words + */ +@Composable +fun MnemonicWordBank( + words: List, + showCompact: Boolean = false, + onClick: (word: MnemonicWordBankCellUiState) -> Unit +) { + FlowRow( + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(), + mainAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, + crossAxisSpacing = if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8, + mainAxisAlignment = MainAxisAlignment.Center, + ) { + words.forEach { + MnemonicWordBankCell(word = it, showCompact = showCompact) { + onClick(it) + } + } + } +} + +@Composable +private fun MnemonicWordBankCell( + word: MnemonicWordBankCellUiState, + showCompact: Boolean, + onClick: () -> Unit +) { + + val textStyle = + if (showCompact) UniswapTheme.typography.body2 else UniswapTheme.typography.body1 + val verticalPadding = + if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8 + val horizontalPadding = + if (showCompact) UniswapTheme.spacing.spacing4 else UniswapTheme.spacing.spacing8 + + Box( + modifier = Modifier + .clip(UniswapTheme.shapes.xlarge) + .background(UniswapTheme.colors.surface2) + .padding(vertical = verticalPadding) + .padding(horizontal = horizontalPadding) + .clickable { onClick() }, + ) { + Text( + text = word.text, + style = textStyle, + color = UniswapTheme.colors.neutral1.copy(if (word.used) 0.6f else 1f), + ) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt new file mode 100644 index 0000000..fe6cb1f --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordCell.kt @@ -0,0 +1,67 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState +import com.uniswap.theme.UniswapTheme + +/** + * Component used to display a single word as part of an overall seed phrase + */ +@Composable +fun MnemonicWordCell( + modifier: Modifier = Modifier, + word: MnemonicWordUiState, + showCompact: Boolean = false, + onClick: (() -> Unit)? = null, +) { + val textStyle = + if (showCompact) UniswapTheme.typography.body2 else UniswapTheme.typography.body1 + + val shape = UniswapTheme.shapes.large + var rowModifier = modifier + .clip(shape) + .shadow(1.dp, shape) + .background(UniswapTheme.colors.surface2) + + if (word.hasError) { + rowModifier = rowModifier.border(1.dp, UniswapTheme.colors.statusCritical, shape) + } else if (word.focused) { + rowModifier = rowModifier.border(1.dp, UniswapTheme.colors.accent1, shape) + } + onClick?.let { + rowModifier = rowModifier.clickable { it() } + } + + Row( + modifier = rowModifier + .padding(horizontal = if (showCompact) UniswapTheme.spacing.spacing12 else UniswapTheme.spacing.spacing16) + .padding(vertical = if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12) + ) { + Text( + text = "${word.num}", + color = UniswapTheme.colors.neutral3, + modifier = Modifier + .defaultMinSize(minWidth = 24.dp) + .align(Alignment.CenterVertically), + textAlign = TextAlign.Center, + style = textStyle, + ) + Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing12)) + Text(modifier = Modifier.weight(1f), text = word.text, style = textStyle) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsColumn.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsColumn.kt new file mode 100644 index 0000000..afaffc1 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsColumn.kt @@ -0,0 +1,34 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState +import com.uniswap.theme.UniswapTheme + +/** + * Component used to display a numbered column of words as part of an overall seed phrase + */ +@Composable +fun MnemonicWordsColumn( + modifier: Modifier = Modifier, + words: List, + showCompact: Boolean = false, + onClick: ((word: MnemonicWordUiState) -> Unit)? = null, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy( + if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12 + ), + ) { + words.forEachIndexed { index, word -> + + val onWordClick = onClick?.let { + { it(word) } + } + MnemonicWordCell(word = word, showCompact = showCompact, onClick = onWordClick) + } + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsGroup.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsGroup.kt new file mode 100644 index 0000000..9f01ed0 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/MnemonicWordsGroup.kt @@ -0,0 +1,45 @@ +package com.uniswap.onboarding.backup.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.uniswap.onboarding.backup.ui.model.MnemonicWordUiState +import com.uniswap.theme.UniswapTheme + +/** + * View used to display mnemonic words for wallet seed phrase + */ +@Composable +fun MnemonicWordsGroup( + modifier: Modifier = Modifier, + words: List, + columnCount: Int = DEFAULT_COLUMN_COUNT, + showCompact: Boolean = false, + onClick: ((word: MnemonicWordUiState) -> Unit)? = null, +) { + Row( + modifier = modifier + .fillMaxWidth() + .wrapContentHeight(), + horizontalArrangement = Arrangement.spacedBy( + if (showCompact) UniswapTheme.spacing.spacing8 else UniswapTheme.spacing.spacing12 + ) + ) { + val size = words.size / columnCount + for (i in 0 until columnCount) { + val starting = i * size + val ending = (i + 1) * size + MnemonicWordsColumn( + modifier = Modifier.weight(1f), + words = words.subList(starting, ending), + showCompact = showCompact, + onClick = onClick, + ) + } + } +} + +private const val DEFAULT_COLUMN_COUNT = 2 diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordBankCellUiState.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordBankCellUiState.kt new file mode 100644 index 0000000..8653c31 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordBankCellUiState.kt @@ -0,0 +1,6 @@ +package com.uniswap.onboarding.backup.ui.model + +data class MnemonicWordBankCellUiState( + val text: String, + val used: Boolean = false, +) diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt new file mode 100644 index 0000000..86b7c3c --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/backup/ui/model/MnemonicWordUiState.kt @@ -0,0 +1,9 @@ +package com.uniswap.onboarding.backup.ui.model + +data class MnemonicWordUiState( + val num: Int, + val text: String, + val focused: Boolean = false, + val hasError: Boolean = false, + val sourceIndex: Int? = null, +) diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt new file mode 100644 index 0000000..295783e --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInput.kt @@ -0,0 +1,232 @@ +package com.uniswap.onboarding.import + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Colors +import androidx.compose.material.Icon +import androidx.compose.material.OutlinedButton +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.LocalContentColor +import androidx.compose.material.ContentAlpha +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.uniswap.R +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.MnemonicError.InvalidPhrase +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.MnemonicError.InvalidWord +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.MnemonicError.NotEnoughWords +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.MnemonicError.TooManyWords +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.MnemonicError.WrongRecoveryPhrase +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.Status.Error +import com.uniswap.onboarding.import.SeedPhraseInputViewModel.Status.Valid +import com.uniswap.theme.UniswapTheme + +@Composable +fun SeedPhraseInput(viewModel: SeedPhraseInputViewModel, onHelpTextPress: () -> Unit) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing12, Alignment.Top), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BasicTextField( + modifier = Modifier + .fillMaxWidth() + .clip(UniswapTheme.shapes.medium) + .background(UniswapTheme.colors.surface2) + .border( + width = 1.dp, + shape = UniswapTheme.shapes.medium, + color = mapStatusToBorderColor(viewModel.status), + ) + .focusRequester(focusRequester), + value = viewModel.input, + onValueChange = { viewModel.handleInputChange(it) }, + cursorBrush = SolidColor(LocalContentColor.current.copy(ContentAlpha.high)), + textStyle = UniswapTheme.typography.body1.copy( + textAlign = TextAlign.Start, + color = UniswapTheme.colors.neutral1 + ), + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + capitalization = KeyboardCapitalization.None + ) + ) { innerTextField -> + Box( + modifier = Modifier + .wrapContentHeight() + .heightIn(min = 120.dp) + .padding(UniswapTheme.spacing.spacing16), + contentAlignment = Alignment.Center, + ) { + val isEmpty = viewModel.input.text.isEmpty() + + Box( + contentAlignment = Alignment.CenterStart, + modifier = if (isEmpty) Modifier else Modifier.fillMaxWidth() + ) { + if (isEmpty) { + SeedPhrasePlaceholder(viewModel, focusRequester) + } + innerTextField() + } + } + } + + Spacer(modifier = Modifier.height(UniswapTheme.spacing.spacing4)) + SeedPhraseError(viewModel) + SeedPhraseRecovery(viewModel, onHelpTextPress) + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun SeedPhrasePlaceholder( + viewModel: SeedPhraseInputViewModel, + focusRequester: FocusRequester +) { + + FlowRow( + modifier = Modifier.height(IntrinsicSize.Max), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + viewModel.rnStrings.inputPlaceholder, + color = UniswapTheme.colors.neutral2, + ) + Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing8)) + SeedPhrasePasteButton(viewModel, focusRequester) + } +} + +@Composable +private fun SeedPhrasePasteButton( + viewModel: SeedPhraseInputViewModel, + focusRequester: FocusRequester +) { + + val clipboardManager = LocalClipboardManager.current + + Button( + onClick = { + clipboardManager.getText()?.toString()?.let { + viewModel.handleInputChange( + TextFieldValue(it, selection = TextRange(it.length)) + ) + focusRequester.requestFocus() + } + }, + colors = ButtonDefaults.buttonColors( + contentColor = UniswapTheme.colors.neutral2, + backgroundColor = UniswapTheme.colors.surface3.compositeOver(UniswapTheme.colors.surface2), + ), + shape = UniswapTheme.shapes.buttonMedium, + contentPadding = PaddingValues( + top = UniswapTheme.spacing.spacing8, + bottom = UniswapTheme.spacing.spacing8, + start = UniswapTheme.spacing.spacing4, + end = UniswapTheme.spacing.spacing8 + ), + ) { + Icon( + painterResource(id = R.drawable.uniswap_icon_paste), + contentDescription = null, + ) + Spacer(modifier = Modifier.width(UniswapTheme.spacing.spacing4)) + Text(viewModel.rnStrings.pasteButton, style = UniswapTheme.typography.buttonLabel4) + } +} + +@Composable +private fun SeedPhraseError(viewModel: SeedPhraseInputViewModel) { + val status = viewModel.status + val rnStrings = viewModel.rnStrings + + if (status is Error) { + val text = when (val error = status.error) { + is InvalidWord -> "${rnStrings.errorInvalidWord} ${error.word}" + is NotEnoughWords, TooManyWords -> rnStrings.errorPhraseLength + is WrongRecoveryPhrase -> rnStrings.errorWrongPhrase + is InvalidPhrase -> rnStrings.errorInvalidPhrase + } + Row(horizontalArrangement = Arrangement.spacedBy(UniswapTheme.spacing.spacing4)) { + Icon( + painter = painterResource(id = R.drawable.uniswap_icon_alert_triangle), + tint = UniswapTheme.colors.statusCritical, + contentDescription = null, + ) + Text(text, color = UniswapTheme.colors.statusCritical) + } + } +} + +@Composable +private fun SeedPhraseRecovery(viewModel: SeedPhraseInputViewModel, onHelpTextPress: () -> Unit) { + val interactionSource = remember { MutableInteractionSource() } + Text( + viewModel.rnStrings.helpText, + color = UniswapTheme.colors.accent1, + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + ) { + onHelpTextPress() + } + ) +} + +@Composable +private fun mapStatusToBorderColor(status: SeedPhraseInputViewModel.Status): Color = + when (status) { + Valid -> UniswapTheme.colors.statusSuccess + is Error -> UniswapTheme.colors.statusCritical + else -> Color.Transparent + } diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewManager.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewManager.kt new file mode 100644 index 0000000..80a4bad --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewManager.kt @@ -0,0 +1,141 @@ +package com.uniswap.onboarding.import + +import android.view.View +import android.view.ViewGroup.LayoutParams +import androidx.annotation.IdRes +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.core.view.updateLayoutParams +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.ReadableArray +import com.facebook.react.bridge.ReadableMap +import com.facebook.react.bridge.WritableMap +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.UIManagerHelper +import com.facebook.react.uimanager.ViewGroupManager +import com.facebook.react.uimanager.ViewManager +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.uimanager.events.RCTEventEmitter +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import com.uniswap.R +import com.uniswap.RnEthersRs +import com.uniswap.theme.UniswapComponent +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.serialization.json.Json + + +/** + * View manager used to import native component into React Native code + * for the MnemonicDisplay component used to show the seed phrases + */ +class SeedPhraseInputViewManager : ViewGroupManager() { + + override fun getName(): String = REACT_CLASS + + private lateinit var viewModel: SeedPhraseInputViewModel + private lateinit var context: ThemedReactContext + + private var rnStrings = MutableStateFlow(emptyMap()) + + override fun createViewInstance(reactContext: ThemedReactContext): ComposeView { + context = reactContext + val ethersRs = RnEthersRs(reactContext) + + return ComposeView(reactContext).apply { + id = R.id.seed_phrase_input_compose_id + viewModel = SeedPhraseInputViewModel( + ethersRs, + onInputValidated = { + val bundle = Arguments.createMap().apply { + putBoolean(FIELD_CAN_SUBMIT, it) + } + sendEvent(id, EVENT_INPUT_VALIDATED, bundle) + }, + onMnemonicStored = { + val bundle = Arguments.createMap().apply { + putString(FIELD_MNEMONIC_ID, it) + } + sendEvent(id, EVENT_MNEMONIC_STORED, bundle) + } + ) + + setContent { + UniswapComponent { + SeedPhraseInput( + viewModel, + onHelpTextPress = { + sendEvent(id, EVENT_HELP_TEXT_PRESS, null) + } + ) + } + } + } + } + + /** + * Maps local event name to expected RN prop. See RN [ViewManager] docs on github for schema. + * Using bubbling instead of direct events because overriding + * getExportedCustomDirectEventTypeConstants leads to a component not found error for some reason. + * Direct events will try find callback prop on native component, and bubbled events will bubble + * up until it finds component with the right callback prop. + */ + override fun getExportedCustomBubblingEventTypeConstants(): Map { + return mapOf( + EVENT_HELP_TEXT_PRESS to mapOf( + "phasedRegistrationNames" to mapOf("bubbled" to EVENT_HELP_TEXT_PRESS, "captured" to EVENT_HELP_TEXT_PRESS) + ), + EVENT_INPUT_VALIDATED to mapOf( + "phasedRegistrationNames" to mapOf("bubbled" to EVENT_INPUT_VALIDATED, "captured" to EVENT_INPUT_VALIDATED) + ), + EVENT_MNEMONIC_STORED to mapOf( + "phasedRegistrationNames" to mapOf("bubbled" to EVENT_MNEMONIC_STORED, "captured" to EVENT_MNEMONIC_STORED) + ), + ) + } + + private fun sendEvent(id: Int, eventName: String, bundle: WritableMap? = null) { + context + .getJSModule(RCTEventEmitter::class.java) + .receiveEvent(id, eventName, bundle) + } + + override fun receiveCommand(root: ComposeView, commandId: String?, args: ReadableArray?) { + super.receiveCommand(root, commandId, args) + when (commandId) { + COMMAND_HANDLE_SUBMIT -> viewModel.handleSubmit() + else -> Unit + } + } + + @ReactProp(name = "mnemonicIdForRecovery") + fun setMnemonicIdForRecovery(view: View, mnemonicId: String?) { + viewModel.mnemonicIdForRecovery = mnemonicId + } + + @ReactProp(name = "strings") + fun setStrings(view: View, strings: ReadableMap) { + viewModel.rnStrings = SeedPhraseInputViewModel.ReactNativeStrings( + helpText = strings.getString("helpText") ?: "", + inputPlaceholder = strings.getString("inputPlaceholder") ?: "", + pasteButton = strings.getString("pasteButton") ?: "", + errorInvalidWord = strings.getString("errorInvalidWord") ?: "", + errorPhraseLength = strings.getString("errorPhraseLength") ?: "", + errorWrongPhrase = strings.getString("errorWrongPhrase") ?: "", + errorInvalidPhrase = strings.getString("errorInvalidPhrase") ?: "", + ) + } + + companion object { + private const val REACT_CLASS = "SeedPhraseInput" + private const val EVENT_HELP_TEXT_PRESS = "onHelpTextPress" + private const val EVENT_INPUT_VALIDATED = "onInputValidated" + private const val EVENT_MNEMONIC_STORED = "onMnemonicStored" + private const val COMMAND_HANDLE_SUBMIT = "handleSubmit" + private const val FIELD_MNEMONIC_ID = "mnemonicId" + private const val FIELD_CAN_SUBMIT = "canSubmit" + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt new file mode 100644 index 0000000..92e6742 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/import/SeedPhraseInputViewModel.kt @@ -0,0 +1,136 @@ +package com.uniswap.onboarding.import + +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.toLowerCase +import androidx.lifecycle.ViewModel +import com.uniswap.EthersRs +import com.uniswap.RnEthersRs + +class SeedPhraseInputViewModel( + private val ethersRs: RnEthersRs, + private val onInputValidated: (canSubmit: Boolean) -> Unit, + private val onMnemonicStored: (mnemonicId: String) -> Unit, +) : ViewModel() { + + sealed interface Status { + object None: Status + object Valid: Status + class Error(val error: MnemonicError): Status + } + + sealed interface MnemonicError { + class InvalidWord(val word: String) : MnemonicError + object TooManyWords : MnemonicError + object NotEnoughWords : MnemonicError + object WrongRecoveryPhrase : MnemonicError + object InvalidPhrase: MnemonicError + } + + data class ReactNativeStrings( + val helpText: String, + val inputPlaceholder: String, + val pasteButton: String, + val errorInvalidWord: String, + val errorPhraseLength: String, + val errorWrongPhrase: String, + val errorInvalidPhrase: String, + ) + + // Sourced externally from RN + var mnemonicIdForRecovery by mutableStateOf(null) + var rnStrings by mutableStateOf(ReactNativeStrings( + helpText = "", + inputPlaceholder = "", + pasteButton = "", + errorInvalidWord = "", + errorPhraseLength = "", + errorWrongPhrase = "", + errorInvalidPhrase = "", + )) + + var input by mutableStateOf(TextFieldValue("")) + private set + var status by mutableStateOf(Status.None) + private set + + fun handleInputChange(value: TextFieldValue) { + input = value + + val normalized = normalizeInput(value) + val skipLastWord = normalized.lastOrNull() != ' ' + val mnemonic = normalized.trim() + val words = mnemonic.split(" ") + + if (words.isEmpty()) { + status = Status.None + return + } + + val isValidLength = words.size in MIN_LENGTH..MAX_LENGTH + val firstInvalidWord = EthersRs.findInvalidWord(mnemonic) + if (firstInvalidWord == words.last() && skipLastWord) { + status = Status.None + } else if (firstInvalidWord.isEmpty() && isValidLength) { + status = Status.Valid + } else if (firstInvalidWord.isNotEmpty()) { + status = Status.Error(MnemonicError.InvalidWord(firstInvalidWord)) + } else { + status = Status.None + } + + val canSubmit = status !is Status.Error && mnemonic != "" && firstInvalidWord.isEmpty() + onInputValidated(canSubmit) + } + + private fun normalizeInput(value: TextFieldValue) = + value.text.replace("\\s+".toRegex(), " ").lowercase() + + fun handleSubmit() { + try { + val normalized = normalizeInput(input) + val mnemonic = normalized.trim() + val words = mnemonic.split(" ") + val valid = EthersRs.validateMnemonic(mnemonic) + + if (words.size < MIN_LENGTH) { + status = Status.Error(MnemonicError.NotEnoughWords) + } else if (words.size > MAX_LENGTH) { + status = Status.Error(MnemonicError.TooManyWords) + } else if (!valid) { + status = Status.Error(MnemonicError.InvalidPhrase) + } else { + submitMnemonic(mnemonic) + } + } catch (e: Exception) { + // TODO gary add production logging and update rust code to convert to Java exceptions + Log.d("SeedPhraseInputViewModel", "Storing mnemonic caused error ${e.message}") + } + } + + private fun submitMnemonic(mnemonic: String) { + if (mnemonicIdForRecovery != null) { + val generatedId = ethersRs.generateAddressForMnemonic(mnemonic, derivationIndex = 0) + if (generatedId != mnemonicIdForRecovery) { + status = Status.Error(MnemonicError.WrongRecoveryPhrase) + } else { + storeMnemonic(mnemonic) + } + } else { + storeMnemonic(mnemonic) + } + } + + private fun storeMnemonic(mnemonic: String) { + val mnemonicId = ethersRs.importMnemonic(mnemonic) + onMnemonicStored(mnemonicId) + } + + companion object { + private const val MIN_LENGTH = 12 + private const val MAX_LENGTH = 24 + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryption.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryption.kt new file mode 100644 index 0000000..94eea5d --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryption.kt @@ -0,0 +1,79 @@ +package com.uniswap.onboarding.scantastic + +import com.uniswap.RnEthersRs +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 javax.crypto.spec.OAEPParameterSpec +import javax.crypto.spec.PSource +import java.security.spec.MGF1ParameterSpec +import java.math.BigInteger +import java.util.Base64 +import java.security.KeyFactory +import java.security.spec.RSAPublicKeySpec +import javax.crypto.Cipher + +class ScantasticError(override val message: String) : Exception(message) + +class ScantasticEncryption(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { + override fun getName() = "ScantasticEncryption" + + private val rnEthersRS = RnEthersRs(reactContext) + + @ReactMethod + fun getEncryptedMnemonic(mnemonicId: String, n: String, e: String, promise: Promise) { + val mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId) ?: run { + promise.reject(ScantasticError("Failed to retrieve mnemonic")) + return + } + + val publicKey = try { + generatePublicRSAKey(n, e) + } catch (ex: Exception) { + promise.reject(ScantasticError("Failed to generate public Key: ${ex.message}")) + return + } + + val encodedCiphertext = try { + encryptForStorage(mnemonic, publicKey) + } catch (ex: Exception) { + promise.reject(ScantasticError("Failed to encrypt the mnemonic: ${ex.message}")) + return + } + // Normal B64 not URL encoded, use getUrlDecoder() if you need URL encoded format + val b64encodedCiphertext = Base64.getEncoder().encodeToString(encodedCiphertext) + promise.resolve(b64encodedCiphertext) + } + + @Throws(Exception::class) + private fun generatePublicRSAKey(modulusStr: String, exponentStr: String): java.security.PublicKey { + val modulus = BigInteger(1, base64UrlToStandardBase64(modulusStr).let { Base64.getDecoder().decode(it) }) + val exponent = BigInteger(1, base64UrlToStandardBase64(exponentStr).let { Base64.getDecoder().decode(it) }) + val keySpec = RSAPublicKeySpec(modulus, exponent) + return KeyFactory.getInstance("RSA").generatePublic(keySpec) + } + + // It is unclear why URLDecoder doesn't do this by default and we have to do it here instead. + private fun base64UrlToStandardBase64(input: String): String { + var base64 = input.replace("-", "+").replace("_", "/") + while (base64.length % 4 != 0) { + base64 += "=" + } + return base64 + } + + @Throws(Exception::class) + private fun encryptForStorage(plaintext: String, publicKey: java.security.PublicKey): ByteArray { + val oaepParams = OAEPParameterSpec( + "SHA-256", + "MGF1", + MGF1ParameterSpec.SHA256, + PSource.PSpecified.DEFAULT + ) + + val cipher = Cipher.getInstance("RSA/ECB/OAEPPadding") + cipher.init(Cipher.ENCRYPT_MODE, publicKey, oaepParams) + return cipher.doFinal(plaintext.toByteArray()) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryptionModule.kt b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryptionModule.kt new file mode 100644 index 0000000..aec8726 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/onboarding/scantastic/ScantasticEncryptionModule.kt @@ -0,0 +1,20 @@ +package com.uniswap.onboarding.scantastic + +import android.view.View +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ReactShadowNode +import com.facebook.react.uimanager.ViewManager + +class ScantasticEncryptionModule : ReactPackage { + + override fun createViewManagers( + reactContext: ReactApplicationContext + ): MutableList>> = mutableListOf() + + override fun createNativeModules( + reactContext: ReactApplicationContext + ): MutableList = listOf(ScantasticEncryption(reactContext)).toMutableList() +} + diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/Color.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Color.kt new file mode 100644 index 0000000..909c0fd --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Color.kt @@ -0,0 +1,249 @@ +package com.uniswap.theme + +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color + +object UniswapColors { + val White = Color(0xFFFFFFFF) + val Black = Color(0xFF000000) + val Gray50 = Color(0xFFF5F6FC) + val Gray100 = Color(0xFFE8ECFB) + val Gray150 = Color(0xFFD2D9EE) + val Gray200 = Color(0xFFB8C0DC) + val Gray250 = Color(0xFFA6AFCA) + val Gray300 = Color(0xFF98A1C0) + val Gray350 = Color(0xFF888FAB) + val Gray400 = Color(0xFF7780A0) + val Gray450 = Color(0xFF6B7594) + val Gray500 = Color(0xFF5D6785) + val Gray550 = Color(0xFF505A78) + val Gray600 = Color(0xFF404A67) + val Gray650 = Color(0xFF333D59) + val Gray700 = Color(0xFF293249) + val Gray750 = Color(0xFF1B2236) + val Gray800 = Color(0xFF131A2A) + val Gray850 = Color(0xFF0E1524) + val Gray900 = Color(0xFF0D111C) + val Pink50 = Color(0xFFFFF2F7) + val Pink100 = Color(0xFFFFD9E4) + val Pink200 = Color(0xFFFBA4C0) + val Pink300 = Color(0xFFFF6FA3) + val Pink400 = Color(0xFFFB118E) + val Pink500 = Color(0xFFC41A69) + val Pink600 = Color(0xFF8C0F49) + val Pink700 = Color(0xFF55072A) + val Pink800 = Color(0xFF39061B) + val Pink900 = Color(0xFF2B000B) + val PinkVibrant = Color(0xFFF51A70) + val Red50 = Color(0xFFFEF0EE) + val Red100 = Color(0xFFFED5CF) + val Red200 = Color(0xFFFEA79B) + val Red300 = Color(0xFFFD766B) + val Red400 = Color(0xFFFA2B39) + val Red500 = Color(0xFFC4292F) + val Red600 = Color(0xFF891E20) + val Red700 = Color(0xFF530F0F) + val Red800 = Color(0xFF380A03) + val Red900 = Color(0xFF240800) + val RedVibrant = Color(0xFFF14544) + val Yellow50 = Color(0xFFFEF8C4) + val Yellow100 = Color(0xFFF0E49A) + val Yellow200 = Color(0xFFDBBC19) + val Yellow300 = Color(0xFFBB9F13) + val Yellow400 = Color(0xFFA08116) + val Yellow500 = Color(0xFF866311) + val Yellow600 = Color(0xFF5D4204) + val Yellow700 = Color(0xFF3E2B04) + val Yellow800 = Color(0xFF231902) + val Yellow900 = Color(0xFF180F02) + val YellowVibrant = Color(0xFFFAF40A) + val Gold50 = Color(0xFFFFF5E8) + val Gold100 = Color(0xFFF8DEB6) + val Gold200 = Color(0xFFEEB317) + val Gold300 = Color(0xFFDB900B) + val Gold400 = Color(0xFFB17900) + val Gold500 = Color(0xFF905C10) + val Gold600 = Color(0xFF643F07) + val Gold700 = Color(0xFF3F2208) + val Gold800 = Color(0xFF29160F) + val Gold900 = Color(0xFF161007) + val GoldVibrant = Color(0xFFFEB239) + val Green50 = Color(0xFFEDFDF0) + val Green100 = Color(0xFFBFEECA) + val Green200 = Color(0xFF76D191) + val Green300 = Color(0xFF40B66B) + val Green400 = Color(0xFF209853) + val Green500 = Color(0xFF0B783E) + val Green600 = Color(0xFF0C522A) + val Green700 = Color(0xFF053117) + val Green800 = Color(0xFF091F10) + val Green900 = Color(0xFF09130B) + val GreenVibrant = Color(0xFF5CFE9D) + val Blue50 = Color(0xFFF3F5FE) + val Blue100 = Color(0xFFDEE1FF) + val Blue200 = Color(0xFFADBCFF) + val Blue300 = Color(0xFF869EFF) + val Blue400 = Color(0xFF4C82FB) + val Blue500 = Color(0xFF1267D6) + val Blue600 = Color(0xFF1D4294) + val Blue700 = Color(0xFF09265E) + val Blue800 = Color(0xFF0B193F) + val Blue900 = Color(0xFF040E34) + val BlueVibrant = Color(0xFF587BFF) + val Lime50 = Color(0xFFF2FEDB) + val Lime100 = Color(0xFFD3EBA3) + val Lime200 = Color(0xFF9BCD46) + val Lime300 = Color(0xFF7BB10C) + val Lime400 = Color(0xFF649205) + val Lime500 = Color(0xFF527318) + val Lime600 = Color(0xFF344F00) + val Lime700 = Color(0xFF233401) + val Lime800 = Color(0xFF171D00) + val Lime900 = Color(0xFF0E1300) + val LimeVibrant = Color(0xFFB1F13C) + val Orange50 = Color(0xFFFEEDE5) + val Orange100 = Color(0xFFFCD9C8) + val Orange200 = Color(0xFFFBAA7F) + val Orange300 = Color(0xFFF67E3E) + val Orange400 = Color(0xFFDC5B14) + val Orange500 = Color(0xFFAF460A) + val Orange600 = Color(0xFF76330F) + val Orange700 = Color(0xFF4D220B) + val Orange800 = Color(0xFF2A1505) + val Orange900 = Color(0xFF1C0E03) + val OrangeVibrant = Color(0xFFFF6F1E) + val Magenta50 = Color(0xFFFFF1FE) + val Magenta100 = Color(0xFFFAD8F8) + val Magenta200 = Color(0xFFF5A1F5) + val Magenta300 = Color(0xFFF06DF3) + val Magenta400 = Color(0xFFDC39E3) + val Magenta500 = Color(0xFFAF2EB4) + val Magenta600 = Color(0xFF7A1C7D) + val Magenta700 = Color(0xFF550D56) + val Magenta800 = Color(0xFF330733) + val Magenta900 = Color(0xFF250225) + val MagentaVibrant = Color(0xFFFC72FF) + val Violet50 = Color(0xFFF1EFFE) + val Violet100 = Color(0xFFE2DEFD) + val Violet200 = Color(0xFFBDB8FA) + val Violet300 = Color(0xFF9D99F5) + val Violet400 = Color(0xFF7A7BEB) + val Violet500 = Color(0xFF515EDC) + val Violet600 = Color(0xFF343F9E) + val Violet700 = Color(0xFF232969) + val Violet800 = Color(0xFF121643) + val Violet900 = Color(0xFF0E0D30) + val VioletVibrant = Color(0xFF5065FD) + val Cyan50 = Color(0xFFD6F5FE) + val Cyan100 = Color(0xFFB0EDFE) + val Cyan200 = Color(0xFF63CDE8) + val Cyan300 = Color(0xFF2FB0CC) + val Cyan400 = Color(0xFF2092AB) + val Cyan500 = Color(0xFF117489) + val Cyan600 = Color(0xFF014F5F) + val Cyan700 = Color(0xFF003540) + val Cyan800 = Color(0xFF011E26) + val Cyan900 = Color(0xFF011418) + val CyanVibrant = Color(0xFF36DBFF) + val Slate50 = Color(0xFFF1FCEF) + val Slate100 = Color(0xFFDAE6D8) + val Slate200 = Color(0xFFB8C3B7) + val Slate300 = Color(0xFF9AA498) + val Slate400 = Color(0xFF7E887D) + val Slate500 = Color(0xFF646B62) + val Slate600 = Color(0xFF434942) + val Slate700 = Color(0xFF2C302C) + val Slate800 = Color(0xFF181B18) + val Slate900 = Color(0xFF0F120E) + val SlateVibrant = Color(0xFF7E887D) + val Transparent = Color.Transparent +} + +data class CustomColors( + val white: Color = UniswapColors.White, + val black: Color = UniswapColors.Black, + val surface1: Color, + val surface2: Color, + val surface3: Color, + val surface4: Color, + val surface5: Color, + val scrim: Color, + val neutral1: Color, + val neutral2: Color, + val neutral3: Color, + val accent1: Color, + val accent2: Color, + val statusActive: Color, + val statusSuccess: Color, + val statusCritical: Color, +) + +val lightCustomColors = CustomColors( + surface1 = Color(0xFFFFFFFF), + surface2 = Color(0xFFF9F9F9), + surface3 = Color(0x0D222222), + surface4 = Color(0xA3FFFFFF), + surface5 = Color(0x0A000000), + + scrim = Color(0x99000000), + + neutral1 = Color(0xFF222222), + neutral2 = Color(0xFF7D7D7D), + neutral3 = Color(0xFFCECECE), + + accent1 = Color(0xFFFC72FF), + accent2 = Color(0xFFFFEFFF), + + statusActive = Color(0xFF236EFF), + statusSuccess = Color(0xFF40B66B), + statusCritical = Color(0xFFFF5F52), +) + +val darkCustomColors = CustomColors( + surface1 = Color(0xFF131313), + surface2 = Color(0xFF1B1B1B), + surface3 = Color(0x1FFFFFFF), + surface4 = Color(0x33FFFFFF), + surface5 = Color(0x0A000000), + + scrim = Color(0x99000000), + + neutral1 = Color(0xFFFFFFFF), + neutral2 = Color(0xFF9B9B9B), + neutral3 = Color(0xFF5E5E5E), + + accent1 = Color(0xFFFC72FF), + accent2 = Color(0xFF311C31), + + statusActive = Color(0xFF236EFF), + statusSuccess = Color(0xFF40B66B), + statusCritical = Color(0xFFFF5F52), +) + +val LightColors = lightColors( + primary = lightCustomColors.accent1, + background = lightCustomColors.surface1, + surface = lightCustomColors.surface1, + error = lightCustomColors.statusCritical, + onPrimary = lightCustomColors.white, + onBackground = lightCustomColors.neutral1, + onSurface = lightCustomColors.neutral1, + onError = lightCustomColors.white, +) + +val DarkColors = darkColors( + primary = darkCustomColors.accent1, + background = darkCustomColors.surface1, + surface = darkCustomColors.surface1, + error = darkCustomColors.statusCritical, + onPrimary = darkCustomColors.white, + onBackground = darkCustomColors.neutral1, + onSurface = darkCustomColors.neutral1, + onError = darkCustomColors.white, +) + +val LocalCustomColors = staticCompositionLocalOf { + lightCustomColors +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/Shape.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Shape.kt new file mode 100644 index 0000000..81ae05e --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Shape.kt @@ -0,0 +1,17 @@ +package com.uniswap.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.dp + +data class CustomShapes( + val medium: RoundedCornerShape = RoundedCornerShape(20.dp), + val large: RoundedCornerShape = RoundedCornerShape(24.dp), + val xlarge: RoundedCornerShape = RoundedCornerShape(100.dp), + val buttonSmall: RoundedCornerShape = RoundedCornerShape(12.dp), + val buttonMedium: RoundedCornerShape = RoundedCornerShape(16.dp), +) + +val LocalCustomShapes = staticCompositionLocalOf { + CustomShapes() +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/Spacing.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Spacing.kt new file mode 100644 index 0000000..d402d6a --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Spacing.kt @@ -0,0 +1,26 @@ +package com.uniswap.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +data class CustomSpacing( + val spacing1: Dp = 1.dp, + val spacing2: Dp = 2.dp, + val spacing4: Dp = 4.dp, + val spacing8: Dp = 8.dp, + val spacing12: Dp = 12.dp, + val spacing16: Dp = 16.dp, + val spacing24: Dp = 24.dp, + val spacing28: Dp = 28.dp, + val spacing32: Dp = 32.dp, + val spacing36: Dp = 36.dp, + val spacing48: Dp = 48.dp, + val spacing60: Dp = 60.dp, +) + +val LocalCustomSpacing = staticCompositionLocalOf { + CustomSpacing() +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/Typography.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Typography.kt new file mode 100644 index 0000000..0d0b523 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/Typography.kt @@ -0,0 +1,117 @@ +package com.uniswap.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.uniswap.R + +// TODO gary update for Spore changes +@Immutable +data class CustomTypography( + val defaultFontFamily: FontFamily = FontFamily( + Font(R.font.basel_book), + Font(R.font.basel_medium, FontWeight.Medium), + Font(R.font.basel_semibold, FontWeight.SemiBold), + Font(R.font.basel_bold, FontWeight.Bold), + ), + val defaultLetterSpacing: TextUnit = 0.sp, + val heading1: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 52.sp, + lineHeight = 60.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val heading2: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 36.sp, + lineHeight = 44.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val heading3: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 24.sp, + lineHeight = 32.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val subheading1: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val subheading2: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 16.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val body1: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val body2: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 16.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val body3: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 14.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val buttonLabel1: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val buttonLabel2: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Medium, + letterSpacing = defaultLetterSpacing, + ), + val buttonLabel3: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 18.sp, + lineHeight = 24.sp, + fontWeight = FontWeight.Normal, + letterSpacing = defaultLetterSpacing, + ), + val buttonLabel4: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 14.sp, + lineHeight = 16.sp, + fontWeight = FontWeight.Medium, + letterSpacing = defaultLetterSpacing, + ), + val monospace: TextStyle = TextStyle( + fontFamily = defaultFontFamily, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = defaultLetterSpacing, + ), +) + +val LocalCustomTypography = staticCompositionLocalOf { + CustomTypography() +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapComponent.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapComponent.kt new file mode 100644 index 0000000..7a1ba91 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapComponent.kt @@ -0,0 +1,13 @@ +package com.uniswap.theme + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun UniswapComponent(modifier: Modifier = Modifier, content: @Composable () -> Unit) { + UniswapTheme { + Surface(modifier = modifier, content = content) + } +} diff --git a/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapTheme.kt b/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapTheme.kt new file mode 100644 index 0000000..3aff515 --- /dev/null +++ b/apps/mobile/android/app/src/main/java/com/uniswap/theme/UniswapTheme.kt @@ -0,0 +1,56 @@ +package com.uniswap.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember + +@Composable +fun UniswapTheme( + darkTheme: Boolean? = null, + content: @Composable () -> Unit +) { + val isDarkTheme = darkTheme ?: isSystemInDarkTheme() + + val customSpacing = CustomSpacing() + val customTypography = CustomTypography() + val customShapes = CustomShapes() + val customColors = remember(isDarkTheme) { + if (isDarkTheme) darkCustomColors else lightCustomColors + } + + CompositionLocalProvider( + LocalCustomSpacing provides customSpacing, + LocalCustomTypography provides customTypography, + LocalCustomShapes provides customShapes, + LocalCustomColors provides customColors, + ) { + MaterialTheme( + colors = if (isDarkTheme) DarkColors else LightColors + ) { + ProvideTextStyle(value = customTypography.body1) { + content() + } + } + } +} + +object UniswapTheme { + val spacing: CustomSpacing + @Composable + get() = LocalCustomSpacing.current + + val typography: CustomTypography + @Composable + get() = LocalCustomTypography.current + + val shapes: CustomShapes + @Composable + get() = LocalCustomShapes.current + + val colors: CustomColors + @Composable + get() = LocalCustomColors.current +} diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..7b89676 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a873e0e Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-hdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png b/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..c16de54 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..a3fcf7c Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-mdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..80fccda Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..13baa54 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..0011ef8 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..51a1fb0 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..333fcad Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_background.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..5e8b2a3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_launcher_foreground.png differ diff --git a/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml b/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml new file mode 100644 index 0000000..03fceb2 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/splashscreen.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/android/app/src/main/res/drawable/splashscreen_icon.xml b/apps/mobile/android/app/src/main/res/drawable/splashscreen_icon.xml new file mode 100644 index 0000000..044ff86 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/splashscreen_icon.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_alert_triangle.xml b/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_alert_triangle.xml new file mode 100644 index 0000000..7e8d695 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_alert_triangle.xml @@ -0,0 +1,9 @@ + + + diff --git a/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_paste.xml b/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_paste.xml new file mode 100644 index 0000000..3d5527d --- /dev/null +++ b/apps/mobile/android/app/src/main/res/drawable/uniswap_icon_paste.xml @@ -0,0 +1,10 @@ + + + diff --git a/apps/mobile/android/app/src/main/res/font/basel_bold.otf b/apps/mobile/android/app/src/main/res/font/basel_bold.otf new file mode 100644 index 0000000..40efff3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/font/basel_bold.otf differ diff --git a/apps/mobile/android/app/src/main/res/font/basel_book.otf b/apps/mobile/android/app/src/main/res/font/basel_book.otf new file mode 100644 index 0000000..a29a625 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/font/basel_book.otf differ diff --git a/apps/mobile/android/app/src/main/res/font/basel_medium.otf b/apps/mobile/android/app/src/main/res/font/basel_medium.otf new file mode 100644 index 0000000..b21566f Binary files /dev/null and b/apps/mobile/android/app/src/main/res/font/basel_medium.otf differ diff --git a/apps/mobile/android/app/src/main/res/font/basel_semibold.otf b/apps/mobile/android/app/src/main/res/font/basel_semibold.otf new file mode 100644 index 0000000..17b9952 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/font/basel_semibold.otf differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..bfa0164 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c05581d Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..9dcb8b3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..3d34734 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..b383b90 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..83c0dc3 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..f7e08bc Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6f11fcf Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a2bfa50 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f10b072 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..1bd12bf Binary files /dev/null and b/apps/mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/apps/mobile/android/app/src/main/res/playstore-icon.png b/apps/mobile/android/app/src/main/res/playstore-icon.png new file mode 100644 index 0000000..20a1535 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/playstore-icon.png differ diff --git a/apps/mobile/android/app/src/main/res/raw/onboarding_dark.riv b/apps/mobile/android/app/src/main/res/raw/onboarding_dark.riv new file mode 100644 index 0000000..64d5092 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/raw/onboarding_dark.riv differ diff --git a/apps/mobile/android/app/src/main/res/raw/onboarding_light.riv b/apps/mobile/android/app/src/main/res/raw/onboarding_light.riv new file mode 100644 index 0000000..5a6da1d Binary files /dev/null and b/apps/mobile/android/app/src/main/res/raw/onboarding_light.riv differ diff --git a/apps/mobile/android/app/src/main/res/raw/pending_send.riv b/apps/mobile/android/app/src/main/res/raw/pending_send.riv new file mode 100644 index 0000000..f4e4bd4 Binary files /dev/null and b/apps/mobile/android/app/src/main/res/raw/pending_send.riv differ diff --git a/apps/mobile/android/app/src/main/res/raw/pending_swap.riv b/apps/mobile/android/app/src/main/res/raw/pending_swap.riv new file mode 100644 index 0000000..fa2f5ff Binary files /dev/null and b/apps/mobile/android/app/src/main/res/raw/pending_swap.riv differ diff --git a/apps/mobile/android/app/src/main/res/values-night/colors.xml b/apps/mobile/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..01d0316 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + #000000 + #303030 + #ffffff + diff --git a/apps/mobile/android/app/src/main/res/values-night/styles.xml b/apps/mobile/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..a6384f9 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/apps/mobile/android/app/src/main/res/values/colors.xml b/apps/mobile/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..6bba0da --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + #FFFFFF + #ffffff + #000000 + diff --git a/apps/mobile/android/app/src/main/res/values/ids.xml b/apps/mobile/android/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..ea6b36b --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/android/app/src/main/res/values/strings.xml b/apps/mobile/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..f95a6ff --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + Uniswap + cover + true + diff --git a/apps/mobile/android/app/src/main/res/values/styles.xml b/apps/mobile/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..bcfbaec --- /dev/null +++ b/apps/mobile/android/app/src/main/res/values/styles.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/apps/mobile/android/app/src/main/res/xml/backup_rules.xml b/apps/mobile/android/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..5221f69 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,6 @@ + + + + diff --git a/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml b/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..ee264a1 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/apps/mobile/android/app/src/main/res/xml/locales_config.xml b/apps/mobile/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..4172a03 --- /dev/null +++ b/apps/mobile/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/prod/AndroidManifest.xml b/apps/mobile/android/app/src/prod/AndroidManifest.xml new file mode 100644 index 0000000..8de66da --- /dev/null +++ b/apps/mobile/android/app/src/prod/AndroidManifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/android/app/src/release/java/com/uniswap/ReactNativeFlipper.java b/apps/mobile/android/app/src/release/java/com/uniswap/ReactNativeFlipper.java new file mode 100644 index 0000000..fd2f251 --- /dev/null +++ b/apps/mobile/android/app/src/release/java/com/uniswap/ReactNativeFlipper.java @@ -0,0 +1,20 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + *

This source code is licensed under the MIT license found in the LICENSE file in the root + * directory of this source tree. + */ +package com.uniswap; + +import android.content.Context; +import com.facebook.react.ReactInstanceManager; + +/** + * Class responsible of loading Flipper inside your React Native application. This is the release + * flavor of it so it's empty as we don't want to load Flipper. + */ +public class ReactNativeFlipper { + public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) { + // Do nothing as we don't want to initialize Flipper on Release. + } +} diff --git a/apps/mobile/android/build.gradle b/apps/mobile/android/build.gradle new file mode 100644 index 0000000..2245a7e --- /dev/null +++ b/apps/mobile/android/build.gradle @@ -0,0 +1,58 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + ext { + buildToolsVersion = "33.0.0" + minSdkVersion = 28 + compileSdkVersion = 34 + targetSdkVersion = 34 + ndkVersion = "25.2.9519653" + kotlinVersion = "1.8.20" + + appCompat = "1.6.1" + compose = "1.4.3" + corePerf = "1.0.0-beta01" + flowlayout = "0.23.1" + kotlinSerialization = "1.5.1" + lifecycle = "2.5.1" + multidexVersion = "2.0.1" + } + repositories { + google() + mavenCentral() + } + dependencies { + classpath('com.android.tools.build:gradle:7.4.2') + classpath("com.facebook.react:react-native-gradle-plugin") + classpath('com.google.gms:google-services:4.3.15') + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath("io.sentry:sentry-android-gradle-plugin:3.13.0") + } +} + +plugins { + id 'org.jetbrains.kotlin.plugin.serialization' version "$kotlinVersion" +} + +allprojects { + repositories { + maven { + url = rootProject.file("../../../node_modules/detox/Detox-android") + } + } + project.pluginManager.withPlugin("com.facebook.react") { + react { + reactNativeDir = rootProject.file("../../../node_modules/react-native/") + codegenDir = rootProject.file("../../../node_modules/react-native-codegen/") + } + } + + repositories { + maven { + // expo-camera bundles a custom com.google.android:cameraview + url "$rootDir/../../../node_modules/expo-camera/android/maven" + } + } +} diff --git a/apps/mobile/android/gradle.properties b/apps/mobile/android/gradle.properties new file mode 100644 index 0000000..22653cd --- /dev/null +++ b/apps/mobile/android/gradle.properties @@ -0,0 +1,34 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# org.gradle.daemon=true +org.gradle.jvmargs=-Xmx8g + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true + +# Version of flipper SDK to use with React Native +FLIPPER_VERSION=0.162.0 + +# Use this property to enable or disable the Hermes JS engine. +# If set to false, you will be using JSC instead. +hermesEnabled=true diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar b/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/apps/mobile/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1cd0a1e --- /dev/null +++ b/apps/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Aug 16 13:06:20 EDT 2023 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/apps/mobile/android/gradlew b/apps/mobile/android/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/apps/mobile/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/apps/mobile/android/gradlew.bat b/apps/mobile/android/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/apps/mobile/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/apps/mobile/android/link-assets-manifest.json b/apps/mobile/android/link-assets-manifest.json new file mode 100644 index 0000000..7f747d8 --- /dev/null +++ b/apps/mobile/android/link-assets-manifest.json @@ -0,0 +1,17 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "src/assets/fonts/Basel-Book.ttf", + "sha1": "7ff6b3f7e5c2857ca3b39fad3ba09c35bb75e302" + }, + { + "path": "src/assets/fonts/Basel-Medium.ttf", + "sha1": "182bf31d0794296a034a2d13b50fffd804709aaa" + }, + { + "path": "src/assets/fonts/InputMono-Regular.ttf", + "sha1": "c27a811b8a8e05a578f67f71bb89ecb03dca5905" + } + ] +} diff --git a/apps/mobile/android/settings.gradle b/apps/mobile/android/settings.gradle new file mode 100644 index 0000000..bb1c5d3 --- /dev/null +++ b/apps/mobile/android/settings.gradle @@ -0,0 +1,13 @@ +rootProject.name = 'Uniswap' +apply from: file("../../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle") +applyNativeModulesSettingsGradle(settings) +includeBuild('../../../node_modules/react-native-gradle-plugin') +include ':app' + +apply from: new File(["node", "--print", "require.resolve('../../../node_modules/expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle") +useExpoModules() + +include ':@sentry_react-native' +project(':@sentry_react-native').projectDir = new File('../../../node_modules/@sentry/react-native/android') +include ':detox' +project(':detox').projectDir = new File('../../../node_modules/detox/android/detox') diff --git a/apps/mobile/app.json b/apps/mobile/app.json new file mode 100644 index 0000000..232f9d5 --- /dev/null +++ b/apps/mobile/app.json @@ -0,0 +1,15 @@ +{ + "name": "Uniswap", + "displayName": "Uniswap", + "appStoreUrl": "https://apps.apple.com/app/apple-store/id6443944476", + "plugins": [ + "expo-localization" + ], + "expo": { + "ios": { + "infoPlist": { + "CFBundleAllowMixedLocalizations": true + } + } + } +} diff --git a/apps/mobile/babel.config.js b/apps/mobile/babel.config.js new file mode 100644 index 0000000..4760310 --- /dev/null +++ b/apps/mobile/babel.config.js @@ -0,0 +1,58 @@ +const { NODE_ENV } = process.env + +const inProduction = NODE_ENV === 'production' + +module.exports = function (api) { + api.cache.using(() => process.env.NODE_ENV) + var plugins = [ + // disable for now as its causing ci to hang + // process.env.NODE_ENV === 'test' + // ? null + // : [ + // '@tamagui/babel-plugin', + // { + // components: ['ui'], + // config: '../../packages/ui/src/tamagui.config.ts', + // }, + // ], + [ + 'module:react-native-dotenv', + { + // ideally use envName here to add a mobile namespace but this doesn't work when sharing with dotenv-webpack + moduleName: 'react-native-dotenv', + path: '../../.env.defaults', // must use this path so this file can be shared with web since dotenv-webpack is less flexible + safe: true, + allowUndefined: false, + }, + ], + // https://github.com/software-mansion/react-native-reanimated/issues/3364#issuecomment-1268591867 + '@babel/plugin-proposal-export-namespace-from', + [ + 'react-native-reanimated/plugin', + { + globals: ['__scanCodes', '__scanOCR'], + }, + ], + 'transform-inline-environment-variables', + // TypeScript compiles this, but in production builds, metro doesn't use tsc + '@babel/plugin-proposal-logical-assignment-operators', + // metro doesn't like these + '@babel/plugin-proposal-numeric-separator', + // automatically require React when using JSX + 'react-require', + ].filter(Boolean) + + if (inProduction) { + // Remove all console statements in production + plugins = [...plugins, 'transform-remove-console'] + } + + return { + ignore: [ + // speeds up compile + '**/@tamagui/**/dist/**', + ], + presets: ['module:metro-react-native-babel-preset'], + plugins, + } +} diff --git a/apps/mobile/declarations.d.ts b/apps/mobile/declarations.d.ts new file mode 100644 index 0000000..6ba3cad --- /dev/null +++ b/apps/mobile/declarations.d.ts @@ -0,0 +1,8 @@ +declare module '*.svg' { + import React from 'react' + import { SvgProps } from 'react-native-svg' + const content: React.FC + export default content +} + +declare module 'react-native-device-info/jest/react-native-device-info-mock' diff --git a/apps/mobile/e2e/Onboarding.e2e.ts b/apps/mobile/e2e/Onboarding.e2e.ts new file mode 100644 index 0000000..d82f6d0 --- /dev/null +++ b/apps/mobile/e2e/Onboarding.e2e.ts @@ -0,0 +1,18 @@ +import { CreateNewWallet } from 'e2e/usecases/onboarding/CreateNewWallet' +import { ImportWallet } from 'e2e/usecases/onboarding/ImportWallet' +import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet' + +describe('Onboarding', () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }) + }) + + afterEach(async () => { + await device.uninstallApp() + await device.installApp() + }) + + it('creates a new wallet', CreateNewWallet) + it('watches wallet', WatchWallet) + it('imports a testing wallet using recovery phrase', ImportWallet) +}) diff --git a/apps/mobile/e2e/README.md b/apps/mobile/e2e/README.md new file mode 100644 index 0000000..89b2fc8 --- /dev/null +++ b/apps/mobile/e2e/README.md @@ -0,0 +1,50 @@ +# E2E Tests + +The e2e tests use [detox](https://github.com/wix/Detox). + +## Running tests + +### iOS + +Detox environment requires installation of the same environment as the main iOS application and additionally the iPhone 15 simulator. +The choice of this simulator is hardcoded in order to reflect e2e environment setup and is dictated by the github actions virtual machine on which the e2e tests will take place. + +#### Debug mode + +To run tests in debug mode, run bundler: + +``` +yarn mobile e2e:packager +``` + +Build debug testing app: + +``` +yarn mobile e2e:ios:build:debug +``` + +Run ios e2e tests in debug mode: + +``` +yarn mobile e2e:ios:test:debug +``` + +#### Release mode + +To run tests in release mode: + +``` +yarn mobile e2e:ios:test:release +``` + +It builds and runs tests in one go. + +## Mocking + +E2E tests should remain as close as possible to production, but sometimes mocking is necessary. + +Only mocking entire files is supported at the moment, so you may need to reorganize functions. To mock a file, create a new one with the same name and extension `mock.ts` (e.g. `AnimatedHeader.ts` -> `AnimatedHeader.mock.ts`) in the same directory. The metro bundler will override any file that has a `mock.ts` equivalent in Detox runs. + +Native views, libraries relying on the native code and libraries utilizing long-running asynchronouse background processes like sentry are not supported by detox currently. Imports mocking is unfortunatelly not supported by detox yet. If such problems occur, the entire component using problematic library needs to be mocked or a component exposing only targeted library needs to be created and then it can be mocked, precisely replacing only targeted library. + +Read more here https://wix.github.io/Detox/docs/guide/mocking/ diff --git a/apps/mobile/e2e/Swap.e2e.ts b/apps/mobile/e2e/Swap.e2e.ts new file mode 100644 index 0000000..27cb159 --- /dev/null +++ b/apps/mobile/e2e/Swap.e2e.ts @@ -0,0 +1,11 @@ +import { WatchWallet } from 'e2e/usecases/onboarding/WatchWallet' +import { SwapBasicInteractions } from 'e2e/usecases/swap/SwapBasicInteractions' + +describe('Swap', () => { + beforeAll(async () => { + await device.launchApp({ newInstance: true }) + await WatchWallet() + }) + + it('tests swap screen interactions', SwapBasicInteractions) +}) diff --git a/apps/mobile/e2e/init.js b/apps/mobile/e2e/init.js new file mode 100644 index 0000000..6920a8c --- /dev/null +++ b/apps/mobile/e2e/init.js @@ -0,0 +1,10 @@ +import { device } from 'detox' +import { permissions } from './utils/fixtures' + +beforeAll(async () => { + await device.installApp() + await device.launchApp({ + newInstance: false, + permissions, + }) +}) diff --git a/apps/mobile/e2e/jest.config.js b/apps/mobile/e2e/jest.config.js new file mode 100644 index 0000000..d6b14e5 --- /dev/null +++ b/apps/mobile/e2e/jest.config.js @@ -0,0 +1,13 @@ +/** @type {import('@jest/types').Config.InitialOptions} */ +module.exports = { + rootDir: '..', + testMatch: ['/e2e/**/*.e2e.ts'], + testTimeout: 240000, + maxWorkers: 1, + globalSetup: 'detox/runners/jest/globalSetup', + globalTeardown: 'detox/runners/jest/globalTeardown', + reporters: ['detox/runners/jest/reporter'], + testEnvironment: 'detox/runners/jest/testEnvironment', + verbose: true, + moduleDirectories: ['node_modules', ''] +}; diff --git a/apps/mobile/e2e/usecases/CreateNewWallet.ts b/apps/mobile/e2e/usecases/CreateNewWallet.ts new file mode 100644 index 0000000..10181a5 --- /dev/null +++ b/apps/mobile/e2e/usecases/CreateNewWallet.ts @@ -0,0 +1,49 @@ +import { by, element, expect } from 'detox' +import { TestWallet } from 'e2e/utils/fixtures' +import { ElementName } from 'wallet/src/telemetry/constants' + +export function CreateNewWallet(): void { + it('creates a new wallet', async () => { + // Selects "Create a new wallet" option on the landing screen + await element(by.id(ElementName.CreateAccount)).tap() + + // Skips unitag flow + await element(by.id(ElementName.Skip)).tap() + + // Taps "Let's keep it safe" on QRAnimation screen + await element(by.id(ElementName.Next)).tap() + + // Check is both manual and cloud backup options are available on BackupScreen + await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() + await expect(element(by.id(ElementName.AddManualBackup))).toBeVisible() + + // Picks "Manual backup" option + await element(by.id(ElementName.AddManualBackup)).tap() + + // Checks if ManualBackupScreen warning displays and taps "I'm ready" button + await expect(element(by.id(ElementName.Confirm))).toBeVisible() + await element(by.id(ElementName.Confirm)).tap() + + // Taps continue on ManualBackupScreen + await element(by.id(ElementName.Next)).tap() + + // Taps continue on manual backup confirmation screen. It is replaced by mock because detox + // can't interact with native screens + await element(by.id(ElementName.Continue)).tap() + + // Skips notification setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Skips biometrics setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Confirms by tapping "Skip" on warning modal + await element(by.id(ElementName.Confirm)).tap() + + // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is + // displayed and other + await expect(element(by.text(TestWallet.name))).toBeVisible() + await expect(element(by.id(ElementName.Swap))).toBeVisible() + await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() + }) +} diff --git a/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts b/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts new file mode 100644 index 0000000..f7bcdaa --- /dev/null +++ b/apps/mobile/e2e/usecases/onboarding/CreateNewWallet.ts @@ -0,0 +1,47 @@ +import { by, element, expect } from 'detox' +import { TestWallet } from 'e2e/utils/fixtures' +import { ElementName } from 'wallet/src/telemetry/constants' + +export async function CreateNewWallet(): Promise { + // Selects "Create a new wallet" option on the landing screen + await element(by.id(ElementName.CreateAccount)).tap() + + // Skips unitag flow + await element(by.id(ElementName.Skip)).tap() + + // Taps "Let's keep it safe" on QRAnimation screen + await element(by.id(ElementName.Next)).tap() + + // Check is both manual and cloud backup options are available on BackupScreen + await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() + await expect(element(by.id(ElementName.AddManualBackup))).toBeVisible() + + // Picks "Manual backup" option + await element(by.id(ElementName.AddManualBackup)).tap() + + // Checks if ManualBackupScreen warning displays and taps "I'm ready" button + await expect(element(by.id(ElementName.Confirm))).toBeVisible() + await element(by.id(ElementName.Confirm)).tap() + + // Taps continue on ManualBackupScreen + await element(by.id(ElementName.Next)).tap() + + // Taps continue on manual backup confirmation screen. It is replaced by mock because detox + // can't interact with native screens + await element(by.id(ElementName.Continue)).tap() + + // Skips notification setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Skips biometrics setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Confirms by tapping "Skip" on warning modal + await element(by.id(ElementName.Confirm)).tap() + + // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is + // displayed and other + await expect(element(by.text(TestWallet.name))).toBeVisible() + await expect(element(by.id(ElementName.Swap))).toBeVisible() + await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() +} diff --git a/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts b/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts new file mode 100644 index 0000000..76b2036 --- /dev/null +++ b/apps/mobile/e2e/usecases/onboarding/ImportWallet.ts @@ -0,0 +1,43 @@ +import { by, element, expect } from 'detox' +import { TestWallet } from 'e2e/utils/fixtures' +import { ElementName } from 'wallet/src/telemetry/constants' + +export async function ImportWallet(): Promise { + // Selects "Add an existing wallet" option on the landing screen + await element(by.id(ElementName.ImportAccount)).tap() + + // Picks Import a wallet by recovery phase option + await element(by.id(ElementName.OnboardingImportSeedPhrase)).tap() + + // Checks if recovery phase input is in focus and types recovery phrase in + await expect(element(by.id(ElementName.ImportAccountInput))).toBeFocused() + await element(by.id(ElementName.ImportAccountInput)).typeText(TestWallet.recoveryPhrase) + + // Taps continue navigating to SelectWalletScreen + await element(by.id(ElementName.Continue)).tap() + + // Taps continue on SelectWalletScreen + await waitFor(element(by.id(`${ElementName.WalletCard}-1`))) + .toBeVisible() + .withTimeout(10000) + await element(by.id(ElementName.Next)).tap() + + // Skips cloud backup step on BackupScreen by clicking "Maybe later" + await expect(element(by.id(ElementName.AddCloudBackup))).toBeVisible() + await element(by.id(ElementName.Next)).tap() + + // Skips notification setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Skips biometrics setup by tapping "Maybe later" button + await element(by.id(ElementName.Skip)).tap() + + // Confirms by tapping "Skip" on warning modal + await element(by.id(ElementName.Confirm)).tap() + + // Confirms if user successfuly finished create new wallet flow by checking if provided wallet name is + // displayed and other + await expect(element(by.text(TestWallet.name))).toBeVisible() + await expect(element(by.id(ElementName.Swap))).toBeVisible() + await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() +} diff --git a/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts b/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts new file mode 100644 index 0000000..f7dfc21 --- /dev/null +++ b/apps/mobile/e2e/usecases/onboarding/WatchWallet.ts @@ -0,0 +1,23 @@ +import { by, element, expect } from 'detox' +import { TestWatchedWallet } from 'e2e/utils/fixtures' +import { ElementName } from 'wallet/src/telemetry/constants' + +export async function WatchWallet(): Promise { + // Selects "Add an existing wallet" option on the landing screen + await element(by.id(ElementName.ImportAccount)).tap() + + // Picks Watch a wallet option on ImportMethodScreen + await element(by.id(ElementName.WatchWallet)).tap() + + // Checks if wallet name is in focus and types recovery phrase in + await expect(element(by.id(ElementName.ImportAccountInput))).toBeFocused() + await element(by.id(ElementName.ImportAccountInput)).typeText(TestWatchedWallet.ens) + + // Confirms the entered wallet name by tapping "continue" + await element(by.id(ElementName.Next)).tap() + + // Checks if Home screen is displayed with a proper user name + await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible() + await expect(element(by.id(ElementName.Swap))).toBeVisible() + await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() +} diff --git a/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts b/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts new file mode 100644 index 0000000..c2e7017 --- /dev/null +++ b/apps/mobile/e2e/usecases/swap/SwapBasicInteractions.ts @@ -0,0 +1,86 @@ +import { by, element, expect } from 'detox' +import { TestWatchedWallet } from 'e2e/utils/fixtures' +import { ElementName } from 'wallet/src/telemetry/constants' + +export async function SwapBasicInteractions(): Promise { + // Navigate to swap screen + await element(by.id(ElementName.Swap)).tap() + + // Checks if currency input is selected + await expect(element(by.id(ElementName.AmountInputIn))).toBeFocused() + + // Checks if "Max" button is available + await expect(element(by.id(ElementName.SetMaxInput))).toBeVisible() + + // Opens token selector modal on Swap screen + await element(by.id(ElementName.ChooseOutputToken)).tap() + + // Picks usdc output token + await element(by.text('USDC')).atIndex(0).tap() + + // Taps 1234567890 number into swap input + await element(by.id('decimal-pad-1')).tap() + await element(by.id('decimal-pad-2')).tap() + await element(by.id('decimal-pad-3')).tap() + await element(by.id('decimal-pad-4')).tap() + await element(by.id('decimal-pad-5')).tap() + await element(by.id('decimal-pad-6')).tap() + await element(by.id('decimal-pad-7')).tap() + await element(by.id('decimal-pad-8')).tap() + await element(by.id('decimal-pad-.')).tap() + await element(by.id('decimal-pad-0')).tap() + await element(by.id('decimal-pad-9')).tap() + await element(by.id('decimal-pad-1')).tap() + await element(by.id('decimal-pad-backspace')).tap() + + // Checks if expected input expected value: "12345678.09" + await expect(element(by.id(ElementName.AmountInputIn))).toHaveValue('12345678.09') + + // Checks if expected error is displayed + await expect(element(by.text('You don’t have enough ETH'))).toBeVisible() + + // Checks if expected output expected value: "0" + await expect(element(by.id(ElementName.AmountInputOut))).not.toHaveValue('0') + + // Swaps input and output currencies + await element(by.id(ElementName.SwitchCurrenciesButton)).tap() + + // Checks if expected input expected value: "0" + await expect(element(by.id(ElementName.AmountInputIn))).toHaveValue('0') + + // Checks if expected error is displayed + await expect(element(by.text('Not enough liquidity'))).toBeVisible() + + // Checks if expected output expected value: "12345678.09" + await expect(element(by.id(ElementName.AmountInputOut))).toHaveValue('12345678.09') + + // Swaps input and output currencies + await element(by.id(ElementName.SwitchCurrenciesButton)).tap() + + // Selects currency output + await element(by.id(ElementName.AmountInputOut)).tap() + + // Clears the output field + await element(by.id(ElementName.AmountInputOut)).clearText() + + await element(by.id('decimal-pad-1')).tap() + await element(by.id('decimal-pad-2')).tap() + await element(by.id('decimal-pad-3')).tap() + + // Checks if output has expected value: "123" + await expect(element(by.id(ElementName.AmountInputOut))).toHaveValue('123') + + // Checks if expected input value to be cleared + await expect(element(by.id(ElementName.AmountInputIn))).not.toHaveValue('0') + + // Checks dollar value to be visible + await expect(element(by.text('$123.00'))).toBeVisible() + + // Swipes swap modal by dragging down SwapFormHeader + await element(by.id(ElementName.SwapFormHeader)).swipe('down', 'fast', 0.75) + + // Checks if Home screen is visible and not covered + await expect(element(by.text(TestWatchedWallet.displayName))).toBeVisible() + await expect(element(by.id(ElementName.Swap))).toBeVisible() + await expect(element(by.id(ElementName.SearchTokensAndWallets))).toBeVisible() +} diff --git a/apps/mobile/e2e/utils/fixtures.ts b/apps/mobile/e2e/utils/fixtures.ts new file mode 100644 index 0000000..1f8f2a4 --- /dev/null +++ b/apps/mobile/e2e/utils/fixtures.ts @@ -0,0 +1,10 @@ +export const TestWallet = { + name: 'Wallet 1', + recoveryPhrase: + 'oak reduce strong borrow control funny library disagree radio clarify degree pistol', +} + +export const TestWatchedWallet = { + ens: 'Spenciefy', + displayName: 'spencer', +} diff --git a/apps/mobile/hardhat.config.js b/apps/mobile/hardhat.config.js new file mode 100644 index 0000000..5fe6674 --- /dev/null +++ b/apps/mobile/hardhat.config.js @@ -0,0 +1,19 @@ +const { ALCHEMY_API_KEY } = require('react-native-dotenv') + +const mainnetFork = { + url: `https://eth-mainnet.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, + blockNumber: 13582625, +} + +/** + * Hardhat config to fork mainnet at a specific block. + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + networks: { + hardhat: { + chainId: 1, + forking: mainnetFork, + }, + }, +} diff --git a/apps/mobile/index.js b/apps/mobile/index.js new file mode 100644 index 0000000..6b0bfed --- /dev/null +++ b/apps/mobile/index.js @@ -0,0 +1,13 @@ +// Disable sorting imports with Prettier for this file so that it doesn't change the order +// organize-imports-ignore +import './wdyr' + +import { AppRegistry } from 'react-native' +import 'react-native-gesture-handler' +import 'react-native-reanimated' +import 'src/logbox' +import 'src/polyfills' +import App from 'src/app/App' +import { name as appName } from './app.json' + +AppRegistry.registerComponent(appName, () => App) diff --git a/apps/mobile/ios/.xcode.env b/apps/mobile/ios/.xcode.env new file mode 100644 index 0000000..772b339 --- /dev/null +++ b/apps/mobile/ios/.xcode.env @@ -0,0 +1 @@ +export NODE_BINARY=$(command -v node) diff --git a/apps/mobile/ios/Appearance/RCTThemeModule.h b/apps/mobile/ios/Appearance/RCTThemeModule.h new file mode 100644 index 0000000..1e0a38d --- /dev/null +++ b/apps/mobile/ios/Appearance/RCTThemeModule.h @@ -0,0 +1,4 @@ +#import + +@interface ThemeModule : NSObject +@end diff --git a/apps/mobile/ios/Appearance/RCTThemeModule.m b/apps/mobile/ios/Appearance/RCTThemeModule.m new file mode 100644 index 0000000..66895ae --- /dev/null +++ b/apps/mobile/ios/Appearance/RCTThemeModule.m @@ -0,0 +1,26 @@ +#import "RCTThemeModule.h" +#import + +@implementation ThemeModule + +RCT_EXPORT_MODULE(); + +RCT_EXPORT_METHOD(setColorScheme : (NSString *)style) { + UIUserInterfaceStyle userInterfaceStyle; + + if ([style isEqualToString:@"dark"]) { + userInterfaceStyle = UIUserInterfaceStyleDark; + } else if ([style isEqualToString:@"light"]) { + userInterfaceStyle = UIUserInterfaceStyleLight; + } else { + userInterfaceStyle = UIUserInterfaceStyleUnspecified; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + for (UIWindow *window in [UIApplication sharedApplication].windows) { + window.overrideUserInterfaceStyle = userInterfaceStyle; + } + }); +} + +@end diff --git a/apps/mobile/ios/Assets.xcassets/Contents.json b/apps/mobile/ios/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/apps/mobile/ios/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Basel-Book.otf b/apps/mobile/ios/Basel-Book.otf new file mode 100644 index 0000000..77dc33d Binary files /dev/null and b/apps/mobile/ios/Basel-Book.otf differ diff --git a/apps/mobile/ios/Basel-Medium.otf b/apps/mobile/ios/Basel-Medium.otf new file mode 100644 index 0000000..344235f Binary files /dev/null and b/apps/mobile/ios/Basel-Medium.otf differ diff --git a/apps/mobile/ios/Components/ScrollFadeExtensions.swift b/apps/mobile/ios/Components/ScrollFadeExtensions.swift new file mode 100644 index 0000000..43fd622 --- /dev/null +++ b/apps/mobile/ios/Components/ScrollFadeExtensions.swift @@ -0,0 +1,26 @@ +// +// ScrollExtension.swift +// Uniswap +// +// Created by Gary Ye on 8/31/23. +// + +import SwiftUI + +extension View { + func fadeOutBottom(fadeLength:CGFloat=32) -> some View { + return mask( + VStack(spacing: 0) { + + Rectangle().fill(Color.black) + + LinearGradient(gradient: + Gradient( + colors: [Color.black, Color.black.opacity(0)]), + startPoint: .top, endPoint: .bottom + ) + .frame(height: fadeLength) + } + ) + } +} diff --git a/apps/mobile/ios/Fonts/Basel-Bold.otf b/apps/mobile/ios/Fonts/Basel-Bold.otf new file mode 100644 index 0000000..40efff3 Binary files /dev/null and b/apps/mobile/ios/Fonts/Basel-Bold.otf differ diff --git a/apps/mobile/ios/Fonts/Basel-Book.otf b/apps/mobile/ios/Fonts/Basel-Book.otf new file mode 100644 index 0000000..a29a625 Binary files /dev/null and b/apps/mobile/ios/Fonts/Basel-Book.otf differ diff --git a/apps/mobile/ios/Fonts/Basel-Medium.otf b/apps/mobile/ios/Fonts/Basel-Medium.otf new file mode 100644 index 0000000..b21566f Binary files /dev/null and b/apps/mobile/ios/Fonts/Basel-Medium.otf differ diff --git a/apps/mobile/ios/Fonts/Basel-Regular.otf b/apps/mobile/ios/Fonts/Basel-Regular.otf new file mode 100644 index 0000000..447dc1c Binary files /dev/null and b/apps/mobile/ios/Fonts/Basel-Regular.otf differ diff --git a/apps/mobile/ios/Fonts/Basel-Semibold.otf b/apps/mobile/ios/Fonts/Basel-Semibold.otf new file mode 100644 index 0000000..17b9952 Binary files /dev/null and b/apps/mobile/ios/Fonts/Basel-Semibold.otf differ diff --git a/apps/mobile/ios/OneSignalNotificationServiceExtension/Info.plist b/apps/mobile/ios/OneSignalNotificationServiceExtension/Info.plist new file mode 100644 index 0000000..57421eb --- /dev/null +++ b/apps/mobile/ios/OneSignalNotificationServiceExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/apps/mobile/ios/OneSignalNotificationServiceExtension/NotificationService.swift b/apps/mobile/ios/OneSignalNotificationServiceExtension/NotificationService.swift new file mode 100644 index 0000000..a74152e --- /dev/null +++ b/apps/mobile/ios/OneSignalNotificationServiceExtension/NotificationService.swift @@ -0,0 +1,38 @@ +// File copied from Onesignal docs: https://documentation.onesignal.com/docs/react-native-sdk-setup +import UserNotifications + +import OneSignalExtension + +class NotificationService: UNNotificationServiceExtension { + + var contentHandler: ((UNNotificationContent) -> Void)? + var receivedRequest: UNNotificationRequest! + var bestAttemptContent: UNMutableNotificationContent? + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.receivedRequest = request + self.contentHandler = contentHandler + self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) + + if let bestAttemptContent = bestAttemptContent { + /* DEBUGGING: Uncomment the 2 lines below to check this extension is executing + Note, this extension only runs when mutable-content is set + Setting an attachment or action buttons automatically adds this */ + #if DEBUG + print("Running NotificationServiceExtension") + bestAttemptContent.body = "[Modified] " + bestAttemptContent.body + #endif + + OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler) + } + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { + OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent) + contentHandler(bestAttemptContent) + } + } +} diff --git a/apps/mobile/ios/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements b/apps/mobile/ios/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements new file mode 100644 index 0000000..583ff21 --- /dev/null +++ b/apps/mobile/ios/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.$(PRODUCT_NAME).onesignal + + + diff --git a/apps/mobile/ios/Podfile b/apps/mobile/ios/Podfile new file mode 100644 index 0000000..5a14f7c --- /dev/null +++ b/apps/mobile/ios/Podfile @@ -0,0 +1,84 @@ +# changes from here: https://docs.expo.dev/bare/installing-expo-modules/ +require File.join(File.dirname(`node --print "require.resolve('expo/package.json')"`), "scripts/autolinking") +require_relative '../../../node_modules/react-native/scripts/react_native_pods' +require_relative '../../../node_modules/@react-native-community/cli-platform-ios/native_modules' + +platform :ios, '15.0' +prepare_react_native_project! + +$FirebaseSDKVersion = '10.15.0' +$RNFirebaseAnalyticsWithoutAdIdSupport=true + +target 'Uniswap' do + use_expo_modules! + post_integrate do |installer| + begin + expo_patch_react_imports!(installer) + rescue => e + Pod::UI.warn e + end + end + use_expo_modules!(exclude: ['expo-constants','expo-file-system', 'expo-font', 'expo-keep-awake', 'expo-error-recovery']) + config = use_native_modules! + + flipper_config = ENV['USE_FLIPPER'] ? FlipperConfiguration.enabled : FlipperConfiguration.disabled + + use_react_native!( + :path => config[:reactNativePath], + # to enable hermes on iOS, change `false` to `true` and then install pods + :hermes_enabled => true, + :flipper_configuration => flipper_config + ) + + target 'UniswapTests' do + inherit! :complete + # Pods for testing + end + + pod 'EthersRS', :path => '../../../node_modules/@uniswap/ethers-rs-mobile' + pod 'Argon2Swift', '1.0.3' + + permissions_path = '../../../node_modules/react-native-permissions/ios' + pod 'Permission-FaceID', :path => "#{permissions_path}/FaceID" + pod 'Permission-Notifications', :path => "#{permissions_path}/Notifications" + pod 'FirebaseCore', $FirebaseSDKVersion, :modular_headers => true + pod 'GoogleUtilities', '7.11.5', :modular_headers => true + + post_install do |installer| + react_native_post_install(installer, "../../../node_modules/react-native") + __apply_Xcode_12_5_M1_post_install_workaround(installer) + + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'No' + end + end + end +end + +target 'OneSignalNotificationServiceExtension' do + pod 'OneSignalXCFramework', '3.12.6' +end + +def widget_pods + pod 'Apollo', '1.2.1' + pod 'UIImageColors', '2.1.0' +end + +target 'Widgets' do + widget_pods + # Pods for widgets +end +target 'WidgetsCore' do + widget_pods + # Pods for widgets core +end +target 'WidgetsCoreTests' do + widget_pods + # Pods for widgets core test +end +target 'WidgetIntentExtension' do + widget_pods + # Pods for intent extension +end diff --git a/apps/mobile/ios/Podfile.lock b/apps/mobile/ios/Podfile.lock new file mode 100644 index 0000000..6f3253a --- /dev/null +++ b/apps/mobile/ios/Podfile.lock @@ -0,0 +1,1801 @@ +PODS: + - abseil/algorithm (1.20220623.0): + - abseil/algorithm/algorithm (= 1.20220623.0) + - abseil/algorithm/container (= 1.20220623.0) + - abseil/algorithm/algorithm (1.20220623.0): + - abseil/base/config + - abseil/algorithm/container (1.20220623.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/base (1.20220623.0): + - abseil/base/atomic_hook (= 1.20220623.0) + - abseil/base/base (= 1.20220623.0) + - abseil/base/base_internal (= 1.20220623.0) + - abseil/base/config (= 1.20220623.0) + - abseil/base/core_headers (= 1.20220623.0) + - abseil/base/dynamic_annotations (= 1.20220623.0) + - abseil/base/endian (= 1.20220623.0) + - abseil/base/errno_saver (= 1.20220623.0) + - abseil/base/fast_type_id (= 1.20220623.0) + - abseil/base/log_severity (= 1.20220623.0) + - abseil/base/malloc_internal (= 1.20220623.0) + - abseil/base/prefetch (= 1.20220623.0) + - abseil/base/pretty_function (= 1.20220623.0) + - abseil/base/raw_logging_internal (= 1.20220623.0) + - abseil/base/spinlock_wait (= 1.20220623.0) + - abseil/base/strerror (= 1.20220623.0) + - abseil/base/throw_delegate (= 1.20220623.0) + - abseil/base/atomic_hook (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/base (1.20220623.0): + - abseil/base/atomic_hook + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/log_severity + - abseil/base/raw_logging_internal + - abseil/base/spinlock_wait + - abseil/meta/type_traits + - abseil/base/base_internal (1.20220623.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/base/config (1.20220623.0) + - abseil/base/core_headers (1.20220623.0): + - abseil/base/config + - abseil/base/dynamic_annotations (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver (1.20220623.0): + - abseil/base/config + - abseil/base/fast_type_id (1.20220623.0): + - abseil/base/config + - abseil/base/log_severity (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal (1.20220623.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/base/prefetch (1.20220623.0): + - abseil/base/config + - abseil/base/pretty_function (1.20220623.0) + - abseil/base/raw_logging_internal (1.20220623.0): + - abseil/base/atomic_hook + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/log_severity + - abseil/base/spinlock_wait (1.20220623.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/strerror (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/errno_saver + - abseil/base/throw_delegate (1.20220623.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/cleanup/cleanup (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/cleanup/cleanup_internal + - abseil/cleanup/cleanup_internal (1.20220623.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/utility/utility + - abseil/container/common (1.20220623.0): + - abseil/meta/type_traits + - abseil/types/optional + - abseil/container/compressed_tuple (1.20220623.0): + - abseil/utility/utility + - abseil/container/container_memory (1.20220623.0): + - abseil/base/config + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/container/fixed_array (1.20220623.0): + - abseil/algorithm/algorithm + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/container/flat_hash_map (1.20220623.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_map + - abseil/memory/memory + - abseil/container/flat_hash_set (1.20220623.0): + - abseil/algorithm/container + - abseil/base/core_headers + - abseil/container/container_memory + - abseil/container/hash_function_defaults + - abseil/container/raw_hash_set + - abseil/memory/memory + - abseil/container/hash_function_defaults (1.20220623.0): + - abseil/base/config + - abseil/hash/hash + - abseil/strings/cord + - abseil/strings/strings + - abseil/container/hash_policy_traits (1.20220623.0): + - abseil/meta/type_traits + - abseil/container/hashtable_debug_hooks (1.20220623.0): + - abseil/base/config + - abseil/container/hashtablez_sampler (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/stacktrace + - abseil/memory/memory + - abseil/profiling/exponential_biased + - abseil/profiling/sample_recorder + - abseil/synchronization/synchronization + - abseil/utility/utility + - abseil/container/inlined_vector (1.20220623.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/container/inlined_vector_internal + - abseil/memory/memory + - abseil/container/inlined_vector_internal (1.20220623.0): + - abseil/base/core_headers + - abseil/container/compressed_tuple + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/span + - abseil/container/layout (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/utility/utility + - abseil/container/raw_hash_map (1.20220623.0): + - abseil/base/throw_delegate + - abseil/container/container_memory + - abseil/container/raw_hash_set + - abseil/container/raw_hash_set (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/prefetch + - abseil/container/common + - abseil/container/compressed_tuple + - abseil/container/container_memory + - abseil/container/hash_policy_traits + - abseil/container/hashtable_debug_hooks + - abseil/container/hashtablez_sampler + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/utility/utility + - abseil/debugging/debugging_internal (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/errno_saver + - abseil/base/raw_logging_internal + - abseil/debugging/demangle_internal (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/stacktrace (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/debugging/debugging_internal + - abseil/debugging/symbolize (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/debugging_internal + - abseil/debugging/demangle_internal + - abseil/strings/strings + - abseil/functional/any_invocable (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/functional/bind_front (1.20220623.0): + - abseil/base/base_internal + - abseil/container/compressed_tuple + - abseil/meta/type_traits + - abseil/utility/utility + - abseil/functional/function_ref (1.20220623.0): + - abseil/base/base_internal + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/hash/city (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/hash/hash (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/container/fixed_array + - abseil/functional/function_ref + - abseil/hash/city + - abseil/hash/low_level_hash + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/types/optional + - abseil/types/variant + - abseil/utility/utility + - abseil/hash/low_level_hash (1.20220623.0): + - abseil/base/config + - abseil/base/endian + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/memory (1.20220623.0): + - abseil/memory/memory (= 1.20220623.0) + - abseil/memory/memory (1.20220623.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/meta (1.20220623.0): + - abseil/meta/type_traits (= 1.20220623.0) + - abseil/meta/type_traits (1.20220623.0): + - abseil/base/config + - abseil/numeric/bits (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/bits + - abseil/numeric/representation (1.20220623.0): + - abseil/base/config + - abseil/profiling/exponential_biased (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/profiling/sample_recorder (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/synchronization/synchronization + - abseil/time/time + - abseil/random/distributions (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/distribution_caller + - abseil/random/internal/fast_uniform_bits + - abseil/random/internal/fastmath + - abseil/random/internal/generate_real + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/traits + - abseil/random/internal/uniform_helper + - abseil/random/internal/wide_multiply + - abseil/strings/strings + - abseil/random/internal/distribution_caller (1.20220623.0): + - abseil/base/config + - abseil/base/fast_type_id + - abseil/utility/utility + - abseil/random/internal/fast_uniform_bits (1.20220623.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/random/internal/traits + - abseil/random/internal/fastmath (1.20220623.0): + - abseil/numeric/bits + - abseil/random/internal/generate_real (1.20220623.0): + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/random/internal/fastmath + - abseil/random/internal/traits + - abseil/random/internal/iostream_state_saver (1.20220623.0): + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/nonsecure_base (1.20220623.0): + - abseil/base/core_headers + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/types/span + - abseil/random/internal/pcg_engine (1.20220623.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/fastmath + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/platform (1.20220623.0): + - abseil/base/config + - abseil/random/internal/pool_urbg (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/random/internal/randen + - abseil/random/internal/seed_material + - abseil/random/internal/traits + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/random/internal/randen (1.20220623.0): + - abseil/base/raw_logging_internal + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes + - abseil/random/internal/randen_slow + - abseil/random/internal/randen_engine (1.20220623.0): + - abseil/base/endian + - abseil/meta/type_traits + - abseil/random/internal/iostream_state_saver + - abseil/random/internal/randen + - abseil/random/internal/randen_hwaes (1.20220623.0): + - abseil/base/config + - abseil/random/internal/platform + - abseil/random/internal/randen_hwaes_impl + - abseil/random/internal/randen_hwaes_impl (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/random/internal/randen_slow (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/numeric/int128 + - abseil/random/internal/platform + - abseil/random/internal/salted_seed_seq (1.20220623.0): + - abseil/container/inlined_vector + - abseil/meta/type_traits + - abseil/random/internal/seed_material + - abseil/types/optional + - abseil/types/span + - abseil/random/internal/seed_material (1.20220623.0): + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/raw_logging_internal + - abseil/random/internal/fast_uniform_bits + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/random/internal/traits (1.20220623.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/uniform_helper (1.20220623.0): + - abseil/base/config + - abseil/meta/type_traits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/random/internal/wide_multiply (1.20220623.0): + - abseil/base/config + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/random/internal/traits + - abseil/random/random (1.20220623.0): + - abseil/random/distributions + - abseil/random/internal/nonsecure_base + - abseil/random/internal/pcg_engine + - abseil/random/internal/pool_urbg + - abseil/random/internal/randen_engine + - abseil/random/seed_sequences + - abseil/random/seed_gen_exception (1.20220623.0): + - abseil/base/config + - abseil/random/seed_sequences (1.20220623.0): + - abseil/base/config + - abseil/random/internal/pool_urbg + - abseil/random/internal/salted_seed_seq + - abseil/random/internal/seed_material + - abseil/random/seed_gen_exception + - abseil/types/span + - abseil/status/status (1.20220623.0): + - abseil/base/atomic_hook + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/base/strerror + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/functional/function_ref + - abseil/strings/cord + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/status/statusor (1.20220623.0): + - abseil/base/base + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/status/status + - abseil/strings/strings + - abseil/types/variant + - abseil/utility/utility + - abseil/strings/cord (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/container/fixed_array + - abseil/container/inlined_vector + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_info + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_scope + - abseil/strings/cordz_update_tracker + - abseil/strings/internal + - abseil/strings/str_format + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/strings/cord_internal (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/container/compressed_tuple + - abseil/container/inlined_vector + - abseil/container/layout + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/strings/strings + - abseil/types/span + - abseil/strings/cordz_functions (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/profiling/exponential_biased + - abseil/strings/cordz_handle (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/synchronization/synchronization + - abseil/strings/cordz_info (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/container/inlined_vector + - abseil/debugging/stacktrace + - abseil/strings/cord_internal + - abseil/strings/cordz_functions + - abseil/strings/cordz_handle + - abseil/strings/cordz_statistics + - abseil/strings/cordz_update_tracker + - abseil/synchronization/synchronization + - abseil/types/span + - abseil/strings/cordz_statistics (1.20220623.0): + - abseil/base/config + - abseil/strings/cordz_update_tracker + - abseil/strings/cordz_update_scope (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/strings/cord_internal + - abseil/strings/cordz_info + - abseil/strings/cordz_update_tracker + - abseil/strings/cordz_update_tracker (1.20220623.0): + - abseil/base/config + - abseil/strings/internal (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/meta/type_traits + - abseil/strings/str_format (1.20220623.0): + - abseil/strings/str_format_internal + - abseil/strings/str_format_internal (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/functional/function_ref + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/numeric/representation + - abseil/strings/strings + - abseil/types/optional + - abseil/types/span + - abseil/utility/utility + - abseil/strings/strings (1.20220623.0): + - abseil/base/base + - abseil/base/config + - abseil/base/core_headers + - abseil/base/endian + - abseil/base/raw_logging_internal + - abseil/base/throw_delegate + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/numeric/bits + - abseil/numeric/int128 + - abseil/strings/internal + - abseil/synchronization/graphcycles_internal (1.20220623.0): + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/synchronization/kernel_timeout_internal (1.20220623.0): + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/time/time + - abseil/synchronization/synchronization (1.20220623.0): + - abseil/base/atomic_hook + - abseil/base/base + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/base/dynamic_annotations + - abseil/base/malloc_internal + - abseil/base/raw_logging_internal + - abseil/debugging/stacktrace + - abseil/debugging/symbolize + - abseil/synchronization/graphcycles_internal + - abseil/synchronization/kernel_timeout_internal + - abseil/time/time + - abseil/time (1.20220623.0): + - abseil/time/internal (= 1.20220623.0) + - abseil/time/time (= 1.20220623.0) + - abseil/time/internal (1.20220623.0): + - abseil/time/internal/cctz (= 1.20220623.0) + - abseil/time/internal/cctz (1.20220623.0): + - abseil/time/internal/cctz/civil_time (= 1.20220623.0) + - abseil/time/internal/cctz/time_zone (= 1.20220623.0) + - abseil/time/internal/cctz/civil_time (1.20220623.0): + - abseil/base/config + - abseil/time/internal/cctz/time_zone (1.20220623.0): + - abseil/base/config + - abseil/time/internal/cctz/civil_time + - abseil/time/time (1.20220623.0): + - abseil/base/base + - abseil/base/core_headers + - abseil/base/raw_logging_internal + - abseil/numeric/int128 + - abseil/strings/strings + - abseil/time/internal/cctz/civil_time + - abseil/time/internal/cctz/time_zone + - abseil/types (1.20220623.0): + - abseil/types/any (= 1.20220623.0) + - abseil/types/bad_any_cast (= 1.20220623.0) + - abseil/types/bad_any_cast_impl (= 1.20220623.0) + - abseil/types/bad_optional_access (= 1.20220623.0) + - abseil/types/bad_variant_access (= 1.20220623.0) + - abseil/types/compare (= 1.20220623.0) + - abseil/types/optional (= 1.20220623.0) + - abseil/types/span (= 1.20220623.0) + - abseil/types/variant (= 1.20220623.0) + - abseil/types/any (1.20220623.0): + - abseil/base/config + - abseil/base/core_headers + - abseil/base/fast_type_id + - abseil/meta/type_traits + - abseil/types/bad_any_cast + - abseil/utility/utility + - abseil/types/bad_any_cast (1.20220623.0): + - abseil/base/config + - abseil/types/bad_any_cast_impl + - abseil/types/bad_any_cast_impl (1.20220623.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/bad_optional_access (1.20220623.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/bad_variant_access (1.20220623.0): + - abseil/base/config + - abseil/base/raw_logging_internal + - abseil/types/compare (1.20220623.0): + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/optional (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/memory/memory + - abseil/meta/type_traits + - abseil/types/bad_optional_access + - abseil/utility/utility + - abseil/types/span (1.20220623.0): + - abseil/algorithm/algorithm + - abseil/base/core_headers + - abseil/base/throw_delegate + - abseil/meta/type_traits + - abseil/types/variant (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/base/core_headers + - abseil/meta/type_traits + - abseil/types/bad_variant_access + - abseil/utility/utility + - abseil/utility/utility (1.20220623.0): + - abseil/base/base_internal + - abseil/base/config + - abseil/meta/type_traits + - amplitude-react-native (1.4.0): + - React-Core + - Apollo (1.2.1): + - Apollo/Core (= 1.2.1) + - Apollo/Core (1.2.1) + - AppsFlyerFramework (6.10.1): + - AppsFlyerFramework/Main (= 6.10.1) + - AppsFlyerFramework/Main (6.10.1) + - Argon2Swift (1.0.3) + - boost (1.76.0) + - BoringSSL-GRPC (0.0.24): + - BoringSSL-GRPC/Implementation (= 0.0.24) + - BoringSSL-GRPC/Interface (= 0.0.24) + - BoringSSL-GRPC/Implementation (0.0.24): + - BoringSSL-GRPC/Interface (= 0.0.24) + - BoringSSL-GRPC/Interface (0.0.24) + - DoubleConversion (1.1.6) + - EthersRS (0.0.5) + - EXApplication (5.1.1): + - ExpoModulesCore + - EXAV (13.4.1): + - ExpoModulesCore + - ReactCommon/turbomodule/core + - EXBarCodeScanner (12.7.0): + - EXImageLoader + - ExpoModulesCore + - ZXingObjC/OneD + - ZXingObjC/PDF417 + - EXCamera (13.4.4): + - ExpoModulesCore + - EXFileSystem (15.3.0): + - ExpoModulesCore + - EXFont (11.1.1): + - ExpoModulesCore + - EXImageLoader (4.4.0): + - ExpoModulesCore + - React-Core + - EXLocalAuthentication (13.0.2): + - ExpoModulesCore + - Expo (48.0.19): + - ExpoModulesCore + - ExpoBlur (12.6.0): + - ExpoModulesCore + - ExpoClipboard (4.1.2): + - ExpoModulesCore + - ExpoHaptics (12.0.1): + - ExpoModulesCore + - ExpoKeepAwake (12.0.1): + - ExpoModulesCore + - ExpoLinearGradient (12.3.0): + - ExpoModulesCore + - ExpoLocalization (14.1.1): + - ExpoModulesCore + - ExpoModulesCore (1.5.8): + - RCT-Folly (= 2021.07.22.00) + - React-Core + - React-RCTAppDelegate + - ReactCommon/turbomodule/core + - ExpoStoreReview (6.2.1): + - ExpoModulesCore + - ExpoWebBrowser (12.0.0): + - ExpoModulesCore + - EXScreenCapture (4.2.0): + - ExpoModulesCore + - FBLazyVector (0.71.13) + - FBReactNativeSpec (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTRequired (= 0.71.13) + - RCTTypeSafety (= 0.71.13) + - React-Core (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - Firebase/AppCheck (10.15.0): + - Firebase/CoreOnly + - FirebaseAppCheck (~> 10.15.0) + - Firebase/Auth (10.15.0): + - Firebase/CoreOnly + - FirebaseAuth (~> 10.15.0) + - Firebase/CoreOnly (10.15.0): + - FirebaseCore (= 10.15.0) + - Firebase/Firestore (10.15.0): + - Firebase/CoreOnly + - FirebaseFirestore (~> 10.15.0) + - FirebaseAppCheck (10.15.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseAppCheckInterop (10.15.0) + - FirebaseAuth (10.15.0): + - FirebaseAppCheckInterop (~> 10.0) + - FirebaseCore (~> 10.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.8) + - GoogleUtilities/Environment (~> 7.8) + - GTMSessionFetcher/Core (< 4.0, >= 2.1) + - RecaptchaInterop (~> 100.0) + - FirebaseCore (10.15.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/Logger (~> 7.8) + - FirebaseCoreInternal (10.15.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseFirestore (10.15.0): + - abseil/algorithm (~> 1.20220623.0) + - abseil/base (~> 1.20220623.0) + - abseil/container/flat_hash_map (~> 1.20220623.0) + - abseil/memory (~> 1.20220623.0) + - abseil/meta (~> 1.20220623.0) + - abseil/strings/strings (~> 1.20220623.0) + - abseil/time (~> 1.20220623.0) + - abseil/types (~> 1.20220623.0) + - FirebaseCore (~> 10.0) + - "gRPC-C++ (~> 1.50.1)" + - leveldb-library (~> 1.22) + - nanopb (< 2.30910.0, >= 2.30908.0) + - fmt (6.2.1) + - glog (0.3.5) + - GoogleUtilities (7.11.5): + - GoogleUtilities/AppDelegateSwizzler (= 7.11.5) + - GoogleUtilities/Environment (= 7.11.5) + - GoogleUtilities/ISASwizzler (= 7.11.5) + - GoogleUtilities/Logger (= 7.11.5) + - GoogleUtilities/MethodSwizzler (= 7.11.5) + - GoogleUtilities/Network (= 7.11.5) + - "GoogleUtilities/NSData+zlib (= 7.11.5)" + - GoogleUtilities/Reachability (= 7.11.5) + - GoogleUtilities/SwizzlerTestHelpers (= 7.11.5) + - GoogleUtilities/UserDefaults (= 7.11.5) + - GoogleUtilities/AppDelegateSwizzler (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.11.5): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/ISASwizzler (7.11.5) + - GoogleUtilities/Logger (7.11.5): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.11.5): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.11.5)" + - GoogleUtilities/Reachability (7.11.5): + - GoogleUtilities/Logger + - GoogleUtilities/SwizzlerTestHelpers (7.11.5): + - GoogleUtilities/MethodSwizzler + - GoogleUtilities/UserDefaults (7.11.5): + - GoogleUtilities/Logger + - "gRPC-C++ (1.50.1)": + - "gRPC-C++/Implementation (= 1.50.1)" + - "gRPC-C++/Interface (= 1.50.1)" + - "gRPC-C++/Implementation (1.50.1)": + - abseil/base/base (= 1.20220623.0) + - abseil/base/core_headers (= 1.20220623.0) + - abseil/cleanup/cleanup (= 1.20220623.0) + - abseil/container/flat_hash_map (= 1.20220623.0) + - abseil/container/flat_hash_set (= 1.20220623.0) + - abseil/container/inlined_vector (= 1.20220623.0) + - abseil/functional/any_invocable (= 1.20220623.0) + - abseil/functional/bind_front (= 1.20220623.0) + - abseil/functional/function_ref (= 1.20220623.0) + - abseil/hash/hash (= 1.20220623.0) + - abseil/memory/memory (= 1.20220623.0) + - abseil/meta/type_traits (= 1.20220623.0) + - abseil/random/random (= 1.20220623.0) + - abseil/status/status (= 1.20220623.0) + - abseil/status/statusor (= 1.20220623.0) + - abseil/strings/cord (= 1.20220623.0) + - abseil/strings/str_format (= 1.20220623.0) + - abseil/strings/strings (= 1.20220623.0) + - abseil/synchronization/synchronization (= 1.20220623.0) + - abseil/time/time (= 1.20220623.0) + - abseil/types/optional (= 1.20220623.0) + - abseil/types/span (= 1.20220623.0) + - abseil/types/variant (= 1.20220623.0) + - abseil/utility/utility (= 1.20220623.0) + - "gRPC-C++/Interface (= 1.50.1)" + - gRPC-Core (= 1.50.1) + - "gRPC-C++/Interface (1.50.1)" + - gRPC-Core (1.50.1): + - gRPC-Core/Implementation (= 1.50.1) + - gRPC-Core/Interface (= 1.50.1) + - gRPC-Core/Implementation (1.50.1): + - abseil/base/base (= 1.20220623.0) + - abseil/base/core_headers (= 1.20220623.0) + - abseil/container/flat_hash_map (= 1.20220623.0) + - abseil/container/flat_hash_set (= 1.20220623.0) + - abseil/container/inlined_vector (= 1.20220623.0) + - abseil/functional/any_invocable (= 1.20220623.0) + - abseil/functional/bind_front (= 1.20220623.0) + - abseil/functional/function_ref (= 1.20220623.0) + - abseil/hash/hash (= 1.20220623.0) + - abseil/memory/memory (= 1.20220623.0) + - abseil/meta/type_traits (= 1.20220623.0) + - abseil/random/random (= 1.20220623.0) + - abseil/status/status (= 1.20220623.0) + - abseil/status/statusor (= 1.20220623.0) + - abseil/strings/cord (= 1.20220623.0) + - abseil/strings/str_format (= 1.20220623.0) + - abseil/strings/strings (= 1.20220623.0) + - abseil/synchronization/synchronization (= 1.20220623.0) + - abseil/time/time (= 1.20220623.0) + - abseil/types/optional (= 1.20220623.0) + - abseil/types/span (= 1.20220623.0) + - abseil/types/variant (= 1.20220623.0) + - abseil/utility/utility (= 1.20220623.0) + - BoringSSL-GRPC (= 0.0.24) + - gRPC-Core/Interface (= 1.50.1) + - gRPC-Core/Interface (1.50.1) + - GTMSessionFetcher/Core (3.1.1) + - hermes-engine (0.71.13): + - hermes-engine/Pre-built (= 0.71.13) + - hermes-engine/Pre-built (0.71.13) + - leveldb-library (1.22.2) + - libevent (2.1.12) + - libwebp (1.2.4): + - libwebp/demux (= 1.2.4) + - libwebp/mux (= 1.2.4) + - libwebp/webp (= 1.2.4) + - libwebp/demux (1.2.4): + - libwebp/webp + - libwebp/mux (1.2.4): + - libwebp/demux + - libwebp/webp (1.2.4) + - MMKV (1.2.15): + - MMKVCore (~> 1.2.15) + - MMKVCore (1.2.15) + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - OneSignalXCFramework (3.12.6): + - OneSignalXCFramework/OneSignalCore (= 3.12.6) + - OneSignalXCFramework/OneSignalExtension (= 3.12.6) + - OneSignalXCFramework/OneSignalOutcomes (= 3.12.6) + - OneSignalXCFramework/OneSignalCore (3.12.6) + - OneSignalXCFramework/OneSignalExtension (3.12.6): + - OneSignalXCFramework/OneSignalCore + - OneSignalXCFramework/OneSignalOutcomes + - OneSignalXCFramework/OneSignalOutcomes (3.12.6): + - OneSignalXCFramework/OneSignalCore + - Permission-FaceID (3.6.0): + - RNPermissions + - Permission-Notifications (3.6.0): + - RNPermissions + - PromisesObjC (2.3.1) + - RCT-Folly (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Default (= 2021.07.22.00) + - RCT-Folly/Default (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - RCT-Folly/Futures (2021.07.22.00): + - boost + - DoubleConversion + - fmt (~> 6.2.1) + - glog + - libevent + - RCTRequired (0.71.13) + - RCTTypeSafety (0.71.13): + - FBLazyVector (= 0.71.13) + - RCTRequired (= 0.71.13) + - React-Core (= 0.71.13) + - React (0.71.13): + - React-Core (= 0.71.13) + - React-Core/DevSupport (= 0.71.13) + - React-Core/RCTWebSocket (= 0.71.13) + - React-RCTActionSheet (= 0.71.13) + - React-RCTAnimation (= 0.71.13) + - React-RCTBlob (= 0.71.13) + - React-RCTImage (= 0.71.13) + - React-RCTLinking (= 0.71.13) + - React-RCTNetwork (= 0.71.13) + - React-RCTSettings (= 0.71.13) + - React-RCTText (= 0.71.13) + - React-RCTVibration (= 0.71.13) + - React-callinvoker (0.71.13) + - React-Codegen (0.71.13): + - FBReactNativeSpec + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - React-jsi + - React-jsiexecutor + - ReactCommon/turbomodule/bridging + - ReactCommon/turbomodule/core + - React-Core (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.71.13) + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/CoreModulesHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/Default (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/DevSupport (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.71.13) + - React-Core/RCTWebSocket (= 0.71.13) + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-jsinspector (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTActionSheetHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTAnimationHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTBlobHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTImageHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTLinkingHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTNetworkHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTSettingsHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTTextHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTVibrationHeaders (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-Core/RCTWebSocket (0.71.13): + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Core/Default (= 0.71.13) + - React-cxxreact (= 0.71.13) + - React-hermes + - React-jsi (= 0.71.13) + - React-jsiexecutor (= 0.71.13) + - React-perflogger (= 0.71.13) + - Yoga + - React-CoreModules (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.71.13) + - React-Codegen (= 0.71.13) + - React-Core/CoreModulesHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - React-RCTBlob + - React-RCTImage (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-cxxreact (0.71.13): + - boost (= 1.76.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.71.13) + - React-jsi (= 0.71.13) + - React-jsinspector (= 0.71.13) + - React-logger (= 0.71.13) + - React-perflogger (= 0.71.13) + - React-runtimeexecutor (= 0.71.13) + - React-hermes (0.71.13): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - RCT-Folly/Futures (= 2021.07.22.00) + - React-cxxreact (= 0.71.13) + - React-jsi + - React-jsiexecutor (= 0.71.13) + - React-jsinspector (= 0.71.13) + - React-perflogger (= 0.71.13) + - React-jsi (0.71.13): + - boost (= 1.76.0) + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-jsiexecutor (0.71.13): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-cxxreact (= 0.71.13) + - React-jsi (= 0.71.13) + - React-perflogger (= 0.71.13) + - React-jsinspector (0.71.13) + - React-logger (0.71.13): + - glog + - react-native-appsflyer (6.10.3): + - AppsFlyerFramework (= 6.10.1) + - React + - react-native-compat (2.11.2): + - RCT-Folly (= 2021.07.22.00) + - React-Core + - react-native-context-menu-view (1.6.0): + - React + - react-native-get-random-values (1.8.0): + - React-Core + - react-native-image-picker (7.0.1): + - React-Core + - react-native-mmkv (2.10.1): + - MMKV (>= 1.2.13) + - React-Core + - react-native-netinfo (9.3.0): + - React-Core + - react-native-onesignal (4.5.2): + - OneSignalXCFramework (= 3.12.6) + - React (< 1.0.0, >= 0.13.0) + - react-native-pager-view (6.0.1): + - React-Core + - react-native-restart (0.0.27): + - React-Core + - react-native-safe-area-context (4.5.0): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - ReactCommon/turbomodule/core + - react-native-skia (0.1.187): + - React + - React-callinvoker + - React-Core + - react-native-splash-screen (3.3.0): + - React-Core + - react-native-webview (11.23.1): + - React-Core + - react-native-widgetkit (1.0.9): + - React + - React-perflogger (0.71.13) + - React-RCTActionSheet (0.71.13): + - React-Core/RCTActionSheetHeaders (= 0.71.13) + - React-RCTAnimation (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.71.13) + - React-Codegen (= 0.71.13) + - React-Core/RCTAnimationHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTAppDelegate (0.71.13): + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-Core + - ReactCommon/turbomodule/core + - React-RCTBlob (0.71.13): + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.71.13) + - React-Core/RCTBlobHeaders (= 0.71.13) + - React-Core/RCTWebSocket (= 0.71.13) + - React-jsi (= 0.71.13) + - React-RCTNetwork (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTImage (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.71.13) + - React-Codegen (= 0.71.13) + - React-Core/RCTImageHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - React-RCTNetwork (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTLinking (0.71.13): + - React-Codegen (= 0.71.13) + - React-Core/RCTLinkingHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTNetwork (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.71.13) + - React-Codegen (= 0.71.13) + - React-Core/RCTNetworkHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTSettings (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - RCTTypeSafety (= 0.71.13) + - React-Codegen (= 0.71.13) + - React-Core/RCTSettingsHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-RCTText (0.71.13): + - React-Core/RCTTextHeaders (= 0.71.13) + - React-RCTVibration (0.71.13): + - RCT-Folly (= 2021.07.22.00) + - React-Codegen (= 0.71.13) + - React-Core/RCTVibrationHeaders (= 0.71.13) + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/core (= 0.71.13) + - React-runtimeexecutor (0.71.13): + - React-jsi (= 0.71.13) + - ReactCommon/turbomodule/bridging (0.71.13): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.71.13) + - React-Core (= 0.71.13) + - React-cxxreact (= 0.71.13) + - React-jsi (= 0.71.13) + - React-logger (= 0.71.13) + - React-perflogger (= 0.71.13) + - ReactCommon/turbomodule/core (0.71.13): + - DoubleConversion + - glog + - hermes-engine + - RCT-Folly (= 2021.07.22.00) + - React-callinvoker (= 0.71.13) + - React-Core (= 0.71.13) + - React-cxxreact (= 0.71.13) + - React-jsi (= 0.71.13) + - React-logger (= 0.71.13) + - React-perflogger (= 0.71.13) + - ReactNativePerformance (4.1.2): + - React-Core + - RecaptchaInterop (100.0.0) + - rive-react-native (6.1.1): + - React-Core + - RiveRuntime (= 5.3.1) + - RiveRuntime (5.3.1) + - RNCAsyncStorage (1.17.10): + - React-Core + - RNCMaskedView (0.2.9): + - React-Core + - RNDeviceInfo (10.0.2): + - React-Core + - RNFastImage (8.6.3): + - React-Core + - SDWebImage (~> 5.15.5) + - SDWebImageWebPCoder (~> 0.8.4) + - RNFBApp (18.4.0): + - Firebase/CoreOnly (= 10.15.0) + - React-Core + - RNFBAppCheck (18.4.0): + - Firebase/AppCheck (= 10.15.0) + - React-Core + - RNFBApp + - RNFBAuth (18.4.0): + - Firebase/Auth (= 10.15.0) + - React-Core + - RNFBApp + - RNFBFirestore (18.4.0): + - Firebase/Firestore (= 10.15.0) + - nanopb (< 2.30910.0, >= 2.30908.0) + - React-Core + - RNFBApp + - RNFlashList (1.6.3): + - React-Core + - RNGestureHandler (2.15.0): + - RCT-Folly (= 2021.07.22.00) + - React-Core + - RNImageColors (1.5.2): + - React-Core + - RNLocalize (2.2.6): + - React-Core + - RNPermissions (3.6.0): + - React-Core + - RNReanimated (3.3.0): + - DoubleConversion + - FBLazyVector + - FBReactNativeSpec + - glog + - hermes-engine + - RCT-Folly + - RCTRequired + - RCTTypeSafety + - React-callinvoker + - React-Core + - React-Core/DevSupport + - React-Core/RCTWebSocket + - React-CoreModules + - React-cxxreact + - React-hermes + - React-jsi + - React-jsiexecutor + - React-jsinspector + - React-RCTActionSheet + - React-RCTAnimation + - React-RCTBlob + - React-RCTImage + - React-RCTLinking + - React-RCTNetwork + - React-RCTSettings + - React-RCTText + - ReactCommon/turbomodule/core + - Yoga + - RNScreens (3.24.0): + - React-Core + - React-RCTImage + - RNSentry (5.5.0): + - React-Core + - Sentry/HybridSDK (= 8.7.1) + - RNSVG (13.8.0): + - React-Core + - SDWebImage (5.15.5): + - SDWebImage/Core (= 5.15.5) + - SDWebImage/Core (5.15.5) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) + - Sentry/HybridSDK (8.7.1): + - SentryPrivate (= 8.7.1) + - SentryPrivate (8.7.1) + - UIImageColors (2.1.0) + - Yoga (1.14.0) + - ZXingObjC/Core (3.6.5) + - ZXingObjC/OneD (3.6.5): + - ZXingObjC/Core + - ZXingObjC/PDF417 (3.6.5): + - ZXingObjC/Core + +DEPENDENCIES: + - "amplitude-react-native (from `../../../node_modules/@amplitude/analytics-react-native`)" + - Apollo (= 1.2.1) + - Argon2Swift (= 1.0.3) + - boost (from `../../../node_modules/react-native/third-party-podspecs/boost.podspec`) + - DoubleConversion (from `../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`) + - "EthersRS (from `../../../node_modules/@uniswap/ethers-rs-mobile`)" + - EXApplication (from `../../../node_modules/expo-application/ios`) + - EXAV (from `../../../node_modules/expo-av/ios`) + - EXBarCodeScanner (from `../../../node_modules/expo-barcode-scanner/ios`) + - EXCamera (from `../../../node_modules/expo-camera/ios`) + - EXFileSystem (from `../../../node_modules/expo-file-system/ios`) + - EXFont (from `../../../node_modules/expo-font/ios`) + - EXImageLoader (from `../../../node_modules/expo-image-loader/ios`) + - EXLocalAuthentication (from `../../../node_modules/expo-local-authentication/ios`) + - Expo (from `../../../node_modules/expo`) + - ExpoBlur (from `../../../node_modules/expo-blur/ios`) + - ExpoClipboard (from `../../../node_modules/expo-clipboard/ios`) + - ExpoHaptics (from `../../../node_modules/expo-haptics/ios`) + - ExpoKeepAwake (from `../../../node_modules/expo-keep-awake/ios`) + - ExpoLinearGradient (from `../../../node_modules/expo-linear-gradient/ios`) + - ExpoLocalization (from `../../../node_modules/expo-localization/ios`) + - ExpoModulesCore (from `../../../node_modules/expo-modules-core`) + - ExpoStoreReview (from `../../../node_modules/expo-store-review/ios`) + - ExpoWebBrowser (from `../../../node_modules/expo-web-browser/ios`) + - EXScreenCapture (from `../../../node_modules/expo-screen-capture/ios`) + - FBLazyVector (from `../../../node_modules/react-native/Libraries/FBLazyVector`) + - FBReactNativeSpec (from `../../../node_modules/react-native/React/FBReactNativeSpec`) + - FirebaseCore (= 10.15.0) + - glog (from `../../../node_modules/react-native/third-party-podspecs/glog.podspec`) + - GoogleUtilities (= 7.11.5) + - hermes-engine (from `../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`) + - libevent (~> 2.1.12) + - OneSignalXCFramework (= 3.12.6) + - Permission-FaceID (from `../../../node_modules/react-native-permissions/ios/FaceID`) + - Permission-Notifications (from `../../../node_modules/react-native-permissions/ios/Notifications`) + - RCT-Folly (from `../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`) + - RCTRequired (from `../../../node_modules/react-native/Libraries/RCTRequired`) + - RCTTypeSafety (from `../../../node_modules/react-native/Libraries/TypeSafety`) + - React (from `../../../node_modules/react-native/`) + - React-callinvoker (from `../../../node_modules/react-native/ReactCommon/callinvoker`) + - React-Codegen (from `build/generated/ios`) + - React-Core (from `../../../node_modules/react-native/`) + - React-Core/RCTWebSocket (from `../../../node_modules/react-native/`) + - React-CoreModules (from `../../../node_modules/react-native/React/CoreModules`) + - React-cxxreact (from `../../../node_modules/react-native/ReactCommon/cxxreact`) + - React-hermes (from `../../../node_modules/react-native/ReactCommon/hermes`) + - React-jsi (from `../../../node_modules/react-native/ReactCommon/jsi`) + - React-jsiexecutor (from `../../../node_modules/react-native/ReactCommon/jsiexecutor`) + - React-jsinspector (from `../../../node_modules/react-native/ReactCommon/jsinspector`) + - React-logger (from `../../../node_modules/react-native/ReactCommon/logger`) + - react-native-appsflyer (from `../../../node_modules/react-native-appsflyer`) + - "react-native-compat (from `../../../node_modules/@walletconnect/react-native-compat`)" + - react-native-context-menu-view (from `../../../node_modules/react-native-context-menu-view`) + - react-native-get-random-values (from `../../../node_modules/react-native-get-random-values`) + - react-native-image-picker (from `../../../node_modules/react-native-image-picker`) + - react-native-mmkv (from `../../../node_modules/react-native-mmkv`) + - "react-native-netinfo (from `../../../node_modules/@react-native-community/netinfo`)" + - react-native-onesignal (from `../../../node_modules/react-native-onesignal`) + - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) + - react-native-restart (from `../../../node_modules/react-native-restart`) + - react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`) + - "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)" + - react-native-splash-screen (from `../../../node_modules/react-native-splash-screen`) + - react-native-webview (from `../../../node_modules/react-native-webview`) + - react-native-widgetkit (from `../../../node_modules/react-native-widgetkit`) + - React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`) + - React-RCTActionSheet (from `../../../node_modules/react-native/Libraries/ActionSheetIOS`) + - React-RCTAnimation (from `../../../node_modules/react-native/Libraries/NativeAnimation`) + - React-RCTAppDelegate (from `../../../node_modules/react-native/Libraries/AppDelegate`) + - React-RCTBlob (from `../../../node_modules/react-native/Libraries/Blob`) + - React-RCTImage (from `../../../node_modules/react-native/Libraries/Image`) + - React-RCTLinking (from `../../../node_modules/react-native/Libraries/LinkingIOS`) + - React-RCTNetwork (from `../../../node_modules/react-native/Libraries/Network`) + - React-RCTSettings (from `../../../node_modules/react-native/Libraries/Settings`) + - React-RCTText (from `../../../node_modules/react-native/Libraries/Text`) + - React-RCTVibration (from `../../../node_modules/react-native/Libraries/Vibration`) + - React-runtimeexecutor (from `../../../node_modules/react-native/ReactCommon/runtimeexecutor`) + - ReactCommon/turbomodule/core (from `../../../node_modules/react-native/ReactCommon`) + - "ReactNativePerformance (from `../../../node_modules/@shopify/react-native-performance`)" + - rive-react-native (from `../../../node_modules/rive-react-native`) + - "RNCAsyncStorage (from `../../../node_modules/@react-native-async-storage/async-storage`)" + - "RNCMaskedView (from `../../../node_modules/@react-native-masked-view/masked-view`)" + - RNDeviceInfo (from `../../../node_modules/react-native-device-info`) + - RNFastImage (from `../../../node_modules/react-native-fast-image`) + - "RNFBApp (from `../../../node_modules/@react-native-firebase/app`)" + - "RNFBAppCheck (from `../../../node_modules/@react-native-firebase/app-check`)" + - "RNFBAuth (from `../../../node_modules/@react-native-firebase/auth`)" + - "RNFBFirestore (from `../../../node_modules/@react-native-firebase/firestore`)" + - "RNFlashList (from `../../../node_modules/@shopify/flash-list`)" + - RNGestureHandler (from `../../../node_modules/react-native-gesture-handler`) + - RNImageColors (from `../../../node_modules/react-native-image-colors`) + - RNLocalize (from `../../../node_modules/react-native-localize`) + - RNPermissions (from `../../../node_modules/react-native-permissions`) + - RNReanimated (from `../../../node_modules/react-native-reanimated`) + - RNScreens (from `../../../node_modules/react-native-screens`) + - "RNSentry (from `../../../node_modules/@sentry/react-native`)" + - RNSVG (from `../../../node_modules/react-native-svg`) + - UIImageColors (= 2.1.0) + - Yoga (from `../../../node_modules/react-native/ReactCommon/yoga`) + +SPEC REPOS: + trunk: + - abseil + - Apollo + - AppsFlyerFramework + - Argon2Swift + - BoringSSL-GRPC + - Firebase + - FirebaseAppCheck + - FirebaseAppCheckInterop + - FirebaseAuth + - FirebaseCore + - FirebaseCoreInternal + - FirebaseFirestore + - fmt + - GoogleUtilities + - "gRPC-C++" + - gRPC-Core + - GTMSessionFetcher + - leveldb-library + - libevent + - libwebp + - MMKV + - MMKVCore + - nanopb + - OneSignalXCFramework + - PromisesObjC + - RecaptchaInterop + - RiveRuntime + - SDWebImage + - SDWebImageWebPCoder + - Sentry + - SentryPrivate + - UIImageColors + - ZXingObjC + +EXTERNAL SOURCES: + amplitude-react-native: + :path: "../../../node_modules/@amplitude/analytics-react-native" + boost: + :podspec: "../../../node_modules/react-native/third-party-podspecs/boost.podspec" + DoubleConversion: + :podspec: "../../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec" + EthersRS: + :path: "../../../node_modules/@uniswap/ethers-rs-mobile" + EXApplication: + :path: "../../../node_modules/expo-application/ios" + EXAV: + :path: "../../../node_modules/expo-av/ios" + EXBarCodeScanner: + :path: "../../../node_modules/expo-barcode-scanner/ios" + EXCamera: + :path: "../../../node_modules/expo-camera/ios" + EXFileSystem: + :path: "../../../node_modules/expo-file-system/ios" + EXFont: + :path: "../../../node_modules/expo-font/ios" + EXImageLoader: + :path: "../../../node_modules/expo-image-loader/ios" + EXLocalAuthentication: + :path: "../../../node_modules/expo-local-authentication/ios" + Expo: + :path: "../../../node_modules/expo" + ExpoBlur: + :path: "../../../node_modules/expo-blur/ios" + ExpoClipboard: + :path: "../../../node_modules/expo-clipboard/ios" + ExpoHaptics: + :path: "../../../node_modules/expo-haptics/ios" + ExpoKeepAwake: + :path: "../../../node_modules/expo-keep-awake/ios" + ExpoLinearGradient: + :path: "../../../node_modules/expo-linear-gradient/ios" + ExpoLocalization: + :path: "../../../node_modules/expo-localization/ios" + ExpoModulesCore: + :path: "../../../node_modules/expo-modules-core" + ExpoStoreReview: + :path: "../../../node_modules/expo-store-review/ios" + ExpoWebBrowser: + :path: "../../../node_modules/expo-web-browser/ios" + EXScreenCapture: + :path: "../../../node_modules/expo-screen-capture/ios" + FBLazyVector: + :path: "../../../node_modules/react-native/Libraries/FBLazyVector" + FBReactNativeSpec: + :path: "../../../node_modules/react-native/React/FBReactNativeSpec" + glog: + :podspec: "../../../node_modules/react-native/third-party-podspecs/glog.podspec" + hermes-engine: + :podspec: "../../../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec" + Permission-FaceID: + :path: "../../../node_modules/react-native-permissions/ios/FaceID" + Permission-Notifications: + :path: "../../../node_modules/react-native-permissions/ios/Notifications" + RCT-Folly: + :podspec: "../../../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec" + RCTRequired: + :path: "../../../node_modules/react-native/Libraries/RCTRequired" + RCTTypeSafety: + :path: "../../../node_modules/react-native/Libraries/TypeSafety" + React: + :path: "../../../node_modules/react-native/" + React-callinvoker: + :path: "../../../node_modules/react-native/ReactCommon/callinvoker" + React-Codegen: + :path: build/generated/ios + React-Core: + :path: "../../../node_modules/react-native/" + React-CoreModules: + :path: "../../../node_modules/react-native/React/CoreModules" + React-cxxreact: + :path: "../../../node_modules/react-native/ReactCommon/cxxreact" + React-hermes: + :path: "../../../node_modules/react-native/ReactCommon/hermes" + React-jsi: + :path: "../../../node_modules/react-native/ReactCommon/jsi" + React-jsiexecutor: + :path: "../../../node_modules/react-native/ReactCommon/jsiexecutor" + React-jsinspector: + :path: "../../../node_modules/react-native/ReactCommon/jsinspector" + React-logger: + :path: "../../../node_modules/react-native/ReactCommon/logger" + react-native-appsflyer: + :path: "../../../node_modules/react-native-appsflyer" + react-native-compat: + :path: "../../../node_modules/@walletconnect/react-native-compat" + react-native-context-menu-view: + :path: "../../../node_modules/react-native-context-menu-view" + react-native-get-random-values: + :path: "../../../node_modules/react-native-get-random-values" + react-native-image-picker: + :path: "../../../node_modules/react-native-image-picker" + react-native-mmkv: + :path: "../../../node_modules/react-native-mmkv" + react-native-netinfo: + :path: "../../../node_modules/@react-native-community/netinfo" + react-native-onesignal: + :path: "../../../node_modules/react-native-onesignal" + react-native-pager-view: + :path: "../../../node_modules/react-native-pager-view" + react-native-restart: + :path: "../../../node_modules/react-native-restart" + react-native-safe-area-context: + :path: "../../../node_modules/react-native-safe-area-context" + react-native-skia: + :path: "../../../node_modules/@shopify/react-native-skia" + react-native-splash-screen: + :path: "../../../node_modules/react-native-splash-screen" + react-native-webview: + :path: "../../../node_modules/react-native-webview" + react-native-widgetkit: + :path: "../../../node_modules/react-native-widgetkit" + React-perflogger: + :path: "../../../node_modules/react-native/ReactCommon/reactperflogger" + React-RCTActionSheet: + :path: "../../../node_modules/react-native/Libraries/ActionSheetIOS" + React-RCTAnimation: + :path: "../../../node_modules/react-native/Libraries/NativeAnimation" + React-RCTAppDelegate: + :path: "../../../node_modules/react-native/Libraries/AppDelegate" + React-RCTBlob: + :path: "../../../node_modules/react-native/Libraries/Blob" + React-RCTImage: + :path: "../../../node_modules/react-native/Libraries/Image" + React-RCTLinking: + :path: "../../../node_modules/react-native/Libraries/LinkingIOS" + React-RCTNetwork: + :path: "../../../node_modules/react-native/Libraries/Network" + React-RCTSettings: + :path: "../../../node_modules/react-native/Libraries/Settings" + React-RCTText: + :path: "../../../node_modules/react-native/Libraries/Text" + React-RCTVibration: + :path: "../../../node_modules/react-native/Libraries/Vibration" + React-runtimeexecutor: + :path: "../../../node_modules/react-native/ReactCommon/runtimeexecutor" + ReactCommon: + :path: "../../../node_modules/react-native/ReactCommon" + ReactNativePerformance: + :path: "../../../node_modules/@shopify/react-native-performance" + rive-react-native: + :path: "../../../node_modules/rive-react-native" + RNCAsyncStorage: + :path: "../../../node_modules/@react-native-async-storage/async-storage" + RNCMaskedView: + :path: "../../../node_modules/@react-native-masked-view/masked-view" + RNDeviceInfo: + :path: "../../../node_modules/react-native-device-info" + RNFastImage: + :path: "../../../node_modules/react-native-fast-image" + RNFBApp: + :path: "../../../node_modules/@react-native-firebase/app" + RNFBAppCheck: + :path: "../../../node_modules/@react-native-firebase/app-check" + RNFBAuth: + :path: "../../../node_modules/@react-native-firebase/auth" + RNFBFirestore: + :path: "../../../node_modules/@react-native-firebase/firestore" + RNFlashList: + :path: "../../../node_modules/@shopify/flash-list" + RNGestureHandler: + :path: "../../../node_modules/react-native-gesture-handler" + RNImageColors: + :path: "../../../node_modules/react-native-image-colors" + RNLocalize: + :path: "../../../node_modules/react-native-localize" + RNPermissions: + :path: "../../../node_modules/react-native-permissions" + RNReanimated: + :path: "../../../node_modules/react-native-reanimated" + RNScreens: + :path: "../../../node_modules/react-native-screens" + RNSentry: + :path: "../../../node_modules/@sentry/react-native" + RNSVG: + :path: "../../../node_modules/react-native-svg" + Yoga: + :path: "../../../node_modules/react-native/ReactCommon/yoga" + +SPEC CHECKSUMS: + abseil: 926fb7a82dc6d2b8e1f2ed7f3a718bce691d1e46 + amplitude-react-native: 57f70a336ec25f5446daa65d50972c28ced47637 + Apollo: fe380f40e55e501a2499dd5885fab0cdf082b2bb + AppsFlyerFramework: 88a6eed37ad52bcee4ad74232efa8e22809d06c9 + Argon2Swift: 99482c1b8122a03524b61e41c4903a9548e7c33b + boost: 0a937fbcfdd646fca221c4f1d9750d7ccfdfc2dc + BoringSSL-GRPC: 3175b25143e648463a56daeaaa499c6cb86dad33 + DoubleConversion: 5189b271737e1565bdce30deb4a08d647e3f5f54 + EthersRS: 56b70e73d22d4e894b7e762eef1129159bcd3135 + EXApplication: d8f53a7eee90a870a75656280e8d4b85726ea903 + EXAV: f393dfc0b28214d62855a31e06eb21d426d6e2da + EXBarCodeScanner: 296dd50f6c03928d1d71d37ea17473b304cfdb00 + EXCamera: 6e6e79bf01a2b8190268d93297d8e79a843d5ede + EXFileSystem: 0b4a2c08c2dc92146849772145d61c1773144283 + EXFont: 6ea3800df746be7233208d80fe379b8ed74f4272 + EXImageLoader: 03063370bc06ea1825713d3f55fe0455f7c88d04 + EXLocalAuthentication: 78cc5a00b13745b41d16d189ab332b61a01299f9 + Expo: 8448e3a2aa1b295f029c81551e1ab6d986517fdb + ExpoBlur: 9e6da7c2bd4a0c5de7e57124694d0a380d7962f7 + ExpoClipboard: 9b87df0ad145e3aa841614fab6aea54b7a604850 + ExpoHaptics: 5a56d30a87ea213dd00b09566dc4b441a4dff97f + ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a + ExpoLinearGradient: 5966dd5d49872cc9c104fedc8bbc298b6049b2e8 + ExpoLocalization: f26cd431ad9ea3533c5b08c4fabd879176a794bb + ExpoModulesCore: bee019333b216be6d5bf997ac9ba7f1fb0767b82 + ExpoStoreReview: d057dcca4b9c95f3c9db11bd2e168dab9cba59f3 + ExpoWebBrowser: 073e50f16669d498fb49063b9b7fe780b24f7fda + EXScreenCapture: cbee2204f313038a1819d31ad99a31e15f8e0f59 + FBLazyVector: 24e08bf294faea0abc0278abb2fcad7f3e446f6f + FBReactNativeSpec: cc06081bbc8420e1c0580008ff6d7af324f32f31 + Firebase: 66043bd4579e5b73811f96829c694c7af8d67435 + FirebaseAppCheck: 66eea1c882cddd1bce9d92a0a7efd596f7204782 + FirebaseAppCheckInterop: a8c555b1c2db1d9445e6c3a08a848167ddb7eb51 + FirebaseAuth: a55ec5f7f8a5b1c2dd750235c1bb419bfb642445 + FirebaseCore: 2cec518b43635f96afe7ac3a9c513e47558abd2e + FirebaseCoreInternal: 2f4bee5ed00301b5e56da0849268797a2dd31fb4 + FirebaseFirestore: b4c0eaaf24efda5732ec21d9e6c788d083118ca6 + fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 + glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b + GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084 + "gRPC-C++": 0968bace703459fd3e5dcb0b2bed4c573dbff046 + gRPC-Core: 17108291d84332196d3c8466b48f016fc17d816d + GTMSessionFetcher: e8647203b65cee28c5f73d0f473d096653945e72 + hermes-engine: 8b9dc37355d2e12879267382f4256afd356349a1 + leveldb-library: f03246171cce0484482ec291f88b6d563699ee06 + libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef + MMKV: 7f34558bbb5a33b0eaefae2de4b6a20a2ffdad6f + MMKVCore: ddf41b9d9262f058419f9ba7598719af56c02cd3 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + OneSignalXCFramework: ff1c970b7aeb4ac0fe48fb35393eb5d8bf378135 + Permission-FaceID: aaf43b353c25aaa2c1a501f93fa33dcb0f76e6c8 + Permission-Notifications: 7f2a467ab97a130a847519705340830d52cfc5f0 + PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 + RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 + RCTRequired: c20235648eeb64a874f55459ceae6b081956318d + RCTTypeSafety: ca004f1fe0b76f7936f7fe7dfd761a4386cf72f5 + React: b27df2b1da30335cf1bf1909056c4e1c3a3603ae + React-callinvoker: f2a69510d781d8226d51342a3cbe8a9b13573ea5 + React-Codegen: 90eb4e8352b823234ee25adbe64233b2fc642aee + React-Core: 0771d135beb41b14e0e2ee9238fda50df6f18b97 + React-CoreModules: 0e081b26ab034992d6a60217fc35a83e8ad9b8ed + React-cxxreact: 3ec43be907f4d818c5113e436d661836d1ab5aa9 + React-hermes: 870871faa5b35c8163361e22241360de26afe07d + React-jsi: c06ec745faeea7bb8845a9b906aefa7c049c86cb + React-jsiexecutor: a2867f1f81301b1f56ad968632b6ebc45c64a530 + React-jsinspector: 7e58fe86c7cc442fd11da0c9d8bef12a8d63f771 + React-logger: a3f6ca0d018749852a2a6f07c154bfc6fcd4195a + react-native-appsflyer: 153e96b97ecc0c26b14fc79911675e51cfd35b36 + react-native-compat: e31d4e4ba7db54f2649c4b34b9b594029b51b5e2 + react-native-context-menu-view: d3b3e77985d5b05674a70f8e7eafe404dfa5bbcc + react-native-get-random-values: a6ea6a8a65dc93e96e24a11105b1a9c8cfe1d72a + react-native-image-picker: 1569cfade34b3a011191ce262423e6ce2f8db5a1 + react-native-mmkv: dea675cf9697ad35940f1687e98e133e1358ef9f + react-native-netinfo: 129bd99f607a2dc5bb096168f3e5c150fd1f1c95 + react-native-onesignal: ab800900cffeca4d9db70a05244013fc8a36ceb8 + react-native-pager-view: 3051346698a0ba0c4e13e40097cc11b00ee03cca + react-native-restart: 7595693413fe3ca15893702f2c8306c62a708162 + react-native-safe-area-context: 39c2d8be3328df5d437ac1700f4f3a4f75716acc + react-native-skia: e7385e2f5ebe284df53f0def573198fe69a7bd72 + react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 + react-native-webview: d33e2db8925d090871ffeb232dfa50cb3a727581 + react-native-widgetkit: efb6680df237463bbe1be3a4d1a1578a1b0bb08f + React-perflogger: 431a655960a02f01257d631b2a9bfbb02fd21064 + React-RCTActionSheet: 38c8d496d0faa63013d16f709e10a3acf6b5f100 + React-RCTAnimation: 6da4d599f3262ed8021433ddd96de45ac9e731b1 + React-RCTAppDelegate: 66498edcd8ba93f0bd727304be671f9f3ddf0a23 + React-RCTBlob: d8f7bf9f32fbde84565a81f4bdf34398f46d45dd + React-RCTImage: 4e31e6ebf2b9705831d1855425a043b40eec1f61 + React-RCTLinking: 22ac16d44e2df03e9ca9125273fc58a7c507f529 + React-RCTNetwork: 4bacd206834633c23475485dbc21c18563627af4 + React-RCTSettings: 4e4ace986ae92a7e1696fdac11615576b698f337 + React-RCTText: 37a1341bdf1f80e9909f6b69a7a9ee747cb682d3 + React-RCTVibration: 2271362cdf9ff2dae6a2156f5101e5c30b02694d + React-runtimeexecutor: 35cec6420c9d4144b0d06f9fdb093cf8f02bd52c + ReactCommon: fc9d1da17fa902910dcba550a54c16e7e1c70d2c + ReactNativePerformance: ab7dee4c4862623d72c1530a9fc71b55458edf71 + RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 + rive-react-native: 08eb103dff9217fc921e1bcd32527603d87fa933 + RiveRuntime: 63fe0407ab96ab1796c492d1e584a3b766e80693 + RNCAsyncStorage: 0c357f3156fcb16c8589ede67cc036330b6698ca + RNCMaskedView: 949696f25ec596bfc697fc88e6f95cf0c79669b6 + RNDeviceInfo: 0a7c1d2532aa7691f9b9925a27e43af006db4dae + RNFastImage: 246de6b52d7642992cfd01e2005dda36d00a6660 + RNFBApp: a3026bdd951dd7a3a88e8e6518b53ddd2f8b3809 + RNFBAppCheck: c5363a0be62f961edfcdf82ed353c69bc37a39f4 + RNFBAuth: 553c6e66d3c70e086799104dad4a554c2663c337 + RNFBFirestore: a60e6005e071b31360a5bf651eb403b36c7db7de + RNFlashList: 4b4b6b093afc0df60ae08f9cbf6ccd4c836c667a + RNGestureHandler: 7909c50383a18f0cb10ce1db7262b9a6da504c03 + RNImageColors: 9ac05083b52d5c350e6972650ae3ba0e556466c1 + RNLocalize: d4b8af4e442d4bcca54e68fc687a2129b4d71a81 + RNPermissions: de7b7c3fe1680d974ac7a85e3e97aa539c0e68ea + RNReanimated: d6b4b867b6d1ee0798f5fb372708fa4bb8d66029 + RNScreens: b21dc57dfa2b710c30ec600786a3fc223b1b92e7 + RNSentry: 4fb2cd7d2d6cb94423c24884488206ef881da136 + RNSVG: c1e76b81c76cdcd34b4e1188852892dc280eb902 + SDWebImage: fd7e1a22f00303e058058278639bf6196ee431fe + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d + Sentry: 11776f6a25a128808d793d0d41bb7ad873b5ae4f + SentryPrivate: b3c448eacdabe9eab7679a2e0af609c608f91572 + UIImageColors: d2ef3b0877d203cbb06489eeb78ea8b7788caabe + Yoga: 135109c9b8c5d1a8af3a58d21cd4c7aa7f3bf555 + ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb + +PODFILE CHECKSUM: 22ab4b87e0bceeaa9e245c485c36fca595139568 + +COCOAPODS: 1.14.3 diff --git a/apps/mobile/ios/RiveAssets/onboarding_dark.riv b/apps/mobile/ios/RiveAssets/onboarding_dark.riv new file mode 100644 index 0000000..64d5092 Binary files /dev/null and b/apps/mobile/ios/RiveAssets/onboarding_dark.riv differ diff --git a/apps/mobile/ios/RiveAssets/onboarding_light.riv b/apps/mobile/ios/RiveAssets/onboarding_light.riv new file mode 100644 index 0000000..5a6da1d Binary files /dev/null and b/apps/mobile/ios/RiveAssets/onboarding_light.riv differ diff --git a/apps/mobile/ios/RiveAssets/pending_send.riv b/apps/mobile/ios/RiveAssets/pending_send.riv new file mode 100644 index 0000000..f4e4bd4 Binary files /dev/null and b/apps/mobile/ios/RiveAssets/pending_send.riv differ diff --git a/apps/mobile/ios/RiveAssets/pending_swap.riv b/apps/mobile/ios/RiveAssets/pending_swap.riv new file mode 100644 index 0000000..fa2f5ff Binary files /dev/null and b/apps/mobile/ios/RiveAssets/pending_swap.riv differ diff --git a/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj new file mode 100644 index 0000000..36a96ef --- /dev/null +++ b/apps/mobile/ios/Uniswap.xcodeproj/project.pbxproj @@ -0,0 +1,3493 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 00E356F31AD99517003FC87E /* UniswapTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* UniswapTests.m */; }; + 0703EE032A5734A600AED1DA /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0703EE022A5734A600AED1DA /* UserDefaults.swift */; }; + 0703EE052A57351800AED1DA /* Logging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C372A44BECC00DA720A /* Logging.swift */; }; + 072E238E2A44D5BD006AD6C9 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; + 072E23952A44D5BD006AD6C9 /* WidgetsCoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072E23942A44D5BD006AD6C9 /* WidgetsCoreTests.swift */; }; + 072E23962A44D5BD006AD6C9 /* WidgetsCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 072E23882A44D5BD006AD6C9 /* WidgetsCore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 072F6C212A44A32E00DA720A /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072F6C202A44A32E00DA720A /* WidgetKit.framework */; }; + 072F6C262A44A32E00DA720A /* WidgetsBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C252A44A32E00DA720A /* WidgetsBundle.swift */; }; + 072F6C282A44A32E00DA720A /* TokenPriceWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C272A44A32E00DA720A /* TokenPriceWidget.swift */; }; + 072F6C2B2A44A32F00DA720A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 072F6C2A2A44A32F00DA720A /* Assets.xcassets */; }; + 072F6C2D2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */; }; + 072F6C2E2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */; }; + 072F6C312A44A32F00DA720A /* Widgets.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 072F6C1F2A44A32E00DA720A /* Widgets.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 07378F492A5C83ED00D26D3E /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078E79492A55EB3300F59CF2 /* IntentHandler.swift */; }; + 074086FA2A703B76006E3053 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074086F92A703B76006E3053 /* FormatTests.swift */; }; + 0741433E2A588CCC00A157D3 /* TokenPriceWidget.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */; }; + 074143402A588F5800A157D3 /* Structs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0741433F2A588F5800A157D3 /* Structs.swift */; }; + 0743218C2A82C6C000F8518D /* Basel-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0743218B2A82C6C000F8518D /* Basel-Book.otf */; }; + 0743218D2A82C6C000F8518D /* Basel-Book.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0743218B2A82C6C000F8518D /* Basel-Book.otf */; }; + 074321EB2A83E3CA00F8518D /* TokenDetailsScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */; }; + 074321ED2A83E3CA00F8518D /* ExploreTokensTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321922A83E3C900F8518D /* ExploreTokensTabQuery.graphql.swift */; }; + 074321EE2A83E3CA00F8518D /* NftsTabQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */; }; + 074321EF2A83E3CA00F8518D /* TokenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */; }; + 074321F02A83E3CA00F8518D /* SearchPopularTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321952A83E3C900F8518D /* SearchPopularTokensQuery.graphql.swift */; }; + 074321F12A83E3CA00F8518D /* TopTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */; }; + 074321F22A83E3CA00F8518D /* TokenPriceHistoryQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */; }; + 074321F32A83E3CA00F8518D /* FavoriteTokenCardQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */; }; + 074321F42A83E3CA00F8518D /* NFTItemScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */; }; + 074321F52A83E3CA00F8518D /* NftCollectionScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */; }; + 074321F62A83E3CA00F8518D /* SearchTokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219B2A83E3C900F8518D /* SearchTokensQuery.graphql.swift */; }; + 074321F72A83E3CA00F8518D /* ExploreSearchQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219C2A83E3C900F8518D /* ExploreSearchQuery.graphql.swift */; }; + 074321F82A83E3CA00F8518D /* TokenProjectsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */; }; + 074321F92A83E3CA00F8518D /* SelectWalletScreenQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */; }; + 074321FA2A83E3CA00F8518D /* PortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */; }; + 074321FB2A83E3CA00F8518D /* NftsQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */; }; + 074321FC2A83E3CA00F8518D /* TransactionListQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */; }; + 074321FD2A83E3CA00F8518D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */; }; + 074321FE2A83E3CA00F8518D /* TopTokenParts.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */; }; + 074321FF2A83E3CA00F8518D /* MobileSchema.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */; }; + 074322002A83E3CA00F8518D /* AssetChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */; }; + 074322012A83E3CA00F8518D /* HistoryDuration.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */; }; + 074322022A83E3CA00F8518D /* TokenStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */; }; + 074322032A83E3CA00F8518D /* Currency.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AC2A83E3C900F8518D /* Currency.graphql.swift */; }; + 074322052A83E3CA00F8518D /* Chain.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AE2A83E3C900F8518D /* Chain.graphql.swift */; }; + 074322062A83E3CA00F8518D /* TokenSortableField.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */; }; + 074322072A83E3CA00F8518D /* TransactionDirection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */; }; + 074322082A83E3CA00F8518D /* NftStandard.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */; }; + 074322092A83E3CA00F8518D /* NftActivityType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */; }; + 0743220A2A83E3CA00F8518D /* SafetyLevel.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */; }; + 0743220B2A83E3CA00F8518D /* NftMarketplace.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */; }; + 0743220C2A83E3CA00F8518D /* TransactionStatus.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */; }; + 0743220D2A83E3CA00F8518D /* Image.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B72A83E3C900F8518D /* Image.graphql.swift */; }; + 0743220E2A83E3CA00F8518D /* NftOrderEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */; }; + 0743220F2A83E3CA00F8518D /* NftApproveForAll.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */; }; + 074322102A83E3CA00F8518D /* NftAssetTrait.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */; }; + 074322112A83E3CA00F8518D /* NftBalanceConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */; }; + 074322122A83E3CA00F8518D /* NftActivityEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */; }; + 074322132A83E3CA00F8518D /* NftCollection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */; }; + 074322142A83E3CA00F8518D /* TimestampedAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */; }; + 074322152A83E3CA00F8518D /* NftAssetConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */; }; + 074322162A83E3CA00F8518D /* TokenProject.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */; }; + 074322172A83E3CA00F8518D /* NftTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */; }; + 074322182A83E3CA00F8518D /* TokenApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */; }; + 074322192A83E3CA00F8518D /* NftOrderConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */; }; + 0743221A2A83E3CA00F8518D /* TokenMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */; }; + 0743221B2A83E3CA00F8518D /* NftCollectionMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */; }; + 0743221C2A83E3CA00F8518D /* TokenBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */; }; + 0743221D2A83E3CA00F8518D /* NftOrder.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */; }; + 0743221E2A83E3CA00F8518D /* Portfolio.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */; }; + 0743221F2A83E3CA00F8518D /* PageInfo.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */; }; + 074322202A83E3CA00F8518D /* NftAssetEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */; }; + 074322212A83E3CA00F8518D /* NftCollectionConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */; }; + 074322222A83E3CA00F8518D /* NftContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */; }; + 074322232A83E3CA00F8518D /* NftActivityConnection.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */; }; + 074322242A83E3CA00F8518D /* Amount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CE2A83E3C900F8518D /* Amount.graphql.swift */; }; + 074322252A83E3CA00F8518D /* Query.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321CF2A83E3C900F8518D /* Query.graphql.swift */; }; + 074322262A83E3CA00F8518D /* NftAsset.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */; }; + 074322272A83E3CA00F8518D /* Dimensions.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */; }; + 074322282A83E3CA00F8518D /* TokenProjectMarket.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */; }; + 074322292A83E3CA00F8518D /* AmountChange.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */; }; + 0743222A2A83E3CA00F8518D /* NftBalanceEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */; }; + 0743222B2A83E3CA00F8518D /* NftActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */; }; + 0743222C2A83E3CA00F8518D /* AssetActivity.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */; }; + 0743222D2A83E3CA00F8518D /* NftProfile.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */; }; + 0743222E2A83E3CA00F8518D /* NftCollectionEdge.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */; }; + 074322302A83E3CA00F8518D /* TokenTransfer.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */; }; + 074322312A83E3CA00F8518D /* NftApproval.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */; }; + 074322322A83E3CA00F8518D /* NftBalance.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */; }; + 074322332A83E3CA00F8518D /* Token.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DD2A83E3C900F8518D /* Token.graphql.swift */; }; + 074322342A83E3CA00F8518D /* SchemaConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */; }; + 074322352A83E3CA00F8518D /* NftBalancesFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */; }; + 074322362A83E3CA00F8518D /* NftCollectionsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E12A83E3C900F8518D /* NftCollectionsFilterInput.graphql.swift */; }; + 074322372A83E3CA00F8518D /* ContractInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */; }; + 074322382A83E3CA00F8518D /* NftActivityFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */; }; + 074322392A83E3CA00F8518D /* NftAssetTraitInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */; }; + 0743223B2A83E3CA00F8518D /* NftAssetsFilterInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */; }; + 0743223C2A83E3CA00F8518D /* SchemaMetadata.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */; }; + 0743223D2A83E3CA00F8518D /* IAmount.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321E92A83E3C900F8518D /* IAmount.graphql.swift */; }; + 0743223E2A83E3CA00F8518D /* IContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 074321EA2A83E3C900F8518D /* IContract.graphql.swift */; }; + 074322402A841BBD00F8518D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0743223F2A841BBD00F8518D /* Constants.swift */; }; + 0767E02F2A61BBDC0042ADA2 /* Basel-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02B2A61BBDB0042ADA2 /* Basel-Regular.otf */; }; + 0767E0302A61BBDC0042ADA2 /* Basel-Regular.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02B2A61BBDB0042ADA2 /* Basel-Regular.otf */; }; + 0767E0312A61BBDC0042ADA2 /* Basel-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02C2A61BBDB0042ADA2 /* Basel-Semibold.otf */; }; + 0767E0322A61BBDC0042ADA2 /* Basel-Semibold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02C2A61BBDB0042ADA2 /* Basel-Semibold.otf */; }; + 0767E0332A61BBDC0042ADA2 /* Basel-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02D2A61BBDB0042ADA2 /* Basel-Medium.otf */; }; + 0767E0342A61BBDC0042ADA2 /* Basel-Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02D2A61BBDB0042ADA2 /* Basel-Medium.otf */; }; + 0767E0352A61BBDC0042ADA2 /* Basel-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02E2A61BBDC0042ADA2 /* Basel-Bold.otf */; }; + 0767E0362A61BBDC0042ADA2 /* Basel-Bold.otf in Resources */ = {isa = PBXBuildFile; fileRef = 0767E02E2A61BBDC0042ADA2 /* Basel-Bold.otf */; }; + 0767E0382A65C8330042ADA2 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E0372A65C8330042ADA2 /* Colors.swift */; }; + 0767E03B2A65D2550042ADA2 /* Styling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0767E03A2A65D2550042ADA2 /* Styling.swift */; }; + 077E60392A85587800ABC4B9 /* TokensQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */; }; + 077E603B2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */; }; + 0783F7B42A619E7C009ED617 /* UIComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0783F7B32A619E7C009ED617 /* UIComponents.swift */; }; + 078E79472A55EB3300F59CF2 /* Intents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 078E79462A55EB3300F59CF2 /* Intents.framework */; }; + 078E794E2A55EB3300F59CF2 /* WidgetIntentExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 07B0676C2A7D6EC8001DD9B9 /* RNWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07B0676A2A7D6EC8001DD9B9 /* RNWidgets.swift */; }; + 07B0676D2A7D6EC8001DD9B9 /* RNWidgets.m in Sources */ = {isa = PBXBuildFile; fileRef = 07B0676B2A7D6EC8001DD9B9 /* RNWidgets.m */; }; + 07C2AC36457A05B810F60317 /* libPods-Uniswap-UniswapTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F50809BC52FC9A48453E6976 /* libPods-Uniswap-UniswapTests.a */; }; + 07F0C28F2A5F3E2E00D5353E /* Env.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F0C28E2A5F3E2E00D5353E /* Env.swift */; }; + 07F136402A575EC00067004F /* DataQueries.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F1363F2A575EC00067004F /* DataQueries.swift */; }; + 07F136422A5763480067004F /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F136412A5763480067004F /* Network.swift */; }; + 07F5CF712A6AD97D00C648A5 /* Chart.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF702A6AD97D00C648A5 /* Chart.swift */; }; + 07F5CF752A7020FD00C648A5 /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F5CF742A7020FD00C648A5 /* Format.swift */; }; + 0DC6ADF02B1E2C100092909C /* PortfolioValueModifier.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 1440B371A1C9A42F3E91DAAE /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */; }; + 1DA5339E6A1956F5FE24DB6C /* libPods-WidgetIntentExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD21B73B081B800A44E7F682 /* libPods-WidgetIntentExtension.a */; }; + 5EFB78362B1E585000E77EAC /* ConvertQuery.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */; }; + 681301B12A3726EE00A5BF43 /* onboarding_dark.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301AD2A3726EE00A5BF43 /* onboarding_dark.riv */; }; + 681301B22A3726EE00A5BF43 /* pending_send.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301AE2A3726EE00A5BF43 /* pending_send.riv */; }; + 681301B32A3726EE00A5BF43 /* onboarding_light.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301AF2A3726EE00A5BF43 /* onboarding_light.riv */; }; + 681301B42A3726EE00A5BF43 /* pending_swap.riv in Resources */ = {isa = PBXBuildFile; fileRef = 681301B02A3726EE00A5BF43 /* pending_swap.riv */; }; + 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */; }; + 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */; }; + 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */; }; + 6C8EFC2D2891B99100FBD8EB /* EncryptionHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C8EFC2C2891B99100FBD8EB /* EncryptionHelperTests.swift */; }; + 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BD92A95223C00C4063E /* RNEthersRS.swift */; }; + 6CA91BDC2A95223C00C4063E /* RnEthersRS.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDA2A95223C00C4063E /* RnEthersRS.m */; }; + 6CA91BE12A95226200C4063E /* RNCloudStorageBackupsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */; }; + 6CA91BE22A95226200C4063E /* EncryptionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */; }; + 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */; }; + 77CF6065C8A24FE48204A2C1 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */; }; + 8385A47D3C765B841F450090 /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */; }; + 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */; }; + 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */; }; + 8E89C3B02AB8AAA400C84DE5 /* MnemonicWordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A82AB8AAA400C84DE5 /* MnemonicWordView.swift */; }; + 8E89C3B12AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */; }; + 8E89C3B22AB8AAA400C84DE5 /* MnemonicTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3AA2AB8AAA400C84DE5 /* MnemonicTextField.swift */; }; + 8E89C3B32AB8AAA400C84DE5 /* MnemonicDisplayManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3AB2AB8AAA400C84DE5 /* MnemonicDisplayManager.m */; }; + 8E89C3B42AB8AAA400C84DE5 /* MnemonicConfirmationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8E89C3AC2AB8AAA400C84DE5 /* MnemonicConfirmationManager.m */; }; + 8EA8AB3B2AB7ED3C004E7EF3 /* SeedPhraseInputManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB302AB7ED3C004E7EF3 /* SeedPhraseInputManager.m */; }; + 8EA8AB3C2AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB312AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift */; }; + 8EA8AB3D2AB7ED3C004E7EF3 /* SeedPhraseInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB322AB7ED3C004E7EF3 /* SeedPhraseInputView.swift */; }; + 8EA8AB3E2AB7ED3C004E7EF3 /* SeedPhraseInputManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB332AB7ED3C004E7EF3 /* SeedPhraseInputManager.swift */; }; + 8EA8AB412AB7ED76004E7EF3 /* AlertTriangleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EA8AB402AB7ED76004E7EF3 /* AlertTriangleIcon.swift */; }; + 8EBFB1552ABA6AA6006B32A8 /* PasteIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */; }; + 8ED0562C2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */; }; + 8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */; }; + 9C9285C0639E520B3453B9C0 /* libPods-OneSignalNotificationServiceExtension.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A82B0304EEE753C6DEB3943E /* libPods-OneSignalNotificationServiceExtension.a */; }; + 9F00A43A2B33894C0088A0D0 /* ApplicationContract.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */; }; + 9F29D4ED2B47126D004D003A /* NftBalanceAssetInput.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */; }; + 9F3500842A8AAAF50077BFC5 /* libreact-native-splash-screen.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F3500832A8AAAF50077BFC5 /* libreact-native-splash-screen.a */; }; + 9F78980B2A819CC4004D5A98 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072F6C222A44A32E00DA720A /* SwiftUI.framework */; }; + 9F78980E2A819D2B004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; + 9F7898112A819D32004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; + 9F7898142A819D62004D5A98 /* WidgetsCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; }; + 9F7898152A819D62004D5A98 /* WidgetsCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 9F813E9E2AA8FB5700438D89 /* ActivityDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */; }; + 9F813EA02AA8FB7500438D89 /* TransactionDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */; }; + 9F813EA22AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */; }; + 9F813EA42AA8FBCF00438D89 /* TransactionType.graphql.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */; }; + 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */; }; + 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */; }; + 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */; }; + 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */; }; + A32F9FBD272343C9002CFCDB /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */; }; + A3F0A5B1272B1DFA00895B25 /* KeychainSwiftDistrib.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */; }; + AEE498F72A85AD86000DDF8E /* Basel-Book.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AEE498F52A85AD86000DDF8E /* Basel-Book.ttf */; }; + AEE498F82A85AD86000DDF8E /* Basel-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = AEE498F62A85AD86000DDF8E /* Basel-Medium.ttf */; }; + B8649A8D70B018E583494128 /* libPods-WidgetsCoreTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F9B12416ED2E83444B0936B2 /* libPods-WidgetsCoreTests.a */; }; + C9E623221156415BD176C348 /* libPods-Widgets.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 0750E42001CAEA9359159B7F /* libPods-Widgets.a */; }; + D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */; }; + F2867EBF8C9DE41CF466D17C /* libPods-Uniswap.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 76700FCF2F2249D9C52E6A4C /* libPods-Uniswap.a */; }; + F35AFD3E27EE49990011A725 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F35AFD3D27EE49990011A725 /* NotificationService.swift */; }; + F35AFD4227EE49990011A725 /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + FD54D51D296C79A4007A37E9 /* GoogleServiceInfo in Resources */ = {isa = PBXBuildFile; fileRef = FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */; }; + FD7304CE28A364FC0085BDEA /* Colors.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FD7304CD28A364FC0085BDEA /* Colors.xcassets */; }; + FD7304D028A3650A0085BDEA /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7304CF28A3650A0085BDEA /* Colors.swift */; }; + FFD8F7BC6B871CC6D7CBE0C7 /* libPods-WidgetsCore.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 26CB04CC7EAFA26FBB06896D /* libPods-WidgetsCore.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = Uniswap; + }; + 0703EE082A57355400AED1DA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 072E23852A44D5BC006AD6C9; + remoteInfo = UniswapWidgetsCore; + }; + 072E238F2A44D5BD006AD6C9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 072E23852A44D5BC006AD6C9; + remoteInfo = UniswapWidgetsCore; + }; + 072E23912A44D5BD006AD6C9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = Uniswap; + }; + 072F6C2F2A44A32F00DA720A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 072F6C1E2A44A32E00DA720A; + remoteInfo = UniswapWidgetExtension; + }; + 078E794C2A55EB3300F59CF2 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 078E79442A55EB3300F59CF2; + remoteInfo = UniswapWidgetsIntentExtension; + }; + 9F7898052A819AA2004D5A98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 072E23852A44D5BC006AD6C9; + remoteInfo = WidgetsCore; + }; + 9F7898162A819D62004D5A98 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 072E23852A44D5BC006AD6C9; + remoteInfo = WidgetsCore; + }; + F35AFD4027EE49990011A725 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = F35AFD3A27EE49990011A725; + remoteInfo = OneSignalNotificationServiceExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9F7898182A819D62004D5A98 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 9F7898152A819D62004D5A98 /* WidgetsCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + F35AFD4627EE49990011A725 /* Embed App Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 072F6C312A44A32F00DA720A /* Widgets.appex in Embed App Extensions */, + 078E794E2A55EB3300F59CF2 /* WidgetIntentExtension.appex in Embed App Extensions */, + F35AFD4227EE49990011A725 /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */, + ); + name = "Embed App Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 00E356EE1AD99517003FC87E /* UniswapTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UniswapTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* UniswapTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UniswapTests.m; sourceTree = ""; }; + 0703EE022A5734A600AED1DA /* UserDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaults.swift; sourceTree = ""; }; + 070480372A58A507009006CE /* WidgetIntentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetIntentExtension.entitlements; sourceTree = ""; }; + 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = WidgetsCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 072E23882A44D5BD006AD6C9 /* WidgetsCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetsCore.h; sourceTree = ""; }; + 072E238D2A44D5BD006AD6C9 /* WidgetsCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WidgetsCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 072E23942A44D5BD006AD6C9 /* WidgetsCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsCoreTests.swift; sourceTree = ""; }; + 072E23A62A44D61A006AD6C9 /* Widgets.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Widgets.entitlements; sourceTree = ""; }; + 072F6C1F2A44A32E00DA720A /* Widgets.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = Widgets.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 072F6C202A44A32E00DA720A /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 072F6C222A44A32E00DA720A /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 072F6C252A44A32E00DA720A /* WidgetsBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsBundle.swift; sourceTree = ""; }; + 072F6C272A44A32E00DA720A /* TokenPriceWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPriceWidget.swift; sourceTree = ""; }; + 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; path = TokenPriceWidget.intentdefinition; sourceTree = ""; }; + 072F6C2A2A44A32F00DA720A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 072F6C2C2A44A32F00DA720A /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 072F6C372A44BECC00DA720A /* Logging.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = ""; }; + 074086F92A703B76006E3053 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = ""; }; + 0741433F2A588F5800A157D3 /* Structs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Structs.swift; sourceTree = ""; }; + 0743218B2A82C6C000F8518D /* Basel-Book.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Basel-Book.otf"; sourceTree = ""; }; + 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenDetailsScreenQuery.graphql.swift; sourceTree = ""; }; + 074321922A83E3C900F8518D /* ExploreTokensTabQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreTokensTabQuery.graphql.swift; sourceTree = ""; }; + 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftsTabQuery.graphql.swift; sourceTree = ""; }; + 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenQuery.graphql.swift; sourceTree = ""; }; + 074321952A83E3C900F8518D /* SearchPopularTokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPopularTokensQuery.graphql.swift; sourceTree = ""; }; + 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokensQuery.graphql.swift; sourceTree = ""; }; + 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenPriceHistoryQuery.graphql.swift; sourceTree = ""; }; + 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FavoriteTokenCardQuery.graphql.swift; sourceTree = ""; }; + 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NFTItemScreenQuery.graphql.swift; sourceTree = ""; }; + 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionScreenQuery.graphql.swift; sourceTree = ""; }; + 0743219B2A83E3C900F8518D /* SearchTokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchTokensQuery.graphql.swift; sourceTree = ""; }; + 0743219C2A83E3C900F8518D /* ExploreSearchQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExploreSearchQuery.graphql.swift; sourceTree = ""; }; + 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectsQuery.graphql.swift; sourceTree = ""; }; + 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectWalletScreenQuery.graphql.swift; sourceTree = ""; }; + 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftsQuery.graphql.swift; sourceTree = ""; }; + 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionListQuery.graphql.swift; sourceTree = ""; }; + 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionHistoryUpdaterQuery.graphql.swift; sourceTree = ""; }; + 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopTokenParts.graphql.swift; sourceTree = ""; }; + 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MobileSchema.graphql.swift; sourceTree = ""; }; + 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetChange.graphql.swift; sourceTree = ""; }; + 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HistoryDuration.graphql.swift; sourceTree = ""; }; + 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenStandard.graphql.swift; sourceTree = ""; }; + 074321AC2A83E3C900F8518D /* Currency.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Currency.graphql.swift; sourceTree = ""; }; + 074321AE2A83E3C900F8518D /* Chain.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Chain.graphql.swift; sourceTree = ""; }; + 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenSortableField.graphql.swift; sourceTree = ""; }; + 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDirection.graphql.swift; sourceTree = ""; }; + 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftStandard.graphql.swift; sourceTree = ""; }; + 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityType.graphql.swift; sourceTree = ""; }; + 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafetyLevel.graphql.swift; sourceTree = ""; }; + 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftMarketplace.graphql.swift; sourceTree = ""; }; + 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionStatus.graphql.swift; sourceTree = ""; }; + 074321B72A83E3C900F8518D /* Image.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Image.graphql.swift; sourceTree = ""; }; + 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrderEdge.graphql.swift; sourceTree = ""; }; + 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftApproveForAll.graphql.swift; sourceTree = ""; }; + 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetTrait.graphql.swift; sourceTree = ""; }; + 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceConnection.graphql.swift; sourceTree = ""; }; + 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityEdge.graphql.swift; sourceTree = ""; }; + 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollection.graphql.swift; sourceTree = ""; }; + 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimestampedAmount.graphql.swift; sourceTree = ""; }; + 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetConnection.graphql.swift; sourceTree = ""; }; + 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProject.graphql.swift; sourceTree = ""; }; + 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftTransfer.graphql.swift; sourceTree = ""; }; + 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenApproval.graphql.swift; sourceTree = ""; }; + 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrderConnection.graphql.swift; sourceTree = ""; }; + 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenMarket.graphql.swift; sourceTree = ""; }; + 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionMarket.graphql.swift; sourceTree = ""; }; + 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenBalance.graphql.swift; sourceTree = ""; }; + 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftOrder.graphql.swift; sourceTree = ""; }; + 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Portfolio.graphql.swift; sourceTree = ""; }; + 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PageInfo.graphql.swift; sourceTree = ""; }; + 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetEdge.graphql.swift; sourceTree = ""; }; + 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionConnection.graphql.swift; sourceTree = ""; }; + 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftContract.graphql.swift; sourceTree = ""; }; + 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityConnection.graphql.swift; sourceTree = ""; }; + 074321CE2A83E3C900F8518D /* Amount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Amount.graphql.swift; sourceTree = ""; }; + 074321CF2A83E3C900F8518D /* Query.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Query.graphql.swift; sourceTree = ""; }; + 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAsset.graphql.swift; sourceTree = ""; }; + 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dimensions.graphql.swift; sourceTree = ""; }; + 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenProjectMarket.graphql.swift; sourceTree = ""; }; + 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountChange.graphql.swift; sourceTree = ""; }; + 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceEdge.graphql.swift; sourceTree = ""; }; + 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivity.graphql.swift; sourceTree = ""; }; + 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetActivity.graphql.swift; sourceTree = ""; }; + 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftProfile.graphql.swift; sourceTree = ""; }; + 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionEdge.graphql.swift; sourceTree = ""; }; + 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenTransfer.graphql.swift; sourceTree = ""; }; + 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftApproval.graphql.swift; sourceTree = ""; }; + 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalance.graphql.swift; sourceTree = ""; }; + 074321DD2A83E3C900F8518D /* Token.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Token.graphql.swift; sourceTree = ""; }; + 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaConfiguration.swift; sourceTree = ""; }; + 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalancesFilterInput.graphql.swift; sourceTree = ""; }; + 074321E12A83E3C900F8518D /* NftCollectionsFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftCollectionsFilterInput.graphql.swift; sourceTree = ""; }; + 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContractInput.graphql.swift; sourceTree = ""; }; + 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftActivityFilterInput.graphql.swift; sourceTree = ""; }; + 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetTraitInput.graphql.swift; sourceTree = ""; }; + 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftAssetsFilterInput.graphql.swift; sourceTree = ""; }; + 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaMetadata.graphql.swift; sourceTree = ""; }; + 074321E92A83E3C900F8518D /* IAmount.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IAmount.graphql.swift; sourceTree = ""; }; + 074321EA2A83E3C900F8518D /* IContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IContract.graphql.swift; sourceTree = ""; }; + 0743223F2A841BBD00F8518D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 0750E42001CAEA9359159B7F /* libPods-Widgets.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Widgets.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 0767E02B2A61BBDB0042ADA2 /* Basel-Regular.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Basel-Regular.otf"; sourceTree = ""; }; + 0767E02C2A61BBDB0042ADA2 /* Basel-Semibold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Basel-Semibold.otf"; sourceTree = ""; }; + 0767E02D2A61BBDB0042ADA2 /* Basel-Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Basel-Medium.otf"; sourceTree = ""; }; + 0767E02E2A61BBDC0042ADA2 /* Basel-Bold.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Basel-Bold.otf"; sourceTree = ""; }; + 0767E0372A65C8330042ADA2 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = ""; }; + 0767E03A2A65D2550042ADA2 /* Styling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Styling.swift; sourceTree = ""; }; + 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensQuery.graphql.swift; sourceTree = ""; }; + 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultiplePortfolioBalancesQuery.graphql.swift; sourceTree = ""; }; + 0783F7B32A619E7C009ED617 /* UIComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIComponents.swift; sourceTree = ""; }; + 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = WidgetIntentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 078E79462A55EB3300F59CF2 /* Intents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Intents.framework; path = System/Library/Frameworks/Intents.framework; sourceTree = SDKROOT; }; + 078E79492A55EB3300F59CF2 /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 078E794B2A55EB3300F59CF2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 07B0676A2A7D6EC8001DD9B9 /* RNWidgets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNWidgets.swift; sourceTree = ""; }; + 07B0676B2A7D6EC8001DD9B9 /* RNWidgets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNWidgets.m; sourceTree = ""; }; + 07F0C28E2A5F3E2E00D5353E /* Env.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Env.swift; sourceTree = ""; }; + 07F1363F2A575EC00067004F /* DataQueries.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataQueries.swift; sourceTree = ""; }; + 07F136412A5763480067004F /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; + 07F5CF702A6AD97D00C648A5 /* Chart.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chart.swift; sourceTree = ""; }; + 07F5CF742A7020FD00C648A5 /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = ""; }; + 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PortfolioValueModifier.graphql.swift; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* Uniswap.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Uniswap.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = Uniswap/AppDelegate.h; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Uniswap/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Uniswap/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Uniswap/main.m; sourceTree = ""; }; + 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "InputMono-Regular.ttf"; path = "../src/assets/fonts/InputMono-Regular.ttf"; sourceTree = ""; }; + 1A4F128CAFB3E71F3B2F3D90 /* Pods-WidgetIntentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.release.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.release.xcconfig"; sourceTree = ""; }; + 1FB4F5FD2BB9AE4452EA8476 /* Pods-Widgets.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.debug.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.debug.xcconfig"; sourceTree = ""; }; + 26CB04CC7EAFA26FBB06896D /* libPods-WidgetsCore.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WidgetsCore.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 29F65370E45F33BCA37F7E02 /* Pods-WidgetsCore.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.dev.xcconfig"; sourceTree = ""; }; + 378A735DFD72359F5CC16F29 /* Pods-WidgetsCore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.debug.xcconfig"; sourceTree = ""; }; + 407451BE42C5147EBB181687 /* Pods-Uniswap-UniswapTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.dev.xcconfig"; sourceTree = ""; }; + 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap/ExpoModulesProvider.swift"; sourceTree = ""; }; + 4E788CB77B4CFEAC6E8FFB3A /* Pods-WidgetIntentExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.debug.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.debug.xcconfig"; sourceTree = ""; }; + 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertQuery.graphql.swift; sourceTree = ""; }; + 63F7391CE0D5231AE38192F9 /* Pods-WidgetIntentExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.beta.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.beta.xcconfig"; sourceTree = ""; }; + 681301AD2A3726EE00A5BF43 /* onboarding_dark.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = onboarding_dark.riv; sourceTree = ""; }; + 681301AE2A3726EE00A5BF43 /* pending_send.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = pending_send.riv; sourceTree = ""; }; + 681301AF2A3726EE00A5BF43 /* onboarding_light.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = onboarding_light.riv; sourceTree = ""; }; + 681301B02A3726EE00A5BF43 /* pending_swap.riv */ = {isa = PBXFileReference; lastKnownFileType = file; path = pending_swap.riv; sourceTree = ""; }; + 68FD07BE7700B63D569EB256 /* Pods-WidgetIntentExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetIntentExtension.dev.xcconfig"; path = "Target Support Files/Pods-WidgetIntentExtension/Pods-WidgetIntentExtension.dev.xcconfig"; sourceTree = ""; }; + 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ScantasticEncryption.m; sourceTree = ""; }; + 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScantasticEncryption.swift; sourceTree = ""; }; + 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionUtils.swift; sourceTree = ""; }; + 6C8EFC2C2891B99100FBD8EB /* EncryptionHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionHelperTests.swift; sourceTree = ""; }; + 6CA91BD82A95223C00C4063E /* RNEthersRS-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RNEthersRS-Bridging-Header.h"; sourceTree = ""; }; + 6CA91BD92A95223C00C4063E /* RNEthersRS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNEthersRS.swift; sourceTree = ""; }; + 6CA91BDA2A95223C00C4063E /* RnEthersRS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RnEthersRS.m; sourceTree = ""; }; + 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RNCloudStorageBackupsManager.m; sourceTree = ""; }; + 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptionHelper.swift; sourceTree = ""; }; + 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RNCloudStorageBackupsManager.swift; sourceTree = ""; }; + 76700FCF2F2249D9C52E6A4C /* libPods-Uniswap.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Uniswap.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 78C41378CE69EDD92DFAD177 /* Pods-OneSignalNotificationServiceExtension.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.dev.xcconfig"; sourceTree = ""; }; + 811E7ED33487936333408382 /* Pods-OneSignalNotificationServiceExtension.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.beta.xcconfig"; sourceTree = ""; }; + 8816FB4EC77AF22919705454 /* Pods-WidgetsCoreTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.beta.xcconfig"; sourceTree = ""; }; + 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationView.swift; sourceTree = ""; }; + 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicDisplayView.swift; sourceTree = ""; }; + 8E89C3A82AB8AAA400C84DE5 /* MnemonicWordView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicWordView.swift; sourceTree = ""; }; + 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicConfirmationWordBankView.swift; sourceTree = ""; }; + 8E89C3AA2AB8AAA400C84DE5 /* MnemonicTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicTextField.swift; sourceTree = ""; }; + 8E89C3AB2AB8AAA400C84DE5 /* MnemonicDisplayManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MnemonicDisplayManager.m; sourceTree = ""; }; + 8E89C3AC2AB8AAA400C84DE5 /* MnemonicConfirmationManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MnemonicConfirmationManager.m; sourceTree = ""; }; + 8E89C3AD2AB8AAA400C84DE5 /* RNSwiftUI-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RNSwiftUI-Bridging-Header.h"; sourceTree = ""; }; + 8EA8AB302AB7ED3C004E7EF3 /* SeedPhraseInputManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SeedPhraseInputManager.m; sourceTree = ""; }; + 8EA8AB312AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseInputViewModel.swift; sourceTree = ""; }; + 8EA8AB322AB7ED3C004E7EF3 /* SeedPhraseInputView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseInputView.swift; sourceTree = ""; }; + 8EA8AB332AB7ED3C004E7EF3 /* SeedPhraseInputManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedPhraseInputManager.swift; sourceTree = ""; }; + 8EA8AB402AB7ED76004E7EF3 /* AlertTriangleIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AlertTriangleIcon.swift; sourceTree = ""; }; + 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteIcon.swift; sourceTree = ""; }; + 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrollFadeExtensions.swift; sourceTree = ""; }; + 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DescriptionTranslations.graphql.swift; sourceTree = ""; }; + 9BF90CF492394DF50D3BAF76 /* Pods-WidgetsCoreTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.debug.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.debug.xcconfig"; sourceTree = ""; }; + 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationContract.graphql.swift; sourceTree = ""; }; + 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NftBalanceAssetInput.graphql.swift; sourceTree = ""; }; + 9F3500812A8AA5890077BFC5 /* EXSplashScreen.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = EXSplashScreen.xcframework; path = "../../../node_modules/expo-splash-screen/ios/EXSplashScreen.xcframework"; sourceTree = ""; }; + 9F3500832A8AAAF50077BFC5 /* libreact-native-splash-screen.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; path = "libreact-native-splash-screen.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityDetails.graphql.swift; sourceTree = ""; }; + 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetails.graphql.swift; sourceTree = ""; }; + 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapOrderDetails.graphql.swift; sourceTree = ""; }; + 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionType.graphql.swift; sourceTree = ""; }; + 9FCEBEFD2A95A8E00079EDDB /* RNWalletConnect.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RNWalletConnect.h; path = Uniswap/WalletConnect/RNWalletConnect.h; sourceTree = ""; }; + 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RNWalletConnect.m; path = Uniswap/WalletConnect/RNWalletConnect.m; sourceTree = ""; }; + 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RNWalletConnect.swift; path = Uniswap/WalletConnect/RNWalletConnect.swift; sourceTree = ""; }; + 9FCEBF022A95A99B0079EDDB /* RCTThemeModule.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RCTThemeModule.h; path = Appearance/RCTThemeModule.h; sourceTree = ""; }; + 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RCTThemeModule.m; path = Appearance/RCTThemeModule.m; sourceTree = ""; }; + 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = Uniswap/AppDelegate.m; sourceTree = ""; }; + A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainSwiftDistrib.swift; sourceTree = ""; }; + A721D9DBF345F037F45E92BE /* Pods-WidgetsCore.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.beta.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.beta.xcconfig"; sourceTree = ""; }; + A82B0304EEE753C6DEB3943E /* libPods-OneSignalNotificationServiceExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-OneSignalNotificationServiceExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + A87B4C3727F6D91B4DAABF14 /* Pods-Uniswap.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.debug.xcconfig"; sourceTree = ""; }; + AEE498F52A85AD86000DDF8E /* Basel-Book.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Basel-Book.ttf"; path = "../src/assets/fonts/Basel-Book.ttf"; sourceTree = ""; }; + AEE498F62A85AD86000DDF8E /* Basel-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; name = "Basel-Medium.ttf"; path = "../src/assets/fonts/Basel-Medium.ttf"; sourceTree = ""; }; + B398F0A4DA17159EA0E2D6DF /* Pods-Uniswap.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.dev.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.dev.xcconfig"; sourceTree = ""; }; + B4CFF4B2DE4CB59FAD9E8C27 /* Pods-Widgets.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.release.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.release.xcconfig"; sourceTree = ""; }; + BD152738B3E6969A7A42D13E /* Pods-WidgetsCoreTests.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.dev.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.dev.xcconfig"; sourceTree = ""; }; + BD41390986108FC8CFA45445 /* Pods-Widgets.dev.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.dev.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.dev.xcconfig"; sourceTree = ""; }; + BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Uniswap/SplashScreen.storyboard; sourceTree = ""; }; + C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Uniswap-UniswapTests/ExpoModulesProvider.swift"; sourceTree = ""; }; + C286F39E616497EA6E1C3010 /* Pods-WidgetsCore.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCore.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCore/Pods-WidgetsCore.release.xcconfig"; sourceTree = ""; }; + C7C23EDACE3BC348607A0E83 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + CA37BC1BA471C071B5090928 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; + DB423A22FFE9EFFCA11E9E1A /* Pods-Uniswap.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.beta.xcconfig"; sourceTree = ""; }; + E8A8C1523EA65F88E3DC16CD /* Pods-Uniswap-UniswapTests.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.beta.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.beta.xcconfig"; sourceTree = ""; }; + EC813AD85060898D4DD580C5 /* Pods-Uniswap-UniswapTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.debug.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.debug.xcconfig"; sourceTree = ""; }; + ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; + F35AFD3627EE49230011A725 /* Uniswap.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; name = Uniswap.entitlements; path = Uniswap/Uniswap.entitlements; sourceTree = ""; }; + F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + F35AFD3D27EE49990011A725 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + F35AFD3F27EE49990011A725 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F35AFD4727EE4B400011A725 /* OneSignalNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalNotificationServiceExtension.entitlements; sourceTree = ""; }; + F50809BC52FC9A48453E6976 /* libPods-Uniswap-UniswapTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Uniswap-UniswapTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F5F2751F56BDA3D0330E56BE /* Pods-Uniswap-UniswapTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap-UniswapTests.release.xcconfig"; path = "Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests.release.xcconfig"; sourceTree = ""; }; + F88A7DA00DA4A893EE80B183 /* Pods-Widgets.beta.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Widgets.beta.xcconfig"; path = "Target Support Files/Pods-Widgets/Pods-Widgets.beta.xcconfig"; sourceTree = ""; }; + F9B12416ED2E83444B0936B2 /* libPods-WidgetsCoreTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WidgetsCoreTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F9D639068E109DD74CBEDF92 /* Pods-WidgetsCoreTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WidgetsCoreTests.release.xcconfig"; path = "Target Support Files/Pods-WidgetsCoreTests/Pods-WidgetsCoreTests.release.xcconfig"; sourceTree = ""; }; + FD21B73B081B800A44E7F682 /* libPods-WidgetIntentExtension.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-WidgetIntentExtension.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GoogleServiceInfo; sourceTree = SOURCE_ROOT; }; + FD72162492D6B68BBC15F3ED /* Pods-Uniswap.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Uniswap.release.xcconfig"; path = "Target Support Files/Pods-Uniswap/Pods-Uniswap.release.xcconfig"; sourceTree = ""; }; + FD7304CD28A364FC0085BDEA /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Colors.xcassets; path = Uniswap/Colors.xcassets; sourceTree = ""; }; + FD7304CF28A3650A0085BDEA /* Colors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Colors.swift; path = Uniswap/Colors.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 07C2AC36457A05B810F60317 /* libPods-Uniswap-UniswapTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E23832A44D5BC006AD6C9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FFD8F7BC6B871CC6D7CBE0C7 /* libPods-WidgetsCore.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E238A2A44D5BD006AD6C9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 072E238E2A44D5BD006AD6C9 /* WidgetsCore.framework in Frameworks */, + B8649A8D70B018E583494128 /* libPods-WidgetsCoreTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072F6C1C2A44A32E00DA720A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F78980B2A819CC4004D5A98 /* SwiftUI.framework in Frameworks */, + 9F7898112A819D32004D5A98 /* WidgetsCore.framework in Frameworks */, + 072F6C212A44A32E00DA720A /* WidgetKit.framework in Frameworks */, + C9E623221156415BD176C348 /* libPods-Widgets.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 078E79422A55EB3300F59CF2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 078E79472A55EB3300F59CF2 /* Intents.framework in Frameworks */, + 1DA5339E6A1956F5FE24DB6C /* libPods-WidgetIntentExtension.a in Frameworks */, + 9F78980E2A819D2B004D5A98 /* WidgetsCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9F3500842A8AAAF50077BFC5 /* libreact-native-splash-screen.a in Frameworks */, + F2867EBF8C9DE41CF466D17C /* libPods-Uniswap.a in Frameworks */, + 9F7898142A819D62004D5A98 /* WidgetsCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F35AFD3827EE49990011A725 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9C9285C0639E520B3453B9C0 /* libPods-OneSignalNotificationServiceExtension.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00E356EF1AD99517003FC87E /* UniswapTests */ = { + isa = PBXGroup; + children = ( + 6C8EFC2C2891B99100FBD8EB /* EncryptionHelperTests.swift */, + 00E356F21AD99517003FC87E /* UniswapTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = UniswapTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 03DD298C2A4CE34B00E3E0F5 /* Appearance */ = { + isa = PBXGroup; + children = ( + 9FCEBF022A95A99B0079EDDB /* RCTThemeModule.h */, + 9FCEBF032A95A99B0079EDDB /* RCTThemeModule.m */, + ); + name = Appearance; + sourceTree = ""; + }; + 0703EE042A57350400AED1DA /* Utils */ = { + isa = PBXGroup; + children = ( + 0741433F2A588F5800A157D3 /* Structs.swift */, + 0767E0392A65CA590042ADA2 /* UI */, + 0703EE022A5734A600AED1DA /* UserDefaults.swift */, + 0743223F2A841BBD00F8518D /* Constants.swift */, + 07F136412A5763480067004F /* Network.swift */, + 072F6C372A44BECC00DA720A /* Logging.swift */, + 07F1363F2A575EC00067004F /* DataQueries.swift */, + 07F5CF742A7020FD00C648A5 /* Format.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 072E23872A44D5BD006AD6C9 /* WidgetsCore */ = { + isa = PBXGroup; + children = ( + 07F0C28E2A5F3E2E00D5353E /* Env.swift */, + 073C67F42A5C8FBE00F6DAD8 /* MobileSchema */, + 072E23882A44D5BD006AD6C9 /* WidgetsCore.h */, + 0703EE042A57350400AED1DA /* Utils */, + ); + path = WidgetsCore; + sourceTree = ""; + }; + 072E23932A44D5BD006AD6C9 /* WidgetsCoreTests */ = { + isa = PBXGroup; + children = ( + 072E23942A44D5BD006AD6C9 /* WidgetsCoreTests.swift */, + 074086F92A703B76006E3053 /* FormatTests.swift */, + ); + path = WidgetsCoreTests; + sourceTree = ""; + }; + 072F6C242A44A32E00DA720A /* Widgets */ = { + isa = PBXGroup; + children = ( + 072E23A62A44D61A006AD6C9 /* Widgets.entitlements */, + 072F6C252A44A32E00DA720A /* WidgetsBundle.swift */, + 072F6C272A44A32E00DA720A /* TokenPriceWidget.swift */, + 072F6C292A44A32E00DA720A /* TokenPriceWidget.intentdefinition */, + 072F6C2A2A44A32F00DA720A /* Assets.xcassets */, + 072F6C2C2A44A32F00DA720A /* Info.plist */, + ); + path = Widgets; + sourceTree = ""; + }; + 073C67F42A5C8FBE00F6DAD8 /* MobileSchema */ = { + isa = PBXGroup; + children = ( + 074321A32A83E3C900F8518D /* Fragments */, + 074321A52A83E3C900F8518D /* MobileSchema.graphql.swift */, + 0743218E2A83E3C900F8518D /* Operations */, + 074321A62A83E3C900F8518D /* Schema */, + ); + path = MobileSchema; + sourceTree = ""; + }; + 074321872A82BA2700F8518D /* Fonts */ = { + isa = PBXGroup; + children = ( + 0743218B2A82C6C000F8518D /* Basel-Book.otf */, + 0767E02C2A61BBDB0042ADA2 /* Basel-Semibold.otf */, + 0767E02B2A61BBDB0042ADA2 /* Basel-Regular.otf */, + 0767E02D2A61BBDB0042ADA2 /* Basel-Medium.otf */, + 0767E02E2A61BBDC0042ADA2 /* Basel-Bold.otf */, + ); + path = Fonts; + sourceTree = ""; + }; + 0743218E2A83E3C900F8518D /* Operations */ = { + isa = PBXGroup; + children = ( + 0743218F2A83E3C900F8518D /* Queries */, + ); + path = Operations; + sourceTree = ""; + }; + 0743218F2A83E3C900F8518D /* Queries */ = { + isa = PBXGroup; + children = ( + 5EFB78352B1E585000E77EAC /* ConvertQuery.graphql.swift */, + 077E60382A85587800ABC4B9 /* TokensQuery.graphql.swift */, + 074321902A83E3C900F8518D /* TokenDetailsScreenQuery.graphql.swift */, + 077E603A2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift */, + 074321922A83E3C900F8518D /* ExploreTokensTabQuery.graphql.swift */, + 074321932A83E3C900F8518D /* NftsTabQuery.graphql.swift */, + 074321942A83E3C900F8518D /* TokenQuery.graphql.swift */, + 074321952A83E3C900F8518D /* SearchPopularTokensQuery.graphql.swift */, + 074321962A83E3C900F8518D /* TopTokensQuery.graphql.swift */, + 074321972A83E3C900F8518D /* TokenPriceHistoryQuery.graphql.swift */, + 074321982A83E3C900F8518D /* FavoriteTokenCardQuery.graphql.swift */, + 074321992A83E3C900F8518D /* NFTItemScreenQuery.graphql.swift */, + 0743219A2A83E3C900F8518D /* NftCollectionScreenQuery.graphql.swift */, + 0743219B2A83E3C900F8518D /* SearchTokensQuery.graphql.swift */, + 0743219C2A83E3C900F8518D /* ExploreSearchQuery.graphql.swift */, + 0743219D2A83E3C900F8518D /* TokenProjectsQuery.graphql.swift */, + 0743219E2A83E3C900F8518D /* SelectWalletScreenQuery.graphql.swift */, + 0743219F2A83E3C900F8518D /* PortfolioBalancesQuery.graphql.swift */, + 074321A02A83E3C900F8518D /* NftsQuery.graphql.swift */, + 074321A12A83E3C900F8518D /* TransactionListQuery.graphql.swift */, + 074321A22A83E3C900F8518D /* TransactionHistoryUpdaterQuery.graphql.swift */, + ); + path = Queries; + sourceTree = ""; + }; + 074321A32A83E3C900F8518D /* Fragments */ = { + isa = PBXGroup; + children = ( + 074321A42A83E3C900F8518D /* TopTokenParts.graphql.swift */, + ); + path = Fragments; + sourceTree = ""; + }; + 074321A62A83E3C900F8518D /* Schema */ = { + isa = PBXGroup; + children = ( + 074321A72A83E3C900F8518D /* Unions */, + 074321A92A83E3C900F8518D /* Enums */, + 074321B62A83E3C900F8518D /* Objects */, + 074321DE2A83E3C900F8518D /* SchemaConfiguration.swift */, + 074321DF2A83E3C900F8518D /* InputObjects */, + 074321E72A83E3C900F8518D /* SchemaMetadata.graphql.swift */, + 074321E82A83E3C900F8518D /* Interfaces */, + ); + path = Schema; + sourceTree = ""; + }; + 074321A72A83E3C900F8518D /* Unions */ = { + isa = PBXGroup; + children = ( + 9F813E9D2AA8FB5700438D89 /* ActivityDetails.graphql.swift */, + 074321A82A83E3C900F8518D /* AssetChange.graphql.swift */, + ); + path = Unions; + sourceTree = ""; + }; + 074321A92A83E3C900F8518D /* Enums */ = { + isa = PBXGroup; + children = ( + 9F813EA32AA8FBCF00438D89 /* TransactionType.graphql.swift */, + 074321AA2A83E3C900F8518D /* HistoryDuration.graphql.swift */, + 074321AB2A83E3C900F8518D /* TokenStandard.graphql.swift */, + 074321AC2A83E3C900F8518D /* Currency.graphql.swift */, + 074321AE2A83E3C900F8518D /* Chain.graphql.swift */, + 074321AF2A83E3C900F8518D /* TokenSortableField.graphql.swift */, + 074321B02A83E3C900F8518D /* TransactionDirection.graphql.swift */, + 074321B12A83E3C900F8518D /* NftStandard.graphql.swift */, + 074321B22A83E3C900F8518D /* NftActivityType.graphql.swift */, + 074321B32A83E3C900F8518D /* SafetyLevel.graphql.swift */, + 074321B42A83E3C900F8518D /* NftMarketplace.graphql.swift */, + 074321B52A83E3C900F8518D /* TransactionStatus.graphql.swift */, + ); + path = Enums; + sourceTree = ""; + }; + 074321B62A83E3C900F8518D /* Objects */ = { + isa = PBXGroup; + children = ( + 9F00A4392B33894C0088A0D0 /* ApplicationContract.graphql.swift */, + 8EE7C0572AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift */, + 9F813EA12AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift */, + 9F813E9F2AA8FB7500438D89 /* TransactionDetails.graphql.swift */, + 074321B72A83E3C900F8518D /* Image.graphql.swift */, + 074321B82A83E3C900F8518D /* NftOrderEdge.graphql.swift */, + 074321B92A83E3C900F8518D /* NftApproveForAll.graphql.swift */, + 074321BA2A83E3C900F8518D /* NftAssetTrait.graphql.swift */, + 074321BB2A83E3C900F8518D /* NftBalanceConnection.graphql.swift */, + 074321BC2A83E3C900F8518D /* NftActivityEdge.graphql.swift */, + 074321BD2A83E3C900F8518D /* NftCollection.graphql.swift */, + 074321BE2A83E3C900F8518D /* TimestampedAmount.graphql.swift */, + 074321BF2A83E3C900F8518D /* NftAssetConnection.graphql.swift */, + 074321C02A83E3C900F8518D /* TokenProject.graphql.swift */, + 074321C12A83E3C900F8518D /* NftTransfer.graphql.swift */, + 074321C22A83E3C900F8518D /* TokenApproval.graphql.swift */, + 074321C32A83E3C900F8518D /* NftOrderConnection.graphql.swift */, + 074321C42A83E3C900F8518D /* TokenMarket.graphql.swift */, + 074321C52A83E3C900F8518D /* NftCollectionMarket.graphql.swift */, + 074321C62A83E3C900F8518D /* TokenBalance.graphql.swift */, + 074321C72A83E3C900F8518D /* NftOrder.graphql.swift */, + 074321C82A83E3C900F8518D /* Portfolio.graphql.swift */, + 074321C92A83E3C900F8518D /* PageInfo.graphql.swift */, + 074321CA2A83E3C900F8518D /* NftAssetEdge.graphql.swift */, + 074321CB2A83E3C900F8518D /* NftCollectionConnection.graphql.swift */, + 074321CC2A83E3C900F8518D /* NftContract.graphql.swift */, + 074321CD2A83E3C900F8518D /* NftActivityConnection.graphql.swift */, + 074321CE2A83E3C900F8518D /* Amount.graphql.swift */, + 074321CF2A83E3C900F8518D /* Query.graphql.swift */, + 074321D02A83E3C900F8518D /* NftAsset.graphql.swift */, + 074321D12A83E3C900F8518D /* Dimensions.graphql.swift */, + 074321D22A83E3C900F8518D /* TokenProjectMarket.graphql.swift */, + 074321D32A83E3C900F8518D /* AmountChange.graphql.swift */, + 074321D42A83E3C900F8518D /* NftBalanceEdge.graphql.swift */, + 074321D52A83E3C900F8518D /* NftActivity.graphql.swift */, + 074321D62A83E3C900F8518D /* AssetActivity.graphql.swift */, + 074321D72A83E3C900F8518D /* NftProfile.graphql.swift */, + 074321D82A83E3C900F8518D /* NftCollectionEdge.graphql.swift */, + 074321DA2A83E3C900F8518D /* TokenTransfer.graphql.swift */, + 074321DB2A83E3C900F8518D /* NftApproval.graphql.swift */, + 074321DC2A83E3C900F8518D /* NftBalance.graphql.swift */, + 074321DD2A83E3C900F8518D /* Token.graphql.swift */, + ); + path = Objects; + sourceTree = ""; + }; + 074321DF2A83E3C900F8518D /* InputObjects */ = { + isa = PBXGroup; + children = ( + 0DC6ADEF2B1E2C0F0092909C /* PortfolioValueModifier.graphql.swift */, + 9F29D4EC2B47126D004D003A /* NftBalanceAssetInput.graphql.swift */, + 074321E02A83E3C900F8518D /* NftBalancesFilterInput.graphql.swift */, + 074321E12A83E3C900F8518D /* NftCollectionsFilterInput.graphql.swift */, + 074321E22A83E3C900F8518D /* ContractInput.graphql.swift */, + 074321E32A83E3C900F8518D /* NftActivityFilterInput.graphql.swift */, + 074321E42A83E3C900F8518D /* NftAssetTraitInput.graphql.swift */, + 074321E62A83E3C900F8518D /* NftAssetsFilterInput.graphql.swift */, + ); + path = InputObjects; + sourceTree = ""; + }; + 074321E82A83E3C900F8518D /* Interfaces */ = { + isa = PBXGroup; + children = ( + 074321E92A83E3C900F8518D /* IAmount.graphql.swift */, + 074321EA2A83E3C900F8518D /* IContract.graphql.swift */, + ); + path = Interfaces; + sourceTree = ""; + }; + 0767E0392A65CA590042ADA2 /* UI */ = { + isa = PBXGroup; + children = ( + 0767E0372A65C8330042ADA2 /* Colors.swift */, + 07F5CF702A6AD97D00C648A5 /* Chart.swift */, + 0783F7B32A619E7C009ED617 /* UIComponents.swift */, + 0767E03A2A65D2550042ADA2 /* Styling.swift */, + ); + path = UI; + sourceTree = ""; + }; + 078E79482A55EB3300F59CF2 /* WidgetIntentExtension */ = { + isa = PBXGroup; + children = ( + 070480372A58A507009006CE /* WidgetIntentExtension.entitlements */, + 078E79492A55EB3300F59CF2 /* IntentHandler.swift */, + 078E794B2A55EB3300F59CF2 /* Info.plist */, + ); + path = WidgetIntentExtension; + sourceTree = ""; + }; + 07B067692A7D6EC8001DD9B9 /* Widget */ = { + isa = PBXGroup; + children = ( + 07B0676A2A7D6EC8001DD9B9 /* RNWidgets.swift */, + 07B0676B2A7D6EC8001DD9B9 /* RNWidgets.m */, + ); + name = Widget; + path = Uniswap/Widget; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* Uniswap */ = { + isa = PBXGroup; + children = ( + 8EA8AB3F2AB7ED76004E7EF3 /* Icons */, + 8E566D9F2AA1095000D4AA76 /* Components */, + 07B067692A7D6EC8001DD9B9 /* Widget */, + 03DD298C2A4CE34B00E3E0F5 /* Appearance */, + E3C8C9DD291C4D2C000E00EA /* RiveAssets */, + F35AFD3627EE49230011A725 /* Uniswap.entitlements */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 9FEC9B8A2A858CF1003CD019 /* AppDelegate.m */, + 6C84F055283D83CF0071FA2E /* Onboarding */, + 6CE631B928186D4500716D29 /* WalletConnect */, + A32F9FBC272343C8002CFCDB /* GoogleService-Info.plist */, + 6CA91BD72A95223C00C4063E /* RNEthersRs */, + 6CA91BDD2A95226200C4063E /* RNCloudBackupsManager */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB71A68108700A75B9A /* main.m */, + BF9176E944C84910B1C0B057 /* SplashScreen.storyboard */, + FD7304CD28A364FC0085BDEA /* Colors.xcassets */, + FD7304CF28A3650A0085BDEA /* Colors.swift */, + ); + name = Uniswap; + sourceTree = ""; + }; + 2D16E6871FA4F8E400B85C8A /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9F3500832A8AAAF50077BFC5 /* libreact-native-splash-screen.a */, + 9F3500812A8AA5890077BFC5 /* EXSplashScreen.xcframework */, + ED297162215061F000B7C4FE /* JavaScriptCore.framework */, + 072F6C202A44A32E00DA720A /* WidgetKit.framework */, + 072F6C222A44A32E00DA720A /* SwiftUI.framework */, + 078E79462A55EB3300F59CF2 /* Intents.framework */, + A82B0304EEE753C6DEB3943E /* libPods-OneSignalNotificationServiceExtension.a */, + 76700FCF2F2249D9C52E6A4C /* libPods-Uniswap.a */, + F50809BC52FC9A48453E6976 /* libPods-Uniswap-UniswapTests.a */, + FD21B73B081B800A44E7F682 /* libPods-WidgetIntentExtension.a */, + 0750E42001CAEA9359159B7F /* libPods-Widgets.a */, + 26CB04CC7EAFA26FBB06896D /* libPods-WidgetsCore.a */, + F9B12416ED2E83444B0936B2 /* libPods-WidgetsCoreTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 47914D9EE3A4DE926EFC5089 /* UniswapTests */ = { + isa = PBXGroup; + children = ( + C26D739993D5C939C6FBB58A /* ExpoModulesProvider.swift */, + ); + name = UniswapTests; + sourceTree = ""; + }; + 5754C6A1B51170788A63F6F3 /* ExpoModulesProviders */ = { + isa = PBXGroup; + children = ( + 9759A762F61D6B2F01C79DBF /* Uniswap */, + 47914D9EE3A4DE926EFC5089 /* UniswapTests */, + ); + name = ExpoModulesProviders; + sourceTree = ""; + }; + 6BC7D07A2B5FF02400617C95 /* Scantastic */ = { + isa = PBXGroup; + children = ( + 6BC7D07B2B5FF02400617C95 /* ScantasticEncryption.m */, + 6BC7D07C2B5FF02400617C95 /* ScantasticEncryption.swift */, + 6BC7D07D2B5FF02400617C95 /* EncryptionUtils.swift */, + ); + name = Scantastic; + path = Uniswap/Onboarding/Scantastic; + sourceTree = ""; + }; + 6C84F055283D83CF0071FA2E /* Onboarding */ = { + isa = PBXGroup; + children = ( + 6BC7D07A2B5FF02400617C95 /* Scantastic */, + 8E89C3A52AB8AAA400C84DE5 /* Backup */, + 8EA8AB2F2AB7ED3C004E7EF3 /* Import */, + ); + name = Onboarding; + sourceTree = ""; + }; + 6CA91BD72A95223C00C4063E /* RNEthersRs */ = { + isa = PBXGroup; + children = ( + A3F0A5B0272B1DFA00895B25 /* KeychainSwiftDistrib.swift */, + 6CA91BD82A95223C00C4063E /* RNEthersRS-Bridging-Header.h */, + 6CA91BD92A95223C00C4063E /* RNEthersRS.swift */, + 6CA91BDA2A95223C00C4063E /* RnEthersRS.m */, + ); + name = RNEthersRs; + path = Uniswap/RNEthersRs; + sourceTree = ""; + }; + 6CA91BDD2A95226200C4063E /* RNCloudBackupsManager */ = { + isa = PBXGroup; + children = ( + 6CA91BDE2A95226200C4063E /* RNCloudStorageBackupsManager.m */, + 6CA91BDF2A95226200C4063E /* EncryptionHelper.swift */, + 6CA91BE02A95226200C4063E /* RNCloudStorageBackupsManager.swift */, + ); + name = RNCloudBackupsManager; + path = Uniswap/RNCloudBackupsManager; + sourceTree = ""; + }; + 6CE631B928186D4500716D29 /* WalletConnect */ = { + isa = PBXGroup; + children = ( + 9FCEBEFD2A95A8E00079EDDB /* RNWalletConnect.h */, + 9FCEBEFE2A95A8E00079EDDB /* RNWalletConnect.m */, + 9FCEBEFF2A95A8E00079EDDB /* RNWalletConnect.swift */, + ); + name = WalletConnect; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + ); + name = Libraries; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 074321872A82BA2700F8518D /* Fonts */, + FD54D51C296C79A4007A37E9 /* GoogleServiceInfo */, + 13B07FAE1A68108700A75B9A /* Uniswap */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* UniswapTests */, + F35AFD3C27EE49990011A725 /* OneSignalNotificationServiceExtension */, + 072F6C242A44A32E00DA720A /* Widgets */, + 072E23872A44D5BD006AD6C9 /* WidgetsCore */, + 072E23932A44D5BD006AD6C9 /* WidgetsCoreTests */, + 078E79482A55EB3300F59CF2 /* WidgetIntentExtension */, + 83CBBA001A601CBA00E9B192 /* Products */, + 2D16E6871FA4F8E400B85C8A /* Frameworks */, + E233CBF5F47BEE60B243DCF8 /* Pods */, + C2C18ECBEF5A4489BF3A314C /* Resources */, + 5754C6A1B51170788A63F6F3 /* ExpoModulesProviders */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + usesTabs = 0; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* Uniswap.app */, + 00E356EE1AD99517003FC87E /* UniswapTests.xctest */, + F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */, + 072F6C1F2A44A32E00DA720A /* Widgets.appex */, + 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */, + 072E238D2A44D5BD006AD6C9 /* WidgetsCoreTests.xctest */, + 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 8E566D9F2AA1095000D4AA76 /* Components */ = { + isa = PBXGroup; + children = ( + 8ED0562B2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift */, + ); + path = Components; + sourceTree = ""; + }; + 8E89C3A52AB8AAA400C84DE5 /* Backup */ = { + isa = PBXGroup; + children = ( + 8E89C3A62AB8AAA400C84DE5 /* MnemonicConfirmationView.swift */, + 8E89C3A72AB8AAA400C84DE5 /* MnemonicDisplayView.swift */, + 8E89C3A82AB8AAA400C84DE5 /* MnemonicWordView.swift */, + 8E89C3A92AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift */, + 8E89C3AA2AB8AAA400C84DE5 /* MnemonicTextField.swift */, + 8E89C3AB2AB8AAA400C84DE5 /* MnemonicDisplayManager.m */, + 8E89C3AC2AB8AAA400C84DE5 /* MnemonicConfirmationManager.m */, + 8E89C3AD2AB8AAA400C84DE5 /* RNSwiftUI-Bridging-Header.h */, + ); + name = Backup; + path = Uniswap/Onboarding/Backup; + sourceTree = ""; + }; + 8EA8AB2F2AB7ED3C004E7EF3 /* Import */ = { + isa = PBXGroup; + children = ( + 8EA8AB302AB7ED3C004E7EF3 /* SeedPhraseInputManager.m */, + 8EA8AB312AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift */, + 8EA8AB322AB7ED3C004E7EF3 /* SeedPhraseInputView.swift */, + 8EA8AB332AB7ED3C004E7EF3 /* SeedPhraseInputManager.swift */, + ); + name = Import; + path = Uniswap/Onboarding/Import; + sourceTree = ""; + }; + 8EA8AB3F2AB7ED76004E7EF3 /* Icons */ = { + isa = PBXGroup; + children = ( + 8EA8AB402AB7ED76004E7EF3 /* AlertTriangleIcon.swift */, + 8EBFB1542ABA6AA6006B32A8 /* PasteIcon.swift */, + ); + name = Icons; + path = Uniswap/Icons; + sourceTree = ""; + }; + 9759A762F61D6B2F01C79DBF /* Uniswap */ = { + isa = PBXGroup; + children = ( + 4DF5F26A06553EFDD4D99214 /* ExpoModulesProvider.swift */, + ); + name = Uniswap; + sourceTree = ""; + }; + C2C18ECBEF5A4489BF3A314C /* Resources */ = { + isa = PBXGroup; + children = ( + AEE498F52A85AD86000DDF8E /* Basel-Book.ttf */, + AEE498F62A85AD86000DDF8E /* Basel-Medium.ttf */, + 1834199AFFB04D91B05FFB64 /* InputMono-Regular.ttf */, + ); + name = Resources; + sourceTree = ""; + }; + E233CBF5F47BEE60B243DCF8 /* Pods */ = { + isa = PBXGroup; + children = ( + C7C23EDACE3BC348607A0E83 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */, + CA37BC1BA471C071B5090928 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */, + A87B4C3727F6D91B4DAABF14 /* Pods-Uniswap.debug.xcconfig */, + FD72162492D6B68BBC15F3ED /* Pods-Uniswap.release.xcconfig */, + EC813AD85060898D4DD580C5 /* Pods-Uniswap-UniswapTests.debug.xcconfig */, + F5F2751F56BDA3D0330E56BE /* Pods-Uniswap-UniswapTests.release.xcconfig */, + 78C41378CE69EDD92DFAD177 /* Pods-OneSignalNotificationServiceExtension.dev.xcconfig */, + 811E7ED33487936333408382 /* Pods-OneSignalNotificationServiceExtension.beta.xcconfig */, + B398F0A4DA17159EA0E2D6DF /* Pods-Uniswap.dev.xcconfig */, + DB423A22FFE9EFFCA11E9E1A /* Pods-Uniswap.beta.xcconfig */, + 407451BE42C5147EBB181687 /* Pods-Uniswap-UniswapTests.dev.xcconfig */, + E8A8C1523EA65F88E3DC16CD /* Pods-Uniswap-UniswapTests.beta.xcconfig */, + 4E788CB77B4CFEAC6E8FFB3A /* Pods-WidgetIntentExtension.debug.xcconfig */, + 1A4F128CAFB3E71F3B2F3D90 /* Pods-WidgetIntentExtension.release.xcconfig */, + 68FD07BE7700B63D569EB256 /* Pods-WidgetIntentExtension.dev.xcconfig */, + 63F7391CE0D5231AE38192F9 /* Pods-WidgetIntentExtension.beta.xcconfig */, + 1FB4F5FD2BB9AE4452EA8476 /* Pods-Widgets.debug.xcconfig */, + B4CFF4B2DE4CB59FAD9E8C27 /* Pods-Widgets.release.xcconfig */, + BD41390986108FC8CFA45445 /* Pods-Widgets.dev.xcconfig */, + F88A7DA00DA4A893EE80B183 /* Pods-Widgets.beta.xcconfig */, + 378A735DFD72359F5CC16F29 /* Pods-WidgetsCore.debug.xcconfig */, + C286F39E616497EA6E1C3010 /* Pods-WidgetsCore.release.xcconfig */, + 29F65370E45F33BCA37F7E02 /* Pods-WidgetsCore.dev.xcconfig */, + A721D9DBF345F037F45E92BE /* Pods-WidgetsCore.beta.xcconfig */, + 9BF90CF492394DF50D3BAF76 /* Pods-WidgetsCoreTests.debug.xcconfig */, + F9D639068E109DD74CBEDF92 /* Pods-WidgetsCoreTests.release.xcconfig */, + BD152738B3E6969A7A42D13E /* Pods-WidgetsCoreTests.dev.xcconfig */, + 8816FB4EC77AF22919705454 /* Pods-WidgetsCoreTests.beta.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + E3C8C9DD291C4D2C000E00EA /* RiveAssets */ = { + isa = PBXGroup; + children = ( + 681301AD2A3726EE00A5BF43 /* onboarding_dark.riv */, + 681301AF2A3726EE00A5BF43 /* onboarding_light.riv */, + 681301AE2A3726EE00A5BF43 /* pending_send.riv */, + 681301B02A3726EE00A5BF43 /* pending_swap.riv */, + ); + path = RiveAssets; + sourceTree = ""; + }; + F35AFD3C27EE49990011A725 /* OneSignalNotificationServiceExtension */ = { + isa = PBXGroup; + children = ( + F35AFD4727EE4B400011A725 /* OneSignalNotificationServiceExtension.entitlements */, + F35AFD3D27EE49990011A725 /* NotificationService.swift */, + F35AFD3F27EE49990011A725 /* Info.plist */, + ); + path = OneSignalNotificationServiceExtension; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 072E23812A44D5BC006AD6C9 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 072E23962A44D5BD006AD6C9 /* WidgetsCore.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* UniswapTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "UniswapTests" */; + buildPhases = ( + 9D219B3D5B50538611181454 /* [CP] Check Pods Manifest.lock */, + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + 1C9BDBE6A5CAB72EB7B13D63 /* [CP] Embed Pods Frameworks */, + D109A6AB136CA8AF093E7EE6 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = UniswapTests; + productName = UniswapTests; + productReference = 00E356EE1AD99517003FC87E /* UniswapTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 072E23852A44D5BC006AD6C9 /* WidgetsCore */ = { + isa = PBXNativeTarget; + buildConfigurationList = 072E23A42A44D5BD006AD6C9 /* Build configuration list for PBXNativeTarget "WidgetsCore" */; + buildPhases = ( + 1EFBA749AD5679D0BC9F3259 /* [CP] Check Pods Manifest.lock */, + 072E23812A44D5BC006AD6C9 /* Headers */, + 072E23822A44D5BC006AD6C9 /* Sources */, + 072E23832A44D5BC006AD6C9 /* Frameworks */, + 072E23842A44D5BC006AD6C9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WidgetsCore; + productName = UniswapWidgetsCore; + productReference = 072E23862A44D5BC006AD6C9 /* WidgetsCore.framework */; + productType = "com.apple.product-type.framework"; + }; + 072E238C2A44D5BD006AD6C9 /* WidgetsCoreTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 072E23A52A44D5BD006AD6C9 /* Build configuration list for PBXNativeTarget "WidgetsCoreTests" */; + buildPhases = ( + 1E1D08469926D25C5C2BD365 /* [CP] Check Pods Manifest.lock */, + 072E23892A44D5BD006AD6C9 /* Sources */, + 072E238A2A44D5BD006AD6C9 /* Frameworks */, + 072E238B2A44D5BD006AD6C9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 072E23902A44D5BD006AD6C9 /* PBXTargetDependency */, + 072E23922A44D5BD006AD6C9 /* PBXTargetDependency */, + ); + name = WidgetsCoreTests; + productName = UniswapWidgetsCoreTests; + productReference = 072E238D2A44D5BD006AD6C9 /* WidgetsCoreTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 072F6C1E2A44A32E00DA720A /* Widgets */ = { + isa = PBXNativeTarget; + buildConfigurationList = 072F6C362A44A32F00DA720A /* Build configuration list for PBXNativeTarget "Widgets" */; + buildPhases = ( + 8A31DD3836C878BD44BB4A9F /* [CP] Check Pods Manifest.lock */, + 072F6C1B2A44A32E00DA720A /* Sources */, + 072F6C1C2A44A32E00DA720A /* Frameworks */, + 072F6C1D2A44A32E00DA720A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0703EE092A57355400AED1DA /* PBXTargetDependency */, + ); + name = Widgets; + productName = UniswapWidgetExtension; + productReference = 072F6C1F2A44A32E00DA720A /* Widgets.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 078E79442A55EB3300F59CF2 /* WidgetIntentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 078E794F2A55EB3400F59CF2 /* Build configuration list for PBXNativeTarget "WidgetIntentExtension" */; + buildPhases = ( + FF9AEEB1546D57276CCEE78A /* [CP] Check Pods Manifest.lock */, + 078E79412A55EB3300F59CF2 /* Sources */, + 078E79422A55EB3300F59CF2 /* Frameworks */, + 078E79432A55EB3300F59CF2 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 9F7898062A819AA2004D5A98 /* PBXTargetDependency */, + ); + name = WidgetIntentExtension; + productName = UniswapWidgetsIntentExtension; + productReference = 078E79452A55EB3300F59CF2 /* WidgetIntentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 13B07F861A680F5B00A75B9A /* Uniswap */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Uniswap" */; + buildPhases = ( + 7053AB6470927D4AFEB0A4F6 /* [CP] Check Pods Manifest.lock */, + FD10A7F022414F080027D42C /* Start Packager */, + 13B07F871A680F5B00A75B9A /* Sources */, + FD54D51B296C780A007A37E9 /* Copy configuration-specific GoogleServices-Info.plist */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + 0312A12D2A77D07B008CAAFD /* Upload Debug Symbols to Sentry */, + F35AFD4627EE49990011A725 /* Embed App Extensions */, + 9F7898182A819D62004D5A98 /* Embed Frameworks */, + 11ABDA19EC3508C14907F53A /* [CP] Embed Pods Frameworks */, + 95C59BDDCB4A750ADC9CF7F1 /* [CP] Copy Pods Resources */, + 5C77EF938EB03E5ED1947B82 /* [CP-User] [RNFB] Core Configuration */, + ); + buildRules = ( + ); + dependencies = ( + F35AFD4127EE49990011A725 /* PBXTargetDependency */, + 072F6C302A44A32F00DA720A /* PBXTargetDependency */, + 078E794D2A55EB3300F59CF2 /* PBXTargetDependency */, + 9F7898172A819D62004D5A98 /* PBXTargetDependency */, + ); + name = Uniswap; + productName = Uniswap; + productReference = 13B07F961A680F5B00A75B9A /* Uniswap.app */; + productType = "com.apple.product-type.application"; + }; + F35AFD3A27EE49990011A725 /* OneSignalNotificationServiceExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = F35AFD4327EE49990011A725 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */; + buildPhases = ( + E7B84E3C2FDEB51A179E3876 /* [CP] Check Pods Manifest.lock */, + F35AFD3727EE49990011A725 /* Sources */, + F35AFD3827EE49990011A725 /* Frameworks */, + F35AFD3927EE49990011A725 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OneSignalNotificationServiceExtension; + productName = OneSignalNotificationServiceExtension; + productReference = F35AFD3B27EE49990011A725 /* OneSignalNotificationServiceExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1420; + LastUpgradeCheck = 1210; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + 072E23852A44D5BC006AD6C9 = { + CreatedOnToolsVersion = 14.2; + LastSwiftMigration = 1420; + }; + 072E238C2A44D5BD006AD6C9 = { + CreatedOnToolsVersion = 14.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + 072F6C1E2A44A32E00DA720A = { + CreatedOnToolsVersion = 14.2; + }; + 078E79442A55EB3300F59CF2 = { + CreatedOnToolsVersion = 14.2; + }; + 13B07F861A680F5B00A75B9A = { + LastSwiftMigration = 1310; + }; + F35AFD3A27EE49990011A725 = { + CreatedOnToolsVersion = 13.2.1; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Uniswap" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* Uniswap */, + 00E356ED1AD99517003FC87E /* UniswapTests */, + F35AFD3A27EE49990011A725 /* OneSignalNotificationServiceExtension */, + 072F6C1E2A44A32E00DA720A /* Widgets */, + 072E23852A44D5BC006AD6C9 /* WidgetsCore */, + 072E238C2A44D5BD006AD6C9 /* WidgetsCoreTests */, + 078E79442A55EB3300F59CF2 /* WidgetIntentExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E23842A44D5BC006AD6C9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E238B2A44D5BD006AD6C9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072F6C1D2A44A32E00DA720A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0743218D2A82C6C000F8518D /* Basel-Book.otf in Resources */, + 0767E0342A61BBDC0042ADA2 /* Basel-Medium.otf in Resources */, + 0767E0302A61BBDC0042ADA2 /* Basel-Regular.otf in Resources */, + 0767E0362A61BBDC0042ADA2 /* Basel-Bold.otf in Resources */, + 0767E0322A61BBDC0042ADA2 /* Basel-Semibold.otf in Resources */, + 072F6C2B2A44A32F00DA720A /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 078E79432A55EB3300F59CF2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FD7304CE28A364FC0085BDEA /* Colors.xcassets in Resources */, + AEE498F82A85AD86000DDF8E /* Basel-Medium.ttf in Resources */, + 681301B32A3726EE00A5BF43 /* onboarding_light.riv in Resources */, + A32F9FBD272343C9002CFCDB /* GoogleService-Info.plist in Resources */, + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 77CF6065C8A24FE48204A2C1 /* SplashScreen.storyboard in Resources */, + 681301B42A3726EE00A5BF43 /* pending_swap.riv in Resources */, + 681301B22A3726EE00A5BF43 /* pending_send.riv in Resources */, + AEE498F72A85AD86000DDF8E /* Basel-Book.ttf in Resources */, + 0767E02F2A61BBDC0042ADA2 /* Basel-Regular.otf in Resources */, + 0767E0352A61BBDC0042ADA2 /* Basel-Bold.otf in Resources */, + 0743218C2A82C6C000F8518D /* Basel-Book.otf in Resources */, + 681301B12A3726EE00A5BF43 /* onboarding_dark.riv in Resources */, + 0767E0312A61BBDC0042ADA2 /* Basel-Semibold.otf in Resources */, + 0767E0332A61BBDC0042ADA2 /* Basel-Medium.otf in Resources */, + D3B63ACA9B0C42F68080B080 /* InputMono-Regular.ttf in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F35AFD3927EE49990011A725 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + FD54D51D296C79A4007A37E9 /* GoogleServiceInfo in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export PROJECT_ROOT=$PWD/..\nexport EXTRA_PACKAGER_ARGS=\"--sourcemap-output $PROJECT_ROOT/main.jsbundle.map\"\n\nset -e\n\nWITH_ENVIRONMENT=\"../../../node_modules/react-native/scripts/xcode/with-environment.sh\"\nREACT_NATIVE_XCODE=\"../../../node_modules/react-native/scripts/react-native-xcode.sh\"\nSENTRY_CLI=\"../../../node_modules/@sentry/cli/bin/sentry-cli\"\n\nif [[ -n \"$SENTRY_AUTH_TOKEN\" && -n \"$SENTRY_ORG\" && -n \"$SENTRY_PROJECT\" ]]; then\n /bin/sh -c \"$WITH_ENVIRONMENT \\\"$SENTRY_CLI react-native xcode $REACT_NATIVE_XCODE\\\"\"\n /bin/sh ../../../node_modules/@sentry/react-native/scripts/collect-modules.sh\nelse\n /bin/sh -c \"$WITH_ENVIRONMENT $REACT_NATIVE_XCODE\"\n echo \"One or more required Sentry environment variables (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT) are missing.\"\nfi\n"; + }; + 0312A12D2A77D07B008CAAFD /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Upload Debug Symbols to Sentry"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "INCLUDE_SOURCES_FLAG=\"\"\nif [[ $SENTRY_INCLUDE_NATIVE_SOURCES == \"true\" ]]; then\n INCLUDE_SOURCES_FLAG=\"--include-sources\"\nfi\n\nSENTRY_CLI=\"../../../node_modules/@sentry/cli/bin/sentry-cli\"\n\nif [[ -n \"$SENTRY_AUTH_TOKEN\" && -n \"$SENTRY_ORG\" && -n \"$SENTRY_PROJECT\" ]]; then\n $SENTRY_CLI debug-files upload \"$INCLUDE_SOURCES_FLAG\" \"$DWARF_DSYM_FOLDER_PATH\"\nelse\n echo \"One or more required Sentry environment variables (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT) are missing.\"\nfi\n"; + }; + 11ABDA19EC3508C14907F53A /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1C9BDBE6A5CAB72EB7B13D63 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 1E1D08469926D25C5C2BD365 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-WidgetsCoreTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 1EFBA749AD5679D0BC9F3259 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-WidgetsCore-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 5C77EF938EB03E5ED1947B82 /* [CP-User] [RNFB] Core Configuration */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)", + ); + name = "[CP-User] [RNFB] Core Configuration"; + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "#!/usr/bin/env bash\n#\n# Copyright (c) 2016-present Invertase Limited & Contributors\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this library except in compliance with the License.\n# You may obtain a copy of the License at\n#\n# http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\nset -e\n\n_MAX_LOOKUPS=2;\n_SEARCH_RESULT=''\n_RN_ROOT_EXISTS=''\n_CURRENT_LOOKUPS=1\n_JSON_ROOT=\"'react-native'\"\n_JSON_FILE_NAME='firebase.json'\n_JSON_OUTPUT_BASE64='e30=' # { }\n_CURRENT_SEARCH_DIR=${PROJECT_DIR}\n_PLIST_BUDDY=/usr/libexec/PlistBuddy\n_TARGET_PLIST=\"${BUILT_PRODUCTS_DIR}/${INFOPLIST_PATH}\"\n_DSYM_PLIST=\"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Info.plist\"\n\n# plist arrays\n_PLIST_ENTRY_KEYS=()\n_PLIST_ENTRY_TYPES=()\n_PLIST_ENTRY_VALUES=()\n\nfunction setPlistValue {\n echo \"info: setting plist entry '$1' of type '$2' in file '$4'\"\n ${_PLIST_BUDDY} -c \"Add :$1 $2 '$3'\" $4 || echo \"info: '$1' already exists\"\n}\n\nfunction getFirebaseJsonKeyValue () {\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$1'); puts output[$_JSON_ROOT]['$2']\"\n else\n echo \"\"\n fi;\n}\n\nfunction jsonBoolToYesNo () {\n if [[ $1 == \"false\" ]]; then\n echo \"NO\"\n elif [[ $1 == \"true\" ]]; then\n echo \"YES\"\n else echo \"NO\"\n fi\n}\n\necho \"info: -> RNFB build script started\"\necho \"info: 1) Locating ${_JSON_FILE_NAME} file:\"\n\nif [[ -z ${_CURRENT_SEARCH_DIR} ]]; then\n _CURRENT_SEARCH_DIR=$(pwd)\nfi;\n\nwhile true; do\n _CURRENT_SEARCH_DIR=$(dirname \"$_CURRENT_SEARCH_DIR\")\n if [[ \"$_CURRENT_SEARCH_DIR\" == \"/\" ]] || [[ ${_CURRENT_LOOKUPS} -gt ${_MAX_LOOKUPS} ]]; then break; fi;\n echo \"info: ($_CURRENT_LOOKUPS of $_MAX_LOOKUPS) Searching in '$_CURRENT_SEARCH_DIR' for a ${_JSON_FILE_NAME} file.\"\n _SEARCH_RESULT=$(find \"$_CURRENT_SEARCH_DIR\" -maxdepth 2 -name ${_JSON_FILE_NAME} -print | /usr/bin/head -n 1)\n if [[ ${_SEARCH_RESULT} ]]; then\n echo \"info: ${_JSON_FILE_NAME} found at $_SEARCH_RESULT\"\n break;\n fi;\n _CURRENT_LOOKUPS=$((_CURRENT_LOOKUPS+1))\ndone\n\nif [[ ${_SEARCH_RESULT} ]]; then\n _JSON_OUTPUT_RAW=$(cat \"${_SEARCH_RESULT}\")\n _RN_ROOT_EXISTS=$(ruby -Ku -e \"require 'rubygems';require 'json'; output=JSON.parse('$_JSON_OUTPUT_RAW'); puts output[$_JSON_ROOT]\" || echo '')\n\n if [[ ${_RN_ROOT_EXISTS} ]]; then\n if ! python3 --version >/dev/null 2>&1; then echo \"python3 not found, firebase.json file processing error.\" && exit 1; fi\n _JSON_OUTPUT_BASE64=$(python3 -c 'import json,sys,base64;print(base64.b64encode(bytes(json.dumps(json.loads(open('\"'${_SEARCH_RESULT}'\"', '\"'rb'\"').read())['${_JSON_ROOT}']), '\"'utf-8'\"')).decode())' || echo \"e30=\")\n fi\n\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n\n # config.app_data_collection_default_enabled\n _APP_DATA_COLLECTION_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_data_collection_default_enabled\")\n if [[ $_APP_DATA_COLLECTION_ENABLED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseDataCollectionDefaultEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_DATA_COLLECTION_ENABLED\")\")\n fi\n\n # config.analytics_auto_collection_enabled\n _ANALYTICS_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_auto_collection_enabled\")\n if [[ $_ANALYTICS_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_COLLECTION\")\")\n fi\n\n # config.analytics_collection_deactivated\n _ANALYTICS_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_collection_deactivated\")\n if [[ $_ANALYTICS_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_DEACTIVATED\")\")\n fi\n\n # config.analytics_idfv_collection_enabled\n _ANALYTICS_IDFV_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_idfv_collection_enabled\")\n if [[ $_ANALYTICS_IDFV_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_IDFV_COLLECTION_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_IDFV_COLLECTION\")\")\n fi\n\n # config.analytics_default_allow_ad_personalization_signals\n _ANALYTICS_PERSONALIZATION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"analytics_default_allow_ad_personalization_signals\")\n if [[ $_ANALYTICS_PERSONALIZATION ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_PERSONALIZATION\")\")\n fi\n\n # config.analytics_registration_with_ad_network_enabled\n _ANALYTICS_REGISTRATION_WITH_AD_NETWORK=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_registration_with_ad_network_enabled\")\n if [[ $_ANALYTICS_REGISTRATION_WITH_AD_NETWORK ]]; then\n _PLIST_ENTRY_KEYS+=(\"GOOGLE_ANALYTICS_REGISTRATION_WITH_AD_NETWORK_ENABLED\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_REGISTRATION_WITH_AD_NETWORK\")\")\n fi\n\n # config.google_analytics_automatic_screen_reporting_enabled\n _ANALYTICS_AUTO_SCREEN_REPORTING=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"google_analytics_automatic_screen_reporting_enabled\")\n if [[ $_ANALYTICS_AUTO_SCREEN_REPORTING ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAutomaticScreenReportingEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_ANALYTICS_AUTO_SCREEN_REPORTING\")\")\n fi\n\n # config.perf_auto_collection_enabled\n _PERF_AUTO_COLLECTION=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_auto_collection_enabled\")\n if [[ $_PERF_AUTO_COLLECTION ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_enabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_AUTO_COLLECTION\")\")\n fi\n\n # config.perf_collection_deactivated\n _PERF_DEACTIVATED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"perf_collection_deactivated\")\n if [[ $_PERF_DEACTIVATED ]]; then\n _PLIST_ENTRY_KEYS+=(\"firebase_performance_collection_deactivated\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_PERF_DEACTIVATED\")\")\n fi\n\n # config.messaging_auto_init_enabled\n _MESSAGING_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"messaging_auto_init_enabled\")\n if [[ $_MESSAGING_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseMessagingAutoInitEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_MESSAGING_AUTO_INIT\")\")\n fi\n\n # config.in_app_messaging_auto_colllection_enabled\n _FIAM_AUTO_INIT=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"in_app_messaging_auto_collection_enabled\")\n if [[ $_FIAM_AUTO_INIT ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseInAppMessagingAutomaticDataCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_FIAM_AUTO_INIT\")\")\n fi\n\n # config.app_check_token_auto_refresh\n _APP_CHECK_TOKEN_AUTO_REFRESH=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"app_check_token_auto_refresh\")\n if [[ $_APP_CHECK_TOKEN_AUTO_REFRESH ]]; then\n _PLIST_ENTRY_KEYS+=(\"FirebaseAppCheckTokenAutoRefreshEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"$(jsonBoolToYesNo \"$_APP_CHECK_TOKEN_AUTO_REFRESH\")\")\n fi\n\n # config.crashlytics_disable_auto_disabler - undocumented for now - mainly for debugging, document if becomes useful\n _CRASHLYTICS_AUTO_DISABLE_ENABLED=$(getFirebaseJsonKeyValue \"$_JSON_OUTPUT_RAW\" \"crashlytics_disable_auto_disabler\")\n if [[ $_CRASHLYTICS_AUTO_DISABLE_ENABLED == \"true\" ]]; then\n echo \"Disabled Crashlytics auto disabler.\" # do nothing\n else\n _PLIST_ENTRY_KEYS+=(\"FirebaseCrashlyticsCollectionEnabled\")\n _PLIST_ENTRY_TYPES+=(\"bool\")\n _PLIST_ENTRY_VALUES+=(\"NO\")\n fi\nelse\n _PLIST_ENTRY_KEYS+=(\"firebase_json_raw\")\n _PLIST_ENTRY_TYPES+=(\"string\")\n _PLIST_ENTRY_VALUES+=(\"$_JSON_OUTPUT_BASE64\")\n echo \"warning: A firebase.json file was not found, whilst this file is optional it is recommended to include it to configure firebase services in React Native Firebase.\"\nfi;\n\necho \"info: 2) Injecting Info.plist entries: \"\n\n# Log out the keys we're adding\nfor i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n echo \" -> $i) ${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\"\ndone\n\nfor plist in \"${_TARGET_PLIST}\" \"${_DSYM_PLIST}\" ; do\n if [[ -f \"${plist}\" ]]; then\n\n # paths with spaces break the call to setPlistValue. temporarily modify\n # the shell internal field separator variable (IFS), which normally\n # includes spaces, to consist only of line breaks\n oldifs=$IFS\n IFS=\"\n\"\n\n for i in \"${!_PLIST_ENTRY_KEYS[@]}\"; do\n setPlistValue \"${_PLIST_ENTRY_KEYS[$i]}\" \"${_PLIST_ENTRY_TYPES[$i]}\" \"${_PLIST_ENTRY_VALUES[$i]}\" \"${plist}\"\n done\n\n # restore the original internal field separator value\n IFS=$oldifs\n else\n echo \"warning: A Info.plist build output file was not found (${plist})\"\n fi\ndone\n\necho \"info: <- RNFB build script finished\"\n"; + }; + 7053AB6470927D4AFEB0A4F6 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Uniswap-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8A31DD3836C878BD44BB4A9F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Widgets-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 95C59BDDCB4A750ADC9CF7F1 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Uniswap/Pods-Uniswap-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9D219B3D5B50538611181454 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Uniswap-UniswapTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + D109A6AB136CA8AF093E7EE6 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Uniswap-UniswapTests/Pods-Uniswap-UniswapTests-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + E7B84E3C2FDEB51A179E3876 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-OneSignalNotificationServiceExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + FD10A7F022414F080027D42C /* Start Packager */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Start Packager"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "export RCT_METRO_PORT=\"${RCT_METRO_PORT:=8081}\"\necho \"export RCT_METRO_PORT=${RCT_METRO_PORT}\" > \"${SRCROOT}/../node_modules/react-native/scripts/.packager.env\"\nif [ -z \"${RCT_NO_LAUNCH_PACKAGER+xxx}\" ] ; then\n if nc -w 5 -z localhost ${RCT_METRO_PORT} ; then\n if ! curl -s \"http://localhost:${RCT_METRO_PORT}/status\" | grep -q \"packager-status:running\" ; then\n echo \"Port ${RCT_METRO_PORT} already in use, packager is either not running or not running correctly\"\n exit 2\n fi\n else\n open \"$SRCROOT/../node_modules/react-native/scripts/launchPackager.command\" || echo \"Can't start packager automatically\"\n fi\nfi\n"; + showEnvVarsInLog = 0; + }; + FD54D51B296C780A007A37E9 /* Copy configuration-specific GoogleServices-Info.plist */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy configuration-specific GoogleServices-Info.plist"; + outputFileListPaths = ( + ); + outputPaths = ( + "", + "$(SRCROOT)/GoogleService-Info.plist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\nif [ $CONFIGURATION == \"Release\" ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Prod.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelif [ $CONFIGURATION == 'Debug' ]\nthen\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-Dev.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nelse\n cp \"${SRCROOT}/GoogleServiceInfo/GoogleService-Info-$CONFIGURATION.plist\" \"${SRCROOT}/GoogleService-Info.plist\"\nfi\n"; + }; + FF9AEEB1546D57276CCEE78A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-WidgetIntentExtension-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* UniswapTests.m in Sources */, + 6C8EFC2D2891B99100FBD8EB /* EncryptionHelperTests.swift in Sources */, + 8385A47D3C765B841F450090 /* ExpoModulesProvider.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E23822A44D5BC006AD6C9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 074322392A83E3CA00F8518D /* NftAssetTraitInput.graphql.swift in Sources */, + 9F813EA42AA8FBCF00438D89 /* TransactionType.graphql.swift in Sources */, + 074322342A83E3CA00F8518D /* SchemaConfiguration.swift in Sources */, + 0743222A2A83E3CA00F8518D /* NftBalanceEdge.graphql.swift in Sources */, + 0703EE032A5734A600AED1DA /* UserDefaults.swift in Sources */, + 074322212A83E3CA00F8518D /* NftCollectionConnection.graphql.swift in Sources */, + 0743220A2A83E3CA00F8518D /* SafetyLevel.graphql.swift in Sources */, + 074322402A841BBD00F8518D /* Constants.swift in Sources */, + 0703EE052A57351800AED1DA /* Logging.swift in Sources */, + 074321EB2A83E3CA00F8518D /* TokenDetailsScreenQuery.graphql.swift in Sources */, + 0767E03B2A65D2550042ADA2 /* Styling.swift in Sources */, + 074322092A83E3CA00F8518D /* NftActivityType.graphql.swift in Sources */, + 074322222A83E3CA00F8518D /* NftContract.graphql.swift in Sources */, + 0743220E2A83E3CA00F8518D /* NftOrderEdge.graphql.swift in Sources */, + 074321FB2A83E3CA00F8518D /* NftsQuery.graphql.swift in Sources */, + 074322022A83E3CA00F8518D /* TokenStandard.graphql.swift in Sources */, + 07F0C28F2A5F3E2E00D5353E /* Env.swift in Sources */, + 074321F82A83E3CA00F8518D /* TokenProjectsQuery.graphql.swift in Sources */, + 074322142A83E3CA00F8518D /* TimestampedAmount.graphql.swift in Sources */, + 074322362A83E3CA00F8518D /* NftCollectionsFilterInput.graphql.swift in Sources */, + 0743223B2A83E3CA00F8518D /* NftAssetsFilterInput.graphql.swift in Sources */, + 0DC6ADF02B1E2C100092909C /* PortfolioValueModifier.graphql.swift in Sources */, + 074322282A83E3CA00F8518D /* TokenProjectMarket.graphql.swift in Sources */, + 0743220F2A83E3CA00F8518D /* NftApproveForAll.graphql.swift in Sources */, + 0743223C2A83E3CA00F8518D /* SchemaMetadata.graphql.swift in Sources */, + 074321FA2A83E3CA00F8518D /* PortfolioBalancesQuery.graphql.swift in Sources */, + 074321FC2A83E3CA00F8518D /* TransactionListQuery.graphql.swift in Sources */, + 0743221E2A83E3CA00F8518D /* Portfolio.graphql.swift in Sources */, + 074322242A83E3CA00F8518D /* Amount.graphql.swift in Sources */, + 07F136422A5763480067004F /* Network.swift in Sources */, + 074322352A83E3CA00F8518D /* NftBalancesFilterInput.graphql.swift in Sources */, + 074322132A83E3CA00F8518D /* NftCollection.graphql.swift in Sources */, + 074321F72A83E3CA00F8518D /* ExploreSearchQuery.graphql.swift in Sources */, + 074322052A83E3CA00F8518D /* Chain.graphql.swift in Sources */, + 074322072A83E3CA00F8518D /* TransactionDirection.graphql.swift in Sources */, + 0743221D2A83E3CA00F8518D /* NftOrder.graphql.swift in Sources */, + 074322292A83E3CA00F8518D /* AmountChange.graphql.swift in Sources */, + 074322302A83E3CA00F8518D /* TokenTransfer.graphql.swift in Sources */, + 074322192A83E3CA00F8518D /* NftOrderConnection.graphql.swift in Sources */, + 074322372A83E3CA00F8518D /* ContractInput.graphql.swift in Sources */, + 0743221F2A83E3CA00F8518D /* PageInfo.graphql.swift in Sources */, + 074322252A83E3CA00F8518D /* Query.graphql.swift in Sources */, + 9F813EA22AA8FB8C00438D89 /* SwapOrderDetails.graphql.swift in Sources */, + 074321F92A83E3CA00F8518D /* SelectWalletScreenQuery.graphql.swift in Sources */, + 9F813E9E2AA8FB5700438D89 /* ActivityDetails.graphql.swift in Sources */, + 0767E0382A65C8330042ADA2 /* Colors.swift in Sources */, + 0743220C2A83E3CA00F8518D /* TransactionStatus.graphql.swift in Sources */, + 074321EF2A83E3CA00F8518D /* TokenQuery.graphql.swift in Sources */, + 074322332A83E3CA00F8518D /* Token.graphql.swift in Sources */, + 074322312A83E3CA00F8518D /* NftApproval.graphql.swift in Sources */, + 074322062A83E3CA00F8518D /* TokenSortableField.graphql.swift in Sources */, + 074322272A83E3CA00F8518D /* Dimensions.graphql.swift in Sources */, + 074322002A83E3CA00F8518D /* AssetChange.graphql.swift in Sources */, + 074322232A83E3CA00F8518D /* NftActivityConnection.graphql.swift in Sources */, + 0743221B2A83E3CA00F8518D /* NftCollectionMarket.graphql.swift in Sources */, + 074322162A83E3CA00F8518D /* TokenProject.graphql.swift in Sources */, + 074321ED2A83E3CA00F8518D /* ExploreTokensTabQuery.graphql.swift in Sources */, + 074322032A83E3CA00F8518D /* Currency.graphql.swift in Sources */, + 077E603B2A86D06100ABC4B9 /* MultiplePortfolioBalancesQuery.graphql.swift in Sources */, + 9F29D4ED2B47126D004D003A /* NftBalanceAssetInput.graphql.swift in Sources */, + 074322102A83E3CA00F8518D /* NftAssetTrait.graphql.swift in Sources */, + 074322382A83E3CA00F8518D /* NftActivityFilterInput.graphql.swift in Sources */, + 0743220D2A83E3CA00F8518D /* Image.graphql.swift in Sources */, + 07F5CF752A7020FD00C648A5 /* Format.swift in Sources */, + 0783F7B42A619E7C009ED617 /* UIComponents.swift in Sources */, + 0743220B2A83E3CA00F8518D /* NftMarketplace.graphql.swift in Sources */, + 5EFB78362B1E585000E77EAC /* ConvertQuery.graphql.swift in Sources */, + 074321F62A83E3CA00F8518D /* SearchTokensQuery.graphql.swift in Sources */, + 0743223E2A83E3CA00F8518D /* IContract.graphql.swift in Sources */, + 074321EE2A83E3CA00F8518D /* NftsTabQuery.graphql.swift in Sources */, + 0743222B2A83E3CA00F8518D /* NftActivity.graphql.swift in Sources */, + 074321F52A83E3CA00F8518D /* NftCollectionScreenQuery.graphql.swift in Sources */, + 074321F02A83E3CA00F8518D /* SearchPopularTokensQuery.graphql.swift in Sources */, + 8EE7C0582AFD7B2100E0D9CD /* DescriptionTranslations.graphql.swift in Sources */, + 074322172A83E3CA00F8518D /* NftTransfer.graphql.swift in Sources */, + 074322202A83E3CA00F8518D /* NftAssetEdge.graphql.swift in Sources */, + 077E60392A85587800ABC4B9 /* TokensQuery.graphql.swift in Sources */, + 0743223D2A83E3CA00F8518D /* IAmount.graphql.swift in Sources */, + 074321F32A83E3CA00F8518D /* FavoriteTokenCardQuery.graphql.swift in Sources */, + 074322262A83E3CA00F8518D /* NftAsset.graphql.swift in Sources */, + 074322012A83E3CA00F8518D /* HistoryDuration.graphql.swift in Sources */, + 074322182A83E3CA00F8518D /* TokenApproval.graphql.swift in Sources */, + 07F5CF712A6AD97D00C648A5 /* Chart.swift in Sources */, + 074321F42A83E3CA00F8518D /* NFTItemScreenQuery.graphql.swift in Sources */, + 0743222D2A83E3CA00F8518D /* NftProfile.graphql.swift in Sources */, + 074322122A83E3CA00F8518D /* NftActivityEdge.graphql.swift in Sources */, + 074143402A588F5800A157D3 /* Structs.swift in Sources */, + 074321FE2A83E3CA00F8518D /* TopTokenParts.graphql.swift in Sources */, + 0743222E2A83E3CA00F8518D /* NftCollectionEdge.graphql.swift in Sources */, + 074322152A83E3CA00F8518D /* NftAssetConnection.graphql.swift in Sources */, + 074321F12A83E3CA00F8518D /* TopTokensQuery.graphql.swift in Sources */, + 074321FF2A83E3CA00F8518D /* MobileSchema.graphql.swift in Sources */, + 074321F22A83E3CA00F8518D /* TokenPriceHistoryQuery.graphql.swift in Sources */, + 074322082A83E3CA00F8518D /* NftStandard.graphql.swift in Sources */, + 0743222C2A83E3CA00F8518D /* AssetActivity.graphql.swift in Sources */, + 0743221C2A83E3CA00F8518D /* TokenBalance.graphql.swift in Sources */, + 9F00A43A2B33894C0088A0D0 /* ApplicationContract.graphql.swift in Sources */, + 074321FD2A83E3CA00F8518D /* TransactionHistoryUpdaterQuery.graphql.swift in Sources */, + 07F136402A575EC00067004F /* DataQueries.swift in Sources */, + 074322112A83E3CA00F8518D /* NftBalanceConnection.graphql.swift in Sources */, + 0743221A2A83E3CA00F8518D /* TokenMarket.graphql.swift in Sources */, + 074322322A83E3CA00F8518D /* NftBalance.graphql.swift in Sources */, + 9F813EA02AA8FB7500438D89 /* TransactionDetails.graphql.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072E23892A44D5BD006AD6C9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 074086FA2A703B76006E3053 /* FormatTests.swift in Sources */, + 072E23952A44D5BD006AD6C9 /* WidgetsCoreTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 072F6C1B2A44A32E00DA720A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 072F6C262A44A32E00DA720A /* WidgetsBundle.swift in Sources */, + 072F6C282A44A32E00DA720A /* TokenPriceWidget.swift in Sources */, + 072F6C2D2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 078E79412A55EB3300F59CF2 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 07378F492A5C83ED00D26D3E /* IntentHandler.swift in Sources */, + 0741433E2A588CCC00A157D3 /* TokenPriceWidget.intentdefinition in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8EA8AB3E2AB7ED3C004E7EF3 /* SeedPhraseInputManager.swift in Sources */, + 8EA8AB412AB7ED76004E7EF3 /* AlertTriangleIcon.swift in Sources */, + 8E89C3B22AB8AAA400C84DE5 /* MnemonicTextField.swift in Sources */, + FD7304D028A3650A0085BDEA /* Colors.swift in Sources */, + 8E89C3AF2AB8AAA400C84DE5 /* MnemonicDisplayView.swift in Sources */, + 9FEC9B8B2A858CF1003CD019 /* AppDelegate.m in Sources */, + 6BC7D0802B5FF02400617C95 /* EncryptionUtils.swift in Sources */, + 8EA8AB3B2AB7ED3C004E7EF3 /* SeedPhraseInputManager.m in Sources */, + 6CA91BDB2A95223C00C4063E /* RNEthersRS.swift in Sources */, + 8EA8AB3C2AB7ED3C004E7EF3 /* SeedPhraseInputViewModel.swift in Sources */, + 072F6C2E2A44A32F00DA720A /* TokenPriceWidget.intentdefinition in Sources */, + 8E89C3B12AB8AAA400C84DE5 /* MnemonicConfirmationWordBankView.swift in Sources */, + 07B0676D2A7D6EC8001DD9B9 /* RNWidgets.m in Sources */, + 8EBFB1552ABA6AA6006B32A8 /* PasteIcon.swift in Sources */, + 6BC7D07F2B5FF02400617C95 /* ScantasticEncryption.swift in Sources */, + 8E89C3B02AB8AAA400C84DE5 /* MnemonicWordView.swift in Sources */, + 07B0676C2A7D6EC8001DD9B9 /* RNWidgets.swift in Sources */, + 8E89C3AE2AB8AAA400C84DE5 /* MnemonicConfirmationView.swift in Sources */, + 8ED0562C2AA78E2C009BD5A2 /* ScrollFadeExtensions.swift in Sources */, + 6CA91BE22A95226200C4063E /* EncryptionHelper.swift in Sources */, + 9FCEBF002A95A8E00079EDDB /* RNWalletConnect.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + 6CA91BE32A95226200C4063E /* RNCloudStorageBackupsManager.swift in Sources */, + 9FCEBF042A95A99C0079EDDB /* RCTThemeModule.m in Sources */, + 9FCEBF012A95A8E00079EDDB /* RNWalletConnect.swift in Sources */, + 6CA91BDC2A95223C00C4063E /* RnEthersRS.m in Sources */, + 6CA91BE12A95226200C4063E /* RNCloudStorageBackupsManager.m in Sources */, + 6BC7D07E2B5FF02400617C95 /* ScantasticEncryption.m in Sources */, + A3F0A5B1272B1DFA00895B25 /* KeychainSwiftDistrib.swift in Sources */, + 8E89C3B42AB8AAA400C84DE5 /* MnemonicConfirmationManager.m in Sources */, + 1440B371A1C9A42F3E91DAAE /* ExpoModulesProvider.swift in Sources */, + 8E89C3B32AB8AAA400C84DE5 /* MnemonicDisplayManager.m in Sources */, + 8EA8AB3D2AB7ED3C004E7EF3 /* SeedPhraseInputView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F35AFD3727EE49990011A725 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F35AFD3E27EE49990011A725 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* Uniswap */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; + 0703EE092A57355400AED1DA /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 072E23852A44D5BC006AD6C9 /* WidgetsCore */; + targetProxy = 0703EE082A57355400AED1DA /* PBXContainerItemProxy */; + }; + 072E23902A44D5BD006AD6C9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 072E23852A44D5BC006AD6C9 /* WidgetsCore */; + targetProxy = 072E238F2A44D5BD006AD6C9 /* PBXContainerItemProxy */; + }; + 072E23922A44D5BD006AD6C9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* Uniswap */; + targetProxy = 072E23912A44D5BD006AD6C9 /* PBXContainerItemProxy */; + }; + 072F6C302A44A32F00DA720A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 072F6C1E2A44A32E00DA720A /* Widgets */; + targetProxy = 072F6C2F2A44A32F00DA720A /* PBXContainerItemProxy */; + }; + 078E794D2A55EB3300F59CF2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 078E79442A55EB3300F59CF2 /* WidgetIntentExtension */; + targetProxy = 078E794C2A55EB3300F59CF2 /* PBXContainerItemProxy */; + }; + 9F7898062A819AA2004D5A98 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 072E23852A44D5BC006AD6C9 /* WidgetsCore */; + targetProxy = 9F7898052A819AA2004D5A98 /* PBXContainerItemProxy */; + }; + 9F7898172A819D62004D5A98 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 072E23852A44D5BC006AD6C9 /* WidgetsCore */; + targetProxy = 9F7898162A819D62004D5A98 /* PBXContainerItemProxy */; + }; + F35AFD4127EE49990011A725 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F35AFD3A27EE49990011A725 /* OneSignalNotificationServiceExtension */; + targetProxy = F35AFD4027EE49990011A725 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EC813AD85060898D4DD580C5 /* Pods-Uniswap-UniswapTests.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = UniswapTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/Uniswap"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F5F2751F56BDA3D0330E56BE /* Pods-Uniswap-UniswapTests.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + INFOPLIST_FILE = UniswapTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/Uniswap"; + }; + name = Release; + }; + 072E239C2A44D5BD006AD6C9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 378A735DFD72359F5CC16F29 /* Pods-WidgetsCore.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = JH3UHGZD75; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 072E239D2A44D5BD006AD6C9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C286F39E616497EA6E1C3010 /* Pods-WidgetsCore.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = JH3UHGZD75; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 072E239E2A44D5BD006AD6C9 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 29F65370E45F33BCA37F7E02 /* Pods-WidgetsCore.dev.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = JH3UHGZD75; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Dev; + }; + 072E239F2A44D5BD006AD6C9 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A721D9DBF345F037F45E92BE /* Pods-WidgetsCore.beta.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = JH3UHGZD75; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCore; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Beta; + }; + 072E23A02A44D5BD006AD6C9 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9BF90CF492394DF50D3BAF76 /* Pods-WidgetsCoreTests.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Uniswap"; + }; + name = Debug; + }; + 072E23A12A44D5BD006AD6C9 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F9D639068E109DD74CBEDF92 /* Pods-WidgetsCoreTests.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Uniswap"; + }; + name = Release; + }; + 072E23A22A44D5BD006AD6C9 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BD152738B3E6969A7A42D13E /* Pods-WidgetsCoreTests.dev.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Uniswap"; + }; + name = Dev; + }; + 072E23A32A44D5BD006AD6C9 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8816FB4EC77AF22919705454 /* Pods-WidgetsCoreTests.beta.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = schemes.WidgetsCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Uniswap"; + }; + name = Beta; + }; + 072F6C322A44A32F00DA720A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1FB4F5FD2BB9AE4452EA8476 /* Pods-Widgets.debug.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Widgets/Widgets.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Widgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Widgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 072F6C332A44A32F00DA720A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B4CFF4B2DE4CB59FAD9E8C27 /* Pods-Widgets.release.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Widgets/Widgets.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Widgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Widgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.widgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.widgets"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 072F6C342A44A32F00DA720A /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = BD41390986108FC8CFA45445 /* Pods-Widgets.dev.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Widgets/Widgets.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Widgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Widgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.widgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev.widgets"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Dev; + }; + 072F6C352A44A32F00DA720A /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F88A7DA00DA4A893EE80B183 /* Pods-Widgets.beta.xcconfig */; + buildSettings = { + APPLICATION_EXTENSION_API_ONLY = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = Widgets/Widgets.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Widgets/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = Widgets; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.widgets; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta.widgets"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Beta; + }; + 078E79502A55EB3400F59CF2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4E788CB77B4CFEAC6E8FFB3A /* Pods-WidgetIntentExtension.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WidgetIntentExtension/WidgetIntentExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetIntentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetIntentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + 078E79512A55EB3400F59CF2 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1A4F128CAFB3E71F3B2F3D90 /* Pods-WidgetIntentExtension.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WidgetIntentExtension/WidgetIntentExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetIntentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetIntentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.WidgetIntentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.WidgetIntentExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + 078E79522A55EB3400F59CF2 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 68FD07BE7700B63D569EB256 /* Pods-WidgetIntentExtension.dev.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WidgetIntentExtension/WidgetIntentExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetIntentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetIntentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.WidgetIntentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev.WidgetIntentExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Dev; + }; + 078E79532A55EB3400F59CF2 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 63F7391CE0D5231AE38192F9 /* Pods-WidgetIntentExtension.beta.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = WidgetIntentExtension/WidgetIntentExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = WidgetIntentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = WidgetIntentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.WidgetIntentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta.WidgetIntentExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Beta; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A87B4C3727F6D91B4DAABF14 /* Pods-Uniswap.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; + BUNDLE_ID_SUFFIX = .dev; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Uniswap/Uniswap.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JH3UHGZD75; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Uniswap/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.24; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_NAME = Uniswap; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FD72162492D6B68BBC15F3ED /* Pods-Uniswap.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; + BUNDLE_ID_SUFFIX = ""; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Uniswap/Uniswap.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JH3UHGZD75; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Uniswap/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.24; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_NAME = Uniswap; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile"; + SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = CGCYLJG7GA; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = CGCYLJG7GA; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F35AFD4427EE49990011A725 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C7C23EDACE3BC348607A0E83 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + F35AFD4527EE49990011A725 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CA37BC1BA471C071B5090928 /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.OneSignalNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.OneSignalNotificationServiceExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; + FDB6FD3C294D3A6E00C7B822 /* Beta */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = CGCYLJG7GA; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Beta; + }; + FDB6FD3D294D3A6E00C7B822 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB423A22FFE9EFFCA11E9E1A /* Pods-Uniswap.beta.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; + BUNDLE_ID_SUFFIX = .beta; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Uniswap/Uniswap.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JH3UHGZD75; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Uniswap/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.24; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_NAME = Uniswap; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta"; + SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Beta; + }; + FDB6FD3E294D3A6E00C7B822 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E8A8C1523EA65F88E3DC16CD /* Pods-Uniswap-UniswapTests.beta.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + INFOPLIST_FILE = UniswapTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/Uniswap"; + }; + name = Beta; + }; + FDB6FD3F294D3A6E00C7B822 /* Beta */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 811E7ED33487936333408382 /* Pods-OneSignalNotificationServiceExtension.beta.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.beta.OneSignalNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.beta.OneSignalNotificationServiceExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Beta; + }; + FDB6FD40294D3A8200C7B822 /* Dev */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_CXX_LANGUAGE_STANDARD = "c++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + DEVELOPMENT_TEAM = CGCYLJG7GA; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + /usr/lib/swift, + "$(inherited)", + ); + LIBRARY_SEARCH_PATHS = ( + "$(SDKROOT)/usr/lib/swift", + "\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"", + "\"$(inherited)\"", + ); + MTL_ENABLE_DEBUG_INFO = NO; + REACT_NATIVE_PATH = "${PODS_ROOT}/../../../../node_modules/react-native"; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Dev; + }; + FDB6FD41294D3A8200C7B822 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B398F0A4DA17159EA0E2D6DF /* Pods-Uniswap.dev.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon${BUNDLE_ID_SUFFIX}"; + ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = ""; + BUNDLE_ID_SUFFIX = .dev; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Uniswap/Uniswap.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JH3UHGZD75; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Uniswap/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.24; + OTHER_LDFLAGS = ( + "$(inherited)", + "-ObjC", + "-lc++", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "com.uniswap.mobile${BUNDLE_ID_SUFFIX}"; + PRODUCT_NAME = Uniswap; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev"; + SWIFT_OBJC_BRIDGING_HEADER = "Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Dev; + }; + FDB6FD42294D3A8200C7B822 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 407451BE42C5147EBB181687 /* Pods-Uniswap-UniswapTests.dev.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + INFOPLIST_FILE = UniswapTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + OTHER_LDFLAGS = ( + "-ObjC", + "-lc++", + "$(inherited)", + ); + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Uniswap.app/Uniswap"; + }; + name = Dev; + }; + FDB6FD43294D3A8200C7B822 /* Dev */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 78C41378CE69EDD92DFAD177 /* Pods-OneSignalNotificationServiceExtension.dev.xcconfig */; + buildSettings = { + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; + CODE_SIGN_IDENTITY = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = JH3UHGZD75; + GCC_C_LANGUAGE_STANDARD = gnu11; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.24; + MTL_FAST_MATH = YES; + OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; + PRODUCT_BUNDLE_IDENTIFIER = com.uniswap.mobile.dev.OneSignalNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.uniswap.mobile.dev.OneSignalNotificationServiceExtension"; + SKIP_INSTALL = YES; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Dev; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "UniswapTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + FDB6FD42294D3A8200C7B822 /* Dev */, + FDB6FD3E294D3A6E00C7B822 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 072E23A42A44D5BD006AD6C9 /* Build configuration list for PBXNativeTarget "WidgetsCore" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 072E239C2A44D5BD006AD6C9 /* Debug */, + 072E239D2A44D5BD006AD6C9 /* Release */, + 072E239E2A44D5BD006AD6C9 /* Dev */, + 072E239F2A44D5BD006AD6C9 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 072E23A52A44D5BD006AD6C9 /* Build configuration list for PBXNativeTarget "WidgetsCoreTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 072E23A02A44D5BD006AD6C9 /* Debug */, + 072E23A12A44D5BD006AD6C9 /* Release */, + 072E23A22A44D5BD006AD6C9 /* Dev */, + 072E23A32A44D5BD006AD6C9 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 072F6C362A44A32F00DA720A /* Build configuration list for PBXNativeTarget "Widgets" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 072F6C322A44A32F00DA720A /* Debug */, + 072F6C332A44A32F00DA720A /* Release */, + 072F6C342A44A32F00DA720A /* Dev */, + 072F6C352A44A32F00DA720A /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 078E794F2A55EB3400F59CF2 /* Build configuration list for PBXNativeTarget "WidgetIntentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 078E79502A55EB3400F59CF2 /* Debug */, + 078E79512A55EB3400F59CF2 /* Release */, + 078E79522A55EB3400F59CF2 /* Dev */, + 078E79532A55EB3400F59CF2 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Uniswap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + FDB6FD41294D3A8200C7B822 /* Dev */, + FDB6FD3D294D3A6E00C7B822 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "Uniswap" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + FDB6FD40294D3A8200C7B822 /* Dev */, + FDB6FD3C294D3A6E00C7B822 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F35AFD4327EE49990011A725 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F35AFD4427EE49990011A725 /* Debug */, + F35AFD4527EE49990011A725 /* Release */, + FDB6FD43294D3A8200C7B822 /* Dev */, + FDB6FD3F294D3A6E00C7B822 /* Beta */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme new file mode 100644 index 0000000..c5e27e3 --- /dev/null +++ b/apps/mobile/ios/Uniswap.xcodeproj/xcshareddata/xcschemes/Uniswap.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Uniswap.xcworkspace/contents.xcworkspacedata b/apps/mobile/ios/Uniswap.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..6c288ca --- /dev/null +++ b/apps/mobile/ios/Uniswap.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/apps/mobile/ios/Uniswap.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/apps/mobile/ios/Uniswap/AppDelegate.h b/apps/mobile/ios/Uniswap/AppDelegate.h new file mode 100644 index 0000000..3151f6d --- /dev/null +++ b/apps/mobile/ios/Uniswap/AppDelegate.h @@ -0,0 +1,7 @@ +#import +#import +#import + +@interface AppDelegate : RCTAppDelegate + +@end diff --git a/apps/mobile/ios/Uniswap/AppDelegate.m b/apps/mobile/ios/Uniswap/AppDelegate.m new file mode 100644 index 0000000..d7e455a --- /dev/null +++ b/apps/mobile/ios/Uniswap/AppDelegate.m @@ -0,0 +1,105 @@ +#import "AppDelegate.h" + +#import "RNFBAppCheckModule.h" +#import + +#import "Uniswap-Swift.h" + +#import +#import +#import +#import "RNSplashScreen.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + // Must be first line in startup routine + [ReactNativePerformance onAppStarted]; + + // Must be before [FIRApp configure], initializes RNFBAppCheckModule + [RNFBAppCheckModule sharedInstance]; + [FIRApp configure]; + + // This is needed so universal links opened from OneSignal notifications navigate to the proper page. + // More details here: + // https://documentation.onesignal.com/v7.0/docs/react-native-sdk in the deep linking warning section. + NSMutableDictionary *newLaunchOptions = [NSMutableDictionary dictionaryWithDictionary:launchOptions]; + if (launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]) { + NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; + if (remoteNotif[@"custom"] && remoteNotif[@"custom"][@"u"]) { + NSString *initialURL = remoteNotif[@"custom"][@"u"]; + if (!launchOptions[UIApplicationLaunchOptionsURLKey]) { + newLaunchOptions[UIApplicationLaunchOptionsURLKey] = [NSURL URLWithString:initialURL]; + } + } + } + + self.moduleName = @"Uniswap"; + self.initialProps = @{}; + + [self.window makeKeyAndVisible]; + + if (@available(iOS 13.0, *)) { + self.window.rootViewController.view.backgroundColor = [UIColor systemBackgroundColor]; + } else { + self.window.rootViewController.view.backgroundColor = [UIColor whiteColor]; + } + + [super application:application didFinishLaunchingWithOptions:newLaunchOptions]; + + [RNSplashScreen show]; + + [[RCTI18nUtil sharedInstance] allowRTL:NO]; + + return YES; +} + +- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge +{ +#if DEBUG + return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"]; +#else + return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; +#endif +} + +/// This method controls whether the `concurrentRoot`feature of React18 is turned on or off. +/// +/// @see: https://reactjs.org/blog/2022/03/29/react-v18.html +/// @note: This requires to be rendering on Fabric (i.e. on the New Architecture). +/// @return: `true` if the `concurrentRoot` feature is enabled. Otherwise, it returns `false`. +- (BOOL)concurrentRootEnabled +{ + return true; +} + +// Enable deep linking +- (BOOL)application:(UIApplication *)application + openURL:(NSURL *)url + options:(NSDictionary *)options +{ + return [RCTLinkingManager application:application openURL:url options:options]; +} + +// Enable universal links +- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity + restorationHandler:(nonnull void (^)(NSArray> * _Nullable))restorationHandler +{ + return [RCTLinkingManager application:application + continueUserActivity:userActivity + restorationHandler:restorationHandler]; +} + +// Disable 3rd party keyboard +-(BOOL)application:(UIApplication *)application shouldAllowExtensionPointIdentifier:(NSString *)extensionPointIdentifier +{ + if (extensionPointIdentifier == UIApplicationKeyboardExtensionPointIdentifier) + { + return NO; + } + + return YES; +} + +@end diff --git a/apps/mobile/ios/Uniswap/Colors.swift b/apps/mobile/ios/Uniswap/Colors.swift new file mode 100644 index 0000000..533fc85 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.swift @@ -0,0 +1,20 @@ +// +// Colors.swift +// Uniswap +// +// Created by Thomas Thachil on 8/8/22. +// + +import SwiftUI + +struct Colors { + static let surface2 = Color("surface2") + static let surface3 = Color("surface3") + static let neutral1 = Color("neutral1") + static let neutral2 = Color("neutral2") + static let neutral3 = Color("neutral3") + static let accent1 = Color("accent1") + static let statusCritical = Color("statusCritical") + static let statusSuccess = Color("statusSuccess") + static let onboardingBlue = Color("onboardingBlue") +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json new file mode 100644 index 0000000..72e5d6d --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/accent1.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "114", + "red" : "252" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "114", + "red" : "252" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json new file mode 100644 index 0000000..f2ef880 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral1.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "34", + "green" : "34", + "red" : "34" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json new file mode 100644 index 0000000..36e475a --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral2.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x7D", + "green" : "0x7D", + "red" : "0x7D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x9B", + "green" : "0x9B", + "red" : "0x9B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json new file mode 100644 index 0000000..7529f06 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/neutral3.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "206", + "green" : "206", + "red" : "206" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "94", + "green" : "94", + "red" : "94" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json new file mode 100644 index 0000000..c282d29 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/onboardingBlue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "251", + "green" : "130", + "red" : "76" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json new file mode 100644 index 0000000..d226ce2 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/statusCritical.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "82", + "green" : "95", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "82", + "green" : "95", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json new file mode 100644 index 0000000..a2260c6 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/statusSuccess.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6B", + "green" : "0xB6", + "red" : "0x40" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x6B", + "green" : "0xB6", + "red" : "0x40" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json new file mode 100644 index 0000000..6c88e84 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/surface2.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF9", + "green" : "0xF9", + "red" : "0xF9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF9", + "green" : "0xF9", + "red" : "0xF9" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x1B", + "green" : "0x1B", + "red" : "0x1B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json b/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json new file mode 100644 index 0000000..34aff13 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Colors.xcassets/surface3.colorset/Contents.json @@ -0,0 +1,56 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0x22", + "green" : "0x22", + "red" : "0x22" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.050", + "blue" : "0x22", + "green" : "0x22", + "red" : "0x22" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.120", + "blue" : "0xFF", + "green" : "0xFF", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Icons/AlertTriangleIcon.swift b/apps/mobile/ios/Uniswap/Icons/AlertTriangleIcon.swift new file mode 100644 index 0000000..7ab556d --- /dev/null +++ b/apps/mobile/ios/Uniswap/Icons/AlertTriangleIcon.swift @@ -0,0 +1,40 @@ +// +// AlertTriangleIcon.swift +// Uniswap +// +// Created by Gary Ye on 9/16/23. +// + +import SwiftUI + +struct AlertTriangeIcon: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.90031*width, y: 0.71468*height)) + path.addLine(to: CGPoint(x: 0.62502*width, y: 0.19983*height)) + path.addCurve(to: CGPoint(x: 0.37502*width, y: 0.19983*height), control1: CGPoint(x: 0.57168*width, y: 0.10008*height), control2: CGPoint(x: 0.42839*width, y: 0.10008*height)) + path.addLine(to: CGPoint(x: 0.09973*width, y: 0.71468*height)) + path.addCurve(to: CGPoint(x: 0.22119*width, y: 0.91667*height), control1: CGPoint(x: 0.05081*width, y: 0.80617*height), control2: CGPoint(x: 0.11723*width, y: 0.91667*height)) + path.addLine(to: CGPoint(x: 0.77885*width, y: 0.91667*height)) + path.addCurve(to: CGPoint(x: 0.90031*width, y: 0.71468*height), control1: CGPoint(x: 0.88276*width, y: 0.91667*height), control2: CGPoint(x: 0.94923*width, y: 0.80613*height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.46877*width, y: 0.41667*height)) + path.addCurve(to: CGPoint(x: 0.50002*width, y: 0.38542*height), control1: CGPoint(x: 0.46877*width, y: 0.39942*height), control2: CGPoint(x: 0.48277*width, y: 0.38542*height)) + path.addCurve(to: CGPoint(x: 0.53127*width, y: 0.41667*height), control1: CGPoint(x: 0.51727*width, y: 0.38542*height), control2: CGPoint(x: 0.53127*width, y: 0.39942*height)) + path.addLine(to: CGPoint(x: 0.53127*width, y: 0.58334*height)) + path.addCurve(to: CGPoint(x: 0.50002*width, y: 0.61459*height), control1: CGPoint(x: 0.53127*width, y: 0.60059*height), control2: CGPoint(x: 0.51727*width, y: 0.61459*height)) + path.addCurve(to: CGPoint(x: 0.46877*width, y: 0.58334*height), control1: CGPoint(x: 0.48277*width, y: 0.61459*height), control2: CGPoint(x: 0.46877*width, y: 0.60059*height)) + path.addLine(to: CGPoint(x: 0.46877*width, y: 0.41667*height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.50085*width, y: 0.75*height)) + path.addCurve(to: CGPoint(x: 0.45897*width, y: 0.70834*height), control1: CGPoint(x: 0.47785*width, y: 0.75*height), control2: CGPoint(x: 0.45897*width, y: 0.73134*height)) + path.addCurve(to: CGPoint(x: 0.50043*width, y: 0.66667*height), control1: CGPoint(x: 0.45897*width, y: 0.68534*height), control2: CGPoint(x: 0.47743*width, y: 0.66667*height)) + path.addLine(to: CGPoint(x: 0.50085*width, y: 0.66667*height)) + path.addCurve(to: CGPoint(x: 0.54252*width, y: 0.70834*height), control1: CGPoint(x: 0.52389*width, y: 0.66667*height), control2: CGPoint(x: 0.54252*width, y: 0.68534*height)) + path.addCurve(to: CGPoint(x: 0.50085*width, y: 0.75*height), control1: CGPoint(x: 0.54252*width, y: 0.73134*height), control2: CGPoint(x: 0.52385*width, y: 0.75*height)) + path.closeSubpath() + return path + } +} diff --git a/apps/mobile/ios/Uniswap/Icons/PasteIcon.swift b/apps/mobile/ios/Uniswap/Icons/PasteIcon.swift new file mode 100644 index 0000000..8c75cbc --- /dev/null +++ b/apps/mobile/ios/Uniswap/Icons/PasteIcon.swift @@ -0,0 +1,43 @@ +// +// PasteIcon.swift +// Uniswap +// +// Created by Gary Ye on 9/19/23. +// + +import SwiftUI + +struct PasteIcon: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + let width = rect.size.width + let height = rect.size.height + path.move(to: CGPoint(x: 0.83332*width, y: 0.33333*height)) + path.addLine(to: CGPoint(x: 0.83332*width, y: 0.75*height)) + path.addCurve(to: CGPoint(x: 0.70832*width, y: 0.875*height), control1: CGPoint(x: 0.83332*width, y: 0.83333*height), control2: CGPoint(x: 0.79166*width, y: 0.875*height)) + path.addLine(to: CGPoint(x: 0.29166*width, y: 0.875*height)) + path.addCurve(to: CGPoint(x: 0.16666*width, y: 0.75*height), control1: CGPoint(x: 0.20832*width, y: 0.875*height), control2: CGPoint(x: 0.16666*width, y: 0.83333*height)) + path.addLine(to: CGPoint(x: 0.16666*width, y: 0.33333*height)) + path.addCurve(to: CGPoint(x: 0.26103*width, y: 0.21071*height), control1: CGPoint(x: 0.16666*width, y: 0.26075*height), control2: CGPoint(x: 0.19799*width, y: 0.21987*height)) + path.addCurve(to: CGPoint(x: 0.27082*width, y: 0.21921*height), control1: CGPoint(x: 0.26607*width, y: 0.20996*height), control2: CGPoint(x: 0.27082*width, y: 0.21412*height)) + path.addLine(to: CGPoint(x: 0.27082*width, y: 0.22913*height)) + path.addCurve(to: CGPoint(x: 0.39582*width, y: 0.35413*height), control1: CGPoint(x: 0.27082*width, y: 0.30496*height), control2: CGPoint(x: 0.31999*width, y: 0.35413*height)) + path.addLine(to: CGPoint(x: 0.60416*width, y: 0.35413*height)) + path.addCurve(to: CGPoint(x: 0.72916*width, y: 0.22913*height), control1: CGPoint(x: 0.67999*width, y: 0.35413*height), control2: CGPoint(x: 0.72916*width, y: 0.30496*height)) + path.addLine(to: CGPoint(x: 0.72916*width, y: 0.21921*height)) + path.addCurve(to: CGPoint(x: 0.73895*width, y: 0.21071*height), control1: CGPoint(x: 0.72916*width, y: 0.21412*height), control2: CGPoint(x: 0.73395*width, y: 0.20996*height)) + path.addCurve(to: CGPoint(x: 0.83332*width, y: 0.33333*height), control1: CGPoint(x: 0.80199*width, y: 0.21987*height), control2: CGPoint(x: 0.83332*width, y: 0.26075*height)) + path.closeSubpath() + path.move(to: CGPoint(x: 0.39582*width, y: 0.29167*height)) + path.addLine(to: CGPoint(x: 0.60416*width, y: 0.29167*height)) + path.addCurve(to: CGPoint(x: 0.66666*width, y: 0.22917*height), control1: CGPoint(x: 0.64582*width, y: 0.29167*height), control2: CGPoint(x: 0.66666*width, y: 0.27083*height)) + path.addLine(to: CGPoint(x: 0.66666*width, y: 0.1875*height)) + path.addCurve(to: CGPoint(x: 0.60416*width, y: 0.125*height), control1: CGPoint(x: 0.66666*width, y: 0.14583*height), control2: CGPoint(x: 0.64582*width, y: 0.125*height)) + path.addLine(to: CGPoint(x: 0.39582*width, y: 0.125*height)) + path.addCurve(to: CGPoint(x: 0.33332*width, y: 0.1875*height), control1: CGPoint(x: 0.35416*width, y: 0.125*height), control2: CGPoint(x: 0.33332*width, y: 0.14583*height)) + path.addLine(to: CGPoint(x: 0.33332*width, y: 0.22917*height)) + path.addCurve(to: CGPoint(x: 0.39582*width, y: 0.29167*height), control1: CGPoint(x: 0.33332*width, y: 0.27083*height), control2: CGPoint(x: 0.35416*width, y: 0.29167*height)) + path.closeSubpath() + return path + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-1024@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-1024@1x.png new file mode 100644 index 0000000..80f9b5e Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-1024@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png new file mode 100644 index 0000000..bd033fc Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png new file mode 100644 index 0000000..c9bbb99 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-20@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@1x.png new file mode 100644 index 0000000..4a62065 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png new file mode 100644 index 0000000..62dd989 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png new file mode 100644 index 0000000..c189fad Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-29@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png new file mode 100644 index 0000000..cc62929 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png new file mode 100644 index 0000000..5bbbe58 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-40@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x.png new file mode 100644 index 0000000..5bbbe58 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x.png new file mode 100644 index 0000000..f57342f Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/AppIcon-60@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..319cc31 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "filename" : "AppIcon-20@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon-29@1x.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon-40@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon-60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "iPad-AppIcon-20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "iPad-AppIcon-20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "iPad-AppIcon-29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "iPad-AppIcon-29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "iPad-AppIcon-40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "iPad-AppIcon-40@2x-1.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "iPad-AppIcon-76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "iPad-AppIcon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "iPad-AppIcon-83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "AppIcon-1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@1x.png new file mode 100644 index 0000000..16f36b6 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@2x.png new file mode 100644 index 0000000..bd033fc Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-20@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@1x.png new file mode 100644 index 0000000..4a62065 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@2x.png new file mode 100644 index 0000000..62dd989 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-29@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@1x.png new file mode 100644 index 0000000..bd033fc Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@2x-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@2x-1.png new file mode 100644 index 0000000..cc62929 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-40@2x-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@1x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@1x.png new file mode 100644 index 0000000..bce9763 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@1x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@2x.png new file mode 100644 index 0000000..d16b588 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-76@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-83.5@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-83.5@2x.png new file mode 100644 index 0000000..8caf3ea Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.appiconset/iPad-AppIcon-83.5@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/100.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/100.png new file mode 100644 index 0000000..577dd0d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/100.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/1024.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/1024.png new file mode 100644 index 0000000..6cfe74d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/1024.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/114.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/114.png new file mode 100644 index 0000000..a3780ff Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/114.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/120.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/120.png new file mode 100644 index 0000000..fec5d6b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/120.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/128.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/128.png new file mode 100644 index 0000000..7101e40 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/128.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/144.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/144.png new file mode 100644 index 0000000..637553f Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/144.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/152.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/152.png new file mode 100644 index 0000000..98ed1a7 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/152.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/16.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/16.png new file mode 100644 index 0000000..f3c9114 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/16.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/167.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/167.png new file mode 100644 index 0000000..42ce7da Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/167.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/172.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/172.png new file mode 100644 index 0000000..eeb1674 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/172.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/180.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/180.png new file mode 100644 index 0000000..9277f46 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/180.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/196.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/196.png new file mode 100644 index 0000000..dcd539b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/196.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/20.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/20.png new file mode 100644 index 0000000..2cce0e3 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/20.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/216.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/216.png new file mode 100644 index 0000000..c61cb4a Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/216.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/256.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/256.png new file mode 100644 index 0000000..06c7972 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/256.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/29.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/29.png new file mode 100644 index 0000000..79a85b6 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/29.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/32.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/32.png new file mode 100644 index 0000000..ccd6ea8 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/32.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/40.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/40.png new file mode 100644 index 0000000..94082ee Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/40.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/48.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/48.png new file mode 100644 index 0000000..dceaeeb Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/48.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/50.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/50.png new file mode 100644 index 0000000..bd818ba Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/50.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/512.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/512.png new file mode 100644 index 0000000..0e16ead Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/512.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/55.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/55.png new file mode 100644 index 0000000..14f1640 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/55.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/57.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/57.png new file mode 100644 index 0000000..2e6c9f5 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/57.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/58.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/58.png new file mode 100644 index 0000000..39f4e99 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/58.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/60.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/60.png new file mode 100644 index 0000000..e0bef9d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/60.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/64.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/64.png new file mode 100644 index 0000000..dd3bea3 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/64.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/66.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/66.png new file mode 100644 index 0000000..4fde9c4 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/66.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/72.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/72.png new file mode 100644 index 0000000..7d75238 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/72.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/76.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/76.png new file mode 100644 index 0000000..e6fc227 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/76.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/80.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/80.png new file mode 100644 index 0000000..85f3210 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/80.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/87.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/87.png new file mode 100644 index 0000000..cf77b56 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/87.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/88.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/88.png new file mode 100644 index 0000000..895250b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/88.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/92.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/92.png new file mode 100644 index 0000000..18f2981 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/92.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json new file mode 100644 index 0000000..8e70699 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.beta.appiconset/Contents.json @@ -0,0 +1,346 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, + { + "filename" : "48.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "55.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "58.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "66.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "80.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "88.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "filename" : "92.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "100.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "172.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "196.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "216.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/100.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/100.png new file mode 100644 index 0000000..5ff0306 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/100.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/1024.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/1024.png new file mode 100644 index 0000000..566c96c Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/1024.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/114.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/114.png new file mode 100644 index 0000000..670f7bf Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/114.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/120.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/120.png new file mode 100644 index 0000000..bf30e76 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/120.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/128.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/128.png new file mode 100644 index 0000000..af395d6 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/128.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/144.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/144.png new file mode 100644 index 0000000..0994327 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/144.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/152.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/152.png new file mode 100644 index 0000000..555e167 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/152.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/16.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/16.png new file mode 100644 index 0000000..ffbb91f Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/16.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/167.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/167.png new file mode 100644 index 0000000..b719863 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/167.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/172.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/172.png new file mode 100644 index 0000000..e06d0e2 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/172.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/180.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/180.png new file mode 100644 index 0000000..92017c4 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/180.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/196.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/196.png new file mode 100644 index 0000000..3a3746a Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/196.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/20.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/20.png new file mode 100644 index 0000000..7939c34 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/20.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/216.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/216.png new file mode 100644 index 0000000..8f7f70d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/216.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/256.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/256.png new file mode 100644 index 0000000..2bbfb7b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/256.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/29.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/29.png new file mode 100644 index 0000000..87b9213 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/29.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/32.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/32.png new file mode 100644 index 0000000..0f7ebb7 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/32.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/40.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/40.png new file mode 100644 index 0000000..50945cf Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/40.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/48.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/48.png new file mode 100644 index 0000000..3ba857f Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/48.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/50.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/50.png new file mode 100644 index 0000000..94aeb0d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/50.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/512.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/512.png new file mode 100644 index 0000000..3fc06cf Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/512.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/55.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/55.png new file mode 100644 index 0000000..e3bc66d Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/55.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/57.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/57.png new file mode 100644 index 0000000..3c161e4 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/57.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/58.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/58.png new file mode 100644 index 0000000..e13f32b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/58.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/60.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/60.png new file mode 100644 index 0000000..449a993 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/60.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/64.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/64.png new file mode 100644 index 0000000..4256252 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/64.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/66.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/66.png new file mode 100644 index 0000000..a4bba16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/66.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/72.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/72.png new file mode 100644 index 0000000..79c1ed2 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/72.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/76.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/76.png new file mode 100644 index 0000000..405cef0 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/76.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/80.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/80.png new file mode 100644 index 0000000..2247ca8 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/80.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/87.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/87.png new file mode 100644 index 0000000..e6eb123 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/87.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/88.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/88.png new file mode 100644 index 0000000..2ec3323 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/88.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/92.png b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/92.png new file mode 100644 index 0000000..80b08cc Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/92.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json new file mode 100644 index 0000000..8e70699 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/AppIcon.dev.appiconset/Contents.json @@ -0,0 +1,346 @@ +{ + "images" : [ + { + "filename" : "40.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "60.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "80.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "180.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "20.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "29.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "40.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "80.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "50.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "50x50" + }, + { + "filename" : "100.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "50x50" + }, + { + "filename" : "72.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "72x72" + }, + { + "filename" : "144.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "72x72" + }, + { + "filename" : "76.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "152.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "1024.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "filename" : "16.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "filename" : "32.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "filename" : "64.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "filename" : "128.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "filename" : "256.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "filename" : "512.png", + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "filename" : "1024.png", + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + }, + { + "filename" : "48.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "24x24", + "subtype" : "38mm" + }, + { + "filename" : "55.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "27.5x27.5", + "subtype" : "42mm" + }, + { + "filename" : "58.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "87.png", + "idiom" : "watch", + "role" : "companionSettings", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "66.png", + "idiom" : "watch", + "role" : "notificationCenter", + "scale" : "2x", + "size" : "33x33", + "subtype" : "45mm" + }, + { + "filename" : "80.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "40x40", + "subtype" : "38mm" + }, + { + "filename" : "88.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "44x44", + "subtype" : "40mm" + }, + { + "filename" : "92.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "46x46", + "subtype" : "41mm" + }, + { + "filename" : "100.png", + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "50x50", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "51x51", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "appLauncher", + "scale" : "2x", + "size" : "54x54", + "subtype" : "49mm" + }, + { + "filename" : "172.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "86x86", + "subtype" : "38mm" + }, + { + "filename" : "196.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "98x98", + "subtype" : "42mm" + }, + { + "filename" : "216.png", + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "108x108", + "subtype" : "44mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "117x117", + "subtype" : "45mm" + }, + { + "idiom" : "watch", + "role" : "quickLook", + "scale" : "2x", + "size" : "129x129", + "subtype" : "49mm" + }, + { + "filename" : "1024.png", + "idiom" : "watch-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/Contents.json new file mode 100644 index 0000000..434c96b --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/Contents.json @@ -0,0 +1,89 @@ +{ + "images" : [ + { + "filename" : "splash-light-1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "splash-light.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "splash-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "splash-light@2x-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "splash-light@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "splash-dark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash-light@3x-1.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "splash-light@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "splash-dark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark.png new file mode 100644 index 0000000..405b295 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@2x.png new file mode 100644 index 0000000..baaa0f4 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@3x.png new file mode 100644 index 0000000..36b7305 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-dark@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light-1.png new file mode 100644 index 0000000..4e87c26 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light.png new file mode 100644 index 0000000..4e87c26 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x-1.png new file mode 100644 index 0000000..647f68b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x.png new file mode 100644 index 0000000..647f68b Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@2x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x-1.png new file mode 100644 index 0000000..064bd55 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x.png new file mode 100644 index 0000000..064bd55 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreen.imageset/splash-light@3x.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json new file mode 100644 index 0000000..d1531a3 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/Contents.json @@ -0,0 +1,89 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "background-3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "background-1-dark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "background-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "background-4.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "background-1-dark-1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "background-2.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "light" + } + ], + "filename" : "background-5.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "background-1-dark-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-1.png new file mode 100644 index 0000000..12690b0 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-2.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-2.png new file mode 100644 index 0000000..12690b0 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark-2.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark.png new file mode 100644 index 0000000..12690b0 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1-dark.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-1.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-2.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-2.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-2.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-3.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-3.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-3.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-4.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-4.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-4.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-5.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-5.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background-5.png differ diff --git a/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background.png b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background.png new file mode 100644 index 0000000..ff3dd16 Binary files /dev/null and b/apps/mobile/ios/Uniswap/Images.xcassets/SplashScreenBackground.imageset/background.png differ diff --git a/apps/mobile/ios/Uniswap/Info.plist b/apps/mobile/ios/Uniswap/Info.plist new file mode 100644 index 0000000..768475b --- /dev/null +++ b/apps/mobile/ios/Uniswap/Info.plist @@ -0,0 +1,141 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Uniswap + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconName + AppIcon + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLocalizations + + en + zh-Hans + zh-Hant + fr + ja + pt + es-ES + es-US + es-419 + + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + uniswap + CFBundleURLSchemes + + uniswap + + + + CFBundleTypeRole + Viewer + CFBundleURLName + org.uniswap.wc + CFBundleURLSchemes + + wc + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + ITSAppUsesNonExemptEncryption + + LSApplicationCategoryType + + LSApplicationQueriesSchemes + + itms-apps + itms-beta + + LSRequiresIPhoneOS + + NSAdvertisingAttributionReportEndpoint + https://appsflyer-skadnetwork.com/ + NSAppTransportSecurity + + NSExceptionDomains + + localhost + + NSExceptionAllowsInsecureHTTPLoads + + + + + NSCameraUsageDescription + $(PRODUCT_NAME) Wallet needs access to your Camera to scan QR codes + NSPhotoLibraryUsageDescription + $(PRODUCT_NAME) Wallet needs access to your Camera Roll to choose an avatar for your username + NSFaceIDUsageDescription + Enabling Face ID helps $(PRODUCT_NAME) Wallet keep your assets secure. + NSLocationAlwaysAndWhenInUseUsageDescription + $(PRODUCT_NAME) Wallet does not require access to your location. + NSLocationWhenInUseUsageDescription + $(PRODUCT_NAME) Wallet does not require access to your location. + NSMicrophoneUsageDescription + $(PRODUCT_NAME) Wallet does not require access to the microphone. + NSUbiquitousContainers + + iCloud.Uniswap + + NSUbiquitousContainerIsDocumentScopePublic + + NSUbiquitousContainerName + Uniswap + NSUbiquitousContainerSupportedFolderLevels + Any + + + NSUserActivityTypes + + TokenPriceConfigurationIntent + + OneSignal_suppress_launch_urls + + UIAppFonts + + InputMono-Regular.ttf + Basel-Book.otf + Basel-Medium.otf + Basel-Semibold.otf + Basel-Bold.otf + + UIBackgroundModes + + remote-notification + + UILaunchStoryboardName + SplashScreen.storyboard + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationManager.m b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationManager.m new file mode 100644 index 0000000..d5a6a39 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationManager.m @@ -0,0 +1,30 @@ +// +// RNTMnemonicTestManager.m +// Uniswap +// +// Created by Thomas Thachil 8/1/2022. +// + +#import "Uniswap-Swift.h" +#import +#import "RNSwiftUI-Bridging-Header.h" + +@interface MnemonicConfirmationManager : RCTViewManager +@end + +@implementation MnemonicConfirmationManager +RCT_EXPORT_MODULE() + +RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicConfirmationView); +RCT_EXPORT_SWIFTUI_PROPERTY(shouldShowSmallText, BOOL, MnemonicConfirmationView); +RCT_EXPORT_SWIFTUI_CALLBACK(onConfirmComplete, RCTDirectEventBlock, MnemonicConfirmationView); + +- (UIView *)view +{ + MnemonicConfirmationView *proxy = [[MnemonicConfirmationView alloc] init]; + UIView *view = [proxy view]; + NSMutableDictionary *storage = [MnemonicConfirmationView storage]; + storage[[NSValue valueWithNonretainedObject:view]] = proxy; + return view; +} +@end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationView.swift b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationView.swift new file mode 100644 index 0000000..547f404 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationView.swift @@ -0,0 +1,131 @@ +// +// MnemonicConfirmationView.swift +// Uniswap +// +// Created by Thomas Thachil on 8/1/22. +// + +import React +import SwiftUI + +@objcMembers class MnemonicConfirmationView: NSObject { + private var vc = UIHostingController(rootView: MnemonicConfirmation()) + + static let storage = NSMutableDictionary() + + var mnemonicId: String { + set { vc.rootView.setMnemonicId(mnemonicId: newValue) } + get { return vc.rootView.props.mnemonicId } + } + + var shouldShowSmallText: Bool { + set { vc.rootView.props.shouldShowSmallText = newValue} + get { return vc.rootView.props.shouldShowSmallText } + } + + var onConfirmComplete: RCTDirectEventBlock { + set { vc.rootView.props.onConfirmComplete = newValue } + get { return vc.rootView.props.onConfirmComplete } + } + + var view: UIView { + vc.view.backgroundColor = .clear + return vc.view + } +} + +class MnemonicConfirmationProps : ObservableObject { + @Published var mnemonicId: String = "" + @Published var shouldShowSmallText: Bool = false + @Published var onConfirmComplete: RCTDirectEventBlock = { _ in } + @Published var mnemonicWords: [String] = Array(repeating: "", count: 12) + @Published var scrambledWords: [String] = Array(repeating: "", count: 12) + @Published var typedWords: [String] = Array(repeating: "", count: 12) + @Published var selectedIndex: Int = 0 +} + +struct MnemonicConfirmation: View { + + @ObservedObject var props = MnemonicConfirmationProps() + + let rnEthersRS = RNEthersRS() + + func setMnemonicId(mnemonicId: String) { + props.mnemonicId = mnemonicId + if let mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId: mnemonicId) { + props.mnemonicWords = mnemonic.components(separatedBy: " ") + props.scrambledWords = mnemonic.components(separatedBy: " ").shuffled() + } + } + + func onSuggestionTapped(word: String) { + props.typedWords[props.selectedIndex] = word + + if (props.typedWords == props.mnemonicWords) { + props.onConfirmComplete([:]) + } else if (props.mnemonicWords[props.selectedIndex] == props.typedWords[props.selectedIndex] && props.selectedIndex < props.mnemonicWords.count - 1) { + props.selectedIndex += 1 + } + } + + func onFieldTapped(fieldNumber: Int) { + props.selectedIndex = fieldNumber - 1 + } + + + func getLabelFocusState(index: Int) -> InputFocusState{ + let isTextFieldFocused = index == props.selectedIndex + let isTextFieldValid = props.mnemonicWords[index] == props.typedWords[index] + let isTextFieldEmpty = props.typedWords[index].count == 0 + + if (isTextFieldFocused && !isTextFieldEmpty && !isTextFieldValid) { + return InputFocusState.focusedWrongInput + } else if (isTextFieldFocused) { + return InputFocusState.focusedNoInput + } else if (!isTextFieldEmpty && !isTextFieldValid) { + return InputFocusState.notFocusedWrongInput + } + return InputFocusState.notFocused + } + + var body: some View { + let end = props.mnemonicWords.count - 1 + let middle = end / 2 + + VStack(alignment: HorizontalAlignment.leading, spacing: 0) { + HStack(alignment: VerticalAlignment.center, spacing: 12) { + VStack(alignment: .leading, spacing: 12) { + ForEach((0...middle), id: \.self) {index in + MnemonicTextField(index: index + 1, + initialText: props.typedWords[index], + shouldShowSmallText: props.shouldShowSmallText, + focusState: getLabelFocusState(index: index), + onFieldTapped: onFieldTapped + ) + .frame(maxWidth: .infinity, alignment: .leading) + } + }.frame(maxWidth: .infinity) + VStack(alignment: .leading, spacing: 12) { + ForEach((middle + 1...end), id: \.self) {index in + MnemonicTextField(index: index + 1, + initialText: props.typedWords[index], + shouldShowSmallText: props.shouldShowSmallText, + focusState: getLabelFocusState(index: index), + onFieldTapped: onFieldTapped + ) + .frame(maxWidth: .infinity, alignment: .leading) + + } + }.frame(maxWidth: .infinity) + }.frame(maxWidth: .infinity) + .padding([.leading, .trailing], 24) + + MnemonicConfirmationWordBankView(words: props.scrambledWords, + usedWords: props.typedWords, + labelCallback: onSuggestionTapped, + shouldShowSmallText: props.shouldShowSmallText) + .frame(maxWidth: .infinity) + .padding([.top, .leading, .trailing], 24) + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationWordBankView.swift b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationWordBankView.swift new file mode 100644 index 0000000..b95b7d5 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicConfirmationWordBankView.swift @@ -0,0 +1,98 @@ +// +// MnemonicTagsView.swift +// Uniswap +// +// Created by Thomas Thachil on 8/8/22. +// + +import SwiftUI + +struct BankWord: Hashable { + var word: String = "" + var used: Bool = false +} + +struct MnemonicConfirmationWordBankView: View { + + let smallFont = UIFont(name: "Basel-Book", size: 14) + let mediumFont = UIFont(name: "Basel-Book", size: 16) + + var groupedWords: [[BankWord]] = [[BankWord]]() + let screenWidth = UIScreen.main.bounds.width // Used to calculate max number of tags per row + var labelCallback: ((String) -> Void)? + let shouldShowSmallText: Bool + + init(words: [String], usedWords: [String], labelCallback: @escaping (String) -> Void, shouldShowSmallText: Bool) { + self.labelCallback = labelCallback + self.shouldShowSmallText = shouldShowSmallText + + // Mark words as used individually to handle case of duplicate words + var wordStructs = words.map { word in BankWord(word: word) } + // Use used words to mark used + usedWords.forEach{ usedWord in + for idx in 0...wordStructs.count-1 { + if (usedWord == wordStructs[idx].word && !wordStructs[idx].used) { + wordStructs[idx].used = true + return + } + } + } + + // Set up grouped words + self.groupedWords = createGroupedWords(wordStructs) + } + + private func createGroupedWords(_ items: [BankWord]) -> [[BankWord]] { + + var groupedItems: [[BankWord]] = [[BankWord]]() + var tempItems: [BankWord] = [BankWord]() + var width: CGFloat = 0 + + for word in items { + + let label = UILabel() + label.text = word.word + label.sizeToFit() + + let labelWidth = label.frame.size.width + 32 + + if (width + labelWidth + 32) < screenWidth { + width += labelWidth + tempItems.append(word) + } else { + width = labelWidth + groupedItems.append(tempItems) + tempItems.removeAll() + tempItems.append(word) + } + + } + + groupedItems.append(tempItems) + return groupedItems + + } + + var body: some View { + + VStack(alignment: .center) { + ForEach(groupedWords, id: \.self) { subItems in + HStack(spacing: 8) { + ForEach(subItems, id: \.self) { bankWord in + Text(bankWord.word) + .font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) + .fixedSize() + .padding(shouldShowSmallText ? EdgeInsets(top: 4, leading: 12, bottom: 4, trailing: 12) : EdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)) + .background(Colors.surface2) + .foregroundColor(Colors.neutral1) + .clipShape(RoundedRectangle(cornerRadius: 100, style: .continuous)) + .onTapGesture { + labelCallback?(bankWord.word) + } + .opacity(bankWord.used ? 0.60 : 1) + } + } + } + } + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayManager.m b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayManager.m new file mode 100644 index 0000000..02d5ed3 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayManager.m @@ -0,0 +1,29 @@ +// +// RNTMnemonicManager.m +// Uniswap +// +// Created by Spencer Yen on 5/24/22. +// + +#import "Uniswap-Swift.h" +#import +#import "RNSwiftUI-Bridging-Header.h" + +@interface MnemonicDisplayManager : RCTViewManager +@end + +@implementation MnemonicDisplayManager +RCT_EXPORT_MODULE() + +RCT_EXPORT_SWIFTUI_PROPERTY(mnemonicId, NSString, MnemonicDisplayView); + +- (UIView *)view +{ + MnemonicDisplayView *proxy = [[MnemonicDisplayView alloc] init]; + UIView *view = [proxy view]; + NSMutableDictionary *storage = [MnemonicDisplayView storage]; + storage[[NSValue valueWithNonretainedObject:view]] = proxy; + return view; +} + +@end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayView.swift b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayView.swift new file mode 100644 index 0000000..0d4f79e --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicDisplayView.swift @@ -0,0 +1,83 @@ +// +// MnemonicDisplayView.swift +// Uniswap +// +// Created by Gary Ye on 8/31/23. +// + +import SwiftUI + +@objcMembers class MnemonicDisplayView: NSObject { + private var vc = UIHostingController(rootView: MnemonicDisplay()) + + static let storage = NSMutableDictionary() + + var mnemonicId: String { + set { vc.rootView.setMnemonicId(mnemonicId: newValue) } + get { return vc.rootView.props.mnemonicId } + } + + var view: UIView { + vc.view.backgroundColor = .clear + return vc.view + } +} + +class MnemonicDisplayProps : ObservableObject { + @Published var mnemonicId: String = "" + @Published var mnemonicWords: [String] = Array(repeating: "", count: 12) +} + + +struct MnemonicDisplay: View { + + @ObservedObject var props = MnemonicDisplayProps() + + let rnEthersRS = RNEthersRS() + let interFont = UIFont(name: "Basel-Semibold", size: 20) + + func setMnemonicId(mnemonicId: String) { + props.mnemonicId = mnemonicId + if let mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId: mnemonicId) { + props.mnemonicWords = mnemonic.components(separatedBy: " ") + } + } + + var body: some View { + if (props.mnemonicWords.count > 12) { + ScrollView { + content + }.fadeOutBottom(fadeLength: 50) + } else { + content + } + } + + @ViewBuilder + var content: some View { + let end = props.mnemonicWords.count - 1 + let middle = end / 2 + + VStack(alignment: HorizontalAlignment.leading, spacing: 0) { + HStack(alignment: VerticalAlignment.center, spacing: 12) { + VStack(alignment: .leading, spacing: 12) { + ForEach((0...middle), id: \.self) {index in + MnemonicTextField(index: index + 1, + initialText: props.mnemonicWords[index] + ) + .frame(maxWidth: .infinity, alignment: .leading) + } + }.frame(maxWidth: .infinity) + VStack(alignment: .leading, spacing: 12) { + ForEach((middle + 1...end), id: \.self) {index in + MnemonicTextField(index: index + 1, + initialText: props.mnemonicWords[index] + ) + .frame(maxWidth: .infinity, alignment: .leading) + } + }.frame(maxWidth: .infinity) + }.frame(maxWidth: .infinity) + }.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .padding(EdgeInsets(top: 0, leading: 16, bottom: 32, trailing: 16)) + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicTextField.swift b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicTextField.swift new file mode 100644 index 0000000..2dc7077 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicTextField.swift @@ -0,0 +1,98 @@ +// +// MnemonicTextField.swift +// Uniswap +// +// Created by Thomas Thachil on 8/8/22. +// + +import SwiftUI + +enum InputFocusState { + case notFocused + case focusedNoInput + case focusedWrongInput + case notFocusedWrongInput +} + +struct MnemonicTextField: View { + + @Environment(\.colorScheme) var colorScheme + + let smallFont = UIFont(name: "Basel-Book", size: 14) + let mediumFont = UIFont(name: "Basel-Book", size: 16) + + var index: Int + var initialText = "" + var shouldShowSmallText: Bool + var onFieldTapped: ((Int) -> Void)? + var focusState: InputFocusState + + + init(index: Int, + initialText: String, + shouldShowSmallText: Bool = false, + focusState: InputFocusState = InputFocusState.notFocused, + onFieldTapped: ((Int) -> Void)? = nil + ) { + self.index = index + self.initialText = initialText + self.shouldShowSmallText = shouldShowSmallText + self.focusState = focusState + self.onFieldTapped = onFieldTapped + } + + func getLabelBackground(focusState: InputFocusState) -> some View { + switch (focusState) { + case .focusedNoInput: + return AnyView(RoundedRectangle(cornerRadius: 100) + .strokeBorder(Colors.accent1, lineWidth: 2) + .background(Colors.surface2) + .cornerRadius(100) + ) + + case .focusedWrongInput: + return AnyView(RoundedRectangle(cornerRadius: 100) + .strokeBorder(Colors.statusCritical, lineWidth: 2) + .background(Colors.surface2) + .cornerRadius(100) + ) + + case .notFocusedWrongInput: + return AnyView(RoundedRectangle(cornerRadius: 100) + .strokeBorder(Colors.statusCritical, lineWidth: 2) + .background(Colors.surface2) + .cornerRadius(100) + ) + + case .notFocused: + return AnyView( + RoundedRectangle(cornerRadius: 100, style: .continuous) + .fill(Colors.surface2) + ) + } + } + + + var body: some View { + HStack(alignment: VerticalAlignment.center, spacing: 0) { + + Text(String(index)).cornerRadius(16) + .font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) + .foregroundColor(Colors.neutral3) + .padding(shouldShowSmallText ? EdgeInsets(top: 6, leading: 16, bottom: 6, trailing: 12) : EdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 12)) + .frame(alignment: Alignment.leading) + + Text(initialText) + .font(Font((shouldShowSmallText ? smallFont : mediumFont)!)) + .multilineTextAlignment(TextAlignment.leading) + .foregroundColor(Colors.neutral1) + .padding(shouldShowSmallText ? EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 16) : EdgeInsets(top: 12, leading: 0, bottom: 12, trailing: 16)) + .frame(maxWidth: .infinity, alignment: Alignment.leading) + } + .background(getLabelBackground(focusState: focusState)) + .frame(maxWidth: .infinity, alignment: .leading) + .onTapGesture { + onFieldTapped?(index) + } + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicWordView.swift b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicWordView.swift new file mode 100644 index 0000000..f148402 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/MnemonicWordView.swift @@ -0,0 +1,68 @@ +// +// MnemonicWord.swift +// Uniswap +// +// Created by Spencer Yen on 5/24/22. +// + +import Foundation + +class MnemonicWordView: UIView { + private var index: Int? + private var word: String? + + required init(index: Int, word: String) { + super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 0)) + self.index = index + self.word = word + self.setupView() + } + + required override init(frame: CGRect) { + super.init(frame: frame) + self.setupView() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + self.setupView() + } + + private func setupView() { + self.layer.cornerRadius = 24 + self.layer.masksToBounds = true + let indexLabel = UILabel() + indexLabel.text = String(describing: self.index!) + indexLabel.adjustsFontSizeToFitWidth = true + indexLabel.font = UIFont.init(name: "Basel-Book", size: 16) + + let wordLabel = UILabel() + wordLabel.text = self.word + wordLabel.adjustsFontSizeToFitWidth = true + wordLabel.font = UIFont.init(name: "Basel-Book", size: 16) + + let stackView = UIStackView(arrangedSubviews: [indexLabel, wordLabel]) + stackView.axis = .horizontal + stackView.distribution = .equalSpacing + stackView.alignment = .leading + stackView.spacing = 12.0 + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.isLayoutMarginsRelativeArrangement = true + stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 12, leading: 16, bottom: 12, trailing: 16) + self.addSubview(stackView) + + stackView.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true + + if traitCollection.userInterfaceStyle == .light { + self.layer.backgroundColor = UIColor.init(red: 249/255.0, green: 249/255.0, blue: 249/255.0, alpha: 1.0).cgColor + self.layer.borderColor = UIColor.init(red: 34/255.0, green: 34/255.0, blue: 34/255.0, alpha: 0.05).cgColor + indexLabel.textColor = UIColor.init(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0) + wordLabel.textColor = UIColor.black + } else { + self.layer.backgroundColor = UIColor.init(red: 27/255.0, green: 27/255.0, blue: 27/255.0, alpha: 1.0).cgColor + self.layer.borderColor = UIColor.init(red: 255/255.0, green: 255/255.0, blue: 255/255.0, alpha: 0.12).cgColor + indexLabel.textColor = UIColor.init(red: 155/255.0, green: 155/255.0, blue: 155/255.0, alpha: 1.0) + wordLabel.textColor = UIColor.white + } + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Backup/RNSwiftUI-Bridging-Header.h b/apps/mobile/ios/Uniswap/Onboarding/Backup/RNSwiftUI-Bridging-Header.h new file mode 100644 index 0000000..6b1512b --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Backup/RNSwiftUI-Bridging-Header.h @@ -0,0 +1,40 @@ +// +// RNSwiftUI-Bridging-Header.h +// Uniswap +// +// Created by Thomas Thachil on 8/3/22. +// +// + +#ifndef RNSwiftUI_Bridging_Header_h +#define RNSwiftUI_Bridging_Header_h + + +#import "React/RCTViewManager.h" +#import "React/RCTConvert.h" +#import "React/RCTComponentData.h" +#import "React/RCTBridgeModule.h" +#import "React/UIView+React.h" + +#define RCT_EXPORT_SWIFTUI_PROPERTY(name, type, proxyClass) \ +RCT_CUSTOM_VIEW_PROPERTY(name, type, proxyClass) { \ + NSMutableDictionary *storage = [proxyClass storage]; \ + proxyClass *proxy = storage[[NSValue valueWithNonretainedObject:view]]; \ + proxy.name = [RCTConvert type:json]; \ +} + +#define RCT_EXPORT_SWIFTUI_CALLBACK(name, type, proxyClass) \ +RCT_REMAP_VIEW_PROPERTY(name, __custom__, type) \ +- (void)set_##name:(id)json forView:(UIView *)view withDefaultView:(UIView *)defaultView RCT_DYNAMIC { \ + NSMutableDictionary *storage = [proxyClass storage]; \ + proxyClass *proxy = storage[[NSValue valueWithNonretainedObject:view]]; \ + void (^eventHandler)(NSDictionary *event) = ^(NSDictionary *event) { \ + RCTComponentEvent *componentEvent = [[RCTComponentEvent alloc] initWithName:@""#name \ + viewTag:view.reactTag \ + body:event]; \ + [self.bridge.eventDispatcher sendEvent:componentEvent]; \ + }; \ + proxy.name = eventHandler; \ +} + +#endif /* RNSwiftUI_Bridging_Header_h */ diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m new file mode 100644 index 0000000..8c06593 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.m @@ -0,0 +1,21 @@ +// +// SeedPhraseInputManager.swift +// Uniswap +// +// Created by Gary Ye on 9/7/23. +// + +#import "React/RCTViewManager.h" + +@interface RCT_EXTERN_MODULE(SeedPhraseInputManager, RCTViewManager) + +RCT_EXPORT_VIEW_PROPERTY(targetMnemonicId, NSString?) +RCT_EXPORT_VIEW_PROPERTY(strings, NSDictionary) +RCT_EXPORT_VIEW_PROPERTY(onHelpTextPress, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onInputValidated, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onMnemonicStored, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPasteStart, RCTDirectEventBlock); +RCT_EXPORT_VIEW_PROPERTY(onPasteEnd, RCTDirectEventBlock); +RCT_EXTERN_METHOD(handleSubmit: (nonnull NSNumber *)node) + +@end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.swift new file mode 100644 index 0000000..51ab266 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputManager.swift @@ -0,0 +1,31 @@ +// +// SeedPhraseInputManager.swift +// Uniswap +// +// Created by Gary Ye on 9/15/23. +// + +// Using a view manager written in Swift instead of bridging headers +// because couldn't get RCT_EXTERN_METHOD to work with that approach +@objc(SeedPhraseInputManager) +class SeedPhraseInputManager: RCTViewManager { + + override func view() -> UIView! { + return SeedPhraseInputView() + } + + // Required by RN to initialize on main thread + override class func requiresMainQueueSetup() -> Bool { + true + } + + @objc func handleSubmit(_ node: NSNumber) -> Void { + DispatchQueue.main.async { + let component = self.bridge.uiManager.view( + forReactTag: node + ) as? SeedPhraseInputView + component?.handleSubmit() + // TODO garydebug add error logging for view not found + } + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift new file mode 100644 index 0000000..4cc6bd3 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputView.swift @@ -0,0 +1,229 @@ +// +// SeedPhraseInputView.swift +// Uniswap +// +// Created by Gary Ye on 9/7/23. +// + +import React +import SwiftUI + +class SeedPhraseInputView: UIView { + private let vc = UIHostingController(rootView: SeedPhraseInput()) + + override init(frame: CGRect) { + super.init(frame: frame) + + vc.view.translatesAutoresizingMaskIntoConstraints = false + vc.view.backgroundColor = .clear + + self.addSubview(vc.view) + + NSLayoutConstraint.activate([ + vc.view.topAnchor.constraint(equalTo: self.topAnchor), + vc.view.bottomAnchor.constraint(equalTo: self.bottomAnchor), + vc.view.leadingAnchor.constraint(equalTo: self.leadingAnchor), + vc.view.trailingAnchor.constraint(equalTo: self.trailingAnchor) + ]) + } + + required init?(coder aDecoder: NSCoder) { + // Used to load view into storyboarder + fatalError("init(coder:) has not been implemented") + } + + override func reactSetFrame(_ frame: CGRect) { + super.reactSetFrame(frame); + vc.view.frame = frame + } + + @objc + var targetMnemonicId: String? { + get { vc.rootView.viewModel.targetMnemonicId } + set { vc.rootView.viewModel.targetMnemonicId = newValue } + } + + @objc + var strings: Dictionary { + get { vc.rootView.viewModel.rawRNStrings } + set { vc.rootView.viewModel.rawRNStrings = newValue } + } + + @objc + var onHelpTextPress: RCTDirectEventBlock { + set { vc.rootView.viewModel.onHelpTextPress = newValue } + get { return vc.rootView.viewModel.onHelpTextPress } + } + + @objc + var onInputValidated: RCTDirectEventBlock { + get { vc.rootView.viewModel.onInputValidated } + set { vc.rootView.viewModel.onInputValidated = newValue } + } + + @objc + var onMnemonicStored: RCTDirectEventBlock { + set { vc.rootView.viewModel.onMnemonicStored = newValue } + get { return vc.rootView.viewModel.onMnemonicStored } + } + + @objc + var onPasteStart: RCTDirectEventBlock { + set { vc.rootView.viewModel.onPasteStart = newValue } + get { return vc.rootView.viewModel.onPasteStart } + } + + @objc + var onPasteEnd: RCTDirectEventBlock { + set { vc.rootView.viewModel.onPasteEnd = newValue } + get { return vc.rootView.viewModel.onPasteEnd } + } + + @objc + var handleSubmit: () -> Void { + get { return vc.rootView.viewModel.handleSubmit } + } +} + +struct SeedPhraseInput: View { + + @ObservedObject var viewModel = SeedPhraseInputViewModel() + @FocusState private var focused: Bool + + private var font = Font(UIFont(name: "Basel-Book", size: 17)!) + private var subtitleFont = Font(UIFont(name: "Basel-Book", size: 17)!) + private var buttonFont = Font(UIFont(name: "Basel-Medium", size: 15)!) + + var body: some View { + VStack(spacing: 16) { + VStack { + ZStack { + if #available(iOS 16.0, *) { + TextEditor(text: $viewModel.input) + .focused($focused) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .multilineTextAlignment(.center) + .scrollContentBackground(.hidden) + } else { + TextEditor(text: $viewModel.input) + .focused($focused) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .multilineTextAlignment(.center) + .onAppear() { + UITextView.appearance().backgroundColor = .clear + } + } + + if (viewModel.input.isEmpty) { + HStack(spacing: 8) { + Text(viewModel.strings.inputPlaceholder) + .foregroundColor(Colors.neutral2) + + Button(action: handlePastePress, label: { + HStack(spacing: 4) { + PasteIcon() + .fill(Colors.neutral2) + .frame(width: 16, height: 16) + + Text(viewModel.strings.pasteButton) + .foregroundColor(Colors.neutral2) + .font(buttonFont) + .fontWeight(.medium) + } + }) + .padding([.top, .bottom, .trailing], 8) + .padding([.leading], 4) + .background(Colors.surface3) + .clipShape(RoundedRectangle(cornerRadius: 12)) + } + .frame(alignment: .center) + } + } + .fixedSize(horizontal: false, vertical: true) + .padding(8) // Adds to default TextEditor padding 8 + .frame(minHeight: 120, alignment: .center) + .background(Colors.surface2) + .cornerRadius(20) + .overlay( + RoundedRectangle(cornerRadius: 20) + .inset(by: 1) + .stroke(mapStatusToColor(status: viewModel.status), lineWidth: 1) + ) + .onTapGesture { + focused = true + } + .onAppear() { + DispatchQueue.main.async { + focused = true + } + } + }.padding(.bottom, 8) + + if (errorMessage() != nil) { + HStack(spacing: 4) { + AlertTriangeIcon() + .frame(width: 24, height: 24) + .foregroundColor(Colors.statusCritical) + Text(errorMessage() ?? "") + .foregroundColor(Colors.statusCritical) + } + .frame(alignment: .center) + } + + Text(viewModel.strings.helpText) + .font(subtitleFont) + .foregroundColor(Colors.accent1) + .minimumScaleFactor(0.8) + .onTapGesture { + viewModel.onHelpTextPress([:]) + } + } + .frame(maxWidth:.infinity, maxHeight: .infinity, alignment: .top) + .padding(.bottom, 16) + .font(font) + } + + private func mapStatusToColor(status: SeedPhraseInputViewModel.Status) -> Color { + switch viewModel.status { + case .none: + return Color.clear + case .valid: + return Colors.statusSuccess + case .error: + return Colors.statusCritical + } + } + + private func errorMessage() -> String? { + switch viewModel.error { + case .invalidPhrase: + return viewModel.strings.errorInvalidPhrase + case .invalidWord(let word): + return "\(viewModel.strings.errorInvalidWord) \(word)" + case .tooManyWords, .notEnoughWords: + return viewModel.strings.errorPhraseLength + case .wrongRecoveryPhrase: + return viewModel.strings.errorWrongPhrase + default: + return nil + } + } + + private func handlePastePress() { + // Arbitrary time necessary for callbacks to trigger while permission modal is opened + let debounceTime = 0.1 + + viewModel.onPasteStart([:]) + DispatchQueue.main.asyncAfter(deadline: .now() + debounceTime) { + let pb = UIPasteboard.general + viewModel.input = pb.string ?? "" + + DispatchQueue.main.asyncAfter(deadline: .now() + debounceTime) { + viewModel.onPasteEnd([:]) + } + } + } +} + diff --git a/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift new file mode 100644 index 0000000..8f8a168 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Import/SeedPhraseInputViewModel.swift @@ -0,0 +1,169 @@ +// +// SeedPhraseInputViewModel.swift +// Uniswap +// +// Created by Gary Ye on 9/10/23. +// + +import Foundation + +class SeedPhraseInputViewModel: ObservableObject { + + enum Status: String { + case none + case valid + case error + } + + enum MnemonicError { + case invalidPhrase + case invalidWord(String) + case notEnoughWords + case tooManyWords + case wrongRecoveryPhrase + } + + struct ReactNativeStrings { + var helpText: String + var inputPlaceholder: String + var pasteButton: String + var errorInvalidWord: String + var errorPhraseLength: String + var errorWrongPhrase: String + var errorInvalidPhrase: String + } + + let rnEthersRS = RNEthersRS() + + // Following block of variables will come from RN + @Published var targetMnemonicId: String? = nil + @Published var rawRNStrings: Dictionary = Dictionary() { + didSet { + strings = ReactNativeStrings( + helpText: rawRNStrings["helpText"] ?? "", + inputPlaceholder: rawRNStrings["inputPlaceholder"] ?? "", + pasteButton: rawRNStrings["pasteButton"] ?? "", + errorInvalidWord: rawRNStrings["errorInvalidWord"] ?? "", + errorPhraseLength: rawRNStrings["errorPhraseLength"] ?? "", + errorWrongPhrase: rawRNStrings["errorWrongPhrase"] ?? "", + errorInvalidPhrase: rawRNStrings["errorInvalidPhrase"] ?? "" + ) + } + } + @Published var strings: ReactNativeStrings = ReactNativeStrings( + helpText: "", + inputPlaceholder: "", + pasteButton: "", + errorInvalidWord: "", + errorPhraseLength: "", + errorWrongPhrase: "", + errorInvalidPhrase: "" + ) + @Published var onHelpTextPress: RCTDirectEventBlock = { _ in } + @Published var onInputValidated: RCTDirectEventBlock = { _ in } + @Published var onMnemonicStored: RCTDirectEventBlock = { _ in } + @Published var onPasteStart: RCTDirectEventBlock = { _ in } + @Published var onPasteEnd: RCTDirectEventBlock = { _ in } + + @Published var input = "" { + didSet { + validateInput() + } + } + @Published var status: Status = .none + @Published var error: MnemonicError? = nil + + private let minCount = 12 + private let maxCount = 24 + + func handleSubmit() { + let normalized = normalizeInput(value: input) + let mnemonic = trimInput(value: normalized) + let words = mnemonic.components(separatedBy: " ") + let valid = rnEthersRS.validateMnemonic(mnemonic: mnemonic) + + if (words.count < minCount) { + status = .error + error = .notEnoughWords + } else if (words.count > maxCount) { + status = .error + error = .tooManyWords + } else if (!valid) { + status = .error + error = .invalidPhrase + } else { + submitMnemonic(mnemonic: mnemonic) + } + } + + private func submitMnemonic(mnemonic: String) { + if (targetMnemonicId != nil) { + rnEthersRS.generateAddressForMnemonic( + mnemonic: mnemonic, + derivationIndex: 0, + resolve: { mnemonicId in + if (targetMnemonicId == String(describing: mnemonicId ?? "")) { + storeMnemonic(mnemonic: mnemonic) + } else { + status = .error + error = .wrongRecoveryPhrase + } + }, reject: { code, message, error in + // TODO gary update ethers library to catch exception or send in reject + print("SeedPhraseInputView model error while generating address: \(message ?? "")") + }) + } else { + storeMnemonic(mnemonic: mnemonic) + } + } + + private func storeMnemonic(mnemonic: String) { + rnEthersRS.importMnemonic( + mnemonic: mnemonic, + resolve: { mnemonicId in + onMnemonicStored(["mnemonicId": String(describing: mnemonicId ?? "")]) + }, + reject: { code, message, error in + // TODO gary update ethers library to catch exception or send in reject + print("SeedPhraseInputView model error while storing mnemonic: \(message ?? "")") + } + ) + } + + private func normalizeInput(value: String) -> String { + return value.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression).lowercased() + } + + private func trimInput(value: String) -> String { + return value.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func validateInput() { + let normalized = normalizeInput(value: input) + let skipLastWord = normalized.last != " " + let mnemonic = trimInput(value: normalized) + + let words = mnemonic.components(separatedBy: " ") + + let isValidLength = words.count >= minCount && words.count <= maxCount + let firstInvalidWord = rnEthersRS.findInvalidWord(mnemonic: mnemonic) + + if (firstInvalidWord == words.last && skipLastWord) { + status = .none + } else if (firstInvalidWord == "" && isValidLength) { + status = .valid + } else if (firstInvalidWord != "") { + status = .error + error = .invalidWord(firstInvalidWord) + } else { + status = .none + } + + if (status != .error) { + error = nil + } + + let canSubmit = error == nil && mnemonic != "" && firstInvalidWord == "" + onInputValidated(["canSubmit": canSubmit]) + } +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/EncryptionUtils.swift b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/EncryptionUtils.swift new file mode 100644 index 0000000..716cadc --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/EncryptionUtils.swift @@ -0,0 +1,139 @@ +// +// EncryptionUtils.swift +// Uniswap +// +// Created by Christine Legge on 1/23/24. +// + +import CryptoKit +import Foundation + +enum EncryptionError: Error { + case invalidModulus + case invalidExponent + case invalidPublicKey + case unknown +} + +// Convert Base64URL to Base64 and add padding if necessary +func Base64URLToBase64(base64url: String) -> String { + var base64 = base64url + .replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + if base64.count % 4 != 0 { + base64.append(String(repeating: "=", count: 4 - base64.count % 4)) + } + return base64 +} + +// Calculate the length field for an ASN.1 sequence. +func lengthField(of valueField: [UInt8]) throws -> [UInt8] { + var count = valueField.count + + if count < 128 { + return [ UInt8(count) ] + } + + // The number of bytes needed to encode count. + let lengthBytesCount = Int((log2(Double(count)) / 8) + 1) + + // The first byte in the length field encoding the number of remaining bytes. + let firstLengthFieldByte = UInt8(128 + lengthBytesCount) + + var lengthField: [UInt8] = [] + for _ in 0..> 8 + } + + // Include the first byte. + lengthField.insert(firstLengthFieldByte, at: 0) + + return lengthField +} + +func generatePublicRSAKey(modulus: String, exponent: String) throws -> SecKey { + // Lets encode them from b64 url to b64 + let encodedModulus = Base64URLToBase64(base64url: modulus) + let encodedExponent = Base64URLToBase64(base64url: exponent) + + // First we need to get our modulus and exponent into UInt8 arrays + // We can do this by decoding the Base64 strings (URL safe) into Data + // and then converting the Data into UInt8 arrays + guard let modulusData = Data(base64Encoded: encodedModulus) else { + throw EncryptionError.invalidModulus + } + + guard let exponentData = Data(base64Encoded: encodedExponent) else { + throw EncryptionError.invalidExponent + } + + var modulus = modulusData.withUnsafeBytes { Data(Array($0)).withUnsafeBytes { Array($0) } } + let exponent = exponentData.withUnsafeBytes { Data(Array($0)).withUnsafeBytes { Array($0) } } + + // Lets add 0x00 at the front of the modulus + modulus.insert(0x00, at: 0) + + var sequenceEncoded: [UInt8] = [] + do { + // encode as integers + var modulusEncoded: [UInt8] = [] + modulusEncoded.append(0x02) + modulusEncoded.append(contentsOf: try lengthField(of: modulus)) + modulusEncoded.append(contentsOf: modulus) + + var exponentEncoded: [UInt8] = [] + exponentEncoded.append(0x02) + exponentEncoded.append(contentsOf: try lengthField(of: exponent)) + exponentEncoded.append(contentsOf: exponent) + + sequenceEncoded.append(0x30) + sequenceEncoded.append(contentsOf: try lengthField(of: (modulusEncoded + exponentEncoded))) + sequenceEncoded.append(contentsOf: (modulusEncoded + exponentEncoded)) + } catch { + throw EncryptionError.invalidPublicKey + } + + let keyData = Data(sequenceEncoded) + + // RSA key size is the number of bits of the modulus. + let keySize = (modulus.count * 8) + + let attributes: [String: Any] = [ + kSecAttrKeyType as String: kSecAttrKeyTypeRSA, + kSecAttrKeyClass as String: kSecAttrKeyClassPublic, + kSecAttrKeySizeInBits as String: keySize + ] + + guard let publicKey = SecKeyCreateWithData(keyData as CFData, attributes as CFDictionary, nil) else { + throw EncryptionError.invalidPublicKey + } + + return publicKey +} + +func encryptForStorage(plaintext: String, publicKey: SecKey) throws -> Data +{ + // Encrypt the plaintext + let plaintextData = Data(plaintext.utf8) + let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256 + + guard SecKeyIsAlgorithmSupported(publicKey, .encrypt, algorithm) else { + throw EncryptionError.invalidPublicKey + } + + var error: Unmanaged? + guard let ciphertextData = SecKeyCreateEncryptedData(publicKey, algorithm, plaintextData as CFData, &error) else { + if let error = error { + throw error.takeRetainedValue() as Error + } else { + throw EncryptionError.unknown + } + } + + return ciphertextData as Data +} diff --git a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m new file mode 100644 index 0000000..2612b52 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.m @@ -0,0 +1,19 @@ +// +// ScantasticEncryption.m +// Uniswap +// +// Created by Christine Legge on 1/23/24. +// + +#import +#import + +@interface RCT_EXTERN_MODULE(ScantasticEncryption, RCTEventEmitter) + +RCT_EXTERN_METHOD(getEncryptedMnemonic: (NSString *)mnemonicId + n: (NSString *)n + e: (NSString *)e + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +@end diff --git a/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift new file mode 100644 index 0000000..edabb1f --- /dev/null +++ b/apps/mobile/ios/Uniswap/Onboarding/Scantastic/ScantasticEncryption.swift @@ -0,0 +1,62 @@ +// +// ScantasticEncryption.swift +// Uniswap +// +// Created by Christine Legge on 1/23/24. +// + +import Foundation +import CryptoKit + +enum ScantasticError: String, Error { + case publicKeyError = "publicKeyError" + case cipherTextError = "cipherTextError" +} + +@objc(ScantasticEncryption) +class ScantasticEncryption: RCTEventEmitter { + let rnEthersRS = RNEthersRS() + + @objc override static func requiresMainQueueSetup() -> Bool { + return false + } + + override func supportedEvents() -> [String]! { + return [] + } + + /** + Retrieves encrypted mnemonic + + - parameter mnemonicId: key string associated with mnemonic to backup + - parameter n: base64encoded value + - parameter e: base64encoded value + */ + @objc(getEncryptedMnemonic:n:e:resolve:reject:) + func getEncryptedMnemonic( + mnemonicId: String, n: String, e: String, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + + guard let mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId: mnemonicId) else { + return reject(RNEthersRSError.retrieveMnemonicError.rawValue, "Failed to retrieve mnemonic", RNEthersRSError.retrieveMnemonicError) + } + + let publicKey: SecKey + do { + publicKey = try generatePublicRSAKey(modulus: n, exponent: e) + } catch { + return reject(ScantasticError.publicKeyError.rawValue, "Failed to generate public Key ", ScantasticError.publicKeyError) + } + + let encodedCiphertext: Data + do { + encodedCiphertext = try encryptForStorage(plaintext:mnemonic,publicKey:publicKey) + } catch { + return reject(ScantasticError.cipherTextError.rawValue, "Failed to encrypt the mnemonic", ScantasticError.cipherTextError) + } + + let b64encodedCiphertext = encodedCiphertext.base64EncodedString() + return resolve(b64encodedCiphertext) + } +} diff --git a/apps/mobile/ios/Uniswap/RNCloudBackupsManager/EncryptionHelper.swift b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/EncryptionHelper.swift new file mode 100644 index 0000000..da1fe62 --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/EncryptionHelper.swift @@ -0,0 +1,85 @@ +// +// EncryptionHelper.swift +// Uniswap +// +// Created by Spencer Yen on 7/26/22. +// + +import CryptoKit +import Argon2Swift + +/** + Encrypts given secret using AES-GCM cipher secured by symmetric key derived from given password. + + - parameter secret: plaintext secret to encrypt + - parameter password: password to generate encryption key + - parameter salt: randomized data string used with password to generate encryption key + - returns: encrypted secret string + */ +func encrypt(secret: String, password: String, salt: String) throws -> String { + let key = try keyFromPassword(password: password, salt: salt) + let secretData = secret.data(using: .utf8)! + + // Encrypt data into SealedBox, return as string + let sealedBox = try AES.GCM.seal(secretData, using: key) + let encryptedData = sealedBox.combined + let encryptedSecret = encryptedData!.base64EncodedString() + + return encryptedSecret +} + +/** + Attempts to decrypt AES-GCM encrypted secret using symmetric key derived from given user pin. + + - parameter encryptedSecret: secret in cipher encrypted form + - parameter password: password to generate encryption key + - parameter salt: randomized data string used when secret was originally encrypted + - returns: decrypted secret string + */ +func decrypt(encryptedSecret: String, password: String, salt: String) throws -> String { + let key = try keyFromPassword(password: password, salt: salt) + + // Recreate SealedBox from encrypted string + let encryptedData = Data(base64Encoded: encryptedSecret)! + let sealedBox = try AES.GCM.SealedBox(combined: encryptedData) + + // Decrypt SealedBox and decode result to string + let decryptedData = try AES.GCM.open(sealedBox, using: key) + let decryptedSecret = String(data: decryptedData, encoding: .utf8)! + + return decryptedSecret +} + +/** + Generate encryption key from user specified password and randomized salt using argon2id. + + The parameters used for Argon2 are based on recommended values from the security audit and the Argon2 RFC (https://datatracker.ietf.org/doc/rfc9106/) + The memory and iterations values are tuned based on benchmark timing tests to take ~1s (see EncryptionHelperTests) + - Mode: argon2id + - Parallelism: 4 + - Memory: 128MiB (2^17 KiB) + - Hash length: 32 bytes + - Iterations: 3 + + - parameter password: password to generate encryption key + - parameter salt: randomized data string used with password to generate encryption key + - returns: SymmetricKey to be used with CryptoKit encryption functions + */ + +func keyFromPassword(password: String, salt: String) throws -> SymmetricKey { + let derivedKey = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: 3, memory: 2 << 16, parallelism: 4, length: 32, type: .id) + let key = SymmetricKey(data: derivedKey.hashData()) + return key +} + +/** + Generate random data string of specified byte length + + - parameter length:number of bytes for random data string + - returns: randomized string + */ +func generateSalt(length: Int) -> String { + var bytes = [UInt8](repeating: 0, count: length) + _ = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + return Data(bytes).base64EncodedString() +} diff --git a/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.m b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.m new file mode 100644 index 0000000..e71442f --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.m @@ -0,0 +1,34 @@ +// +// RNCloudStorageBackupsManager.m +// Uniswap +// +// Created by Spencer Yen on 7/13/22. +// + +#import +#import + +@interface RCT_EXTERN_MODULE(RNCloudStorageBackupsManager, RCTEventEmitter) + +RCT_EXTERN_METHOD(isCloudStorageAvailable: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(backupMnemonicToCloudStorage: (NSString *)mnemonicId + password: (NSString *)password + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(restoreMnemonicFromCloudStorage: (NSString *)mnemonicId + password: (NSString *)password + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(deleteCloudStorageMnemonicBackup: (NSString *)mnemonicId + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(startFetchingCloudStorageBackups) + +RCT_EXTERN_METHOD(stopFetchingCloudStorageBackups) + +@end diff --git a/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.swift b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.swift new file mode 100644 index 0000000..b1190a0 --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNCloudBackupsManager/RNCloudStorageBackupsManager.swift @@ -0,0 +1,276 @@ +// +// RNCloudStorageBackupsManager.swift +// Uniswap +// +// Created by Spencer Yen on 7/13/22. +// + +import Foundation +import CryptoKit + +struct CloudStorageMnemonicBackup: Codable { + let mnemonicId: String + let encryptedMnemonic: String + let encryptionSalt: String + let createdAt: Double +} + +enum ICloudBackupError: String, Error { + case backupNotFoundError = "backupNotFoundError" + case backupEncryptionError = "backupEncryptionError" + case backupDecryptionError = "backupDecryptionError" + case backupIncorrectPasswordError = "backupIncorrectPasswordError" + case deleteBackupError = "deleteBackupError" + case iCloudContainerError = "iCloudContainerError" + case iCloudError = "iCloudError" +} + +enum ICloudManagerEventType: String, CaseIterable { + case foundCloudBackup = "FoundCloudBackup" +} + +@objc(RNCloudStorageBackupsManager) +class RNCloudStorageBackupsManager: RCTEventEmitter { + + private let backupsQuery = NSMetadataQuery() + + let rnEthersRS = RNEthersRS() + + @objc override static func requiresMainQueueSetup() -> Bool { + return false + } + + override func supportedEvents() -> [String]! { + return ICloudManagerEventType.allCases.map { $0.rawValue } + } + + /** + Determine if iCloud Documents is available on device + + - returns: boolean if iCloud Documents is available or not + */ + @objc(isCloudStorageAvailable:reject:) + func isCloudStorageAvailable(resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + if FileManager.default.ubiquityIdentityToken == nil { + return resolve(false) + } else { + return resolve(true) + } + } + + /** + Stores mnemonic to iCloud Documents + + - parameter mnemonicId: key string associated with mnemonic to backup + - parameter password: user provided password to encrypt the mnemonic + - returns: true if successful, otherwise throws an error + */ + @objc(backupMnemonicToCloudStorage:password:resolve:reject:) + func backupMnemonicToCloudStorage( + mnemonicId: String, password: String, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + guard let mnemonic = rnEthersRS.retrieveMnemonic(mnemonicId: mnemonicId) else { + return reject(RNEthersRSError.retrieveMnemonicError.rawValue, "Failed to retrieve mnemonic", RNEthersRSError.retrieveMnemonicError) + } + + // Access Uniswap iCloud Documents container + guard let containerUrl = FileManager.default.url(forUbiquityContainerIdentifier: nil) else { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to find iCloud container", ICloudBackupError.iCloudError) + } + + // Create iCloud container if empty + if !FileManager.default.fileExists(atPath: containerUrl.path, isDirectory: nil) { + do { + try FileManager.default.createDirectory(at: containerUrl, withIntermediateDirectories: true, attributes: nil) + } catch { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to create iCloud container \(error)", ICloudBackupError.iCloudError) + } + } + + let encryptedMnemonic: String + let encryptionSalt: String + do { + encryptionSalt = generateSalt(length: 16) + encryptedMnemonic = try encrypt(secret: mnemonic, password: password, salt: encryptionSalt) + } catch { + return reject(ICloudBackupError.backupEncryptionError.rawValue, "Failed to password encrypt mnemonic", ICloudBackupError.backupEncryptionError) + } + + // Write backup file to iCloud + let iCloudFileURL = containerUrl.appendingPathComponent("\(mnemonicId).json") + do { + let backup = CloudStorageMnemonicBackup(mnemonicId: mnemonicId, encryptedMnemonic: encryptedMnemonic, encryptionSalt: encryptionSalt, createdAt: Date().timeIntervalSince1970) + try JSONEncoder().encode(backup).write(to: iCloudFileURL) + return resolve(true) + } catch { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to write backup file to iCloud", ICloudBackupError.iCloudError) + } + } + + /** + + Attempts to restore mnemonic into native keychain from iCloud backup file. Assumes that the backup file `[mnemonicId].json` has already been downloaded from iCloud Documents using `RNCloudStorageBackupsManager` + + - parameter mnemonicId: key string associated with JSON backup file stored in iCloud + - parameter password: user inputted password used to decrypt backup + - returns: true if mnemonic successfully restored, otherwise a relevant error will be thrown + */ + @objc(restoreMnemonicFromCloudStorage:password:resolve:reject:) + func restoreMnemonicFromCloudStorage(mnemonicId: String, password: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + // Access Uniswap iCloud Documents container + guard let containerUrl = FileManager.default.url(forUbiquityContainerIdentifier: nil) else { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to find iCloud container", ICloudBackupError.iCloudError) + } + + // Fetch backup file from iCloud + let iCloudFileURL = containerUrl.appendingPathComponent("\(mnemonicId).json") + + guard FileManager.default.fileExists(atPath: iCloudFileURL.path) else { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to locate iCloud backup", ICloudBackupError.iCloudError) + } + + let data = try? Data(contentsOf: iCloudFileURL) + guard let backup = try? JSONDecoder().decode(CloudStorageMnemonicBackup.self, from: data!) else { + return reject(ICloudBackupError.iCloudError.rawValue, "Failed to load iCloud backup", ICloudBackupError.iCloudError) + } + + let decryptedMnemonic: String + do { + decryptedMnemonic = try decrypt(encryptedSecret: backup.encryptedMnemonic, password: password, salt: backup.encryptionSalt) + } catch CryptoKitError.authenticationFailure { + return reject(ICloudBackupError.backupIncorrectPasswordError.rawValue, "Invalid password. Please try again.", ICloudBackupError.backupIncorrectPasswordError) + } catch { + return reject(ICloudBackupError.backupDecryptionError.rawValue, "Failed to password decrypt mnemonic", ICloudBackupError.backupDecryptionError) + } + + // Restore mnemonic from backup into native keychain + let res = rnEthersRS.storeNewMnemonic(mnemonic: decryptedMnemonic, address: backup.mnemonicId) + if res == nil { + return reject(RNEthersRSError.storeMnemonicError.rawValue, "Failed to restore mnemonic into native keychain", RNEthersRSError.storeMnemonicError) + } + + return resolve(true) + } + + /** + Deletes mnemonic backup in iCloud Documents container + + - parameter mnemonicId: mnemonic backup filename to delete + - returns: boolean if deletion successful, otherwise throws error + */ + @objc(deleteCloudStorageMnemonicBackup:resolve:reject:) + func deleteCloudStorageMnemonicBackup(mnemonicId: String, resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock) { + // Access Uniswap iCloud Documents container + guard let containerUrl = FileManager.default.url(forUbiquityContainerIdentifier: nil) else { + return reject(ICloudBackupError.iCloudContainerError.rawValue, "Failed to find iCloud container", ICloudBackupError.iCloudContainerError) + } + + // Ensure backup file exists + let iCloudFileURL = containerUrl.appendingPathComponent("\(mnemonicId).json") + guard FileManager.default.fileExists(atPath: iCloudFileURL.path) else { + return reject(ICloudBackupError.backupNotFoundError.rawValue, "Failed to locate iCloud backup", ICloudBackupError.backupNotFoundError) + } + + // Delete backup file from iCloud + DispatchQueue.global(qos: .default).async { + let fileCoordinator = NSFileCoordinator(filePresenter: nil) + fileCoordinator.coordinate(writingItemAt: URL(fileURLWithPath: iCloudFileURL.path), options: NSFileCoordinator.WritingOptions.forDeleting, error: nil) { + url in + do { + try FileManager.default.removeItem(at: url) + return resolve(true) + } catch { + return reject(ICloudBackupError.deleteBackupError.rawValue, "Failed to delete iCloud backup", ICloudBackupError.deleteBackupError) + } + } + } + } + + /** + Starts NSMetadataQuery to discover backup files stored in iCloud Documents. Initializes listeners to handle downloading and sending found backups to JS. + + Referenced sample implementation here: https://developer.apple.com/documentation/uikit/documents_data_and_pasteboard/synchronizing_documents_in_the_icloud_environment + */ + @objc + func startFetchingCloudStorageBackups() { + // Fetch all JSON files in Uniswap iCloud container + backupsQuery.searchScopes = [NSMetadataQueryUbiquitousDataScope] + backupsQuery.predicate = + NSPredicate(format: "%K LIKE %@", NSMetadataItemFSNameKey, "*.json") + + NotificationCenter.default.addObserver(self, + selector: #selector(backupsMetadataDidChange), + name: .NSMetadataQueryDidFinishGathering, + object: nil) + NotificationCenter.default.addObserver(self, + selector: #selector(backupsMetadataDidChange), + name: .NSMetadataQueryDidUpdate, + object: nil) + + DispatchQueue.main.async { + self.backupsQuery.start() + } + } + + /** + Stops listening to updates from the NSMetadataQuery + */ + @objc + func stopFetchingCloudStorageBackups() { + NotificationCenter.default.removeObserver(self) + self.backupsQuery.stop() + } + + /** + Handles any updates to discovered backup files from the NSMetadataQuery. Downloads backup files from iCloud if they are not found locally and sends discovered backups' metadata to JS. + */ + @objc func backupsMetadataDidChange() { + self.backupsQuery.disableUpdates() + + self.backupsQuery.enumerateResults { (item: Any, _: Int, _: UnsafeMutablePointer) in + if let metadataItem = item as? NSMetadataItem, let url = metadataItem.value(forAttribute: NSMetadataItemURLKey) as? URL { + if isMetadataItemDownloaded(item: metadataItem) { + handleDownloadedBackup(url: url) + } else { + try? FileManager.default.startDownloadingUbiquitousItem(at: url) + } + } + } + + self.backupsQuery.enableUpdates() + } + + /** + Decodes a backup JSON file and sends backup metadata to JS via RCTEventEmitter + + - parameter url: URL of downloaded backup JSON file + */ + func handleDownloadedBackup(url: URL) { + let data = try? Data(contentsOf: url) + if let backup = try? JSONDecoder().decode(CloudStorageMnemonicBackup.self, from: data!) { + sendEvent(withName: ICloudManagerEventType.foundCloudBackup.rawValue, body: ["mnemonicId": backup.mnemonicId, "createdAt": backup.createdAt ]) + } else { + print("Error decoding iCloud backup JSON at \(url)") + } + } + + + /** + Determines if an iCloud Documents file discovered from NSMetadataQuery exists locally. + + - parameter item: NSMetadataItem that represents a file found in iCloud Documents + - returns: boolean for whether the file is downloaded from iCloud yet + */ + func isMetadataItemDownloaded(item : NSMetadataItem) -> Bool { + if item.value(forAttribute: NSMetadataUbiquitousItemDownloadingStatusKey) as? String == NSMetadataUbiquitousItemDownloadingStatusCurrent { + return true + } else { + return false + } + } +} diff --git a/apps/mobile/ios/Uniswap/RNEthersRs/KeychainSwiftDistrib.swift b/apps/mobile/ios/Uniswap/RNEthersRs/KeychainSwiftDistrib.swift new file mode 100644 index 0000000..9017d78 --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNEthersRs/KeychainSwiftDistrib.swift @@ -0,0 +1,486 @@ +// +// Keychain helper for iOS/Swift. +// +// https://github.com/evgenyneu/keychain-swift +// +// This file was automatically generated by combining multiple Swift source files. +// + +// ---------------------------- +// +// KeychainSwift.swift +// +// ---------------------------- +import Security +import Foundation + +/** +A collection of helper functions for saving text and data in the keychain. +*/ +open class KeychainSwift { + + var lastQueryParameters: [String: Any]? // Used by the unit tests + + /// Contains result code from the last operation. Value is noErr (0) for a successful result. + open var lastResultCode: OSStatus = noErr + + var keyPrefix = "" // Can be useful in test. + + /** + Specify an access group that will be used to access keychain items. Access groups can be used to share keychain items between applications. When access group value is nil all application access groups are being accessed. Access group name is used by all functions: set, get, delete and clear. + */ + open var accessGroup: String? + + + /** + + Specifies whether the items can be synchronized with other devices through iCloud. Setting this property to true will + add the item to other devices with the `set` method and obtain synchronizable items with the `get` command. Deleting synchronizable items will remove them from all devices. In order for keychain synchronization to work the user must enable "Keychain" in iCloud settings. + + Does not work on macOS. + + */ + open var synchronizable: Bool = false + + private let lock = NSLock() + + + /// Instantiate a KeychainSwift object + public init() { } + + /** + + - parameter keyPrefix: a prefix that is added before the key in get/set methods. Note that `clear` method still clears everything from the Keychain. + */ + public init(keyPrefix: String) { + self.keyPrefix = keyPrefix + } + + /** + + Stores the text value in the keychain item under the given key. + + - parameter key: Key under which the text value is stored in the keychain. + - parameter value: Text string to be written to the keychain. + - parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. + + - returns: True if the text was successfully written to the keychain. + */ + @discardableResult + open func set(_ value: String, forKey key: String, + withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { + + if let value = value.data(using: String.Encoding.utf8) { + return set(value, forKey: key, withAccess: access) + } + + return false + } + + /** + + Stores the data in the keychain item under the given key. + + - parameter key: Key under which the data is stored in the keychain. + - parameter value: Data to be written to the keychain. + - parameter withAccess: Value that indicates when your app needs access to the text in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. + + - returns: True if the text was successfully written to the keychain. + + */ + @discardableResult + open func set(_ value: Data, forKey key: String, + withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { + + // The lock prevents the code to be run simultaneously + // from multiple threads which may result in crashing + lock.lock() + defer { lock.unlock() } + + deleteNoLock(key) // Delete any existing key before saving it + let accessible = access?.value ?? KeychainSwiftAccessOptions.defaultOption.value + + let prefixedKey = keyWithPrefix(key) + + var query: [String : Any] = [ + KeychainSwiftConstants.klass : kSecClassGenericPassword, + KeychainSwiftConstants.attrAccount : prefixedKey, + KeychainSwiftConstants.valueData : value, + KeychainSwiftConstants.accessible : accessible + ] + + query = addAccessGroupWhenPresent(query) + query = addSynchronizableIfRequired(query, addingItems: true) + lastQueryParameters = query + + lastResultCode = SecItemAdd(query as CFDictionary, nil) + + return lastResultCode == noErr + } + + /** + Stores the boolean value in the keychain item under the given key. + - parameter key: Key under which the value is stored in the keychain. + - parameter value: Boolean to be written to the keychain. + - parameter withAccess: Value that indicates when your app needs access to the value in the keychain item. By default the .AccessibleWhenUnlocked option is used that permits the data to be accessed only while the device is unlocked by the user. + - returns: True if the value was successfully written to the keychain. + */ + @discardableResult + open func set(_ value: Bool, forKey key: String, + withAccess access: KeychainSwiftAccessOptions? = nil) -> Bool { + + let bytes: [UInt8] = value ? [1] : [0] + let data = Data(bytes) + + return set(data, forKey: key, withAccess: access) + } + + /** + + Retrieves the text value from the keychain that corresponds to the given key. + + - parameter key: The key that is used to read the keychain item. + - returns: The text value from the keychain. Returns nil if unable to read the item. + + */ + open func get(_ key: String) -> String? { + if let data = getData(key) { + + if let currentString = String(data: data, encoding: .utf8) { + return currentString + } + + lastResultCode = -67853 // errSecInvalidEncoding + } + + return nil + } + + /** + + Retrieves the data from the keychain that corresponds to the given key. + + - parameter key: The key that is used to read the keychain item. + - parameter asReference: If true, returns the data as reference (needed for things like NEVPNProtocol). + - returns: The text value from the keychain. Returns nil if unable to read the item. + + */ + open func getData(_ key: String, asReference: Bool = false) -> Data? { + // The lock prevents the code to be run simultaneously + // from multiple threads which may result in crashing + lock.lock() + defer { lock.unlock() } + + let prefixedKey = keyWithPrefix(key) + + var query: [String: Any] = [ + KeychainSwiftConstants.klass : kSecClassGenericPassword, + KeychainSwiftConstants.attrAccount : prefixedKey, + KeychainSwiftConstants.matchLimit : kSecMatchLimitOne + ] + + if asReference { + query[KeychainSwiftConstants.returnReference] = kCFBooleanTrue + } else { + query[KeychainSwiftConstants.returnData] = kCFBooleanTrue + } + + query = addAccessGroupWhenPresent(query) + query = addSynchronizableIfRequired(query, addingItems: false) + lastQueryParameters = query + + var result: AnyObject? + + lastResultCode = withUnsafeMutablePointer(to: &result) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + + if lastResultCode == noErr { + return result as? Data + } + + return nil + } + + /** + Retrieves the boolean value from the keychain that corresponds to the given key. + - parameter key: The key that is used to read the keychain item. + - returns: The boolean value from the keychain. Returns nil if unable to read the item. + */ + open func getBool(_ key: String) -> Bool? { + guard let data = getData(key) else { return nil } + guard let firstBit = data.first else { return nil } + return firstBit == 1 + } + + /** + Deletes the single keychain item specified by the key. + + - parameter key: The key that is used to delete the keychain item. + - returns: True if the item was successfully deleted. + + */ + @discardableResult + open func delete(_ key: String) -> Bool { + // The lock prevents the code to be run simultaneously + // from multiple threads which may result in crashing + lock.lock() + defer { lock.unlock() } + + return deleteNoLock(key) + } + + /** + Return all keys from keychain + + - returns: An string array with all keys from the keychain. + + */ + public var allKeys: [String] { + var query: [String: Any] = [ + KeychainSwiftConstants.klass : kSecClassGenericPassword, + KeychainSwiftConstants.returnData : true, + KeychainSwiftConstants.returnAttributes: true, + KeychainSwiftConstants.returnReference: true, + KeychainSwiftConstants.matchLimit: KeychainSwiftConstants.secMatchLimitAll + ] + + query = addAccessGroupWhenPresent(query) + query = addSynchronizableIfRequired(query, addingItems: false) + + var result: AnyObject? + + let lastResultCode = withUnsafeMutablePointer(to: &result) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + + if lastResultCode == noErr { + return (result as? [[String: Any]])?.compactMap { + $0[KeychainSwiftConstants.attrAccount] as? String } ?? [] + } + + return [] + } + + /** + + Same as `delete` but is only accessed internally, since it is not thread safe. + + - parameter key: The key that is used to delete the keychain item. + - returns: True if the item was successfully deleted. + + */ + @discardableResult + func deleteNoLock(_ key: String) -> Bool { + let prefixedKey = keyWithPrefix(key) + + var query: [String: Any] = [ + KeychainSwiftConstants.klass : kSecClassGenericPassword, + KeychainSwiftConstants.attrAccount : prefixedKey + ] + + query = addAccessGroupWhenPresent(query) + query = addSynchronizableIfRequired(query, addingItems: false) + lastQueryParameters = query + + lastResultCode = SecItemDelete(query as CFDictionary) + + return lastResultCode == noErr + } + + /** + + Deletes all Keychain items used by the app. Note that this method deletes all items regardless of the prefix settings used for initializing the class. + + - returns: True if the keychain items were successfully deleted. + + */ + @discardableResult + open func clear() -> Bool { + // The lock prevents the code to be run simultaneously + // from multiple threads which may result in crashing + lock.lock() + defer { lock.unlock() } + + var query: [String: Any] = [ kSecClass as String : kSecClassGenericPassword ] + query = addAccessGroupWhenPresent(query) + query = addSynchronizableIfRequired(query, addingItems: false) + lastQueryParameters = query + + lastResultCode = SecItemDelete(query as CFDictionary) + + return lastResultCode == noErr + } + + /// Returns the key with currently set prefix. + func keyWithPrefix(_ key: String) -> String { + return "\(keyPrefix)\(key)" + } + + func addAccessGroupWhenPresent(_ items: [String: Any]) -> [String: Any] { + guard let accessGroup = accessGroup else { return items } + + var result: [String: Any] = items + result[KeychainSwiftConstants.accessGroup] = accessGroup + return result + } + + /** + + Adds kSecAttrSynchronizable: kSecAttrSynchronizableAny` item to the dictionary when the `synchronizable` property is true. + + - parameter items: The dictionary where the kSecAttrSynchronizable items will be added when requested. + - parameter addingItems: Use `true` when the dictionary will be used with `SecItemAdd` method (adding a keychain item). For getting and deleting items, use `false`. + + - returns: the dictionary with kSecAttrSynchronizable item added if it was requested. Otherwise, it returns the original dictionary. + + */ + func addSynchronizableIfRequired(_ items: [String: Any], addingItems: Bool) -> [String: Any] { + if !synchronizable { return items } + var result: [String: Any] = items + result[KeychainSwiftConstants.attrSynchronizable] = addingItems == true ? true : kSecAttrSynchronizableAny + return result + } +} + + +// ---------------------------- +// +// TegKeychainConstants.swift +// +// ---------------------------- +import Foundation +import Security + +/// Constants used by the library +public struct KeychainSwiftConstants { + /// Specifies a Keychain access group. Used for sharing Keychain items between apps. + public static var accessGroup: String { return toString(kSecAttrAccessGroup) } + + /** + + A value that indicates when your app needs access to the data in a keychain item. The default value is AccessibleWhenUnlocked. For a list of possible values, see KeychainSwiftAccessOptions. + + */ + public static var accessible: String { return toString(kSecAttrAccessible) } + + /// Used for specifying a String key when setting/getting a Keychain value. + public static var attrAccount: String { return toString(kSecAttrAccount) } + + /// Used for specifying synchronization of keychain items between devices. + public static var attrSynchronizable: String { return toString(kSecAttrSynchronizable) } + + /// An item class key used to construct a Keychain search dictionary. + public static var klass: String { return toString(kSecClass) } + + /// Specifies the number of values returned from the keychain. The library only supports single values. + public static var matchLimit: String { return toString(kSecMatchLimit) } + + /// A return data type used to get the data from the Keychain. + public static var returnData: String { return toString(kSecReturnData) } + + /// Used for specifying a value when setting a Keychain value. + public static var valueData: String { return toString(kSecValueData) } + + /// Used for returning a reference to the data from the keychain + public static var returnReference: String { return toString(kSecReturnPersistentRef) } + + /// A key whose value is a Boolean indicating whether or not to return item attributes + public static var returnAttributes : String { return toString(kSecReturnAttributes) } + + /// A value that corresponds to matching an unlimited number of items + public static var secMatchLimitAll : String { return toString(kSecMatchLimitAll) } + + static func toString(_ value: CFString) -> String { + return value as String + } +} + + +// ---------------------------- +// +// KeychainSwiftAccessOptions.swift +// +// ---------------------------- +import Security + +/** +These options are used to determine when a keychain item should be readable. The default value is AccessibleWhenUnlocked. +*/ +public enum KeychainSwiftAccessOptions { + + /** + + The data in the keychain item can be accessed only while the device is unlocked by the user. + + This is recommended for items that need to be accessible only while the application is in the foreground. Items with this attribute migrate to a new device when using encrypted backups. + + This is the default value for keychain items added without explicitly setting an accessibility constant. + + */ + case accessibleWhenUnlocked + + /** + + The data in the keychain item can be accessed only while the device is unlocked by the user. + + This is recommended for items that need to be accessible only while the application is in the foreground. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. + + */ + case accessibleWhenUnlockedThisDeviceOnly + + /** + + The data in the keychain item cannot be accessed after a restart until the device has been unlocked once by the user. + + After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute migrate to a new device when using encrypted backups. + + */ + case accessibleAfterFirstUnlock + + /** + + The data in the keychain item cannot be accessed after a restart until the device has been unlocked once by the user. + + After the first unlock, the data remains accessible until the next restart. This is recommended for items that need to be accessed by background applications. Items with this attribute do not migrate to a new device. Thus, after restoring from a backup of a different device, these items will not be present. + + */ + case accessibleAfterFirstUnlockThisDeviceOnly + + /** + + The data in the keychain can only be accessed when the device is unlocked. Only available if a passcode is set on the device. + + This is recommended for items that only need to be accessible while the application is in the foreground. Items with this attribute never migrate to a new device. After a backup is restored to a new device, these items are missing. No items can be stored in this class on devices without a passcode. Disabling the device passcode causes all items in this class to be deleted. + + */ + case accessibleWhenPasscodeSetThisDeviceOnly + + static var defaultOption: KeychainSwiftAccessOptions { + return .accessibleWhenUnlocked + } + + var value: String { + switch self { + case .accessibleWhenUnlocked: + return toString(kSecAttrAccessibleWhenUnlocked) + + case .accessibleWhenUnlockedThisDeviceOnly: + return toString(kSecAttrAccessibleWhenUnlockedThisDeviceOnly) + + case .accessibleAfterFirstUnlock: + return toString(kSecAttrAccessibleAfterFirstUnlock) + + case .accessibleAfterFirstUnlockThisDeviceOnly: + return toString(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly) + + case .accessibleWhenPasscodeSetThisDeviceOnly: + return toString(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly) + } + } + + func toString(_ value: CFString) -> String { + return KeychainSwiftConstants.toString(value) + } +} + diff --git a/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h new file mode 100644 index 0000000..ff3d140 --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS-Bridging-Header.h @@ -0,0 +1,12 @@ +// +// RNEthersRS-Bridging-Header.h +// Uniswap +// +// Created by Connor McEwen on 10/28/21. +// + +#import +#import +#import "libethers_ffi.h" +#import +#import diff --git a/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift new file mode 100644 index 0000000..c2a392f --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNEthersRs/RNEthersRS.swift @@ -0,0 +1,244 @@ +// +// RNEthers.swift +// Uniswap +// +// Created by Connor McEwen on 10/28/21. + +/** + Provides the generation, storage, and signing logic for mnemonics and private keys so that they never passed to JS. + + Mnemonics and private keys are stored and accessed in the native iOS secure keychain key-value store via associated keys formed from concatenating a constant prefix with the associated public address. + + Uses KeychainSwift as a wrapper utility to interface with the native iOS secure keychain. */ + +import Foundation +import CryptoKit + +// TODO: [MOB-200] move constants to another file +let prefix = "com.uniswap.mobile" +let mnemonicPrefix = ".mnemonic." +let privateKeyPrefix = ".privateKey." +let entireMnemonicPrefix = prefix + mnemonicPrefix +let entirePrivateKeyPrefix = prefix + privateKeyPrefix + +enum RNEthersRSError: String, Error { + case storeMnemonicError = "storeMnemonicError" + case retrieveMnemonicError = "retrieveMnemonicError" +} + +@objc(RNEthersRS) + +class RNEthersRS: NSObject { + private let keychain = KeychainSwift(keyPrefix: prefix) + // TODO: [MOB-208] LRU cache to ensure we don't create too many (unlikely to happen) + private var walletCache: [String: OpaquePointer] = [:] + + @objc static func requiresMainQueueSetup() -> Bool { + return false + } + + func findInvalidWord(mnemonic: String) -> String { + let firstInvalidMnemonic = find_invalid_word(mnemonic) + return String(cString: firstInvalidMnemonic!) + } + + func validateMnemonic(mnemonic: String) -> Bool { + return validate_mnemonic(mnemonic) + } + + /** + Fetches all mnemonic IDs, which are used as keys to access the actual mnemonics in the native keychain secure key-value store. + + - returns: array of mnemonic IDs + */ + @objc(getMnemonicIds:reject:) + func getMnemonicIds(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let mnemonicIds = keychain.allKeys.filter { key in + key.contains(mnemonicPrefix) + }.map { key in + key.replacingOccurrences(of: entireMnemonicPrefix, with: "") + } + resolve(mnemonicIds) + } + + /** + Derives private key from mnemonic with derivation index 0 and retrieves associated public address. Stores imported mnemonic in native keychain with the mnemonic ID key as the public address. + + - parameter mnemonic: The mnemonic phrase to import + - returns: public address from the mnemonic's first derived private key + */ + @objc(importMnemonic:resolve:reject:) + func importMnemonic( + mnemonic: String, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let private_key = private_key_from_mnemonic( + mnemonic, UInt32(exactly: 0)!) + let address = String(cString: private_key.address!) + + let res = storeNewMnemonic(mnemonic: mnemonic, address: address) + if res != nil { + resolve(res) + return + } + let err = NSError.init() + reject("error", "error", err) + return + } + + /** + Generates a new mnemonic and retrieves associated public address. Stores new mnemonic in native keychain with the mnemonic ID key as the public address. + + - returns: public address from the mnemonic's first derived private key + */ + @objc(generateAndStoreMnemonic:reject:) + func generateAndStoreMnemonic(resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) { + let mnemonic_ptr = generate_mnemonic() + let mnemonic_str = String(cString: mnemonic_ptr.mnemonic!) + let address_str = String(cString: mnemonic_ptr.address!) + let res = storeNewMnemonic(mnemonic: mnemonic_str, address: address_str) + mnemonic_free(mnemonic_ptr) + resolve(res) + } + + /** + Stores mnemonic phrase in Native Keychain under the address + + - returns: public address if successfully stored in native keychain + */ + func storeNewMnemonic(mnemonic: String, address: String) -> String? { + let checkStored = retrieveMnemonic(mnemonicId: address) + + if checkStored == nil { + let newMnemonicKey = keychainKeyForMnemonicId(mnemonicId: address); + keychain.set(mnemonic, forKey: newMnemonicKey, withAccess: .accessibleWhenUnlockedThisDeviceOnly) + return address + } + + return address + } + + func keychainKeyForMnemonicId(mnemonicId: String) -> String { + return mnemonicPrefix + mnemonicId + } + + func retrieveMnemonic(mnemonicId: String) -> String? { + return keychain.get(keychainKeyForMnemonicId(mnemonicId: mnemonicId)) + } + + /** + Fetches all public addresses from private keys stored under `privateKeyPrefix` in native keychain. Used from React Native to verify the native keychain has the private key for an account that is attempting create a NativeSigner that calls native signing methods + + - returns: public addresses for all stored private keys + */ + @objc(getAddressesForStoredPrivateKeys:reject:) + func getAddressesForStoredPrivateKeys( + resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock + ) { + let addresses = keychain.allKeys.filter { key in + key.contains(privateKeyPrefix) + }.map { key in + key.replacingOccurrences(of: entirePrivateKeyPrefix, with: "") + } + resolve(addresses) + } + + func storeNewPrivateKey(address: String, privateKey: String) { + let newKey = keychainKeyForPrivateKey(address: address); + keychain.set(privateKey, forKey: newKey, withAccess: .accessibleWhenUnlockedThisDeviceOnly) + } + + /** + Derives public address from mnemonic for given `derivationIndex`. + + - parameter mnemonic: mnemonic to generate public key for + - parameter derivationIndex: number used to specify a which derivation index to use for deriving a private key from the mnemonic + - returns: public address associated with private key generated from the mnemonic at given derivation index + */ + @objc(generateAddressForMnemonic:derivationIndex:resolve:reject:) + func generateAddressForMnemonic( + mnemonic: String, derivationIndex: Int, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let private_key = private_key_from_mnemonic( + mnemonic, UInt32(exactly: derivationIndex)!) + let address = String(cString: private_key.address!) + private_key_free(private_key) + resolve(address) + } + + /** + Derives private key and public address from mnemonic associated with `mnemonicId` for given `derivationIndex`. Stores the private key in native keychain with key. + + - parameter mnemonicId: key string associated with mnemonic to generate private key for (currently convention is to use public address associated with mnemonic) + - parameter derivationIndex: number used to specify a which derivation index to use for deriving a private key from the mnemonic + - returns: public address associated with private key generated from the mnemonic at given derivation index + */ + @objc(generateAndStorePrivateKey:derivationIndex:resolve:reject:) + func generateAndStorePrivateKey( + mnemonicId: String, derivationIndex: Int, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let mnemonic = retrieveMnemonic(mnemonicId: mnemonicId) + let private_key = private_key_from_mnemonic( + mnemonic, UInt32(exactly: derivationIndex)!) + let xprv = String(cString: private_key.private_key!) + let address = String(cString: private_key.address!) + storeNewPrivateKey(address: address, privateKey: xprv) + private_key_free(private_key) + resolve(address) + } + + @objc(signTransactionHashForAddress:hash:chainId:resolve:reject:) + func signTransactionForAddress( + address: String, hash: String, chainId: NSNumber, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let wallet = retrieveOrCreateWalletForAddress(address: address) + let signedHash = sign_tx_with_wallet(wallet, hash, UInt64(chainId)) + let result = String(cString: signedHash.signature!) + resolve(result); + } + + @objc(signMessageForAddress:message:resolve:reject:) + func signMessageForAddress( + address: String, message: String, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let wallet = retrieveOrCreateWalletForAddress(address: address) + let signedMessage = sign_message_with_wallet(wallet, message) + let result = String(cString: signedMessage!) + string_free(signedMessage) + resolve(result) + } + + @objc(signHashForAddress:hash:chainId:resolve:reject:) + func signHashForAddress( + address: String, hash: String, chainId: NSNumber, resolve: RCTPromiseResolveBlock, + reject: RCTPromiseRejectBlock + ) { + let wallet = retrieveOrCreateWalletForAddress(address: address) + let signedHash = sign_hash_with_wallet(wallet, hash, UInt64(chainId)) + let result = String(cString: signedHash!) + string_free(signedHash) + resolve(result) + } + + func retrieveOrCreateWalletForAddress(address: String) -> OpaquePointer { + if walletCache[address] != nil { + return walletCache[address]! + } + let privateKey = retrievePrivateKey(address: address) + let wallet = wallet_from_private_key(privateKey) + walletCache[address] = wallet + return wallet! + } + + func retrievePrivateKey(address: String) -> String? { + return keychain.get(keychainKeyForPrivateKey(address: address)) + } + + func keychainKeyForPrivateKey(address: String) -> String { + return privateKeyPrefix + address + } +} diff --git a/apps/mobile/ios/Uniswap/RNEthersRs/RnEthersRS.m b/apps/mobile/ios/Uniswap/RNEthersRs/RnEthersRS.m new file mode 100644 index 0000000..122fac5 --- /dev/null +++ b/apps/mobile/ios/Uniswap/RNEthersRs/RnEthersRS.m @@ -0,0 +1,52 @@ +// +// RnEthersBridge.m +// Uniswap +// +// Created by Connor McEwen on 10/28/21. +// + +#import + +@interface RCT_EXTERN_MODULE(RNEthersRS, NSObject) + +RCT_EXTERN_METHOD(getMnemonicIds: (RCTPromiseResolveBlock)resolve + reject:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(importMnemonic: (NSString *)mnemonic + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(generateAndStoreMnemonic: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(getAddressesForStoredPrivateKeys: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(generateAddressForMnemonic: (NSString *)mnemonic + derivationIndex: (NSInteger)index + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(generateAndStorePrivateKey: (NSString *)mnemonicId + derivationIndex: (NSInteger)index + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(signTransactionHashForAddress: (NSString *)address + hash: (NSString *)hash + chainId: NSNumber + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(signMessageForAddress: (NSString *)address + message: (NSString *)message + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(signHashForAddress: (NSString *)address + hash: (NSString *)hash + chainId: NSNumber + resolve: (RCTPromiseResolveBlock)resolve + reject: (RCTPromiseRejectBlock)reject) + +@end diff --git a/apps/mobile/ios/Uniswap/SplashScreen.storyboard b/apps/mobile/ios/Uniswap/SplashScreen.storyboard new file mode 100644 index 0000000..6207c45 --- /dev/null +++ b/apps/mobile/ios/Uniswap/SplashScreen.storyboard @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/ios/Uniswap/Uniswap.entitlements b/apps/mobile/ios/Uniswap/Uniswap.entitlements new file mode 100644 index 0000000..ec5fce6 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Uniswap.entitlements @@ -0,0 +1,33 @@ + + + + + aps-environment + development + com.apple.developer.associated-domains + + applinks:uniswap.org + applinks:app.uniswap.org + applinks:app.corn-staging.com + + com.apple.developer.devicecheck.appattest-environment + production + com.apple.developer.icloud-container-identifiers + + iCloud.Uniswap + + com.apple.developer.icloud-services + + CloudDocuments + + com.apple.developer.ubiquity-container-identifiers + + iCloud.Uniswap + + com.apple.security.application-groups + + group.com.$(PRODUCT_NAME).onesignal + group.com.uniswap.widgets + + + diff --git a/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.h b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.h new file mode 100644 index 0000000..0c777d6 --- /dev/null +++ b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.h @@ -0,0 +1,15 @@ +// +// RNWalletConnect.h +// Uniswap +// +// Created by Tina Zheng on 3/4/22. +// + +#ifndef RNWalletConnect_h +#define RNWalletConnect_h + +#import +@interface RNWalletConnect : NSObject +@end + +#endif /* RNWalletConnect_h */ diff --git a/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.m b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.m new file mode 100644 index 0000000..8ed7c0b --- /dev/null +++ b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.m @@ -0,0 +1,14 @@ +// +// RNWalletConnect.m +// Uniswap +// +// Created by Tina Zheng on 3/7/22. +// + +#import + +@interface RCT_EXTERN_MODULE(RNWalletConnect, NSObject) + +RCT_EXTERN_METHOD(returnToPreviousApp) + +@end diff --git a/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.swift b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.swift new file mode 100644 index 0000000..bd65cee --- /dev/null +++ b/apps/mobile/ios/Uniswap/WalletConnect/RNWalletConnect.swift @@ -0,0 +1,47 @@ +// +// RNWalletConnect.swift +// Uniswap +// +// Created by Tina Zheng on 3/4/22. +// + +import Foundation + +// Used to return to previously opened app (wallet to dapp in mobile browser) +@objc private protocol PrivateSelectors: NSObjectProtocol { + var destinations: [NSNumber] { get } + func sendResponseForDestination(_ destination: NSNumber) +} + +@objc(RNWalletConnect) +class RNWalletConnect: NSObject { + + /* + * Open the previously opened app that deep linked to Uniswap app + * (eg. Dapp website in Safari -> Wallet -> Dapp website in Safari). + * Returns false and does nothing if there is no previous opened app to link back to. + * Returns true if successfully opened previous app + */ + @objc + func returnToPreviousApp() -> Bool { + let sys = "_system" + let nav = "Navigation" + let action = "Action" + guard + let sysNavIvar = class_getInstanceVariable(UIApplication.self, sys + nav + action), + let action = object_getIvar(UIApplication.shared, sysNavIvar) as? NSObject, + let destinations = action.perform(#selector(getter: PrivateSelectors.destinations)).takeUnretainedValue() as? [NSNumber], + let firstDestination = destinations.first + else { + return false + } + + action.perform(#selector(PrivateSelectors.sendResponseForDestination), with: firstDestination) + return true + } + + @objc static func requiresMainQueueSetup() -> Bool { + return false + } + +} diff --git a/apps/mobile/ios/Uniswap/Widget/RNWidgets.m b/apps/mobile/ios/Uniswap/Widget/RNWidgets.m new file mode 100644 index 0000000..faa9d95 --- /dev/null +++ b/apps/mobile/ios/Uniswap/Widget/RNWidgets.m @@ -0,0 +1,15 @@ +// +// RNWidgets.m +// Uniswap +// +// Created by Eric Huang on 8/2/23. +// + +#import + +@interface RCT_EXTERN_MODULE(RNWidgets, NSObject) + +RCT_EXTERN_METHOD(hasWidgetsInstalled: (RCTPromiseResolveBlock *)resolve + reject:(RCTPromiseRejectBlock *)reject) + +@end diff --git a/apps/mobile/ios/Uniswap/Widget/RNWidgets.swift b/apps/mobile/ios/Uniswap/Widget/RNWidgets.swift new file mode 100644 index 0000000..74eadaa --- /dev/null +++ b/apps/mobile/ios/Uniswap/Widget/RNWidgets.swift @@ -0,0 +1,29 @@ +// +// RNWidgets.swift +// Uniswap +// +// Created by Eric Huang on 8/2/23. +// + +import Foundation +import WidgetKit + +@objc(RNWidgets) +class RNWidgets: NSObject { + + @objc + func hasWidgetsInstalled(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) { + WidgetCenter.shared.getCurrentConfigurations() { result in + if case .success(let config) = result { + resolve(config.count > 0) + } else { + resolve(false) + } + } + } + + @objc + static func requiresMainQueueSetup() -> Bool { + return false + } +} diff --git a/apps/mobile/ios/Uniswap/main.m b/apps/mobile/ios/Uniswap/main.m new file mode 100644 index 0000000..b1df44b --- /dev/null +++ b/apps/mobile/ios/Uniswap/main.m @@ -0,0 +1,9 @@ +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/apps/mobile/ios/UniswapTests/EncryptionHelperTests.swift b/apps/mobile/ios/UniswapTests/EncryptionHelperTests.swift new file mode 100644 index 0000000..dc237c0 --- /dev/null +++ b/apps/mobile/ios/UniswapTests/EncryptionHelperTests.swift @@ -0,0 +1,107 @@ +// +// EncryptionHelperTests.swift +// UniswapTests +// +// Created by Spencer Yen on 7/27/22. +// + +import XCTest +import Argon2Swift +@testable import Uniswap + +class EncryptionHelperTests: XCTestCase { + + private let secret = "student zone flight quote trial case shadow alien yard choose quiz produce" + private let password = "012345" + private let saltLength = 16 + + func testEncryptAndDecrypt() throws { + let salt = generateSalt(length: saltLength) + print("Secret: \(secret)") + print("Password: \(password)") + print("Salt: \(salt)") + + let encryptedSecret = try encrypt(secret: secret, password: password, salt: salt) + XCTAssertNotNil(encryptedSecret, "Failed to encrypt secret") + print("Encrypted Secret: \(encryptedSecret)") + + let decryptedSecret = try decrypt(encryptedSecret: encryptedSecret, password: password, salt: salt) + XCTAssertEqual(secret, decryptedSecret, "Decrypted secret does not match plaintext secret") + print("Decrypted Secret: \(decryptedSecret)") + } + + func testEncryptAndDecryptFail() throws { + let salt = generateSalt(length: saltLength) + + let encryptedSecret = try encrypt(secret: secret, password: password, salt: salt) + XCTAssertNotNil(encryptedSecret, "Failed to encrypt secret") + + XCTAssertThrowsError(try decrypt(encryptedSecret: encryptedSecret, password: "wrong", salt: salt), "No error thrown when decrypting with invalid password") + } + + func testArgon2KDF1Iteration1GBMemory() throws { + measure { + do { + let iterations = 1 + let memory = 2 << 19 // 2^20 KiB = 1024MiB + let salt = generateSalt(length: saltLength) + let _ = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: iterations, memory: memory, parallelism: 4, length: 32, type: .id) + } catch { + XCTAssertNil(error, "Error hashing password with Argon2") + } + } + } + + func testArgon2KDF3Iteration512MBMemory() throws { + measure { + do { + let iterations = 3 + let memory = 2 << 18 // 2^19 KiB = 512MiB + let salt = generateSalt(length: saltLength) + let _ = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: iterations, memory: memory, parallelism: 4, length: 32, type: .id) + } catch { + XCTAssertNil(error, "Error hashing password with Argon2") + } + } + } + + func testArgon2KDF3Iteration256MBMemory() throws { + measure { + do { + let iterations = 3 + let memory = 2 << 17 // 2^18 KiB = 256MiB + let salt = generateSalt(length: saltLength) + let _ = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: iterations, memory: memory, parallelism: 4, length: 32, type: .id) + } catch { + XCTAssertNil(error, "Error hashing password with Argon2") + } + } + } + + func testArgon2KDF3Iteration128MBMemory() throws { + measure { + do { + let iterations = 3 + let memory = 2 << 16 // 2^17 KiB = 128MiB + let salt = generateSalt(length: saltLength) + let _ = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: iterations, memory: memory, parallelism: 4, length: 32, type: .id) + } catch { + XCTAssertNil(error, "Error hashing password with Argon2") + } + } + } + + func testArgon2KDF3Iteration64MBMemory() throws { + measure { + do { + let iterations = 3 + let memory = 2 << 15 // 2^16 KiB = 64MiB + let salt = generateSalt(length: saltLength) + let _ = try Argon2Swift.hashPasswordString(password: password, salt: Salt(bytes: Data(salt.utf8)), iterations: iterations, memory: memory, parallelism: 4, length: 32, type: .id) + } catch { + XCTAssertNil(error, "Error hashing password with Argon2") + } + } + } + +} diff --git a/apps/mobile/ios/UniswapTests/Info.plist b/apps/mobile/ios/UniswapTests/Info.plist new file mode 100644 index 0000000..a7d41dc --- /dev/null +++ b/apps/mobile/ios/UniswapTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/apps/mobile/ios/UniswapTests/UniswapTests.m b/apps/mobile/ios/UniswapTests/UniswapTests.m new file mode 100644 index 0000000..9de5f53 --- /dev/null +++ b/apps/mobile/ios/UniswapTests/UniswapTests.m @@ -0,0 +1,65 @@ +#import +#import + +#import +#import + +#define TIMEOUT_SECONDS 600 +#define TEXT_TO_LOOK_FOR @"Welcome to React" + +@interface UniswapTests : XCTestCase + +@end + +@implementation UniswapTests + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRendersWelcomeScreen +{ + UIViewController *vc = [[[RCTSharedApplication() delegate] window] rootViewController]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + + __block NSString *redboxError = nil; +#ifdef DEBUG + RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); +#endif + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { + return YES; + } + return NO; + }]; + } + +#ifdef DEBUG + RCTSetLogFunction(RCTDefaultLogFunction); +#endif + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); +} + + +@end diff --git a/apps/mobile/ios/WidgetIntentExtension/Info.plist b/apps/mobile/ios/WidgetIntentExtension/Info.plist new file mode 100644 index 0000000..0bb4a44 --- /dev/null +++ b/apps/mobile/ios/WidgetIntentExtension/Info.plist @@ -0,0 +1,24 @@ + + + + + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + TokenPriceConfigurationIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/apps/mobile/ios/WidgetIntentExtension/IntentHandler.swift b/apps/mobile/ios/WidgetIntentExtension/IntentHandler.swift new file mode 100644 index 0000000..9fa75c6 --- /dev/null +++ b/apps/mobile/ios/WidgetIntentExtension/IntentHandler.swift @@ -0,0 +1,82 @@ +// +// IntentHandler.swift +// WidgetIntentExtension +// +// Created by Eric Huang on 7/5/23. +// + +import Intents +import WidgetsCore +import OSLog + + +class IntentHandler: INExtension, TokenPriceConfigurationIntentHandling { + + enum Section: String { + case top = ".top" + case favorite = ".favorite" + case owned = ".owned" + } + + lazy var ETHTokenResponse = TokenResponse(chain: WidgetConstants.ethereumChain, symbol: WidgetConstants.ethereumSymbol, name: "Ethereum") + + func tokenResponseToIntentToken(_ result: TokenResponse, section: Section) -> IntentToken { + let intentToken: IntentToken = IntentToken(identifier: result.name + section.rawValue, display: "\(result.name)", subtitle: "\(result.symbol)", image: nil) + intentToken.name = result.name + intentToken.symbol = result.symbol + intentToken.address = result.address + intentToken.chain = result.chain + return intentToken + } + + // Dedupes the tokens list and keeps the first instance of the token in the list. + // If there are two of the same tokens and 1 is mainnet, use the mainet token + func dedupeTokens(_ intentTokens: [IntentToken]) -> [IntentToken] { + var dedupedTokens: [IntentToken] = [] + for intentToken in intentTokens { + if let index = dedupedTokens.firstIndex(where: {$0.name == intentToken.name && $0.symbol == intentToken.symbol}) { + if intentToken.chain == WidgetConstants.ethereumChain { + dedupedTokens[index] = intentToken + } + } else { + dedupedTokens.append(intentToken) + } + } + return dedupedTokens + } + + func provideSelectedTokenOptionsCollection(for intent: TokenPriceConfigurationIntent) async throws -> INObjectCollection { + let favorites = UniswapUserDefaults.readFavorites() + let addresses = UniswapUserDefaults.readAccounts().accounts.filter{$0.isSigner}.map{$0.address} + + async let pendingOwnedTokensResponses = try DataQueries.fetchWalletsTokensData(addresses: addresses) + async let pendingFavoriteTokenReponses = try DataQueries.fetchTokensData(tokenInputs: favorites.favorites) + async let pendingTopTokensResponse = try DataQueries.fetchTopTokensData() + let (ownedTokenResponses ,favoriteTokenReponses, topTokensResponse) = await (try pendingOwnedTokensResponses, try pendingFavoriteTokenReponses, try pendingTopTokensResponse) + + let ownedTokens = dedupeTokens(ownedTokenResponses.map {tokenResponseToIntentToken($0, section: Section.owned)}) + let favoriteTokens = favoriteTokenReponses.map {tokenResponseToIntentToken($0, section: Section.favorite)} + let topTokens = topTokensResponse.map { (result) -> IntentToken in + // replace wETH with ETH in the configuration + if (result.address == WidgetConstants.WETHAddress && result.chain == WidgetConstants.ethereumChain) { + return tokenResponseToIntentToken(ETHTokenResponse, section: Section.top) + } + return tokenResponseToIntentToken(result, section: Section.top) + } + + let ownedSection = INObjectSection(title: "Your Tokens", items: ownedTokens) + let favoriteSection = INObjectSection(title: "Favorite Tokens", items: favoriteTokens) + let topTokensSection = INObjectSection(title: "Top Tokens", items: topTokens) + + return INObjectCollection(sections: [ownedSection, favoriteSection, topTokensSection]) + } + + func defaultSelectedToken(for intent: TokenPriceConfigurationIntent) -> IntentToken? { + return tokenResponseToIntentToken(ETHTokenResponse, section: Section.top) + } + + override func handler(for intent: INIntent) -> Any { + return self + } + +} diff --git a/apps/mobile/ios/WidgetIntentExtension/WidgetIntentExtension.entitlements b/apps/mobile/ios/WidgetIntentExtension/WidgetIntentExtension.entitlements new file mode 100644 index 0000000..aa6880a --- /dev/null +++ b/apps/mobile/ios/WidgetIntentExtension/WidgetIntentExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.uniswap.widgets + + + diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json new file mode 100644 index 0000000..c7fea1d --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "caret-up.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/caret-up.svg b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/caret-up.svg new file mode 100644 index 0000000..ede1854 --- /dev/null +++ b/apps/mobile/ios/Widgets/Assets.xcassets/caret-up.imageset/caret-up.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/ios/Widgets/Info.plist b/apps/mobile/ios/Widgets/Info.plist new file mode 100644 index 0000000..f0a00dc --- /dev/null +++ b/apps/mobile/ios/Widgets/Info.plist @@ -0,0 +1,19 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + UIAppFonts + + Basel-Book.otf + Basel-Bold.otf + Basel-Medium.otf + Basel-Book.otf + Basel-Semibold.otf + + + diff --git a/apps/mobile/ios/Widgets/TokenPriceWidget.intentdefinition b/apps/mobile/ios/Widgets/TokenPriceWidget.intentdefinition new file mode 100644 index 0000000..e596c4c --- /dev/null +++ b/apps/mobile/ios/Widgets/TokenPriceWidget.intentdefinition @@ -0,0 +1,223 @@ + + + + + INEnums + + INIntentDefinitionModelVersion + 1.2 + INIntentDefinitionNamespace + 88xZPY + INIntentDefinitionSystemVersion + 22F82 + INIntentDefinitionToolsBuildVersion + 14C18 + INIntentDefinitionToolsVersion + 14.2 + INIntents + + + INIntentCategory + information + INIntentDescription + Select a token to track its price + INIntentDescriptionID + tVvJ9c + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentLastParameterTag + 2 + INIntentName + TokenPriceConfiguration + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Selected Token + INIntentParameterDisplayNameID + 94tJAN + INIntentParameterDisplayPriority + 1 + INIntentParameterName + selectedToken + INIntentParameterObjectType + IntentToken + INIntentParameterObjectTypeNamespace + 88xZPY + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterTag + 2 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Token Price Configuration + INIntentTitleID + gpCwrM + INIntentType + Custom + INIntentVerb + View + + + INTypes + + + INTypeDisplayName + Intent Token + INTypeDisplayNameID + QK0Gto + INTypeLastPropertyTag + 107 + INTypeName + IntentToken + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + INTypePropertyDisplayName + Symbol + INTypePropertyDisplayNameID + mO9gGm + INTypePropertyDisplayPriority + 5 + INTypePropertyName + symbol + INTypePropertyTag + 100 + INTypePropertyType + String + + + INTypePropertyDisplayName + Name + INTypePropertyDisplayNameID + aKn8VW + INTypePropertyDisplayPriority + 6 + INTypePropertyName + name + INTypePropertyTag + 105 + INTypePropertyType + String + + + INTypePropertyDisplayName + Address + INTypePropertyDisplayNameID + rCOb5U + INTypePropertyDisplayPriority + 7 + INTypePropertyName + address + INTypePropertyTag + 107 + INTypePropertyType + String + + + INTypePropertyDisplayName + Chain + INTypePropertyDisplayNameID + OOvm01 + INTypePropertyDisplayPriority + 8 + INTypePropertyName + chain + INTypePropertyTag + 103 + INTypePropertyType + String + + + + + + diff --git a/apps/mobile/ios/Widgets/TokenPriceWidget.swift b/apps/mobile/ios/Widgets/TokenPriceWidget.swift new file mode 100644 index 0000000..fa6212e --- /dev/null +++ b/apps/mobile/ios/Widgets/TokenPriceWidget.swift @@ -0,0 +1,357 @@ +// +// TokenPriceWidget.swift +// TokenPriceWidget +// +// Created by Eric Huang on 6/22/23. +// + +import WidgetKit +import SwiftUI +import Intents +import WidgetsCore +import Apollo +import Charts + +let placeholderPriceHistory = [ + PriceHistory(timestamp: 1689792001, price: 2161), + PriceHistory(timestamp: 1689792245, price: 2160), + PriceHistory(timestamp: 1689792571, price: 2163), + PriceHistory(timestamp: 1689792894, price: 2164), + PriceHistory(timestamp: 1689793209, price: 2166), + PriceHistory(timestamp: 1689793465, price: 2163), + PriceHistory(timestamp: 1689793781, price: 2164), + PriceHistory(timestamp: 1689794035, price: 2163), + PriceHistory(timestamp: 1689794381, price: 2164), + PriceHistory(timestamp: 1689794701, price: 2167), + PriceHistory(timestamp: 1689794997, price: 2167), + PriceHistory(timestamp: 1689795264, price: 2165) +] +let previewEntry = TokenPriceEntry( + date: Date(), + configuration: TokenPriceConfigurationIntent(), + currency: WidgetConstants.currencyUsd, + spotPrice: 2165, + pricePercentChange: -9.87, + symbol: "ETH", + logo: UIImage(url: URL(string: "https://token-icons.s3.amazonaws.com/eth.png")), + backgroundColor: ColorExtraction.extractImageColorWithSpecialCase( + imageURL: "https://token-icons.s3.amazonaws.com/eth.png" + ), + tokenPriceHistory: TokenPriceHistoryResponse(priceHistory: placeholderPriceHistory) +) + +let placeholderEntry = TokenPriceEntry( + date: previewEntry.date, + configuration: previewEntry.configuration, + currency: previewEntry.currency, + spotPrice: previewEntry.spotPrice, + pricePercentChange: previewEntry.pricePercentChange, + symbol: previewEntry.symbol, + logo: nil, + backgroundColor: nil, + tokenPriceHistory: previewEntry.tokenPriceHistory +) + +let refreshMinutes = 5 +let displayName = "Token Prices" +let description = "Keep up to date on your favorite tokens." + + +struct Provider: IntentTimelineProvider { + + func getEntry(configuration: TokenPriceConfigurationIntent, context: Context, isSnapshot: Bool) async throws -> TokenPriceEntry { + let entryDate = Date() + async let tokenPriceRequest = isSnapshot ? + await DataQueries.fetchTokenPriceData(chain: WidgetConstants.ethereumChain, address: nil) : + await DataQueries.fetchTokenPriceData(chain: configuration.selectedToken?.chain ?? "", address: configuration.selectedToken?.address) + async let conversionRequest = await DataQueries.fetchCurrencyConversion( + toCurrency: UniswapUserDefaults.readI18n().currency) + + let (tokenPriceResponse, conversionResponse) = try await (tokenPriceRequest, conversionRequest) + + let spotPrice = tokenPriceResponse.spotPrice != nil ? + tokenPriceResponse.spotPrice! * conversionResponse.conversionRate : nil + let pricePercentChange = tokenPriceResponse.pricePercentChange + let symbol = tokenPriceResponse.symbol + let logo = UIImage(url: URL(string: tokenPriceResponse.logoUrl ?? "")) + var backgroundColor: UIColor? = nil + if let logoUrl = tokenPriceResponse.logoUrl { + backgroundColor = ColorExtraction.extractImageColorWithSpecialCase(imageURL: logoUrl) + } + var tokenPriceHistory: TokenPriceHistoryResponse? = nil + + if (context.family == .systemMedium) { + tokenPriceHistory = isSnapshot ? + try await DataQueries.fetchTokenPriceHistoryData( + chain: WidgetConstants.ethereumChain, + address: nil) : + try await DataQueries.fetchTokenPriceHistoryData( + chain: configuration.selectedToken?.chain ?? WidgetConstants.ethereumChain, + address: configuration.selectedToken?.address) + } + + return TokenPriceEntry( + date: entryDate, + configuration: configuration, + currency: conversionResponse.currency, + spotPrice: spotPrice, + pricePercentChange: pricePercentChange, + symbol: symbol, + logo: logo, + backgroundColor: backgroundColor, + tokenPriceHistory: tokenPriceHistory + ) + } + + func placeholder(in context: Context) -> TokenPriceEntry { + return placeholderEntry + } + + func getSnapshot(for configuration: TokenPriceConfigurationIntent, in context: Context, completion: @escaping (TokenPriceEntry) -> ()) { + Task { + let entry = try await getEntry(configuration: configuration, context: context, isSnapshot: true) + completion(entry) + } + } + + func getTimeline(for configuration: TokenPriceConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { + Metrics.logWidgetConfigurationChange() + Task { + let entry = try await getEntry(configuration: configuration, context: context, isSnapshot: false) + let nextDate = Calendar.current.date(byAdding: .minute, value: refreshMinutes, to: entry.date)! + let timeline = Timeline(entries: [entry], policy: .after(nextDate)) + completion(timeline) + } + } +} + +struct TokenPriceEntry: TimelineEntry { + let date: Date + let configuration: TokenPriceConfigurationIntent + let currency: String + let spotPrice: Double? + let pricePercentChange: Double? + let symbol: String + let logo: UIImage? + let backgroundColor: UIColor? + let tokenPriceHistory: TokenPriceHistoryResponse? +} + +struct TokenPriceWidgetEntryView: View { + @Environment(\.widgetFamily) var family + // redactionReasons stores context on how a widget is being asked to render covering data invalidation, loading and placeholders, and privacy reasons. + // We use any reason to mean we should render a full placeholder UI + @Environment(\.redactionReasons) var reasons + @Environment(\.colorScheme) var colorScheme + + var entry: Provider.Entry + + func widgetPriceHeader(isPlaceholder: Bool) -> some View { + return HStack(alignment: .top) { + if (!isPlaceholder) { + if let logo = entry.logo { + Image(uiImage: logo).withIconStyle(background: .white, border: entry.backgroundColor != nil ? Color(entry.backgroundColor!) : Color.widgetTokenShadow) + } else { + Placeholder.Circle(width: 40, height: 40) + } + Spacer() + Text(entry.symbol) + .withHeading3Style() + .padding(.vertical, 2) + } else { + Placeholder.Circle(width: 40, height: 40) + Spacer() + Placeholder.Rectangle(width: 24, height: 16) + } + } + } + + func priceSection(isPlaceholder: Bool) -> some View { + return VStack(alignment: .leading, spacing: 0) { + if (!isPlaceholder && entry.spotPrice != nil && entry.pricePercentChange != nil) { + let i18nSettings = UniswapUserDefaults.readI18n() + Text( + NumberFormatter.fiatTokenDetailsFormatter( + price: entry.spotPrice, + locale: Locale(identifier: i18nSettings.locale), + currencyCode: entry.currency + ) + ) + .withHeading1Style() + .frame(minHeight: 28) + .minimumScaleFactor(0.3) + .padding(.bottom, 4) + PricePercentChangeTextWithIcon(pricePercentChange: entry.pricePercentChange) + .padding(.bottom, 8) + .padding([.trailing, .leading], 4) + } else { + Placeholder.Rectangle(width: 108, height: 22) + .padding(.bottom, 4) + Placeholder.Rectangle(width: 75, height: 22) + .padding(.bottom, 8) + .padding(.trailing, 4) + } + } + } + + func timeStamp() -> some View { + return Text("\(Date().formatted(date: .omitted, time: .shortened).lowercased())") + .withHeading3Style() + } + + func smallWidget() -> some View { + let body = ZStack { + VStack(alignment: .leading, spacing: 0) { + widgetPriceHeader(isPlaceholder: false).padding(.bottom, 2) + Spacer() + priceSection(isPlaceholder: false).padding(.bottom, 2) + timeStamp() + } + .withMaxFrame() + } + + if #available(iOSApplicationExtension 17.0, *) { + return body + } else { + return body.padding(12) + } + } + + func smallWidgetPlaceholder() -> some View { + let body = ZStack { + VStack(alignment: .leading, spacing: 0) { + widgetPriceHeader(isPlaceholder: true).padding(.bottom, 12) + Spacer() + priceSection(isPlaceholder: true) + } + .withMaxFrame() + } + + if #available(iOSApplicationExtension 17.0, *) { + return body + } else { + return body.padding(12) + } + } + + func mediumWidget() -> some View { + let body = ZStack { + VStack(alignment: .leading, spacing: 0) { + widgetPriceHeader(isPlaceholder: false).padding(.bottom, 4) + Spacer() + HStack(alignment: .top, spacing: 32) { + if let spotPrice = entry.spotPrice { + widgetPriceHistoryChart(priceHistory: entry.tokenPriceHistory?.priceHistory ?? [], spotPrice: spotPrice) + .frame(width: 115.0, height: 50.0) + } else { + Placeholder.Rectangle(width: 115, height: 50) + } + priceSection(isPlaceholder: false) + } + .padding(.bottom, 2) + timeStamp() + } + .withMaxFrame() + } + + if #available(iOSApplicationExtension 17.0, *) { + return body + } else { + return body.padding(16) + } + } + + func mediumWidgetPlaceholder() -> some View { + let body = ZStack { + VStack(alignment: .leading, spacing: 0) { + widgetPriceHeader(isPlaceholder: true).padding(.bottom, 8) + Spacer() + HStack(alignment: .top, spacing: 32) { + Placeholder.Rectangle(width: 115, height: 50) + priceSection(isPlaceholder: true) + } + } + .withMaxFrame() + } + + if #available(iOSApplicationExtension 17.0, *) { + return body + } else { + return body.padding(16) + } + } + + func widgetColor() -> Color { + if let color = entry.backgroundColor { + return Color(color) + } else { + return Color.UNI + } + } + + func placeholderColor() -> Color { + Color(colorScheme == .light ? .white : UIColor(.surface1)) + } + + var body: some View { + let deeplinkURL = URL(string: "uniswap://widget/#/tokens/\(entry.configuration.selectedToken?.chain?.lowercased() ?? "")/\(entry.configuration.selectedToken?.address ?? "NATIVE")") + let shouldRenderPlaceholder = !reasons.isEmpty + let body = ZStack { + switch family { + case .systemMedium: + if (!shouldRenderPlaceholder) { + mediumWidget() + } else { + mediumWidgetPlaceholder() + } + default: + if (!shouldRenderPlaceholder) { + smallWidget() + } else { + smallWidgetPlaceholder() + } + } + }.widgetURL(deeplinkURL) + + if #available(iOSApplicationExtension 17.0, *) { + return body.containerBackground(for: .widget) { + if (!shouldRenderPlaceholder) { + widgetColor() + } else { + placeholderColor() + } + } + } else { + if (!shouldRenderPlaceholder) { + return body.background(widgetColor()) + } else { + return body.background(placeholderColor()) + } + } + } +} + +struct TokenPriceWidget: Widget { + let kind: String = "TokenPriceWidget" + + var body: some WidgetConfiguration { + IntentConfiguration(kind: kind, intent: TokenPriceConfigurationIntent.self, provider: Provider()) { entry in + TokenPriceWidgetEntryView(entry: entry) + } + .configurationDisplayName(displayName) + .description(description) + .supportedFamilies([.systemSmall, .systemMedium]) + } +} + +struct TokenPriceWidget_Previews: PreviewProvider { + static var previews: some View { + Group{ + TokenPriceWidgetEntryView(entry: previewEntry) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + TokenPriceWidgetEntryView(entry: previewEntry) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + } + } +} + diff --git a/apps/mobile/ios/Widgets/Widgets.entitlements b/apps/mobile/ios/Widgets/Widgets.entitlements new file mode 100644 index 0000000..aa6880a --- /dev/null +++ b/apps/mobile/ios/Widgets/Widgets.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.uniswap.widgets + + + diff --git a/apps/mobile/ios/Widgets/WidgetsBundle.swift b/apps/mobile/ios/Widgets/WidgetsBundle.swift new file mode 100644 index 0000000..dc1d971 --- /dev/null +++ b/apps/mobile/ios/Widgets/WidgetsBundle.swift @@ -0,0 +1,16 @@ +// +// WidgetsBundle.swift +// TokenPriceWidget +// +// Created by Eric Huang on 6/22/23. +// + +import WidgetKit +import SwiftUI + +@main +struct WidgetsBundle: WidgetBundle { + var body: some Widget { + TokenPriceWidget() + } +} diff --git a/apps/mobile/ios/WidgetsCore/MobileSchema/README.md b/apps/mobile/ios/WidgetsCore/MobileSchema/README.md new file mode 100644 index 0000000..1550753 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/MobileSchema/README.md @@ -0,0 +1,22 @@ +# Swift Mobile GraphQL Schema + +This Framework contains autogenerated code that is generated from running Apollo iOS graphQL codegen. This code is not checked in, but the generated files are referenced for the XCode build process. + +## Generating the Swift GraphQL Schema + +`yarn mobile ios:prebuild` + +## Verifying Schema Changes + +When the schema is changed, please verify the build process still works. + +Run `yarn mobile ios` and ensure it builds successfully. This will implicitly run `yarn mobile ios:prebuild` which will clean and regenerate the swift files. + +If an error does occur, check the errors via building in XCode for more detailed logs. Then either remove freshly deleted file references or add files to the project that may be required. If needed, adapt the Swift usages of the schema. + +## Adding Generated Files + +To add new graphql queries or fragments to Swift: + +1. Ensure the file is listed in `apps/mobile/ios/apollo-codegen-config.json`'s `"operationSearchPaths"` and `"schemaSearchPaths"` +2. Add the needed generated files to the XCode project. To add new files, right-click the `WidgetsCore` folder in XCode, and select `add files to "Uniswap"...`. Then select the fragments, operations, and schema folder and keep create groups checked. Then click add. diff --git a/apps/mobile/ios/WidgetsCore/Utils/Constants.swift b/apps/mobile/ios/WidgetsCore/Utils/Constants.swift new file mode 100644 index 0000000..53ef553 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/Constants.swift @@ -0,0 +1,26 @@ +// +// +// Constants.swift +// WidgetsCore +// +// Created by Eric Huang on 8/9/23. +// + +import Foundation + +public struct WidgetConstants { + public static let ethereumChain = "ETHEREUM" + public static let WETHAddress = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" + public static let ethereumSymbol = "ETH" + public static let currencyUsd = "USD" +} + +// Needed to handle different bundle ids, cannot map directly but handles arbitrary bundle ids that conform to the existing convention +func getBuildVariantString(bundleId: String) -> String { + let bundleComponents = bundleId.components(separatedBy: ".") + if (bundleComponents.count > 3 && (bundleComponents[3] == "dev" || bundleComponents[3] == "beta")) { + return bundleComponents[3] + } else { + return "prod" + } +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/DataQueries.swift b/apps/mobile/ios/WidgetsCore/Utils/DataQueries.swift new file mode 100644 index 0000000..b745853 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/DataQueries.swift @@ -0,0 +1,163 @@ +// +// Network.swift +// WidgetsCore +// +// Created by Eric Huang on 7/6/23. +// + +import Foundation +import Apollo +import OSLog + +public class DataQueries { + + static let cachePolicy: CachePolicy = CachePolicy.fetchIgnoringCacheData + + public static func fetchTokensData(tokenInputs: [TokenInput]) async throws -> [TokenResponse] { + return try await withCheckedThrowingContinuation { continuation in + let contractInputs = tokenInputs.map {MobileSchema.ContractInput(chain: GraphQLEnum(rawValue: $0.chain), address: $0.address == nil ? GraphQLNullable.null: GraphQLNullable(stringLiteral: $0.address!))} + Network.shared.apollo.fetch(query: MobileSchema.TokensQuery(contracts: contractInputs)) { result in + switch result { + case .success(let graphQLResult): + let tokens = graphQLResult.data?.tokens ?? [] + let tokenResponses = tokens.map { + let symbol = $0?.symbol + let name = $0?.project?.name + let chain = $0?.chain + let address = $0?.address + return TokenResponse(chain: chain?.rawValue ?? "", address: address, symbol: symbol ?? "", name: name ?? "") + } + continuation.resume(returning: tokenResponses) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public static func fetchTopTokensData() async throws -> [TokenResponse] { + return try await withCheckedThrowingContinuation { continuation in + Network.shared.apollo.fetch(query: MobileSchema.TopTokensQuery(chain: GraphQLNullable(MobileSchema.Chain.ethereum)), cachePolicy: cachePolicy) { result in + switch result { + case .success(let graphQLResult): + let topTokens = graphQLResult.data?.topTokens ?? [] + let tokenResponses = topTokens.map { (tokenData) -> TokenResponse in + let symbol = tokenData?.symbol + let name = tokenData?.project?.name + let chain = tokenData?.chain + let address = tokenData?.address + return TokenResponse(chain: chain?.rawValue ?? "", address: address, symbol: symbol ?? "", name: name ?? "") + } + continuation.resume(returning: tokenResponses) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public static func fetchTokenPriceData(chain: String, address: String?) async throws -> TokenPriceResponse { + return try await withCheckedThrowingContinuation { continuation in + Network.shared.apollo.fetch(query: MobileSchema.FavoriteTokenCardQuery(chain: GraphQLEnum(rawValue: chain), address: address == nil ? GraphQLNullable.null : GraphQLNullable(stringLiteral: address!)), cachePolicy: cachePolicy) { result in + switch result { + case .success(let graphQLResult): + let token = graphQLResult.data?.token + let symbol = token?.symbol + let name = token?.project?.name + let logoUrl = token?.project?.logoUrl ?? nil + let markets = token?.project?.markets + let spotPrice = (markets != nil) && !markets!.isEmpty ? markets?[0]?.price?.value : nil + let pricePercentChange = (markets != nil) && !markets!.isEmpty ? markets?[0]?.pricePercentChange24h?.value : nil + let tokenPriceResponse = TokenPriceResponse(chain: chain, address: address, symbol: symbol ?? "", name: name ?? "", logoUrl: logoUrl ?? "", spotPrice: spotPrice, pricePercentChange: pricePercentChange) + continuation.resume(returning: tokenPriceResponse) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public static func fetchTokenPriceHistoryData(chain: String, address: String?) async throws -> TokenPriceHistoryResponse { + return try await withCheckedThrowingContinuation { continuation in + Network.shared.apollo.fetch(query: MobileSchema.TokenPriceHistoryQuery(contract: MobileSchema.ContractInput(chain: GraphQLEnum(rawValue: chain), address: address == nil ? GraphQLNullable.null: GraphQLNullable(stringLiteral: address!))), cachePolicy: cachePolicy) { result in + switch result { + case .success(let graphQLResult): + let tokenProject = graphQLResult.data?.tokenProjects?[0] + let markets = tokenProject?.markets + let priceHistory = (markets != nil) && !markets!.isEmpty ? + tokenProject?.markets?[0]?.priceHistory?.map { (result) -> PriceHistory in + return PriceHistory(timestamp: result?.timestamp ?? 0 * 1000, price: result?.value ?? 0) + } : [] + let priceHistoryResponse = TokenPriceHistoryResponse(priceHistory: priceHistory ?? []) + continuation.resume(returning: priceHistoryResponse) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public static func fetchWalletsTokensData(addresses: [String], maxLength: Int = 25) async throws -> [TokenResponse] { + return try await withCheckedThrowingContinuation { continuation in + Network.shared.apollo.fetch(query: MobileSchema.MultiplePortfolioBalancesQuery(ownerAddresses: addresses, valueModifiers: GraphQLNullable.null)){ result in + switch result { + case .success(let graphQLResult): + // Takes all the signer accounts and sums up the balances of the tokens, then sorts them by descending order, ignoring spam + var tokens: [TokenResponse: Double] = [:] + let portfolios = graphQLResult.data?.portfolios + portfolios?.forEach { + $0?.tokenBalances?.forEach { tokenBalance in + let value = tokenBalance?.denominatedValue?.value + let token = tokenBalance?.token + let tokenResponse = TokenResponse(chain: token?.chain.rawValue ?? "", address: token?.address, symbol: token?.symbol ?? "", name: token?.project?.name ?? "") + let isSpam = token?.project?.isSpam ?? false + if (!isSpam) { + tokens[tokenResponse] = (tokens[tokenResponse] ?? 0) + (value ?? 0) + } + } + } + let tokenResponses = tokens.keys.sorted { tokens[$0]! > tokens[$1]!} + continuation.resume(returning: Array(tokenResponses.prefix(maxLength))) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + public static func fetchCurrencyConversion(toCurrency: String) async throws -> CurrencyConversionResponse { + return try await withCheckedThrowingContinuation { continuation in + let usdResponse = CurrencyConversionResponse(conversionRate: 1, currency: WidgetConstants.currencyUsd) + + // Assuming all server currency amounts are in USD + if (toCurrency == WidgetConstants.currencyUsd) { + return continuation.resume(returning: usdResponse) + } + + Network.shared.apollo.fetch( + query: MobileSchema.ConvertQuery( + fromCurrency: GraphQLEnum(MobileSchema.Currency.usd), + toCurrency: GraphQLEnum(rawValue: toCurrency) + ) + ) { result in + switch result { + case .success(let graphQLResult): + let conversionRate = graphQLResult.data?.convert?.value + let currency = graphQLResult.data?.convert?.currency?.rawValue + + continuation.resume( + returning: conversionRate == nil || currency == nil ? usdResponse : + CurrencyConversionResponse( + conversionRate: conversionRate!, + currency: currency! + ) + ) + case .failure: + continuation.resume(returning: usdResponse) + } + } + } + } +} + + diff --git a/apps/mobile/ios/WidgetsCore/Utils/Format.swift b/apps/mobile/ios/WidgetsCore/Utils/Format.swift new file mode 100644 index 0000000..9d3a683 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/Format.swift @@ -0,0 +1,64 @@ +// +// Format.swift +// WidgetsCore +// +// Created by Eric Huang on 7/24/23. +// +import Foundation + +// Based on https://www.notion.so/uniswaplabs/Number-standards-fbb9f533f10e4e22820722c2f66d23c0 +// React native code: https://github.com/Uniswap/universe/blob/main/packages/wallet/src/utils/format.ts +extension NumberFormatter { + + static func formatShorthandWithDecimals(number: Double, fractionDigits: Int, locale: Locale, currencyCode: String, placeholder: String) -> String { + if (number < 1000000) { + return formatWithDecimals(number: number, fractionDigits: fractionDigits, locale: locale, currencyCode: currencyCode) + } + let maxNumber = 1000000000000000.0 + let maxed = number >= maxNumber + let limitedNumber = maxed ? maxNumber : number + + // Replace when Swift supports notation configuration for currency + // https://developer.apple.com/documentation/foundation/currencyformatstyleconfiguration + let compactFormatted = limitedNumber.formatted(.number.locale(locale).precision(.fractionLength(fractionDigits)).notation(.compactName)) + let currencyFormatted = limitedNumber.formatted(.currency(code: currencyCode).locale(locale).precision(.fractionLength(fractionDigits)).grouping(.never)) + + guard let numberRegex = try? NSRegularExpression(pattern: "(\\d+(\\\(locale.decimalSeparator!)\\d+)?)") else { + return placeholder + } + let output = numberRegex.stringByReplacingMatches(in: currencyFormatted, range: NSMakeRange(0, currencyFormatted.count), withTemplate: compactFormatted) + + return maxed ? ">\(output)" : "\(output)" + } + + static func formatWithDecimals(number: Double, fractionDigits: Int, locale: Locale, currencyCode: String) -> String { + return number.formatted(.currency(code: currencyCode).locale(locale).precision(.fractionLength(fractionDigits))) + } + + static func formatWithSigFigs(number: Double, sigFigsDigits: Int, locale: Locale, currencyCode: String) -> String { + return number.formatted(.currency(code: currencyCode).locale(locale).precision(.significantDigits(sigFigsDigits))) + } + + public static func fiatTokenDetailsFormatter(price: Double?, locale: Locale, currencyCode: String) -> String { + let placeholder = "--.--" + + guard let price = price else { + return placeholder + } + + if (price < 0.00000001) { + let formattedPrice = formatWithDecimals(number: price, fractionDigits: 8, locale: locale, currencyCode: currencyCode) + return "<\(formattedPrice)" + } + + if (price < 0.01) { + return formatWithSigFigs(number: price, sigFigsDigits: 3, locale: locale, currencyCode: currencyCode) + } else if (price < 1.05) { + return formatWithDecimals(number: price, fractionDigits: 3, locale: locale, currencyCode: currencyCode) + } else if (price < 1e6) { + return formatWithDecimals(number: price, fractionDigits: 2, locale: locale, currencyCode: currencyCode) + } else { + return formatShorthandWithDecimals(number: price, fractionDigits: 2, locale: locale, currencyCode: currencyCode, placeholder: placeholder) + } + } +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/Logging.swift b/apps/mobile/ios/WidgetsCore/Utils/Logging.swift new file mode 100644 index 0000000..e9af58b --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/Logging.swift @@ -0,0 +1,66 @@ +// +// Logging.swift +// WidgetsCore +// +// Created by Eric Huang on 6/22/23. +// + +import Foundation +import os.log +import WidgetKit + +public extension Logger { + private static var subsystem = Bundle.main.bundleIdentifier! + + /// Logs the view cycles like viewDidLoad. + static let viewCycle = Logger(subsystem: subsystem, category: "viewcycle") +} + +public struct Differences { + var added: [WidgetInfoDecodable] + var removed: [WidgetInfoDecodable] +} + +public struct Metrics { + // finds the difference between the 2 widgetInfoDecodable arrays, with duplicate elements + static func findMultiDifferenceFromCache(current: [WidgetInfoDecodable], fromCached: [WidgetInfoDecodable]) -> Differences { + var output = Differences(added: [], removed: []) + var cachedElementCounts = [WidgetInfoDecodable:Int]() + for cached in fromCached { + cachedElementCounts[cached] = (cachedElementCounts[cached] ?? 0) + 1 + } + for element in current { + if (cachedElementCounts[element] == nil) { + cachedElementCounts[element] = 0 + } + cachedElementCounts[element]! -= 1 + } + for (key, value) in cachedElementCounts { + if (value > 0) { + for _ in 1 ... value { output.removed.append(key) } + } + else if (value < 0) { + for _ in 1 ... -value { output.added.append(key) } + } + } + + return output + } + + public static func logWidgetConfigurationChange() { + WidgetCenter.shared.getCurrentConfigurations { result in + if case .success(let config) = result { + let currConfig = WidgetDataConfiguration(config) + let cachedConfig = UniswapUserDefaults.readConfiguration() + let diff = findMultiDifferenceFromCache(current: currConfig.configuration, fromCached: cachedConfig.configuration) + + var widgetEvents = UniswapUserDefaults.readEventChanges() + var newEvents = diff.added.map {WidgetEvent(family: $0.family, kind: $0.kind, change: .added)} + newEvents.append(contentsOf: diff.removed.map {WidgetEvent(family: $0.family, kind: $0.kind, change: .removed)}) + widgetEvents.events.append(contentsOf: newEvents) + UniswapUserDefaults.writeEventsChanges(data: widgetEvents) + UniswapUserDefaults.writeConfiguration(data: currConfig) + } + } + } +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/Network.swift b/apps/mobile/ios/WidgetsCore/Utils/Network.swift new file mode 100644 index 0000000..22f2ff1 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/Network.swift @@ -0,0 +1,70 @@ +// +// Network.swift +// WidgetsCore +// +// Created by Eric Huang on 7/6/23. +// + +import Foundation +import Apollo + +public class Network { + public static let shared = Network() + + private let UNISWAP_API_URL = Env.UNISWAP_API_BASE_URL + "/v1/graphql" + + public lazy var apollo: ApolloClient = { + let cache = InMemoryNormalizedCache() + let store = ApolloStore(cache: cache) + let client = URLSessionClient() + + let provider = NetworkInterceptorProvider(store: store, client: client) + let url = URL(string: UNISWAP_API_URL)! + let transport = RequestChainNetworkTransport(interceptorProvider: provider, endpointURL: url) + return ApolloClient(networkTransport: transport, store: store) + }() +} + +class NetworkInterceptorProvider: InterceptorProvider { + private let store: ApolloStore + private let client: URLSessionClient + + init(store: ApolloStore, client: URLSessionClient) { + self.store = store + self.client = client + } + + func interceptors(for operation: Operation) -> [ApolloInterceptor] { + return [ + AuthorizationInterceptor(), + MaxRetryInterceptor(), + CacheReadInterceptor(store: self.store), + NetworkFetchInterceptor(client: self.client), + ResponseCodeInterceptor(), + MultipartResponseParsingInterceptor(), + JSONResponseParsingInterceptor(), + AutomaticPersistedQueryInterceptor(), + CacheWriteInterceptor(store: self.store) + ] + } +} + +class AuthorizationInterceptor: ApolloInterceptor { + + + func interceptAsync( + chain: RequestChain, + request: HTTPRequest, + response: HTTPResponse?, + completion: @escaping (Result, Error>) -> Void + ) where Operation : GraphQLOperation { + request.addHeader(name: "X-API-KEY", value: Env.UNISWAP_API_KEY) + request.addHeader(name: "Content-Type", value: "application/json") + request.addHeader(name: "Origin", value: "https://app.uniswap.org") + + chain.proceedAsync(request: request, + response: response, + completion: completion) + } + +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/Structs.swift b/apps/mobile/ios/WidgetsCore/Utils/Structs.swift new file mode 100644 index 0000000..0718c51 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/Structs.swift @@ -0,0 +1,58 @@ +// +// Structs.swift +// WidgetsCore +// +// Created by Eric Huang on 7/7/23. +// + +import Foundation + +public struct TokenResponse: Hashable { + public init(chain: String, address: String? = nil, symbol: String, name: String) { + self.chain = chain + self.address = address + self.symbol = symbol + self.name = name + } + + public let chain: String + public let address: String? + public let symbol: String + public let name: String +} + +public struct TokenPriceResponse { + public let chain: String + public let address: String? + public let symbol: String + public let name: String + public let logoUrl: String? + public let spotPrice: Double? + public let pricePercentChange: Double? +} + +public struct TokenPriceHistoryResponse { + public init() { + priceHistory = [] + } + + public init(priceHistory: [PriceHistory]) { + self.priceHistory = priceHistory + } + + public let priceHistory: [PriceHistory] +} + +public struct PriceHistory { + public init(timestamp: Int, price: Double) { + self.timestamp = timestamp + self.price = price + } + public let timestamp: Int + public let price: Double +} + +public struct CurrencyConversionResponse { + public let conversionRate: Double + public let currency: String +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/UI/Chart.swift b/apps/mobile/ios/WidgetsCore/Utils/UI/Chart.swift new file mode 100644 index 0000000..bd26fa3 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/UI/Chart.swift @@ -0,0 +1,55 @@ +// +// Chart.swift +// UniswapWidgetsCore +// +// Created by Eric Huang on 7/21/23. +// + +import Foundation +import SwiftUI +import Charts + +public func widgetPriceHistoryChart(priceHistory: [PriceHistory], spotPrice: Double) -> some View { + let priceValues: [Double] = priceHistory.map {$0.price} + let timestampValues: [Int] = priceHistory.map {$0.timestamp} + let minY = priceValues.min() ?? 0 + let maxY = priceValues.max() ?? 0 + let minX = timestampValues.min() ?? 0 + let maxX = timestampValues.max() ?? 0 + if #available(iOS 16.0, *) { + return Chart { + ForEach(priceHistory, id: \.timestamp) { + dataPoint in + LineMark(x: .value("Time",dataPoint.timestamp), + y: .value("Price", dataPoint.price)) + .foregroundStyle(.linearGradient( + colors: [.clear, .white], + startPoint: .leading, + endPoint: .trailing)) + .lineStyle(StrokeStyle(lineWidth: 1)) + .interpolationMethod(.monotone) + } + + PointMark(x: .value("Time", maxX), + y: .value("Price", spotPrice)) + .foregroundStyle(Color(red: 1, green: 1, blue: 1).opacity(0.25)) + .symbolSize(150) + + PointMark(x: .value("Time", maxX), + y: .value("Price", spotPrice)) + .foregroundStyle(Color(red: 1, green: 1, blue: 1).opacity(0.5)) + .symbolSize(75) + + PointMark(x: .value("Time", maxX), + y: .value("Price", spotPrice)) + .foregroundStyle(.white) + .symbolSize(20) + } + .chartXScale(domain: minX...maxX) + .chartYScale(domain: minY...maxY) + .chartXAxis(.hidden) + .chartYAxis(.hidden) + } else { + return Spacer() + } +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/UI/Colors.swift b/apps/mobile/ios/WidgetsCore/Utils/UI/Colors.swift new file mode 100644 index 0000000..1c1266c --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/UI/Colors.swift @@ -0,0 +1,236 @@ +// +// Colors.swift +// WidgetsCore +// +// Created by Eric Huang on 7/17/23. +// + +import Foundation +import SwiftUI +import UIImageColors +import OSLog + +public extension Color { + static let widgetGrey = Color(red: 0.96, green: 0.96, blue: 0.99).opacity(0.5) + static let widgetLightGrey = Color(red: 0.96, green: 0.96, blue: 0.99).opacity(0.72) + static let widgetTokenShadow = Color(red: 0.13, green: 0.13, blue: 0.13).opacity(0.08) + + static let WBTC = Color(red: 0.94, green: 0.57, blue: 0.25) + static let DAI = Color(red: 0.98, green: 0.69, blue: 0.11) + static let BUSD = Color(red: 0.94, green: 0.73, blue: 0.04) + static let X = Color(red: 0.16, green: 0.63, blue: 0.94) + static let ETH = Color(red: 0.29, green: 0.42, blue: 0.83) + static let HARRYPOTTERBITCOIN = Color(red: 0.86, green: 0.19, blue: 0.04) + static let PEPE = Color(red: 0.24, green: 0.68, blue: 0.08) + static let UNI = Color(red: 0.90, green: 0.21, blue: 0.55) + static let UNIBOT = Color(red: 0.29, green: 0.05, blue: 0.31) + static let USDC = Color(red: 0, green: 0.4, blue: 0.85) + static let HEX = Color(red: 0.97, green: 0.25, blue: 0.55) + static let MONG = Color(red: 0.64, green: 0.34, blue: 1) + static let ARB = Color(red: 0.16, green: 0.63, blue: 0.94) + static let SHIB = Color(red: 0.86, green: 0.19, blue: 0.04) + static let QNT = Color.black + static let XEN = Color.black + static let PSYOP = Color(red: 0.91, green: 0.57, blue: 0) + static let MATIC = Color(red: 0.64, green: 0.34, blue: 1) + static let POOH = Color(red: 0.21, green: 0.45, blue: 0.26) + static let TURBO = Color(red: 0.74, green: 0.43, blue: 0.16) + static let AIDOGE = Color(red: 0.16, green: 0.63, blue: 0.94) + static let SIMPSON = Color(red: 0.91, green: 0.57, blue: 0) + static let RENQ = Color(red: 0.18, green: 0.52, blue: 1) + static let MAKER = Color(red: 0.31, green: 0.7, blue: 0.59) + static let OX = Color(red: 0.16, green: 0.35, blue: 0.85) + static let ANGLE = Color(red: 1, green: 0.33, blue: 0.33) + static let APE = Color(red: 0.01, green: 0.29, blue: 0.84) + static let GUSD = Color(red: 0, green: 0.64, blue: 0.74) + static let OGN = Color(red: 0.01, green: 0.29, blue: 0.84) + static let GALA = Color.black + static let RPL = Color(red: 1, green: 0.48, blue: 0.31) + static let FWB = Color.black + + static let magentaVibrant = Color(red: 0.99, green: 0.45, blue: 1.00) + static let backgroundGray = Color.gray + static let surface1 = Color(red: 0.075, green: 0.075, blue: 0.075) +} + +extension UIColor { + // Calculates contrast between two colors + // https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-tests + static func contrastRatio(between color1: UIColor, and color2: UIColor) -> CGFloat { + let luminance1 = color1.luminance() + let luminance2 = color2.luminance() + let luminanceDarker = min(luminance1, luminance2) + let luminanceLighter = max(luminance1, luminance2) + return (luminanceLighter + 0.05) / (luminanceDarker + 0.05) + } + // Calculates color luminance + // https://www.w3.org/TR/WCAG20-TECHS/G18.html#G18-tests + func luminance() -> CGFloat { + let ciColor = CIColor(color: self) + func adjust(colorComponent: CGFloat) -> CGFloat { + return (colorComponent < 0.04045) ? (colorComponent / 12.92) : pow((colorComponent + 0.055) / 1.055, 2.4) + } + return 0.2126 * adjust(colorComponent: ciColor.red) + 0.7152 * adjust(colorComponent: ciColor.green) + 0.0722 * adjust(colorComponent: ciColor.blue) + } + + // Color comparators + // https://stackoverflow.com/a/44246991 + static func == (l: UIColor, r: UIColor) -> Bool { + var r1: CGFloat = 0 + var g1: CGFloat = 0 + var b1: CGFloat = 0 + var a1: CGFloat = 0 + l.getRed(&r1, green: &g1, blue: &b1, alpha: &a1) + var r2: CGFloat = 0 + var g2: CGFloat = 0 + var b2: CGFloat = 0 + var a2: CGFloat = 0 + r.getRed(&r2, green: &g2, blue: &b2, alpha: &a2) + return r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2 + } +} + +public struct ColorExtraction { + + static let contrastThresh = 1.95 + static let defaultColor = Color.magentaVibrant + + static let specialCaseTokenColors: [String: UIColor] = [ + // old WBTC + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png": + UIColor(Color.WBTC), + // new WBTC + "https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1548822744": + UIColor(Color.WBTC), + // DAI + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png": + UIColor(Color.DAI), + // UNI + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png": + UIColor(Color.UNI), + // BUSD + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4Fabb145d64652a948d72533023f6E7A623C7C53/logo.png": + UIColor(Color.BUSD), + // AI-X + "https://s2.coinmarketcap.com/static/img/coins/64x64/26984.png": + UIColor(Color.X), + // ETH + "https://token-icons.s3.amazonaws.com/eth.png": + UIColor(Color.ETH), + // HARRYPOTTERSHIBAINUBITCOIN + "https://assets.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1684117567": + UIColor(Color.HARRYPOTTERBITCOIN), + // PEPE + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png": + UIColor(Color.PEPE), + // APE + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4d224452801ACEd8B2F0aebE155379bb5D594381/logo.png": + UIColor(Color.APE), + // UNIBOT v2 + "https://s2.coinmarketcap.com/static/img/coins/64x64/25436.png": + UIColor(Color.UNIBOT), + // UNIBOT v1 + "https://assets.coingecko.com/coins/images/30462/small/logonoline_%281%29.png?1687510315": + UIColor(Color.UNIBOT), + // USDC + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png": + UIColor(Color.USDC), + // HEX + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png": + UIColor(Color.HEX), + // MONG + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1ce270557C1f68Cfb577b856766310Bf8B47FD9C/logo.png": + UIColor(Color.MONG), + // ARB + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1/logo.png": + UIColor(Color.ARB), + // Quant + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4a220E6096B25EADb88358cb44068A3248254675/logo.png": + UIColor(Color.QNT), + // Xen + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8/logo.png": + UIColor(Color.XEN), + // PSYOP + "https://s2.coinmarketcap.com/static/img/coins/64x64/25422.png": + UIColor(Color.PSYOP), + // MATIC + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png": + UIColor(Color.MATIC), + // TURBO + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA35923162C49cF95e6BF26623385eb431ad920D3/logo.png": + UIColor(Color.TURBO), + // AIDOGE + "https://assets.coingecko.com/coins/images/29852/large/photo_2023-04-18_14-25-28.jpg?1681799160": + UIColor(Color.AIDOGE), + // SIMPSON + "https://assets.coingecko.com/coins/images/30243/large/1111.png?1683692033": + UIColor(Color.SIMPSON), + // MAKER + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2/logo.png": + UIColor(Color.MAKER), + // OX + "https://assets.coingecko.com/coins/images/30604/large/Logo2.png?1685522119": + UIColor(Color.OX), + // ANGLE + "https://assets.coingecko.com/coins/images/19060/large/ANGLE_Token-light.png?1666774221": + UIColor(Color.ANGLE), + // GUSD + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd/logo.png": + UIColor(Color.GUSD), + // OGN + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26/logo.png": + UIColor(Color.OGN), + // GALA + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x15D4c048F83bd7e37d49eA4C83a07267Ec4203dA/logo.png": + UIColor(Color.GALA), + // RPL + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xD33526068D116cE69F19A9ee46F0bd304F21A51f/logo.png": + UIColor(Color.RPL), + // FWB + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x35bD01FC9d6D5D81CA9E055Db88Dc49aa2c699A8/logo.png": + UIColor(Color.FWB) + ] + + static func passesContrast( + color: UIColor, + backgroundColor: UIColor, + contrastThreshold: Double + ) -> Bool { + // sometimes the extracted colors come back as white or black, discard those + if (color == UIColor.white || color == UIColor.black) { + return false + } + + let contrast = UIColor.contrastRatio(between: color, and: backgroundColor) + return contrast >= contrastThreshold + } + + static func pickContrastPassingColor(colors: [UIColor?]) -> UIColor { + for color in colors { + if let value = color { + if passesContrast(color: value, backgroundColor: UIColor.white, contrastThreshold: contrastThresh) { + return value + } + } + } + return UIColor(defaultColor) + } + + public static func extractImageColor(imageURL: String) -> UIColor? { + let image: UIImage? = UIImage(url: URL(string: imageURL)) + guard let image = image else { + return nil + } + let colors = image.getColors() + let colorsArray = [colors?.background, colors?.primary, colors?.detail, colors?.secondary] + return pickContrastPassingColor(colors: colorsArray) + } + + public static func extractImageColorWithSpecialCase(imageURL: String) -> UIColor? { + if let color = specialCaseTokenColors[imageURL] { + return color + } + return extractImageColor(imageURL: imageURL) + } + +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/UI/Styling.swift b/apps/mobile/ios/WidgetsCore/Utils/UI/Styling.swift new file mode 100644 index 0000000..7f9a46f --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/UI/Styling.swift @@ -0,0 +1,49 @@ +// +// Styling.swift +// UniswapWidgetsCore +// +// Created by Eric Huang on 7/17/23. +// + +import Foundation +import SwiftUI + +public extension Text { + func withHeading1Style() -> some View { + self.font(.custom("Basel-Book", size: 28)) + .foregroundColor(.white) + } + + func withHeading2Style() -> some View { + self.font(.custom("Basel-Book", size: 20)) + .foregroundColor(.widgetLightGrey) + } + + func withHeading3Style() -> some View { + self.font(.custom("Basel", size: 12)) + .fontWeight(.medium) + .foregroundColor(.widgetGrey) + } +} + +public extension Image { + func withIconStyle(background: Color, border: Color) -> some View { + self.resizable() + .frame(width: 40, height: 40) + .background(background) + .clipShape(Circle()) + .overlay( + Circle().stroke(border, lineWidth: 0.5)) + .shadow(color: .widgetTokenShadow, radius: 5, x: 0, y: 2) + } +} + +public extension View { + func withMaxFrame() -> some View { + self.frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading + ) + } +} diff --git a/apps/mobile/ios/WidgetsCore/Utils/UI/UIComponents.swift b/apps/mobile/ios/WidgetsCore/Utils/UI/UIComponents.swift new file mode 100644 index 0000000..d9d31a1 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/UI/UIComponents.swift @@ -0,0 +1,76 @@ +// +// UIComponents.swift +// WidgetsCore +// +// Created by Eric Huang on 7/14/23. +// + +import Foundation +import UIKit +import SwiftUI + +public extension UIImage { + // Constructor for creating UIImages from a URL + convenience init?(url: URL?) { + guard let url = url else { return nil } + + do { + self.init(data: try Data(contentsOf: url)) + } catch { + return nil + } + } +} + +public struct PricePercentChangeTextWithIcon: View { + public var pricePercentChange: Double? + + public init(pricePercentChange: Double?) { + self.pricePercentChange = pricePercentChange + } + + public var body: some View { + HStack(alignment: .center) { + if let pricePercentChange = pricePercentChange { + if (pricePercentChange >= 0) { + Image("caret-up") + Text("\(pricePercentChange, specifier: "%.2f")%") + .withHeading2Style() + } else { + Image("caret-up").rotationEffect(.degrees(-180)) + Text("\(-pricePercentChange, specifier: "%.2f")%") + .withHeading2Style() + } + } else { + Text("--") + .withHeading2Style() + } + } + } +} + +public struct Placeholder { + static let placeholderGradient = LinearGradient( + stops: [ + Gradient.Stop(color: .widgetLightGrey.opacity(0.12), location: 0.00), + Gradient.Stop(color: .widgetLightGrey.opacity(0.05), location: 1.00), + ], + startPoint: UnitPoint(x: 0.13, y: 0.5), + endPoint: UnitPoint(x: 0.94, y: 0.5) + ) + + public static func Circle(width: Double, height: Double) -> some View { + return SwiftUI.Circle() + .frame(width: width, height: height) + .foregroundStyle(placeholderGradient) + } + + public static func Rectangle(width: Double, height: Double) -> some View { + return SwiftUI.Rectangle() + .frame(width: width, height: height) + .foregroundStyle(placeholderGradient) + .cornerRadius(6) + } +} + + diff --git a/apps/mobile/ios/WidgetsCore/Utils/UserDefaults.swift b/apps/mobile/ios/WidgetsCore/Utils/UserDefaults.swift new file mode 100644 index 0000000..1b019b3 --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/Utils/UserDefaults.swift @@ -0,0 +1,198 @@ +// +// UserDefaults.swift +// WidgetsCore +// +// Created by Eric Huang on 7/6/23. +// + +import Foundation +import OSLog +import WidgetKit + +let APP_GROUP = "group.com.uniswap.widgets" + +public struct WidgetDataFavorites: Decodable { + + public init(_ favorites: [TokenInput]) { + self.favorites = favorites + } + + public var favorites: [TokenInput] +} + +public struct TokenInput: Decodable { + public var address: String? + public var chain: String +} + +public struct WidgetDataAccounts: Decodable { + + public init(_ accounts: [Account]) { + self.accounts = accounts + } + + public var accounts: [Account] +} + +public struct WidgetDataI18n: Decodable { + + public init() { + self.locale = "en" + self.currency = WidgetConstants.currencyUsd + } + + public var locale: String + public var currency: String +} + +public struct Account: Decodable { + public var address: String + public var name: String? + public var isSigner: Bool +} + +public struct WidgetDataConfiguration: Codable { + + public init(_ widgetInfos: [WidgetInfo]) { + configuration = widgetInfos.map { WidgetInfoDecodable($0) } + } + + public var configuration: [WidgetInfoDecodable] +} + +public struct WidgetInfoDecodable: Hashable, Codable { + + public init(_ widgetInfo: WidgetInfo) { + family = widgetInfo.family.description + kind = widgetInfo.kind + } + + public let family: String + public let kind: String + + static public func == (lhs: WidgetInfoDecodable, rhs: WidgetInfoDecodable) -> Bool { + + return lhs.family == rhs.family && lhs.kind == rhs.kind + } +} + +public struct WidgetEvents: Codable { + public var events: [WidgetEvent] +} + +public struct WidgetEvent: Codable { + public let family: String + public let kind: String + public let change: Change +} + +public enum Change: String, Codable { + case added = "added" + case removed = "removed" +} + +public struct UniswapUserDefaults { + private static var buildString = getBuildVariantString(bundleId: Bundle.main.bundleIdentifier!) + + static let keyEvents = buildString + ".widgets.configuration.events" + static let keyCache = buildString + ".widgets.configuration.cache" + static let keyFavorites = buildString + ".widgets.favorites" + static let keyAccounts = buildString + ".widgets.accounts" + static let keyI18n = buildString + ".widgets.i18n" + + static let userDefaults = UserDefaults.init(suiteName: APP_GROUP) + + static func readData(key: String) -> Data? { + // parses data from user defaults + guard let userDefaults = userDefaults else { + return nil + } + guard let savedData = userDefaults.string(forKey: key) else { + return nil + } + let data = savedData.data(using: .utf8) + return data + } + + public static func readAccounts() -> WidgetDataAccounts { + let data = readData(key: keyAccounts) + guard let data = data else { + return WidgetDataAccounts([]) + } + let decoder = JSONDecoder() + guard let parsedData = try? decoder.decode(WidgetDataAccounts.self, from: data) else { + // case when failing to parse + return WidgetDataAccounts([]) + } + return parsedData + } + + public static func readFavorites() -> WidgetDataFavorites { + let data = readData(key: keyFavorites) + guard let data = data else { + return WidgetDataFavorites([]) + } + let decoder = JSONDecoder() + guard let parsedData = try? decoder.decode(WidgetDataFavorites.self, from: data) else { + // case when failing to parse + return WidgetDataFavorites([]) + } + return parsedData + } + + public static func readI18n() -> WidgetDataI18n { + let data = readData(key: keyI18n) + guard let data = data else { + return WidgetDataI18n() + } + let decoder = JSONDecoder() + guard let parsedData = try? decoder.decode(WidgetDataI18n.self, from: data) else { + return WidgetDataI18n() + } + return parsedData + } + + public static func readConfiguration() -> WidgetDataConfiguration { + let data = readData(key: keyCache) + guard let data = data else { + return WidgetDataConfiguration([]) + } + let decoder = JSONDecoder() + guard let parsedData = try? decoder.decode(WidgetDataConfiguration.self, from: data) else { + // case when failing to parse + return WidgetDataConfiguration([]) + } + return parsedData + } + + public static func writeConfiguration(data: WidgetDataConfiguration) { + if userDefaults != nil { + let encoder = JSONEncoder() + let JSONdata = try! encoder.encode(data) + let json = String(data: JSONdata, encoding: String.Encoding.utf8) + userDefaults!.set(json, forKey: keyCache) + } + } + + public static func readEventChanges() -> WidgetEvents { + let data = readData(key: keyEvents) + guard let data = data else { + return WidgetEvents(events: []) + } + let decoder = JSONDecoder() + guard let parsedData = try? decoder.decode(WidgetEvents.self, from: data) else { + // case when failing to parse + return WidgetEvents(events: []) + } + return parsedData + } + + public static func writeEventsChanges(data: WidgetEvents) { + if userDefaults != nil { + let encoder = JSONEncoder() + let JSONdata = try! encoder.encode(data) + let json = String(data: JSONdata, encoding: String.Encoding.utf8) + userDefaults!.set(json, forKey: keyEvents) + } + } +} diff --git a/apps/mobile/ios/WidgetsCore/WidgetsCore.h b/apps/mobile/ios/WidgetsCore/WidgetsCore.h new file mode 100644 index 0000000..b3296ad --- /dev/null +++ b/apps/mobile/ios/WidgetsCore/WidgetsCore.h @@ -0,0 +1,18 @@ +// +// WidgetsCore.h +// WidgetsCore +// +// Created by Eric Huang on 6/22/23. +// + +#import + +//! Project version number for WidgetsCore. +FOUNDATION_EXPORT double WidgetsCoreVersionNumber; + +//! Project version string for WidgetsCore. +FOUNDATION_EXPORT const unsigned char WidgetsCoreVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/apps/mobile/ios/WidgetsCoreTests/FormatTests.swift b/apps/mobile/ios/WidgetsCoreTests/FormatTests.swift new file mode 100644 index 0000000..eaba4f5 --- /dev/null +++ b/apps/mobile/ios/WidgetsCoreTests/FormatTests.swift @@ -0,0 +1,106 @@ +// +// FormatTests.swift +// UniswapWidgetsCoreTests +// +// Created by Eric Huang on 7/25/23. +// + +import XCTest +import WidgetsCore + +final class FormatTests: XCTestCase { + + let localeEnglish = Locale(identifier: "en") + let localeFrench = Locale(identifier: "fr-FR") + let localeChinese = Locale(identifier: "zh-Hans") + + let currencyCodeUsd = WidgetConstants.currencyUsd + let currencyCodeEuro = "EUR" + let currencyCodeYuan = "CNY" + + struct TestCase { + public init(_ price: Double, _ output: String) { + self.price = price + self.output = output + } + + public let price: Double + public let output: String + } + + func testFormatterHandlesEnglish() throws { + + let testCases = [ + TestCase(0.05, "$0.050"), + TestCase(0.056666666, "$0.057"), + TestCase(1234567.891, "$1.23M"), + TestCase(1234.5678, "$1,234.57"), + TestCase(1.048952, "$1.049"), + TestCase(0.001231, "$0.00123"), + TestCase(0.00001231, "$0.0000123"), + TestCase(0.0000001234, "$0.000000123"), + TestCase(0.000000009876, "<$0.00000001"), + ] + + testCases.forEach { testCase in + XCTAssertEqual( + NumberFormatter.fiatTokenDetailsFormatter( + price: testCase.price, + locale: localeEnglish, + currencyCode: currencyCodeUsd + ), + testCase.output + ) + } + } + + func testFormatterHandlesFrench() throws { + let testCases = [ + TestCase(0.05, "0,050 €"), + TestCase(0.056666666, "0,057 €"), + TestCase(1234567.891, "1,23 M €"), + TestCase(123456.7890, "123 456,79 €"), + TestCase(1.048952, "1,049 €"), + TestCase(0.001231, "0,00123 €"), + TestCase(0.00001231, "0,0000123 €"), + TestCase(0.0000001234, "0,000000123 €"), + TestCase(0.000000009876, "<0,00000001 €"), + ] + + testCases.forEach { testCase in + XCTAssertEqual( + NumberFormatter.fiatTokenDetailsFormatter( + price: testCase.price, + locale: localeFrench, + currencyCode: currencyCodeEuro + ), + testCase.output + ) + } + } + + func testFormatterHandlesChinese() throws { + let testCases = [ + TestCase(0.05, "¥0.050"), + TestCase(0.056666666, "¥0.057"), + TestCase(1234567.891, "¥123.46万"), + TestCase(1234.5678, "¥1,234.57"), + TestCase(1.048952, "¥1.049"), + TestCase(0.001231, "¥0.00123"), + TestCase(0.00001231, "¥0.0000123"), + TestCase(0.0000001234, "¥0.000000123"), + TestCase(0.000000009876, "<¥0.00000001"), + ] + + testCases.forEach { testCase in + XCTAssertEqual( + NumberFormatter.fiatTokenDetailsFormatter( + price: testCase.price, + locale: localeChinese, + currencyCode: currencyCodeYuan + ), + testCase.output + ) + } + } +} diff --git a/apps/mobile/ios/WidgetsCoreTests/WidgetsCoreTests.swift b/apps/mobile/ios/WidgetsCoreTests/WidgetsCoreTests.swift new file mode 100644 index 0000000..3aeae9b --- /dev/null +++ b/apps/mobile/ios/WidgetsCoreTests/WidgetsCoreTests.swift @@ -0,0 +1,36 @@ +// +// WidgetsCoreTests.swift +// WidgetsCoreTests +// +// Created by Eric Huang on 6/22/23. +// + +import XCTest +@testable import WidgetsCore + +final class WidgetsCoreTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/apps/mobile/ios/apollo-codegen-config.json b/apps/mobile/ios/apollo-codegen-config.json new file mode 100644 index 0000000..6b524eb --- /dev/null +++ b/apps/mobile/ios/apollo-codegen-config.json @@ -0,0 +1,32 @@ +{ + "schemaNamespace": "MobileSchema", + "options": { + "cocoapodsCompatibleImportStatements": true + }, + "input": { + "operationSearchPaths": [ + "../../../apps/mobile/src/components/PriceExplorer/TokenPriceHistory.graphql", + "../../../apps/mobile/src/components/explore/search/SearchPopularTokens.graphql", + "../../../packages/uniswap/src/data/graphql/uniswap-data-api/queries.graphql" + ], + "schemaSearchPaths": [ + "../../../packages/uniswap/src/data/graphql/uniswap-data-api/schema.graphql" + ] + }, + "output": { + "testMocks": { + "none": {} + }, + "schemaTypes": { + "path": "./WidgetsCore/MobileSchema", + "moduleType": { + "embeddedInTarget": { + "name": "WidgetsCore" + } + } + }, + "operations": { + "inSchemaModule": {} + } + } +} diff --git a/apps/mobile/ios/link-assets-manifest.json b/apps/mobile/ios/link-assets-manifest.json new file mode 100644 index 0000000..7f747d8 --- /dev/null +++ b/apps/mobile/ios/link-assets-manifest.json @@ -0,0 +1,17 @@ +{ + "migIndex": 1, + "data": [ + { + "path": "src/assets/fonts/Basel-Book.ttf", + "sha1": "7ff6b3f7e5c2857ca3b39fad3ba09c35bb75e302" + }, + { + "path": "src/assets/fonts/Basel-Medium.ttf", + "sha1": "182bf31d0794296a034a2d13b50fffd804709aaa" + }, + { + "path": "src/assets/fonts/InputMono-Regular.ttf", + "sha1": "c27a811b8a8e05a578f67f71bb89ecb03dca5905" + } + ] +} diff --git a/apps/mobile/jest-setup.js b/apps/mobile/jest-setup.js new file mode 100644 index 0000000..577fccd --- /dev/null +++ b/apps/mobile/jest-setup.js @@ -0,0 +1,127 @@ +// Setups and mocks can go here +// For example: https://reactnavigation.org/docs/testing/ + +import 'core-js' // necessary so setImmediate works in tests +import 'wallet/src/i18n/i18n' // Uses real translations for tests + +import mockRNCNetInfo from '@react-native-community/netinfo/jest/netinfo-mock.js' +import { localizeMock as mockRNLocalize } from 'react-native-localize/mock' +import { AppearanceSettingType } from 'wallet/src/features/appearance/slice' +import { mockLocalizationContext } from 'wallet/src/test/mocks/utils' + +// avoids polluting console in test runs, while keeping important log levels +global.console = { + ...console, + // uncomment to ignore a specific log level + log: jest.fn(), + debug: jest.fn(), + info: jest.fn(), + // warn: jest.fn(), + // error: jest.fn(), +} + +// Mock Sentry crash reporting +jest.mock('@sentry/react-native', () => ({ + init: () => jest.fn(), + wrap: (val) => val, + ReactNavigationInstrumentation: jest.fn(), + ReactNativeTracing: jest.fn(), +})) + +// Disables animated driver warning +jest.mock('react-native/Libraries/Animated/NativeAnimatedHelper') + +jest.mock('src/lib/RNEthersRs') + +// Mock OneSignal package +jest.mock('react-native-onesignal', () => { + return { + setLogLevel: jest.fn(), + setAppId: jest.fn(), + promptForPushNotificationsWithUserResponse: jest.fn(), + setNotificationWillShowInForegroundHandler: jest.fn(), + setNotificationOpenedHandler: jest.fn(), + getDeviceState: () => ({ userId: 'dummyUserId', pushToken: 'dummyPushToken' }), + } +}) + +jest.mock('react-native-appsflyer', () => { + return { + initSdk: jest.fn(), + } +}) + +jest.mock('react-native-permissions', () => ({})) + +// NetInfo mock does not export typescript types +const NetInfoStateType = { + unknown: 'unknown', + none: 'none', + cellular: 'cellular', + wifi: 'wifi', + bluetooth: 'bluetooth', + ethernet: 'ethernet', + wimax: 'wimax', + vpn: 'vpn', + other: 'other', +} +jest.mock('@react-native-community/netinfo', () => ({ ...mockRNCNetInfo, NetInfoStateType })) + +// from https://github.com/facebook/react-native/issues/28839#issuecomment-625453688 +jest.mock('react-native', () => { + const RN = jest.requireActual('react-native') // use original implementation, which comes with mocks out of the box + + return RN +}) + +jest.mock('react-native-safe-area-context', () => ({ + useSafeAreaInsets: jest.fn().mockImplementation(() => ({})), + SafeAreaProvider: jest.fn(({ children }) => children), +})) + +jest.mock('@react-navigation/elements', () => ({ + useHeaderHeight: jest.fn().mockImplementation(() => 200), +})) + +require('react-native-reanimated').setUpTests() + +jest.mock('wallet/src/features/language/LocalizationContext', () => mockLocalizationContext) + +jest.mock('react-native/Libraries/Share/Share', () => ({ + share: jest.fn(), +})) + +jest.mock('react-native-localize', () => mockRNLocalize) + +jest.mock('@react-native-firebase/auth', () => () => ({ + signInAnonymously: jest.fn(), +})) + +jest.mock('@react-native-firebase/app-check', () => () => ({ + appCheck: jest.fn(), + newReactNativeFirebaseAppCheckProvider: jest.fn(() => ({ + configure: jest.fn(), + })), + initializeAppCheck: jest.fn().mockReturnValue(Promise.resolve()), // Return a resolved Promise +})) + +jest.mock('react-native/Libraries/Linking/Linking', () => ({ + openURL: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + canOpenURL: jest.fn(), + getInitialURL: jest.fn(), +})) + +// Mock the appearance hook for all tests +const mockAppearanceSetting = AppearanceSettingType.System +jest.mock('wallet/src/features/appearance/hooks', () => { + return { + useCurrentAppearanceSetting: () => mockAppearanceSetting, + } +}) +jest.mock('wallet/src/features/appearance/hooks', () => { + return { + useSelectedColorScheme: () => 'light', + } +}) diff --git a/apps/mobile/jest.config.js b/apps/mobile/jest.config.js new file mode 100644 index 0000000..285f047 --- /dev/null +++ b/apps/mobile/jest.config.js @@ -0,0 +1,30 @@ +const preset = require('../../config/jest-presets/jest/jest-preset') + +/** @type {import('jest').Config} */ +module.exports = { + ...preset, + preset: 'jest-expo', + displayName: 'Mobile Wallet', + collectCoverageFrom: [ + 'src/**/*.{js,ts,tsx}', + '!src/test/**', // test helpers + '!src/**/*.stories.**', + '!**/node_modules/**', + ], + coverageThreshold: { + global: { + lines: 0, + }, + }, + setupFiles: [ + '../../config/jest-presets/jest/setup.js', + './jest-setup.js', + '../../node_modules/react-native-gesture-handler/jestSetup.js', + ], + // we map core/web to tamagui's test bundle, this just makes setup simpler for jest + moduleNameMapper: { + ...preset.moduleNameMapper, + '@tamagui/core': '@tamagui/core/native-test', + '@tamagui/web': '@tamagui/core/native-test', + }, +} diff --git a/apps/mobile/metro.config.js b/apps/mobile/metro.config.js new file mode 100644 index 0000000..aabaffb --- /dev/null +++ b/apps/mobile/metro.config.js @@ -0,0 +1,48 @@ +/** + * Metro configuration for React Native with support for SVG files + * https://github.com/react-native-svg/react-native-svg#use-with-svg-files + * + * @format + */ +const { getMetroAndroidAssetsResolutionFix } = require('react-native-monorepo-tools') +const androidAssetsResolutionFix = getMetroAndroidAssetsResolutionFix() + +const path = require('path') +const { getDefaultConfig } = require('metro-config') + +const mobileRoot = path.resolve(__dirname) +const workspaceRoot = path.resolve(mobileRoot, '../..') + +const watchFolders = [mobileRoot, `${workspaceRoot}/node_modules`, `${workspaceRoot}/packages`] + +const detoxExtensions = process.env.DETOX_MODE === 'mocked' ? ['mock.tsx', 'mock.ts'] : [] + +module.exports = (async () => { + const { + resolver: { sourceExts, assetExts }, + } = await getDefaultConfig() + return { + resolver: { + nodeModulesPaths: [`${workspaceRoot}/node_modules`], + assetExts: assetExts.filter((ext) => ext !== 'svg'), + // detox mocking works properly only being spreaded at the beginning of sourceExts array + sourceExts: [...detoxExtensions, ...sourceExts, 'svg', 'cjs'] + }, + transformer: { + getTransformOptions: async () => ({ + transform: { + experimentalImportSupport: false, + inlineRequires: true, + }, + }), + babelTransformerPath: require.resolve('react-native-svg-transformer'), + publicPath: androidAssetsResolutionFix.publicPath, + }, + server: { + enhanceMiddleware: (middleware) => { + return androidAssetsResolutionFix.applyMiddleware(middleware) + }, + }, + watchFolders, + } +})() diff --git a/apps/mobile/openapi-config.json b/apps/mobile/openapi-config.json new file mode 100644 index 0000000..aa3428e --- /dev/null +++ b/apps/mobile/openapi-config.json @@ -0,0 +1,8 @@ +{ + "apiFile": "./src/features/dataApi/coingecko/emptyApi.ts", + "apiImport": "emptyApi", + "exportName": "generatedApi", + "hooks": true, + "outputFile": "./src/features/dataApi/coingecko/generatedApi.ts", + "schemaFile": "./src/features/dataApi/coingecko/swagger.json" +} \ No newline at end of file diff --git a/apps/mobile/package.json b/apps/mobile/package.json new file mode 100644 index 0000000..db67b43 --- /dev/null +++ b/apps/mobile/package.json @@ -0,0 +1,214 @@ +{ + "name": "@uniswap/mobile", + "version": "0.0.1", + "private": true, + "license": "GPL-3.0-or-later", + "scripts": { + "android": "react-native run-android --variant=devDebug --appIdSuffix=dev", + "android:release": "react-native run-android --variant=devRelease --appIdSuffix=dev", + "android:beta": "react-native run-android --variant=betaDebug --appIdSuffix=beta", + "android:beta:release": "react-native run-android --variant=betaRelease --appIdSuffix=beta", + "android:prod": "react-native run-android --variant=prodDebug", + "android:prod:release": "react-native run-android --variant=prodRelease", + "check:deps:usage": "./scripts/checkDepsUsage.sh", + "clean": "react-native-clean-project", + "debug": "react-devtools", + "deduplicate": "yarn-deduplicate --strategy=fewer", + "depcheck": "depcheck", + "env:android:keystore:download": "bash ./scripts/downloadAndroidKeystore.sh", + "env:fastlane:download": "bash ./scripts/downloadFastlaneEnv.sh", + "env:fastlane:upload": "bash ./scripts/uploadFastlaneEnv.sh", + "env:local:download": "bash ../../scripts/downloadEnvLocal.sh mobile-local-envs ../../.env.defaults.local && yarn env:local:copy:swift", + "env:local:upload": "bash ../../scripts/uploadEnvLocal.sh mobile-local-envs ../../.env.defaults.local", + "env:local:copy:swift": "python3 scripts/copy_env_vars_to_swift.py", + "e2e:packager": "DETOX_MODE=mocked yarn start", + "e2e:android:build:debug": "DETOX_MODE=mocked detox build -c android.emu.debug", + "e2e:android:test:debug": "detox test -c android.emu.debug", + "e2e:android:build:release": "DETOX_MODE=mocked detox build -c android.emu.release", + "e2e:android:test:release": "DETOX_MODE=mocked detox test -c android.emu.release --cleanup --headless --record-logs all", + "e2e:ios:build:debug": "DETOX_MODE=mocked detox build -c ios.sim.debug", + "e2e:ios:test:debug": "detox test -c ios.sim.debug", + "e2e:ios:test:release": "DETOX_MODE=mocked detox build -c ios.sim.release && detox test -c ios.sim.release --cleanup --headless --record-logs all", + "firestore:deploy:rules": "firebase deploy --only firestore:rules", + "link:assets": "react-native-asset", + "graphql:generate:swift": "cd ios && ./Pods/Apollo/apollo-ios-cli generate", + "hardhat": "hardhat node", + "check:circular": "../../scripts/check-circular-imports.sh ./src/app/App.tsx 8", + "ios": "yarn ios:prebuild && SKIP_BUNDLING=1 react-native run-ios", + "ios:prebuild": "yarn graphql:generate:swift && cd ios/WidgetsCore/MobileSchema && rm -rf !(README.md) && cd ../../.. && yarn graphql:generate:swift && yarn env:local:copy:swift", + "ios:smol": "SKIP_BUNDLING=1 react-native run-ios --simulator=\"iPhone SE (3rd generation)\"", + "ios:dev:release": "react-native run-ios --configuration Dev", + "ios:beta": "react-native run-ios --configuration Beta", + "ios:bundle": "react-native bundle --entry-file='index.js' --bundle-output='./ios/main.jsbundle' --dev=false --platform='ios' --assets-dest='./ios'", + "ios:release": "react-native run-ios --configuration Release", + "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0", + "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", + "start": "NODE_ENV=development react-native start", + "start:production": "NODE_ENV=production react-native start --reset-cache", + "test": "jest", + "snapshots": "jest -u", + "typecheck": "tsc -b", + "unicons": "cd scripts && python3 populate_svgs.py && cd .. && yarn lint --fix", + "pod": "./scripts/podinstall.sh" + }, + "dependencies": { + "@amplitude/analytics-react-native": "1.4.0", + "@apollo/client": "3.9.6", + "@ethersproject/shims": "5.6.0", + "@formatjs/intl-datetimeformat": "4.5.1", + "@formatjs/intl-getcanonicallocales": "1.9.0", + "@formatjs/intl-locale": "2.4.44", + "@formatjs/intl-numberformat": "7.4.1", + "@formatjs/intl-pluralrules": "4.3.1", + "@formatjs/intl-relativetimeformat": "11.1.2", + "@gorhom/bottom-sheet": "4.5.1", + "@react-native-async-storage/async-storage": "1.17.10", + "@react-native-community/netinfo": "9.3.0", + "@react-native-firebase/app": "18.4.0", + "@react-native-firebase/app-check": "18.4.0", + "@react-native-firebase/auth": "18.4.0", + "@react-native-firebase/firestore": "18.4.0", + "@react-native-masked-view/masked-view": "0.2.9", + "@react-navigation/core": "6.2.2", + "@react-navigation/native": "6.0.11", + "@react-navigation/native-stack": "6.7.0", + "@react-navigation/stack": "6.2.2", + "@reduxjs/toolkit": "1.9.3", + "@sentry/react": "7.80.0", + "@sentry/react-native": "5.5.0", + "@shopify/flash-list": "1.6.3", + "@shopify/react-native-performance": "4.1.2", + "@shopify/react-native-performance-navigation": "3.0.0", + "@shopify/react-native-skia": "0.1.187", + "@uniswap/analytics": "1.7.0", + "@uniswap/analytics-events": "2.31.0", + "@uniswap/ethers-rs-mobile": "0.0.5", + "@uniswap/sdk-core": "4.1.2", + "@uniswap/v3-sdk": "3.10.2", + "@walletconnect/core": "2.11.2", + "@walletconnect/react-native-compat": "2.11.2", + "@walletconnect/utils": "2.11.2", + "@walletconnect/web3wallet": "1.10.2", + "apollo3-cache-persist": "0.14.1", + "babel-plugin-transform-inline-environment-variables": "0.4.4", + "babel-plugin-transform-remove-console": "6.9.4", + "cross-fetch": "3.1.5", + "dayjs": "1.11.7", + "ethers": "5.7.2", + "expo": "48.0.19", + "expo-av": "13.4.1", + "expo-barcode-scanner": "12.7.0", + "expo-blur": "12.6.0", + "expo-camera": "13.4.4", + "expo-clipboard": "4.1.2", + "expo-haptics": "12.0.1", + "expo-linear-gradient": "12.3.0", + "expo-linking": "4.0.1", + "expo-local-authentication": "13.0.2", + "expo-localization": "14.1.1", + "expo-modules-core": "1.5.8", + "expo-screen-capture": "4.2.0", + "expo-store-review": "~6.2.1", + "fuse.js": "6.5.3", + "i18next": "23.10.0", + "lodash": "4.17.21", + "no-yolo-signatures": "0.0.2", + "qrcode": "1.5.1", + "react": "18.2.0", + "react-freeze": "1.0.3", + "react-i18next": "14.0.5", + "react-native": "0.71.13", + "react-native-appsflyer": "6.10.3", + "react-native-context-menu-view": "1.6.0", + "react-native-device-info": "10.0.2", + "react-native-fast-image": "8.6.3", + "react-native-gesture-handler": "2.15.0", + "react-native-get-random-values": "1.8.0", + "react-native-image-colors": "1.5.2", + "react-native-image-picker": "7.0.1", + "react-native-localize": "2.2.6", + "react-native-markdown-display": "7.0.0-alpha.2", + "react-native-mmkv": "2.10.1", + "react-native-onesignal": "4.5.2", + "react-native-pager-view": "6.0.1", + "react-native-permissions": "3.6.0", + "react-native-reanimated": "3.3.0", + "react-native-restart": "0.0.27", + "react-native-safe-area-context": "4.5.0", + "react-native-screens": "3.24.0", + "react-native-splash-screen": "3.3.0", + "react-native-svg": "13.9.0", + "react-native-tab-view": "3.5.2", + "react-native-url-polyfill": "1.3.0", + "react-native-wagmi-charts": "2.3.0", + "react-native-webview": "11.23.1", + "react-native-widgetkit": "1.0.9", + "react-redux": "8.0.5", + "redux": "4.2.1", + "redux-mock-store": "1.5.4", + "redux-persist": "6.0.0", + "redux-saga": "1.2.2", + "rive-react-native": "6.1.1", + "statsig-react-native": "4.11.0", + "typed-redux-saga": "1.5.0", + "uniswap": "workspace:^", + "utilities": "workspace:^", + "wallet": "workspace:^" + }, + "devDependencies": { + "@babel/core": "^7.20.5", + "@babel/plugin-proposal-export-namespace-from": "7.18.9", + "@babel/plugin-proposal-logical-assignment-operators": "7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6", + "@babel/plugin-proposal-numeric-separator": "7.16.7", + "@babel/plugin-proposal-optional-chaining": "7.21.0", + "@babel/runtime": "7.18.9", + "@faker-js/faker": "7.6.0", + "@storybook/react": "7.0.2", + "@tamagui/babel-plugin": "1.91.3", + "@testing-library/react-hooks": "7.0.2", + "@testing-library/react-native": "11.5.0", + "@types/react-native": "0.71.3", + "@types/redux-mock-store": "1.0.6", + "@uniswap/eslint-config": "workspace:^", + "@walletconnect/types": "2.11.2", + "@welldone-software/why-did-you-render": "7.0.1", + "babel-jest": "29.6.1", + "babel-loader": "8.2.3", + "babel-plugin-react-native-web": "0.17.5", + "babel-plugin-react-require": "4.0.0", + "core-js": "2.6.12", + "detox": "20.18.1", + "eslint": "8.44.0", + "hardhat": "2.14.0", + "jest": "29.7.0", + "jest-expo": "49.0.0", + "jest-extended": "4.0.1", + "jest-transformer-svg": "2.0.0", + "madge": "6.1.0", + "mockdate": "3.0.5", + "postinstall-postinstall": "2.1.0", + "react-devtools": "4.28.0", + "react-dom": "18.2.0", + "react-native-apollo-devtools-client": "1.0.4", + "react-native-asset": "2.1.1", + "react-native-clean-project": "4.0.1", + "react-native-dotenv": "3.2.0", + "react-native-flipper": "0.187.1", + "react-native-mmkv-flipper-plugin": "1.0.0", + "react-native-monorepo-tools": "1.2.1", + "react-native-svg-transformer": "1.0.0", + "react-test-renderer": "18.2.0", + "redux-flipper": "2.0.2", + "redux-saga-test-plan": "4.0.4", + "typescript": "5.3.3", + "yarn-deduplicate": "6.0.0" + }, + "expo": { + "autolinking": { + "exclude": [ + "expo-constants" + ] + } + } +} diff --git a/apps/mobile/react-native.config.js b/apps/mobile/react-native.config.js new file mode 100644 index 0000000..6ee1c3e --- /dev/null +++ b/apps/mobile/react-native.config.js @@ -0,0 +1,6 @@ +module.exports = { + assets: ['./src/assets/fonts'], + dependencies: { + ...(process.env.USE_FLIPPER ? {} : { 'react-native-flipper': { platforms: { ios: null } } }), + }, +} diff --git a/apps/mobile/scripts/checkDepsUsage.sh b/apps/mobile/scripts/checkDepsUsage.sh new file mode 100755 index 0000000..2a32ce3 --- /dev/null +++ b/apps/mobile/scripts/checkDepsUsage.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +mv src/package.json src/ignore.json +yarn run depcheck +result_status=$? +mv src/ignore.json src/package.json + +exit $result_status diff --git a/apps/mobile/scripts/copy_env_vars_to_swift.py b/apps/mobile/scripts/copy_env_vars_to_swift.py new file mode 100644 index 0000000..0ce7e87 --- /dev/null +++ b/apps/mobile/scripts/copy_env_vars_to_swift.py @@ -0,0 +1,57 @@ +import os + +ENV_DEFAULTS_FILE = '../../.env.defaults' +ENV_DEFAULTS_LOCAL_FILE = '../../.env.defaults.local' +SWIFT_FILE_PATH = 'ios/WidgetsCore/Env.swift' +SWIFT_ENV_VARIABLES = ['UNISWAP_API_BASE_URL','UNISWAP_API_KEY'] + +def to_swift_constant_line(key, value): + return f' static let {key.upper()} = "{value}"' + +def process_lines(lines, search_vars): + env_var_declarations = [] + for line in lines: + line = line.strip() + if line and not line.startswith('#'): + # Split variable name and value + key, value = line.split('=', 1) + if key in search_vars: + env_var_declarations.append(to_swift_constant_line(key.upper(), value)) + search_vars.remove(key) + + return env_var_declarations + +# convert env variables to swift constants and writes to a swift file. +def copy_env_vars_to_swift(env_defaults_file, env_defaults_local_file, swift_file, env_variables): + envs_left_to_find = env_variables.copy() + env_var_declarations = [] + + # Search for env vars in the system first + for key in env_variables: + if key in os.environ: + env_var_declarations.append(to_swift_constant_line(key.upper(), os.environ[key])) + envs_left_to_find.remove(key) + + # read from local env file if it exists + if os.path.isfile(env_defaults_local_file): + with open(env_defaults_local_file, 'r') as f: + env_lines = f.readlines() + env_var_declarations.extend(process_lines(env_lines, envs_left_to_find)) + + # read from checked in env file for non-secret variables + with open(env_defaults_file, 'r') as f: + default_env_lines = f.readlines() + env_var_declarations.extend(process_lines(default_env_lines, envs_left_to_find)) + + # write to swift file + with open(swift_file, 'w') as f: + f.write('struct Env {\n') + f.write('\n'.join(env_var_declarations)) + f.write('\n}') + + # If not all env variables are set + if len(env_variables) != len(env_var_declarations): + print('WARNING: Not all environment variables were converted to Swift.') + exit(1) + +copy_env_vars_to_swift(ENV_DEFAULTS_FILE, ENV_DEFAULTS_LOCAL_FILE, SWIFT_FILE_PATH, SWIFT_ENV_VARIABLES) diff --git a/apps/mobile/scripts/podinstall.sh b/apps/mobile/scripts/podinstall.sh new file mode 100755 index 0000000..e6b5f42 --- /dev/null +++ b/apps/mobile/scripts/podinstall.sh @@ -0,0 +1,2 @@ +#!/bin/bash +cd ios/ && bundle install && bundle exec pod install && cd .. diff --git a/apps/mobile/scripts/populate_svgs.py b/apps/mobile/scripts/populate_svgs.py new file mode 100644 index 0000000..85ef949 --- /dev/null +++ b/apps/mobile/scripts/populate_svgs.py @@ -0,0 +1,131 @@ +import os +from os.path import isfile, join + + +def find_closing_quote_index(s, starting_index=0): + for i in range(starting_index, len(s)): + if s[i] == '"': + return i + return None + + +def grab_svg_attribute_prop(prop_name, line): + # if prop_name = fill-rule, then this function + # will return the string between the quotes: + # returns + i = line.index(f"{prop_name}=") + idx_after_quote = i + len(prop_name) + 2 + j = find_closing_quote_index(line, idx_after_quote) + if not j: + return None + return line[idx_after_quote:j] + + +folder_location = "../src/assets/unicons/" +folders = ["Container", "Emblem"] +unicons_location = '../src/components/unicons' + + +def generate_arrays_from_svgs(): + print("Generating ShapeSvg Arrays") + for folder in folders: + folder_path = join(folder_location, folder) + print("Looking in", folder_path) + print("Found", len(os.listdir(folder_path)), "files") + result = "import { PathProps } from 'src/components/unicons/types'\nexport const svgPaths: PathProps[][] = [" + count = 0 + for filename in os.listdir(folder_path): + if not isfile(join(folder_path, filename)) or filename[-4:] != ".svg": + continue + f = open(join(folder_path, filename), "r") + cur_svgs = [] + for line in f: + if "svg" in line: + continue + if " mockRNLocalize) + +it('renders correctly', async () => { + render() + + await act(async () => { + // Wait for component cleanup + }) +}) diff --git a/apps/mobile/src/app/App.tsx b/apps/mobile/src/app/App.tsx new file mode 100644 index 0000000..f1de1a1 --- /dev/null +++ b/apps/mobile/src/app/App.tsx @@ -0,0 +1,322 @@ +import { ApolloProvider } from '@apollo/client' +import { BottomSheetModalProvider } from '@gorhom/bottom-sheet' +import * as Sentry from '@sentry/react-native' +import { PerformanceProfiler, RenderPassReport } from '@shopify/react-native-performance' +import { PropsWithChildren, default as React, StrictMode, useCallback, useEffect } from 'react' +import { I18nextProvider } from 'react-i18next' +import { LogBox, NativeModules, StatusBar } from 'react-native' +import appsFlyer from 'react-native-appsflyer' +import { getUniqueId } from 'react-native-device-info' +import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { SafeAreaProvider } from 'react-native-safe-area-context' +import { enableFreeze } from 'react-native-screens' +import { PersistGate } from 'redux-persist/integration/react' +import { ErrorBoundary } from 'src/app/ErrorBoundary' +import { MobileWalletNavigationProvider } from 'src/app/MobileWalletNavigationProvider' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { AppModals } from 'src/app/modals/AppModals' +import { NavigationContainer } from 'src/app/navigation/NavigationContainer' +import { useIsPartOfNavigationTree } from 'src/app/navigation/hooks' +import { AppStackNavigator } from 'src/app/navigation/navigation' +import { persistor, store } from 'src/app/store' +import Trace from 'src/components/Trace/Trace' +import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' +import { OfflineBanner } from 'src/components/banners/OfflineBanner' +import { usePersistedApolloClient } from 'src/data/usePersistedApolloClient' +import { initAppsFlyer } from 'src/features/analytics/appsflyer' +import { LockScreenContextProvider } from 'src/features/authentication/lockScreenContext' +import { BiometricContextProvider } from 'src/features/biometrics/context' +import { NotificationToastWrapper } from 'src/features/notifications/NotificationToastWrapper' +import { initOneSignal } from 'src/features/notifications/Onesignal' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { shouldLogScreen } from 'src/features/telemetry/directLogScreens' +import { selectAllowAnalytics } from 'src/features/telemetry/selectors' +import { + processWidgetEvents, + setAccountAddressesUserDefaults, + setFavoritesUserDefaults, + setI18NUserDefaults, +} from 'src/features/widgets/widgets' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' +import { getSentryEnvironment, getStatsigEnvironmentTier } from 'src/utils/version' +import { Statsig, StatsigProvider } from 'statsig-react-native' +import { flexStyles, useIsDarkMode } from 'ui/src' +import { config } from 'uniswap/src/config' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' +import { isDetoxBuild } from 'utilities/src/environment' +import { registerConsoleOverrides } from 'utilities/src/logger/console' +import { logger } from 'utilities/src/logger/logger' +import { useAsyncData } from 'utilities/src/react/hooks' +import { AnalyticsNavigationContextProvider } from 'utilities/src/telemetry/trace/AnalyticsNavigationContext' +import { initFirebaseAppCheck } from 'wallet/src/features/appCheck' +import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' +import { + DUMMY_STATSIG_SDK_KEY, + EXPERIMENT_NAMES, + FEATURE_FLAGS, +} from 'wallet/src/features/experiments/constants' +import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { LocalizationContextProvider } from 'wallet/src/features/language/LocalizationContext' +import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' +import { updateLanguage } from 'wallet/src/features/language/slice' +import { clearNotificationQueue } from 'wallet/src/features/notifications/slice' +import { TransactionHistoryUpdater } from 'wallet/src/features/transactions/TransactionHistoryUpdater' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { WalletContextProvider } from 'wallet/src/features/wallet/context' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import i18n from 'wallet/src/i18n/i18n' +import { SharedProvider } from 'wallet/src/provider' +import { CurrencyId } from 'wallet/src/utils/currencyId' +import { beforeSend } from 'wallet/src/utils/sentry' + +enableFreeze(true) + +if (__DEV__) { + registerConsoleOverrides() +} + +// Construct a new instrumentation instance. This is needed to communicate between the integration and React +const routingInstrumentation = new Sentry.ReactNavigationInstrumentation() + +if (!__DEV__ && !isDetoxBuild) { + Sentry.init({ + environment: getSentryEnvironment(), + dsn: config.sentryDsn, + attachViewHierarchy: true, + enableCaptureFailedRequests: true, + tracesSampler: (_) => { + return 0.2 + }, + integrations: [ + new Sentry.ReactNativeTracing({ + enableUserInteractionTracing: true, + enableNativeFramesTracking: true, + enableStallTracking: true, + // Pass instrumentation to be used as `routingInstrumentation` + routingInstrumentation, + }), + ], + // By default, the Sentry SDK normalizes any context to a depth of 3. + // We're increasing this to be able to see the full depth of the Redux state. + normalizeDepth: 10, + beforeSend, + }) +} + +// Log boxes on simulators can block detox tap event when they cover buttons placed at +// the bottom of the screen and cause tests to fail. +if (isDetoxBuild) { + LogBox.ignoreAllLogs() +} + +initOneSignal() +initAppsFlyer() +initFirebaseAppCheck() + +function App(): JSX.Element | null { + // We want to ensure deviceID is used as the identifier to link with analytics + const fetchAndSetDeviceId = useCallback(async () => { + const uniqueId = await getUniqueId() + Sentry.setUser({ + id: uniqueId, + }) + return uniqueId + }, []) + + const deviceId = useAsyncData(fetchAndSetDeviceId).data + + const statSigOptions = { + options: { + environment: { + tier: getStatsigEnvironmentTier(), + }, + api: uniswapUrls.statsigProxyUrl, + disableAutoMetricsLogging: true, + disableErrorLogging: true, + }, + sdkKey: DUMMY_STATSIG_SDK_KEY, + user: deviceId ? { userID: deviceId } : {}, + waitForInitialization: true, + } + + return ( + + + + + + + + + + + + + + + + + + ) +} + +function SentryTags({ children }: PropsWithChildren): JSX.Element { + useEffect(() => { + Object.entries(FEATURE_FLAGS).map(([_, featureFlagName]) => { + Sentry.setTag( + `featureFlag.${featureFlagName}`, + Statsig.checkGateWithExposureLoggingDisabled(featureFlagName) + ) + }) + + Object.entries(EXPERIMENT_NAMES).map(([_, experimentName]) => { + Sentry.setTag( + `experiment.${experimentName}`, + Statsig.getExperimentWithExposureLoggingDisabled(experimentName).getGroupName() + ) + }) + }, []) + + return <>{children} +} + +// Ensures redux state is available inside usePersistedApolloClient for the custom endpoint +function AppOuter(): JSX.Element | null { + const client = usePersistedApolloClient() + + const onReportPrepared = useCallback((report: RenderPassReport) => { + sendMobileAnalyticsEvent(MobileEventName.PerformanceReport, report) + }, []) + + if (!client) { + return null + } + + return ( + + + + + + + + + + + + { + routingInstrumentation.registerNavigationContainer(navigationRef) + }}> + + + + + + + + + + + + + + + + + + + + + ) +} + +function AppInner(): JSX.Element { + const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() + const themeSetting = useCurrentAppearanceSetting() + const allowAnalytics = useAppSelector(selectAllowAnalytics) + + useEffect(() => { + if (allowAnalytics) { + appsFlyer.startSdk() + logger.info('AppsFlyer', 'status', 'started') + } else { + appsFlyer.stop(!allowAnalytics, (res: unknown) => { + if (typeof res === 'string' && res === 'Success') { + logger.info('AppsFlyer', 'status', 'stopped') + } else { + logger.warn( + 'AppsFlyer', + 'stop', + `Got an error when trying to stop the AppsFlyer SDK: ${res}` + ) + } + }) + } + }, [allowAnalytics]) + + useEffect(() => { + dispatch(clearNotificationQueue()) // clear all in-app toasts on app start + dispatch(updateLanguage(null)) + }, [dispatch]) + + useEffect(() => { + // TODO: This is a temporary solution (it should be replaced with Appearance.setColorScheme + // after updating RN to 0.72.0 or higher) + NativeModules.ThemeModule.setColorScheme(themeSetting) + }, [themeSetting]) + + return ( + <> + + + + + ) +} + +function DataUpdaters(): JSX.Element { + const favoriteTokens: CurrencyId[] = useAppSelector(selectFavoriteTokens) + const accountsMap: Record = useAccounts() + const { locale } = useCurrentLanguageInfo() + const { code } = useAppFiatCurrencyInfo() + + // Refreshes widgets when bringing app to foreground + useAppStateTrigger('background', 'active', processWidgetEvents) + + useEffect(() => { + setFavoritesUserDefaults(favoriteTokens) + }, [favoriteTokens]) + + useEffect(() => { + setAccountAddressesUserDefaults(Object.values(accountsMap)) + }, [accountsMap]) + + useEffect(() => { + setI18NUserDefaults({ locale, currency: code }) + }, [code, locale]) + + return ( + <> + + + + ) +} + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +function getApp() { + return __DEV__ ? App : Sentry.wrap(App) +} + +export default getApp() diff --git a/apps/mobile/src/app/ErrorBoundary.tsx b/apps/mobile/src/app/ErrorBoundary.tsx new file mode 100644 index 0000000..8a2dd91 --- /dev/null +++ b/apps/mobile/src/app/ErrorBoundary.tsx @@ -0,0 +1,98 @@ +import React, { ErrorInfo, PropsWithChildren } from 'react' +import { useTranslation } from 'react-i18next' +import { Image, StyleSheet } from 'react-native' +import RNRestart from 'react-native-restart' +import { useAppDispatch } from 'src/app/hooks' +import { Button, Flex, Text } from 'ui/src' +import { DEAD_LUNI } from 'ui/src/assets' +import { logger } from 'utilities/src/logger/logger' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' + +interface ErrorBoundaryState { + error: Error | null +} + +// Uncaught errors during renders of subclasses will be caught here +// Errors in handlers (e.g. press handler) will not reach here +export class ErrorBoundary extends React.Component, ErrorBoundaryState> { + constructor(props: PropsWithChildren) { + super(props) + this.state = { error: null } + } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error } + } + + componentDidCatch(error: Error & { cause?: Error }, errorInfo: ErrorInfo): void { + // Based on https://github.com/getsentry/sentry-javascript/blob/develop/packages/react/src/errorboundary.tsx + const errorBoundaryError = new Error(error.message) + errorBoundaryError.name = `React ErrorBoundary ${errorBoundaryError.name}` + errorBoundaryError.stack = errorInfo.componentStack + error.cause = errorBoundaryError + + logger.error(error, { + level: 'fatal', + tags: { + file: 'ErrorBoundary', + function: 'componentDidCatch', + }, + }) + } + + render(): React.ReactNode { + const { error } = this.state + if (error !== null) { + return + } + + return this.props.children + } +} + +function ErrorScreen({ error }: { error: Error }): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const accounts = useAccounts() + + // If there is no active account, we need to reset the onboarding flow + if (error.message === 'No active account' && Object.values(accounts).length === 0) { + dispatch(setFinishedOnboarding({ finishedOnboarding: false })) + } + + return ( + + + + + {t('errors.crash.title')} + {t('errors.crash.message')} + + {error.message && __DEV__ && {error.message}} + + + + + + ) +} + +const styles = StyleSheet.create({ + errorImage: { + height: 150, + resizeMode: 'contain', + width: 150, + }, +}) diff --git a/apps/mobile/src/app/MobileWalletNavigationProvider.tsx b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx new file mode 100644 index 0000000..975e793 --- /dev/null +++ b/apps/mobile/src/app/MobileWalletNavigationProvider.tsx @@ -0,0 +1,107 @@ +import { PropsWithChildren, useCallback } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' +import { + NavigateToNftItemArgs, + NavigateToSwapFlowArgs, + WalletNavigationProvider, +} from 'wallet/src/contexts/WalletNavigationContext' +import { useFiatOnRampIpAddressQuery } from 'wallet/src/features/fiatOnRamp/api' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function MobileWalletNavigationProvider({ children }: PropsWithChildren): JSX.Element { + const navigateToAccountActivityList = useNavigateToHomepageTab(HomeScreenTabIndex.Activity) + const navigateToAccountTokenList = useNavigateToHomepageTab(HomeScreenTabIndex.Tokens) + const navigateToBuyOrReceiveWithEmptyWallet = useNavigateToBuyOrReceiveWithEmptyWallet() + const navigateToNftDetails = useNavigateToNftDetails() + const navigateToSwapFlow = useNavigateToSwapFlow() + const navigateToTokenDetails = useNavigateToTokenDetails() + + return ( + + {children} + + ) +} + +function useNavigateToHomepageTab(tab: HomeScreenTabIndex): () => void { + const { navigate } = useAppStackNavigation() + + return useCallback((): void => { + navigate(Screens.Home, { tab }) + }, [navigate, tab]) +} + +function useNavigateToSwapFlow(): (args: NavigateToSwapFlowArgs) => void { + const dispatch = useAppDispatch() + + return useCallback( + (args: NavigateToSwapFlowArgs): void => { + const initialState = args?.initialState + + dispatch(closeModal({ name: ModalName.Swap })) + dispatch(openModal({ name: ModalName.Swap, initialState })) + }, + [dispatch] + ) +} + +function useNavigateToTokenDetails(): (currencyId: string) => void { + const navigation = useAppStackNavigation() + + return useCallback( + (currencyId: string): void => { + navigation.navigate(Screens.TokenDetails, { currencyId }) + }, + [navigation] + ) +} + +function useNavigateToNftDetails(): (args: NavigateToNftItemArgs) => void { + const navigation = useAppStackNavigation() + + return useCallback( + ({ owner, address, tokenId, isSpam, fallbackData }: NavigateToNftItemArgs): void => { + navigation.navigate(Screens.NFTItem, { + owner, + address, + tokenId, + isSpam, + fallbackData, + }) + }, + [navigation] + ) +} + +function useNavigateToBuyOrReceiveWithEmptyWallet(): () => void { + const dispatch = useAppDispatch() + + const { data } = useFiatOnRampIpAddressQuery() + const fiatOnRampEligible = Boolean(data?.isBuyAllowed) + + return useCallback((): void => { + dispatch(closeModal({ name: ModalName.Send })) + + if (fiatOnRampEligible) { + dispatch(openModal({ name: ModalName.FiatOnRamp })) + } else { + dispatch( + openModal({ + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.WalletQr, + }) + ) + } + }, [dispatch, fiatOnRampEligible]) +} diff --git a/apps/mobile/src/app/createMigrate.ts b/apps/mobile/src/app/createMigrate.ts new file mode 100644 index 0000000..76ee257 --- /dev/null +++ b/apps/mobile/src/app/createMigrate.ts @@ -0,0 +1,59 @@ +// Adapted from https://github.com/rt2zz/redux-persist/blob/master/src/createMigrate.ts to add more logging +import type { MigrationManifest, PersistedState } from 'redux-persist' +import { DEFAULT_VERSION } from 'redux-persist/es/constants' +import { logger } from 'utilities/src/logger/logger' + +export default function createMigrate( + migrations: MigrationManifest +): (state: PersistedState, currentVersion: number) => Promise { + return function (state: PersistedState, currentVersion: number): Promise { + try { + if (!state) { + logger.debug('redux-persist', 'createMigrate', 'no inbound state, skipping migration') + return Promise.resolve(undefined) + } + + const inboundVersion: number = state._persist?.version ?? DEFAULT_VERSION + + if (inboundVersion === currentVersion) { + logger.debug( + 'redux-persist', + 'createMigrate', + `versions match (${currentVersion}), noop migration` + ) + return Promise.resolve(state) + } + + if (inboundVersion > currentVersion) { + logger.debug('redux-persist', 'createMigrate', 'downgrading version is not supported') + return Promise.resolve(state) + } + + const migrationKeys = Object.keys(migrations) + .map((ver) => parseInt(ver, 10)) + .filter((key) => currentVersion >= key && key > inboundVersion) + .sort((a, b) => a - b) + + logger.debug('redux-persist', 'createMigrate', `migrationKeys: ${migrationKeys}`) + + const migratedState: PersistedState = migrationKeys.reduce( + (versionState: PersistedState, versionKey) => { + logger.debug( + 'redux-persist', + 'createMigrate', + `running migration for versionKey: ${versionKey}` + ) + // Safe non-null assertion because `versionKey` comes from `Object.keys(migrations)` + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return migrations[versionKey]!(versionState) + }, + state + ) + + return Promise.resolve(migratedState) + } catch (error) { + logger.error(error, { tags: { file: 'redux-persist', function: 'createMigrate' } }) + return Promise.reject(error) + } + } +} diff --git a/apps/mobile/src/app/globalActions.ts b/apps/mobile/src/app/globalActions.ts new file mode 100644 index 0000000..e554c57 --- /dev/null +++ b/apps/mobile/src/app/globalActions.ts @@ -0,0 +1,7 @@ +// Copied from https://github.com/Uniswap/interface/blob/main/src/state/global/actions.ts + +import { createAction } from '@reduxjs/toolkit' + +// fired once when the app reloads but before the app renders +// allows any updates to be applied to store data loaded from localStorage +export const updateVersion = createAction('global/updateVersion') diff --git a/apps/mobile/src/app/hooks.test.ts b/apps/mobile/src/app/hooks.test.ts new file mode 100644 index 0000000..c6eaa56 --- /dev/null +++ b/apps/mobile/src/app/hooks.test.ts @@ -0,0 +1,52 @@ +import { renderHook } from '@testing-library/react-hooks' +import { LayoutChangeEvent } from 'react-native' +import { act } from 'react-test-renderer' +import { useShouldShowNativeKeyboard } from './hooks' + +describe(useShouldShowNativeKeyboard, () => { + it('returns false if layout calculation is pending', () => { + const { result } = renderHook(() => useShouldShowNativeKeyboard()) + + expect(result.current.showNativeKeyboard).toBe(false) + }) + + it('returns isLayoutPending as true if layout calculation is pending', () => { + const { result } = renderHook(() => useShouldShowNativeKeyboard()) + + expect(result.current.isLayoutPending).toBe(true) + }) + + it("shouldn't show native keyboard if decimal pad is rendered below the input panel", async () => { + const { result } = renderHook(() => useShouldShowNativeKeyboard()) + + await act(async () => { + result.current.onInputPanelLayout({ + nativeEvent: { layout: { height: 100 } }, + } as LayoutChangeEvent) + result.current.onDecimalPadLayout({ + nativeEvent: { layout: { y: 200 } }, + } as LayoutChangeEvent) + }) + + expect(result.current.showNativeKeyboard).toBe(false) + expect(result.current.maxContentHeight).toBeDefined() + expect(result.current.isLayoutPending).toBe(false) + }) + + it('should show native keyboard if decimal pad is rendered above the input panel', async () => { + const { result } = renderHook(() => useShouldShowNativeKeyboard()) + + await act(async () => { + result.current.onInputPanelLayout({ + nativeEvent: { layout: { height: 100 } }, + } as LayoutChangeEvent) + result.current.onDecimalPadLayout({ + nativeEvent: { layout: { y: 50 } }, + } as LayoutChangeEvent) + }) + + expect(result.current.showNativeKeyboard).toBe(true) + expect(result.current.maxContentHeight).not.toBeDefined() + expect(result.current.isLayoutPending).toBe(false) + }) +}) diff --git a/apps/mobile/src/app/hooks.ts b/apps/mobile/src/app/hooks.ts new file mode 100644 index 0000000..b7b1496 --- /dev/null +++ b/apps/mobile/src/app/hooks.ts @@ -0,0 +1,61 @@ +import { ThunkDispatch } from '@reduxjs/toolkit' +import { useState } from 'react' +import { LayoutChangeEvent } from 'react-native' +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux' +import type { AppDispatch } from 'src/app/store' +import { SagaGenerator, select } from 'typed-redux-saga' +import { spacing } from 'ui/src/theme' +import type { MobileState } from './reducer' + +// Use throughout the app instead of plain `useDispatch` and `useSelector` +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const useAppDispatch = (): ThunkDispatch => useDispatch() +export const useAppSelector: TypedUseSelectorHook = useSelector + +// Use in sagas for better typing when selecting from redux state +export function* appSelect(fn: (state: MobileState) => T): SagaGenerator { + const state = yield* select(fn) + return state +} + +const MIN_INPUT_DECIMAL_PAD_GAP = spacing.spacing8 + +export function useShouldShowNativeKeyboard(): { + onInputPanelLayout: (event: LayoutChangeEvent) => void + onDecimalPadLayout: (event: LayoutChangeEvent) => void + isLayoutPending: boolean + showNativeKeyboard: boolean + maxContentHeight?: number +} { + const [containerHeight, setContainerHeight] = useState() + const [decimalPadY, setDecimalPadY] = useState() + + const onInputPanelLayout = (event: LayoutChangeEvent): void => { + if (containerHeight === undefined) { + setContainerHeight(event.nativeEvent.layout.height) + } + } + + const onDecimalPadLayout = (event: LayoutChangeEvent): void => { + if (decimalPadY === undefined) { + setDecimalPadY(event.nativeEvent.layout.y) + } + } + + const isLayoutPending = containerHeight === undefined || decimalPadY === undefined + + // If decimal pad renders below the input panel, we need to show the native keyboard + const showNativeKeyboard = isLayoutPending + ? false + : containerHeight + MIN_INPUT_DECIMAL_PAD_GAP > decimalPadY + + return { + onInputPanelLayout, + onDecimalPadLayout, + isLayoutPending, + showNativeKeyboard, + // can be used to imitate flexGrow=1 for the input panel + maxContentHeight: + isLayoutPending || showNativeKeyboard ? undefined : decimalPadY - MIN_INPUT_DECIMAL_PAD_GAP, + } +} diff --git a/apps/mobile/src/app/migrations.test.ts b/apps/mobile/src/app/migrations.test.ts new file mode 100644 index 0000000..eb97d4f --- /dev/null +++ b/apps/mobile/src/app/migrations.test.ts @@ -0,0 +1,1336 @@ +/* eslint-disable max-lines */ +import { BigNumber } from 'ethers' +import mockdate from 'mockdate' +import createMigrate from 'src/app/createMigrate' +import { migrations, OLD_DEMO_ACCOUNT_ADDRESS } from 'src/app/migrations' +import { + getSchema, + initialSchema, + v10Schema, + v11Schema, + v12Schema, + v13Schema, + v14Schema, + v15Schema, + v16Schema, + v17Schema, + v18Schema, + v19Schema, + v1Schema, + v20Schema, + v21Schema, + v22Schema, + v23Schema, + v24Schema, + v25Schema, + v26Schema, + v27Schema, + v28Schema, + v29Schema, + v2Schema, + v31Schema, + v32Schema, + v33Schema, + v34Schema, + v35Schema, + v36Schema, + v37Schema, + v38Schema, + v39Schema, + v3Schema, + v40Schema, + v41Schema, + v42Schema, + v43Schema, + v44Schema, + v45Schema, + v46Schema, + v47Schema, + v48Schema, + v49Schema, + v4Schema, + v50Schema, + v51Schema, + v52Schema, + v53Schema, + v54Schema, + v55Schema, + v56Schema, + v57Schema, + v58Schema, + v59Schema, + v5Schema, + v6Schema, + v7Schema, + v8Schema, + v9Schema, +} from 'src/app/schema' +import { persistConfig } from 'src/app/store' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { initialBiometricsSettingsState } from 'src/features/biometrics/slice' +import { initialCloudBackupState } from 'src/features/CloudBackup/cloudBackupSlice' +import { initialPasswordLockoutState } from 'src/features/CloudBackup/passwordLockoutSlice' +import { initialModalsState } from 'src/features/modals/modalSlice' +import { initialTelemetryState } from 'src/features/telemetry/slice' +import { initialTweaksState } from 'src/features/tweaks/slice' +import { initialWalletConnectState } from 'src/features/walletConnect/walletConnectSlice' +import { ChainId } from 'wallet/src/constants/chains' +import { initialBehaviorHistoryState } from 'wallet/src/features/behaviorHistory/slice' +import { initialFavoritesState } from 'wallet/src/features/favorites/slice' +import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' +import { initialLanguageState } from 'wallet/src/features/language/slice' +import { initialNotificationsState } from 'wallet/src/features/notifications/slice' +import { initialSearchHistoryState } from 'wallet/src/features/search/searchHistorySlice' +import { initialTokensState } from 'wallet/src/features/tokens/tokensSlice' +import { + initialTransactionsState, + TransactionStateMap, +} from 'wallet/src/features/transactions/slice' +import { + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { + Account, + AccountType, + SignerMnemonicAccount, +} from 'wallet/src/features/wallet/accounts/types' +import { initialWalletState, SwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { ModalName } from 'wallet/src/telemetry/constants' +import { + fiatPurchaseTransactionInfo, + signerMnemonicAccount, + transactionDetails, +} from 'wallet/src/test/fixtures' + +const account = signerMnemonicAccount() + +const txDetailsConfirmed = transactionDetails({ + status: TransactionStatus.Success, +}) +const fiatOnRampTxDetailsFailed = transactionDetails({ + status: TransactionStatus.Failed, + typeInfo: fiatPurchaseTransactionInfo({ + explorerUrl: + 'https://buy-sandbox.moonpay.com/transaction_receipt?transactionId=d6c32bb5-7cd9-4c22-8f46-6bbe786c599f', + id: 'd6c32bb5-7cd9-4c22-8f46-6bbe786c599f', + }), +}) + +// helps with object assignment +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getAllKeysOfNestedObject = (obj: any, prefix = ''): string[] => { + const keys = Object.keys(obj) + if (!keys.length && prefix !== '') { + return [prefix.slice(0, -1)] + } + return keys.reduce((res, el) => { + if (Array.isArray(obj[el])) { + return [...res] + } + + if (typeof obj[el] === 'object' && obj[el] !== null) { + return [...res, ...getAllKeysOfNestedObject(obj[el], prefix + el + '.')] + } + + return [...res, prefix + el] + }, []) +} + +describe('Redux state migrations', () => { + it('is able to perform all migrations starting from the initial schema', async () => { + const initialSchemaStub = { + ...initialSchema, + _persist: { version: -1, rehydrated: false }, + } + + const migrate = createMigrate(migrations) + const migratedSchema = await migrate(initialSchemaStub, persistConfig.version) + expect(typeof migratedSchema).toBe('object') + }) + + // If this test fails then it's likely a required property was added to the Redux state but a migration was not defined + it('migrates all the properties correctly', async () => { + const initialSchemaStub = { + ...initialSchema, + _persist: { version: -1, rehydrated: false }, + } + + const migrate = createMigrate(migrations) + const migratedSchema = await migrate(initialSchemaStub, persistConfig.version) + + // Add new slices here! + const initialState = { + appearanceSettings: { selectedAppearanceSettings: 'system' }, + biometricSettings: initialBiometricsSettingsState, + blocks: { byChainId: {} }, + chains: { + byChainId: { + '1': { isActive: true }, + '10': { isActive: true }, + '137': { isActive: true }, + '42161': { isActive: true }, + }, + }, + cloudBackup: initialCloudBackupState, + ens: { ensForAddress: {} }, + favorites: initialFavoritesState, + fiatCurrencySettings: initialFiatCurrencyState, + languageSettings: initialLanguageState, + modals: initialModalsState, + notifications: initialNotificationsState, + passwordLockout: initialPasswordLockoutState, + behaviorHistory: initialBehaviorHistoryState, + providers: { isInitialized: false }, + saga: {}, + searchHistory: initialSearchHistoryState, + telemetry: initialTelemetryState, + tokenLists: {}, + tokens: initialTokensState, + transactions: initialTransactionsState, + tweaks: initialTweaksState, + wallet: initialWalletState, + walletConnect: initialWalletConnectState, + _persist: { + version: persistConfig.version, + rehydrated: true, + }, + } + + const migratedSchemaKeys = new Set(getAllKeysOfNestedObject(migratedSchema)) + const latestSchemaKeys = new Set(getAllKeysOfNestedObject(getSchema())) + const initialStateKeys = new Set(getAllKeysOfNestedObject(initialState)) + + for (const key of initialStateKeys) { + if (latestSchemaKeys.has(key)) { + latestSchemaKeys.delete(key) + } + if (migratedSchemaKeys.has(key)) { + migratedSchemaKeys.delete(key) + } + initialStateKeys.delete(key) + } + + expect(migratedSchemaKeys.size).toBe(0) + expect(latestSchemaKeys.size).toBe(0) + expect(initialStateKeys.size).toBe(0) + }) + + // This is a precaution to ensure we do not attempt to access undefined properties during migrations + // If this test fails, make sure all property references to state are using optional chaining + it('uses optional chaining when accessing old state variables', async () => { + const emptyStub = { _persist: { version: -1, rehydrated: false } } + + const migrate = createMigrate(migrations) + const migratedSchema = await migrate(emptyStub, persistConfig.version) + expect(typeof migratedSchema).toBe('object') + }) + + it('migrates from initialSchema to v0Schema', () => { + const txDetails0: TransactionDetails = { + chainId: ChainId.Mainnet, + id: '0', + from: '0xShadowySuperCoder', + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + + const txDetails1: TransactionDetails = { + chainId: ChainId.Goerli, + id: '1', + from: '0xKingHodler', + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Success, + addedTime: 1487076708000, + hash: '0x123', + } + + const initialSchemaStub = { + ...initialSchema, + transactions: { + byChainId: { + [ChainId.Mainnet]: { + '0': txDetails0, + }, + [ChainId.Goerli]: { + '1': txDetails1, + }, + }, + lastTxHistoryUpdate: { + '0xShadowySuperCoder': 12345678912345, + '0xKingHodler': 9876543210987, + }, + }, + } + + const newSchema = migrations[0](initialSchemaStub) + expect(newSchema.transactions[ChainId.Mainnet]).toBeUndefined() + expect(newSchema.transactions.lastTxHistoryUpdate).toBeUndefined() + + expect(newSchema.transactions['0xShadowySuperCoder'][ChainId.Mainnet]['0'].status).toEqual( + TransactionStatus.Pending + ) + expect(newSchema.transactions['0xKingHodler'][ChainId.Mainnet]).toBeUndefined() + expect(newSchema.transactions['0xKingHodler'][ChainId.Goerli]['0']).toBeUndefined() + expect(newSchema.transactions['0xKingHodler'][ChainId.Goerli]['1'].from).toEqual('0xKingHodler') + + expect(newSchema.notifications.lastTxNotificationUpdate).toBeDefined() + expect( + newSchema.notifications.lastTxNotificationUpdate['0xShadowySuperCoder'][ChainId.Mainnet] + ).toEqual(12345678912345) + }) + + it('migrates from v0 to v1', () => { + const initialSchemaStub = { + ...initialSchema, + walletConnect: { + ...initialSchema.wallet, + modalState: ScannerModalState.ScanQr, + }, + } + + const v0 = migrations[0](initialSchemaStub) + const v1 = migrations[1](v0) + expect(v1.walletConnect.modalState).toEqual(undefined) + }) + + it('migrates from v1 to v2', () => { + const TEST_ADDRESSES = ['0xTest'] + + const v1SchemaStub = { + ...v1Schema, + favorites: { + ...v1Schema.favorites, + followedAddresses: TEST_ADDRESSES, + }, + } + + const v2 = migrations[2](v1SchemaStub) + + expect(v2.favorites.watchedAddresses).toEqual(TEST_ADDRESSES) + expect(v2.favorites.followedAddresses).toBeUndefined() + }) + + it('migrates from v2 to v3', () => { + const v3 = migrations[3](v2Schema) + expect(v3.searchHistory.results).toEqual([]) + }) + + it('migrates from v3 to v4', () => { + const TEST_ADDRESSES = ['0xTest', '0xTest2', '0xTest3', '0xTest4'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v3SchemaStub = { + ...v3Schema, + wallet: { + ...v3Schema.wallet, + accounts: [ + { + type: AccountType.Readonly, + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + }, + { + type: AccountType.Readonly, + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + }, + { + type: 'native', + address: TEST_ADDRESSES[2], + name: 'Test Account 3', + pending: false, + }, + { + type: 'native', + address: TEST_ADDRESSES[3], + name: 'Test Account 4', + pending: false, + }, + ], + }, + } + + mockdate.set(TEST_IMPORT_TIME_MS) + + const v4 = migrations[4](v3SchemaStub) + expect(v4.wallet.accounts[0].timeImportedMs).toEqual(TEST_IMPORT_TIME_MS) + expect(v4.wallet.accounts[2].derivationIndex).toBeDefined() + }) + + it('migrates from v4 to v5', () => { + const v5 = migrations[5](v4Schema) + + expect(v4Schema.balances).toBeDefined() + expect(v5.balances).toBeUndefined() + + expect(v5.modals[ModalName.Swap].isOpen).toEqual(false) + expect(v5.modals[ModalName.Send].isOpen).toEqual(false) + }) + + it('migrates from v5 to v6', () => { + const v6 = migrations[6](v5Schema) + + expect(v6.walletConnect.pendingSession).toBe(null) + + expect(typeof v6.wallet.settings).toBe('object') + + expect(v5Schema.wallet.bluetooth).toBeDefined() + expect(v6.wallet.bluetooth).toBeUndefined() + }) + + it('migrates from v6 to v7', () => { + const TEST_ADDRESSES: [string, string, string, string] = [ + '0xTest', + '0xTest2', + '0xTest3', + '0xTest4', + ] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v6SchemaStub = { + ...v6Schema, + wallet: { + ...v6Schema.wallet, + accounts: { + [TEST_ADDRESSES[0]]: { + type: 'native', + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + derivationIndex: 0, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[1]]: { + type: 'native', + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + derivationIndex: 1, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[2]]: { + type: 'native', + address: TEST_ADDRESSES[2], + name: 'Test Account 3', + pending: false, + derivationIndex: 2, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[3]]: { + type: 'native', + address: TEST_ADDRESSES[3], + name: 'Test Account 4', + pending: false, + derivationIndex: 3, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + }, + }, + } + + expect(Object.values(v6SchemaStub.wallet.accounts)).toHaveLength(4) + const v7 = migrations[7](v6SchemaStub) + + const accounts = Object.values(v7.wallet.accounts) as SignerMnemonicAccount[] + expect(accounts).toHaveLength(1) + expect(accounts[0]?.mnemonicId).toEqual(TEST_ADDRESSES[0]) + }) + + it('migrates from v7 to v8', () => { + const v8 = migrations[8](v7Schema) + expect(v8.cloudBackup.backupsFound).toEqual([]) + }) + + it('migrates from v8 to v9', () => { + const TEST_ADDRESSES: [string, string, string, string] = [ + '0xTest', + '0xTest2', + '0xTest3', + '0xTest4', + ] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const v8SchemaStub = { + ...v8Schema, + wallet: { + ...v6Schema.wallet, + accounts: { + [TEST_ADDRESSES[0]]: { + type: 'native', + address: TEST_ADDRESSES[0], + name: 'Test Account 1', + pending: false, + derivationIndex: 0, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + [TEST_ADDRESSES[1]]: { + type: 'local', + address: TEST_ADDRESSES[1], + name: 'Test Account 2', + pending: false, + timeImportedMs: TEST_IMPORT_TIME_MS, + }, + }, + }, + } + + expect(Object.values(v8SchemaStub.wallet.accounts)).toHaveLength(2) + const v9 = migrations[9](v8SchemaStub) + expect(Object.values(v9.wallet.accounts)).toHaveLength(1) + }) + + it('migrates from v9 to v10', () => { + const TEST_ADDRESSES = ['0xTest', OLD_DEMO_ACCOUNT_ADDRESS, '0xTest2', '0xTest3'] + const TEST_IMPORT_TIME_MS = 12345678912345 + + const accounts = TEST_ADDRESSES.reduce((acc, address) => { + acc[address] = { + address, + timeImportedMs: TEST_IMPORT_TIME_MS, + type: 'native', + } as unknown as Account + + return acc + }, {} as { [address: string]: Account }) + + const v9SchemaStub = { + ...v9Schema, + wallet: { + ...v9Schema.wallet, + accounts, + }, + } + + expect(Object.values(v9SchemaStub.wallet.accounts)).toHaveLength(4) + expect(Object.keys(v9SchemaStub.wallet.accounts)).toContain(OLD_DEMO_ACCOUNT_ADDRESS) + + const migratedSchema = migrations[10](v9SchemaStub) + expect(Object.values(migratedSchema.wallet.accounts)).toHaveLength(3) + expect(Object.keys(migratedSchema.wallet.accounts)).not.toContain(OLD_DEMO_ACCOUNT_ADDRESS) + }) + + it('migrates from v10 to v11', () => { + const v11 = migrations[11](v10Schema) + + expect(v11.biometricSettings).toBeDefined() + expect(v11.biometricSettings.requiredForAppAccess).toBeDefined() + expect(v11.biometricSettings.requiredForTransactions).toBeDefined() + }) + + it('migrates from v11 to v12', () => { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v11Stub = { + ...v11Schema, + wallet: { + ...v11Schema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + }, + }, + }, + } + + const v12 = migrations[12](v11Stub) + + expect(v12.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) + expect(v12.wallet.accounts[TEST_ADDRESS].type).toEqual('native') + expect(v12.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) + expect(v12.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) + }) + + it('migrates from v12 to v13', () => { + const v13 = migrations[13](v12Schema) + expect(v13.ens.ensForAddress).toEqual({}) + }) + + it('migrates from v13 to v14', () => { + const v13Stub = { + ...v13Schema, + wallet: { + ...v13Schema.wallet, + isBiometricAuthEnabled: true, + }, + biometricSettings: { + requiredForAppAccess: false, + requiredForTransactions: false, + }, + } + + const v14 = migrations[14](v13Stub) + expect(v14.biometricSettings.requiredForAppAccess).toEqual(true) + expect(v14.biometricSettings.requiredForTransactions).toEqual(true) + }) + + it('migrates from v14 to v15', () => { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v14Stub = { + ...v14Schema, + wallet: { + ...v14Schema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + }, + }, + }, + } + + const v15 = migrations[15](v14Stub) + const accounts = Object.values(v15.wallet.accounts) + expect((accounts[0] as Account)?.type).toEqual(AccountType.SignerMnemonic) + }) + + it('migrates from v15 to v16', () => { + const v15Stub = { + ...v15Schema, + dataApi: {}, + } + + const v16 = migrations[16](v15Stub) + + expect(v16.dataApi).toBeUndefined() + }) + + it('migrates from v16 to v17', () => { + const TEST_ADDRESS = '0xTestAddress' + const ACCOUNT_NAME = 'Test Account' + const v16Stub = { + ...v16Schema, + wallet: { + ...v16Schema.wallet, + accounts: { + [TEST_ADDRESS]: { + type: 'native', + address: TEST_ADDRESS, + name: ACCOUNT_NAME, + pending: false, + derivationIndex: 0, + timeImportedMs: 123, + pushNotificationsEnabled: true, + }, + }, + }, + } + + const v17 = migrations[17](v16Stub) + + expect(v17.wallet.accounts[TEST_ADDRESS].pushNotificationsEnabled).toEqual(false) + expect(v17.wallet.accounts[TEST_ADDRESS].type).toEqual('native') + expect(v17.wallet.accounts[TEST_ADDRESS].address).toEqual(TEST_ADDRESS) + expect(v17.wallet.accounts[TEST_ADDRESS].name).toEqual(ACCOUNT_NAME) + }) + + it('migrates from v17 to v18', () => { + const v17Stub = { + ...v17Schema, + ens: {}, + } + const v18 = migrations[18](v17Stub) + expect(v18.ens).toBeUndefined() + }) + + it('migrates from v18 to v19', () => { + const TEST_ADDRESS = '0xShadowySuperCoder' + const txDetails0: TransactionDetails = { + chainId: ChainId.Mainnet, + id: '0', + from: TEST_ADDRESS, + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + + const TEST_ADDRESS_2 = '0xKingHodler' + const txDetails1: TransactionDetails = { + chainId: ChainId.Goerli, + id: '1', + from: TEST_ADDRESS_2, + options: { + request: { + from: '0x123', + to: '0x456', + value: '0x0', + data: '0x789', + nonce: 10, + gasPrice: BigNumber.from('10000'), + }, + }, + typeInfo: { + type: TransactionType.Approve, + tokenAddress: '0xtokenAddress', + spender: '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45', + }, + status: TransactionStatus.Success, + addedTime: 1487076708000, + hash: '0x123', + } + + const ROPSTEN = 3 as ChainId + const RINKEBY = 4 as ChainId + const KOVAN = 42 as ChainId + + const transactions: TransactionStateMap = { + [TEST_ADDRESS]: { + [ChainId.Mainnet]: { + '0': txDetails0, + }, + [ChainId.Goerli]: { + '0': txDetails0, + '1': txDetails1, + }, + [ROPSTEN]: { + '0': txDetails0, + '1': txDetails1, + }, + [RINKEBY]: { + '0': txDetails1, + }, + [KOVAN]: { + '1': txDetails1, + }, + }, + [TEST_ADDRESS_2]: { + [ChainId.ArbitrumOne]: { + '0': txDetails0, + }, + [ChainId.Optimism]: { + '0': txDetails0, + '1': txDetails1, + }, + [ROPSTEN]: { + '0': txDetails0, + '1': txDetails1, + }, + [RINKEBY]: { + '0': txDetails1, + }, + [KOVAN]: { + '1': txDetails1, + }, + }, + } + + const blocks = { + byChainId: { + [ChainId.Mainnet]: { latestBlockNumber: 123456789 }, + [ChainId.Goerli]: { latestBlockNumber: 123456789 }, + [ROPSTEN]: { latestBlockNumber: 123456789 }, + [RINKEBY]: { latestBlockNumber: 123456789 }, + [KOVAN]: { latestBlockNumber: 123456789 }, + [ChainId.Optimism]: { latestBlockNumber: 123456789 }, + }, + } + + const chains = { + byChainId: { + [ChainId.ArbitrumOne]: { isActive: true }, + [ChainId.Goerli]: { isActive: true }, + [ROPSTEN]: { isActive: true }, + [RINKEBY]: { isActive: true }, + [KOVAN]: { isActive: true }, + [ChainId.Optimism]: { isActive: true }, + }, + } + + const v18Stub = { + ...v18Schema, + transactions, + blocks, + chains, + } + + const v19 = migrations[19](v18Stub) + + expect(v19.transactions[TEST_ADDRESS][ChainId.Mainnet]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS][ChainId.Goerli]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS][ROPSTEN]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS][RINKEBY]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS][KOVAN]).toBeUndefined() + + expect(v19.transactions[TEST_ADDRESS_2][ChainId.ArbitrumOne]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS_2][ChainId.Optimism]).toBeDefined() + expect(v19.transactions[TEST_ADDRESS_2][ROPSTEN]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS_2][RINKEBY]).toBeUndefined() + expect(v19.transactions[TEST_ADDRESS_2][KOVAN]).toBeUndefined() + + expect(v19.blocks.byChainId[ChainId.Mainnet]).toBeDefined() + expect(v19.blocks.byChainId[ChainId.Goerli]).toBeDefined() + expect(v19.blocks.byChainId[ChainId.Optimism]).toBeDefined() + expect(v19.blocks.byChainId[ROPSTEN]).toBeUndefined() + expect(v19.blocks.byChainId[RINKEBY]).toBeUndefined() + expect(v19.blocks.byChainId[KOVAN]).toBeUndefined() + + expect(v19.chains.byChainId[ChainId.ArbitrumOne]).toBeDefined() + expect(v19.chains.byChainId[ChainId.Goerli]).toBeDefined() + expect(v19.chains.byChainId[ChainId.Optimism]).toBeDefined() + expect(v19.chains.byChainId[ROPSTEN]).toBeUndefined() + expect(v19.chains.byChainId[RINKEBY]).toBeUndefined() + expect(v19.chains.byChainId[KOVAN]).toBeUndefined() + }) + + it('migrates from v19 to v20', () => { + const v19Stub = { + ...v19Schema, + notifications: { + ...v19Schema.notifications, + lastTxNotificationUpdate: { [1]: 122342134 }, + }, + } + + const v20 = migrations[20](v19Stub) + expect(v20.notifications.lastTxNotificationUpdate).toEqual({}) + }) + + it('migrates from v20 to v21', () => { + const v20Stub = { + ...v20Schema, + } + + const v21 = migrations[21](v20Stub) + expect(v21.experiments).toBeDefined() + }) + + it('migrates from v21 to v22', () => { + const v21Stub = { + ...v21Schema, + coingeckoApi: {}, + } + const v22 = migrations[22](v21Stub) + expect(v22.coingeckoApi).toBeUndefined() + expect(v22.tokens.watchedTokens).toBeUndefined() + expect(v22.tokens.tokenPairs).toBeUndefined() + }) + + it('migrates from v22 to v23', () => { + const v22Stub = { + ...v22Schema, + } + const v23 = migrations[23](v22Stub) + expect(v23.wallet.settings.tokensOrderBy).toBeUndefined() + expect(v23.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + }) + + it('migrates from v23 to v24', () => { + const dummyAddress1 = '0xDumDum1' + const dummyAddress2 = '0xDumDum2' + const dummyAddress3 = '0xDumDum3' + const v23Stub = { + ...v23Schema, + notifications: { + ...v23Schema.notifications, + notificationCount: { [dummyAddress1]: 5, [dummyAddress2]: 0, [dummyAddress3]: undefined }, + }, + } + const v24 = migrations[24](v23Stub) + expect(v24.notifications.notificationCount).toBeUndefined() + expect(v24.notifications.notificationStatus[dummyAddress1]).toBe(true) + expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) + expect(v24.notifications.notificationStatus[dummyAddress2]).toBe(false) + }) + + it('migrates from v24 to v25', () => { + const v24Stub = { + ...v24Schema, + } + const v25 = migrations[25](v24Stub) + expect(v25.passwordLockout.passwordAttempts).toBe(0) + }) + + it('migrates from v25 to v26', () => { + const v25Stub = { + ...v25Schema, + } + const v26 = migrations[26](v25Stub) + expect(v26.wallet.settings.showSmallBalances).toBeUndefined() + }) + + it('migrates from v26 to v27', () => { + const v26Stub = { + ...v26Schema, + } + const v27 = migrations[27](v26Stub) + expect(v27.wallet.settings.tokensOrderBy).toBeUndefined() + }) + + it('migrates from v27 to v28', () => { + const v27Stub = { + ...v27Schema, + } + const v28 = migrations[28](v27Stub) + expect(v28.wallet.settings.tokensMetadataDisplayType).toBeUndefined() + }) + + it('migrates from v28 to v29', () => { + const v28Stub = { + ...v28Schema, + } + const v29 = migrations[29](v28Stub) + expect(v29.tokenLists).toBeUndefined() + expect(v29.tokens.customTokens).toBeUndefined() + }) + + it('migrates from v29 to v30', () => { + const oldFiatOnRampTxDetails = { + chainId: ChainId.Mainnet, + id: '0', + from: account.address, + options: { + request: {}, + }, + // expect this payload to change + typeInfo: { + type: TransactionType.FiatPurchase, + explorerUrl: 'explorer', + outputTokenAddress: '0xtokenAddress', + outputCurrencyAmountFormatted: 50, + outputCurrencyAmountPrice: 2, + syncedWithBackend: true, + }, + status: TransactionStatus.Pending, + addedTime: 1487076708000, + hash: '0x123', + } + const expectedTypeInfo = { + type: TransactionType.FiatPurchase, + explorerUrl: 'explorer', + inputCurrency: undefined, + inputCurrencyAmount: 25, + outputCurrency: { + type: 'crypto', + metadata: { + chainId: undefined, + contractAddress: '0xtokenAddress', + }, + }, + outputCurrencyAmount: undefined, + syncedWithBackend: true, + } + const transactions = { + [account.address]: { + [ChainId.Mainnet]: { + '0': oldFiatOnRampTxDetails, + '1': txDetailsConfirmed, + }, + [ChainId.Goerli]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + '1': txDetailsConfirmed, + }, + [ChainId.ArbitrumOne]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + }, + }, + ['0xshadowySuperCoder']: { + [ChainId.ArbitrumOne]: { + '0': oldFiatOnRampTxDetails, + '1': txDetailsConfirmed, + }, + [ChainId.Optimism]: { + '0': oldFiatOnRampTxDetails, + '1': oldFiatOnRampTxDetails, + '2': txDetailsConfirmed, + }, + }, + ['0xdeleteMe']: { + [ChainId.Mainnet]: { + '0': { ...oldFiatOnRampTxDetails, status: TransactionStatus.Failed }, + }, + }, + } + const v29Stub = { ...v29Schema, transactions } + + const v30 = migrations[30](v29Stub) + + // expect fiat onramp txdetails to change + expect(v30.transactions[account.address][ChainId.Mainnet]['0'].typeInfo).toEqual( + expectedTypeInfo + ) + expect(v30.transactions[account.address][ChainId.Goerli]['0']).toBeUndefined() + expect(v30.transactions[account.address][ChainId.ArbitrumOne]).toBeUndefined() // does not create an object for chain + expect(v30.transactions['0xshadowySuperCoder'][ChainId.ArbitrumOne]['0'].typeInfo).toEqual( + expectedTypeInfo + ) + expect(v30.transactions['0xshadowySuperCoder'][ChainId.Optimism]['0'].typeInfo).toEqual( + expectedTypeInfo + ) + expect(v30.transactions['0xshadowySuperCoder'][ChainId.Optimism]['1'].typeInfo).toEqual( + expectedTypeInfo + ) + expect(v30.transactions['0xdeleteMe']).toBe(undefined) + // expect non-for txDetails to not change + expect(v30.transactions[account.address][ChainId.Mainnet]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions[account.address][ChainId.Goerli]['1']).toEqual(txDetailsConfirmed) + expect(v30.transactions['0xshadowySuperCoder'][ChainId.ArbitrumOne]['1']).toEqual( + txDetailsConfirmed + ) + expect(v30.transactions['0xshadowySuperCoder'][ChainId.Optimism]['2']).toEqual( + txDetailsConfirmed + ) + }) + + it('migrates from v31 to 32', () => { + const v31Stub = { ...v31Schema, ENS: 'defined' } + + const v32 = migrations[32](v31Stub) + + expect(v32.ENS).toBe(undefined) + }) + + it('migrates from v32 to 33', () => { + const v32Stub = { ...v32Schema } + + const v33 = migrations[33](v32Stub) + + expect(v33.wallet.replaceAccountOptions.isReplacingAccount).toBe(false) + expect(v33.wallet.replaceAccountOptions.skipToSeedPhrase).toBe(false) + }) + + it('migrates from v33 to 34', () => { + const v33Stub = { ...v33Schema } + + const v34 = migrations[34](v33Stub) + + expect(v34.telemetry.lastBalancesReport).toBe(0) + }) + + it('migrates from v34 to 35', () => { + const v34Stub = { ...v34Schema } + + const v35 = migrations[35](v34Stub) + + expect(v35.appearanceSettings.selectedAppearanceSettings).toBe('system') + }) + + it('migrates from v35 to 36', () => { + const v35Stub = { ...v35Schema } + + const v36 = migrations[36](v35Stub) + + expect(v36.favorites.hiddenNfts).toEqual({}) + }) + + it('migrates from v36 to 37', () => { + const id1 = '123' + const id2 = '456' + const id3 = '789' + const transactions = { + [account.address]: { + [ChainId.Mainnet]: { + [id1]: { + ...fiatOnRampTxDetailsFailed, + typeInfo: { + ...fiatOnRampTxDetailsFailed.typeInfo, + id: undefined, + }, + }, + [id2]: { + ...fiatOnRampTxDetailsFailed, + typeInfo: { + ...fiatOnRampTxDetailsFailed.typeInfo, + id: undefined, + explorerUrl: undefined, + }, + }, + [id3]: txDetailsConfirmed, + }, + }, + } + + const v36Stub = { ...v36Schema, transactions } + + expect( + v36Stub.transactions[account.address]?.[ChainId.Mainnet][id1].typeInfo.id + ).toBeUndefined() + expect( + v36Stub.transactions[account.address]?.[ChainId.Mainnet][id2].typeInfo.id + ).toBeUndefined() + + const v37 = migrations[37](v36Stub) + + expect(v37.transactions[account.address]?.[ChainId.Mainnet][id1].typeInfo.id).toEqual( + fiatOnRampTxDetailsFailed.typeInfo.id + ) + expect( + v36Stub.transactions[account.address]?.[ChainId.Mainnet][id2].typeInfo.id + ).toBeUndefined() + expect(v36Stub.transactions[account.address]?.[ChainId.Mainnet][id3]).toEqual( + txDetailsConfirmed + ) + }) + + it('migrates from v37 to 38', () => { + const v37Stub = { ...v37Schema } + const v38 = migrations[38](v37Stub) + expect(v38.wallet.replaceAccountOptions).toBeUndefined() + }) + + it('migrates from v38 to 39', () => { + const v38Stub = { ...v38Schema } + expect(v38Stub.experiments).toBeDefined() + const v39 = migrations[39](v38Stub) + expect(v39.experiments).toBeUndefined() + }) + + it('migrates from v39 to 40', () => { + const v39Stub = { ...v39Schema } + + const v40 = migrations[40](v39Stub) + + // walletConnect slice still exists but should not be persisted + expect(v40.walletConnect).toBeUndefined() + }) + + it('migrates from v40 to 41', () => { + const v40Stub = { ...v40Schema } + + const v41 = migrations[41](v40Stub) + + expect(v41.telemetry.lastBalancesReportValue).toBe(0) + }) + + it('migrates from v41 to 42', () => { + const v41Stub = { ...v41Schema } + + const v42 = migrations[42](v41Stub) + + expect(v42.wallet.flashbotsenabled).toBeUndefined() + }) + + it('migrates from v42 to 43', () => { + const v42Stub = { ...v42Schema } + + v42Stub.favorites.hiddenNfts = { + '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { + 'nftItem.0xb96e881BD4Cd7BCCc8CB47d3aa0e254a72d2F074.3971': true, // checksummed 1 + 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': true, // not checksummed 1 + 'nftItem.0x25E503331e69EFCBbc50d2a4D661900B23D47662.2': true, // checksummed 2 + 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': true, // not checksummed 3 + }, + } + + const v43 = migrations[43](v42Stub) + + // expect(v43.favorites.hiddenNfts).toEqual(undefined) + // all checksummed keys should be converted to not checksummed ones and duplicates should be removed + expect(v43.favorites.nftsData).toEqual({ + '0xAFa9bAb987E3D7bcD40EB510838aEC663C8b7264': { + 'nftItem.0xb96e881bd4cd7bccc8cb47d3aa0e254a72d2f074.3971': { isHidden: true }, // not checksummed 1 + 'nftItem.0x25e503331e69efcbbc50d2a4d661900b23d47662.2': { isHidden: true }, // not checksummed 2 + 'nftItem.0xe94abea3932576ff957a0b92190d0191aeb1a782.2': { isHidden: true }, // not checksummed 3 + }, + }) + }) + + it('migrates from v43 to v44', () => { + const v43Stub = { ...v43Schema } + + v43Stub.providers = { isInitialized: true } + + const v44 = migrations[44](v43Stub) + + expect(v44.providers).toBeUndefined() + }) + + it('migrates from v44 to 45', () => { + const v44Stub = { ...v44Schema } + + const v45 = migrations[45](v44Stub) + + expect(v45.favorites.tokensVisibility).toEqual({}) + }) + + it('migrates from v45 to 46', () => { + const v45Stub = { ...v45Schema } + const v46 = migrations[46](v45Stub) + + expect(v46.ENS).toBeUndefined() + expect(v46.ens).toBeUndefined() + expect(v46.gasApi).toBeUndefined() + expect(v46.onChainBalanceApi).toBeUndefined() + expect(v46.routingApi).toBeUndefined() + expect(v46.trmApi).toBeUndefined() + }) + + it('migrates from v46 to 47', () => { + const v46Stub = { ...v46Schema } + const v47 = migrations[47](v46Stub) + + expect(v47.chains.byChainId).toStrictEqual({ + '1': { isActive: true }, + '10': { isActive: true }, + '56': { isActive: true }, + '137': { isActive: true }, + '8453': { isActive: true }, + '42161': { isActive: true }, + }) + }) + + it('migrates from v47 to 48', () => { + const v47Stub = { ...v47Schema } + const v48 = migrations[48](v47Stub) + + expect(v48.tweaks).toEqual({}) + }) + + it('migrates from v48 to 49', () => { + const v48Stub = { ...v48Schema } + const v49 = migrations[49](v48Stub) + + expect(v49.wallet.settings.swapProtection).toEqual(SwapProtectionSetting.On) + }) + + it('migrates from v49 to 50', () => { + const v449Stub = { ...v49Schema } + const v50 = migrations[50](v449Stub) + + expect(v50.chains).toBeUndefined() + }) + + it('migrates from v50 to 51', () => { + const v50Stub = { ...v50Schema } + const v51 = migrations[51](v50Stub) + + expect(v51.languageSettings).not.toBeUndefined() + }) + + it('migrates from v51 to 52', () => { + const v51Stub = { ...v51Schema } + const v52 = migrations[52](v51Stub) + + expect(v52.fiatCurrencySettings).not.toBeUndefined() + }) + + it('migrates from v52 to 53', () => { + const v52Stub = { ...v52Schema } + const v53 = migrations[53](v52Stub) + + expect(v53.languageSettings).not.toBeUndefined() + }) + + it('migrates from v53 to 54', () => { + const v53Stub = { ...v53Schema } + const v54 = migrations[54](v53Stub) + + expect(v54.telemetry.walletIsFunded).toBe(false) + }) + + it('migrates from v54 to 55', () => { + const v54Stub = { ...v54Schema } + const v55 = migrations[55](v54Stub) + + expect(v55.behaviorHistory.hasViewedReviewScreen).toBe(false) + }) + + it('migrates from v55 to 56', () => { + const v55Stub = { ...v55Schema } + const v56 = migrations[56](v55Stub) + + expect(v56.telemetry.allowAnalytics).toBe(true) + expect(v56.telemetry.lastHeartbeat).toBe(0) + }) + + it('migrates from v56 to 57', () => { + const v56Stub = { + ...v56Schema, + wallet: { + ...v56Schema.wallet, + accounts: [ + { + type: AccountType.Readonly, + address: '0x', + name: 'Test Account 1', + pending: false, + hideSpamTokens: true, + }, + ], + }, + } + const v57 = migrations[57](v56Stub) + expect(v57.wallet.settings.hideSmallBalances).toBe(true) + expect(v57.wallet.settings.hideSpamTokens).toBe(true) + expect(v57.wallet.accounts[0].showSpamTokens).toBeUndefined() + expect(v57.wallet.accounts[0].showSmallBalances).toBeUndefined() + }) + + it('migrates from v57 to 58', () => { + const v57Stub = { ...v57Schema } + const v58 = migrations[58](v57Stub) + + expect(v58.behaviorHistory.hasSkippedUnitagPrompt).toBe(false) + }) + + it('migrates from v58 to 59', () => { + const v58Stub = { ...v58Schema } + const v59 = migrations[59](v58Stub) + + expect(v59.behaviorHistory.hasCompletedUnitagsIntroModal).toBe(false) + }) + + it('migrates from v59 to 60', () => { + const v59Stub = { ...v59Schema } + const v60 = migrations[60](v59Stub) + + expect(v60.behaviorHistory.hasViewedUniconV2IntroModal).toBe(false) + }) +}) diff --git a/apps/mobile/src/app/migrations.ts b/apps/mobile/src/app/migrations.ts new file mode 100644 index 0000000..e66ab2e --- /dev/null +++ b/apps/mobile/src/app/migrations.ts @@ -0,0 +1,818 @@ +// Type information currently gets lost after a migration +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable max-lines */ + +import dayjs from 'dayjs' +import { ChainId } from 'wallet/src/constants/chains' +import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { AccountToNftData } from 'wallet/src/features/favorites/slice' +import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' +import { initialLanguageState } from 'wallet/src/features/language/slice' +import { getNFTAssetKey } from 'wallet/src/features/nfts/utils' +import { TransactionStateMap } from 'wallet/src/features/transactions/slice' +import { + ChainIdToTxIdToDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { ModalName } from 'wallet/src/telemetry/constants' + +export const OLD_DEMO_ACCOUNT_ADDRESS = '0xdd0E380579dF30E38524F9477808d9eE37E2dEa6' + +export const migrations = { + 0: (state: any) => { + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const chainIds = Object.keys(oldTransactionState?.byChainId ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState.byChainId?.[chainId] ?? [] + const txIds = Object.keys(transactions) + for (const txId of txIds) { + const txDetails = transactions[txId] + const address = txDetails.from + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = { ...txDetails } + } + } + + const oldNotificationState = state.notifications + const newNotificationState = { ...oldNotificationState, lastTxNotificationUpdate: {} } + const addresses = Object.keys(oldTransactionState?.lastTxHistoryUpdate || []) + for (const address of addresses) { + newNotificationState.lastTxNotificationUpdate[address] = { + [ChainId.Mainnet]: oldTransactionState.lastTxHistoryUpdate[address], + } + } + + return { ...state, transactions: newTransactionState, notifications: newNotificationState } + }, + + 1: (state: any) => { + const newState = { ...state } + delete newState.walletConnect?.modalState + return newState + }, + + 2: (state: any) => { + const newState = { ...state } + const oldFollowingAddresses = state?.favorites?.followedAddresses + if (oldFollowingAddresses) { + newState.favorites.watchedAddresses = oldFollowingAddresses + } + delete newState?.favorites?.followedAddresses + return newState + }, + + 3: (state: any) => { + const newState = { ...state } + newState.searchHistory = { results: [] } + return newState + }, + + 4: (state: any) => { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + let derivationIndex = 0 + for (const account of Object.keys(accounts)) { + newState.wallet.accounts[account].timeImportedMs = dayjs().valueOf() + if (newState.wallet.accounts[account].type === 'native') { + newState.wallet.accounts[account].derivationIndex = derivationIndex + derivationIndex += 1 + } + } + return newState + }, + + 5: (state: any) => { + const newState = { ...state } + newState.modals = { + [ModalName.WalletConnectScan]: { + isOpen: false, + initialState: 0, + }, + [ModalName.Swap]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Send]: { + isOpen: false, + initialState: undefined, + }, + } + + delete newState?.balances + return newState + }, + + 6: (state: any) => { + const newState = { ...state } + newState.walletConnect = { ...newState.walletConnect, pendingSession: null } + newState.wallet = { ...newState.wallet, settings: {} } + + delete newState?.wallet?.bluetooth + return newState + }, + + 7: (state: any) => { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + const originalAccountValues = Object.keys(accounts) + for (const account of originalAccountValues) { + if (accounts[account].type === 'native' && accounts[account].derivationIndex !== 0) { + delete accounts[account] + } else if (accounts[account].type === 'native' && accounts[account].derivationIndex === 0) { + accounts[account].mnemonicId = accounts[account].address + } + } + return newState + }, + + 8: (state: any) => { + const newState = { ...state } + newState.cloudBackup = { backupsFound: [] } + return newState + }, + + 9: (state: any) => { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + for (const account of Object.keys(accounts)) { + if (newState.wallet.accounts[account].type === 'local') { + delete newState.wallet.accounts[account] + } + } + return newState + }, + + 10: (state: any) => { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + + if (accounts[OLD_DEMO_ACCOUNT_ADDRESS]) { + delete accounts[OLD_DEMO_ACCOUNT_ADDRESS] + } + + return newState + }, + + 11: (state: any) => { + const newState = { ...state } + newState.biometricSettings = { + requiredForAppAccess: false, + requiredForTransactions: false, + } + + return newState + }, + + 12: (state: any) => { + const accounts: Record | undefined = state?.wallet?.accounts + const newAccounts = Object.values(accounts ?? {}).map((account: Account) => { + const newAccount = { ...account } + newAccount.pushNotificationsEnabled = false + return newAccount + }) + + const newAccountObj = newAccounts.reduce>((accountObj, account) => { + accountObj[account.address] = account + return accountObj + }, {}) + + const newState = { ...state } + newState.wallet = { ...state.wallet, accounts: newAccountObj } + return newState + }, + + 13: (state: any) => { + const newState = { ...state } + newState.ens = { ensForAddress: {} } + return newState + }, + + 14: (state: any) => { + const newState = { ...state } + newState.biometricSettings = { + requiredForAppAccess: state.wallet.isBiometricAuthEnabled, + requiredForTransactions: state.wallet.isBiometricAuthEnabled, + } + delete newState.wallet?.isBiometricAuthEnabled + return newState + }, + + 15: (state: any) => { + const newState = { ...state } + const accounts = newState?.wallet?.accounts ?? {} + for (const account of Object.keys(accounts)) { + if (newState.wallet.accounts[account].type === 'native') { + newState.wallet.accounts[account].type = AccountType.SignerMnemonic + } + } + return newState + }, + + 16: (state: any) => { + const newState = { ...state } + delete newState.dataApi + return newState + }, + + 17: (state: any) => { + const accounts: Record | undefined = state?.wallet?.accounts + if (!accounts) { + return + } + + for (const account of Object.values(accounts)) { + account.pushNotificationsEnabled = false + } + + const newState = { ...state } + newState.wallet = { ...state.wallet, accounts } + return newState + }, + + 18: (state: any) => { + const newState = { ...state } + delete newState.ens + return newState + }, + + 19: (state: any) => { + const newState = { ...state } + + const chainState: + | { + byChainId: Partial> + } + | undefined = newState?.chains + const newChainState = Object.keys(chainState?.byChainId ?? {}).reduce<{ + byChainId: Partial> + }>( + (tempState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempState + } + + const chainInfo = chainState?.byChainId[chainId] + if (!chainInfo) { + return tempState + } + + tempState.byChainId[chainId] = chainInfo + return tempState + }, + { byChainId: {} } + ) + + const blockState: any | undefined = newState?.blocks + const newBlockState = Object.keys(blockState?.byChainId ?? {}).reduce( + (tempState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempState + } + + const blockInfo = blockState?.byChainId[chainId] + if (!blockInfo) { + return tempState + } + + tempState.byChainId[chainId] = blockInfo + return tempState + }, + { byChainId: {} } + ) + + const transactionState: TransactionStateMap | undefined = newState?.transactions + const newTransactionState = Object.keys(transactionState ?? {}).reduce( + (tempState, address) => { + const txs = transactionState?.[address] + if (!txs) { + return tempState + } + + const newAddressTxState = Object.keys(txs).reduce( + (tempAddressState, chainIdString) => { + const chainId = toSupportedChainId(chainIdString) + if (!chainId) { + return tempAddressState + } + + const txInfo = txs[chainId] + if (!txInfo) { + return tempAddressState + } + + tempAddressState[chainId] = txInfo + return tempAddressState + }, + {} + ) + + tempState[address] = newAddressTxState + return tempState + }, + {} + ) + + return { + ...newState, + chains: newChainState, + blocks: newBlockState, + transactions: newTransactionState, + } + }, + + 20: (state: any) => { + const newState = { ...state } + newState.notifications = { ...state?.notifications, lastTxNotificationUpdate: {} } + return newState + }, + + 21: (state: any) => { + const newState = { ...state } + // newState.experiments = { experiments: {}, featureFlags: {} } + return { + ...newState, + experiments: { experiments: {}, featureFlags: {} }, + } + }, + + 22: (state: any) => { + const newState = { ...state } + delete newState.coingeckoApi + delete newState.tokens?.watchedTokens + delete newState.tokens?.tokenPairs + return newState + }, + + 23: (state: any) => { + const newState = { ...state } + // Reset values because of changed types for these two optional variables + delete newState.wallet.settings?.tokensOrderBy + delete newState.wallet.settings?.tokensMetadataDisplayType + return newState + }, + + 24: (state: any) => { + const newState = { ...state } + const notificationCount = state.notifications?.notificationCount + const notificationStatus = Object.keys(notificationCount ?? {}).reduce((obj, address) => { + const count = notificationCount[address] + if (count) { + return { ...obj, [address]: true } + } + + return { ...obj, [address]: false } + }, {}) + + delete newState.notifications?.notificationCount + newState.notifications = { ...newState.notifications, notificationStatus } + return newState + }, + + 25: (state: any) => { + return { + ...state, + passwordLockout: { passwordAttempts: 0 }, + } + }, + + 26: (state: any) => { + const newState = { ...state } + delete newState.wallet.settings.showSmallBalances + return newState + }, + + 27: (state: any) => { + const newState = { ...state } + // Reset tokensOrder by because of updated types of TokensOrderBy + delete newState.wallet.settings.tokensOrderBy + return newState + }, + + 28: (state: any) => { + const newState = { ...state } + // Removed storing tokensMetadataDisplayType + delete newState.wallet.settings.tokensMetadataDisplayType + return newState + }, + + 29: (state: any) => { + const newState = { ...state } + delete newState.tokenLists + delete newState.tokens?.customTokens + return newState + }, + + // Fiat onramp tx typeInfo schema changed + // Updates every fiat onramp tx in store to new schema + // leaves non-for txs untouched + 30: function MigrateFiatPurchaseTransactionInfo(state: any) { + const newState = { ...state } + + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const addresses = Object.keys(oldTransactionState ?? {}) + for (const address of addresses) { + const chainIds = Object.keys(oldTransactionState[address] ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState[address][chainId] + const txIds = Object.keys(transactions ?? {}) + + for (const txId of txIds) { + const txDetails = transactions[txId] + + if (!txDetails) { + // we iterative over very chain, need to no-op on some combinations + continue + } + + if (txDetails.typeInfo.type !== TransactionType.FiatPurchase) { + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = txDetails + + continue + } + + if (txDetails.status === TransactionStatus.Failed) { + // delete failed moonpay transactions as we do not have enough information to migrate + continue + } + + const { + explorerUrl, + outputTokenAddress, + outputCurrencyAmountFormatted, + outputCurrencyAmountPrice, + syncedWithBackend, + } = txDetails.typeInfo + + const newTypeInfo = { + type: TransactionType.FiatPurchase, + explorerUrl, + inputCurrency: undefined, + inputCurrencyAmount: outputCurrencyAmountFormatted / outputCurrencyAmountPrice, + outputCurrency: { + type: 'crypto', + metadata: { chainId: undefined, contractAddress: outputTokenAddress }, + }, + outputCurrencyAmount: undefined, + syncedWithBackend, + } + + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = { ...txDetails, typeInfo: newTypeInfo } + } + } + } + + return { ...newState, transactions: newTransactionState } + }, + + 31: function emptyMigration(state: any) { + // no persisted state removed but need to update schema + return state + }, + + 32: function resetEnsApi(state: any) { + const newState = { ...state } + + delete newState.ENS + + return newState + }, + + 33: function addReplaceAccount(state: any) { + const newState = { ...state } + + newState.wallet.replaceAccountOptions = { + isReplacingAccount: false, + skipToSeedPhrase: false, + } + return newState + }, + + 34: function addLastBalancesReport(state: any) { + const newState = { ...state } + + newState.telemetry = { + lastBalancesReport: 0, + } + return newState + }, + + 35: function addAppearanceSetting(state: any) { + const newState = { ...state } + + newState.appearanceSettings = { + selectedAppearanceSettings: 'system', + } + return newState + }, + + 36: function addNfts(state: any) { + const newState = { ...state } + + newState.favorites = { + ...state.favorites, + hiddenNfts: {}, + } + return newState + }, + 37: function correctFailedFiatOnRampTxIds(state: any) { + const newState = { ...state } + + const oldTransactionState = state?.transactions + const newTransactionState: any = {} + + const addresses = Object.keys(oldTransactionState ?? {}) + for (const address of addresses) { + const chainIds = Object.keys(oldTransactionState[address] ?? {}) + for (const chainId of chainIds) { + const transactions = oldTransactionState[address][chainId] + const txIds = Object.keys(transactions ?? {}) + + for (const txId of txIds) { + const txDetails = transactions[txId] + + if (!txDetails) { + // we iterate over every chain, need to no-op on some combinations + continue + } + + newTransactionState[address] ??= {} + newTransactionState[address][chainId] ??= {} + newTransactionState[address][chainId][txId] = + txDetails.typeInfo.type === TransactionType.FiatPurchase && + txDetails.status === TransactionStatus.Failed + ? { + ...txDetails, + typeInfo: { + ...txDetails.typeInfo, + id: txDetails.typeInfo?.explorerUrl?.split('=')?.[1], + }, + } + : txDetails + } + } + } + return { ...newState, transactions: newTransactionState } + }, + 38: function removeReplaceAccountOptions(state: any) { + const newState = { ...state } + delete newState.wallet.replaceAccountOptions + return newState + }, + 39: function removeExperimentsSlice(state: any) { + const newState = { ...state } + delete newState.experiments + return newState + }, + 40: function removePersistedWalletConnectSlice(state: any) { + // Remove `walletConnect` slice from persisted whitelist + const newState = { ...state } + delete newState.walletConnect + return newState + }, + + 41: function addLastBalancesReportValue(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + lastBalancesReportValue: 0, + } + return newState + }, + + 42: function removeFlashbotsEnabledFromWalletSlice(state: any) { + const newState = { ...state } + + delete newState.wallet.flashbotsEnabled + + return newState + }, + + 43: function convertHiddenNftsToNftsData(state: any) { + // see its test to get a better idea of what this migration does + const newState = { ...state } + + const accountAddresses = Object.keys(state.favorites?.hiddenNfts ?? {}) + + const nftsData: AccountToNftData = {} + for (const accountAddress of accountAddresses) { + nftsData[accountAddress] ??= {} + const hiddenNftKeys = Object.keys(state.favorites.hiddenNfts[accountAddress]) + + for (const hiddenNftKey of hiddenNftKeys) { + const [, nftKey, tokenId] = hiddenNftKey.split('.') + + // we need to convert NFTs key to the new all not checksummed version + const newNftKey = nftKey && tokenId && getNFTAssetKey(nftKey, tokenId) + + const accountNftsData = nftsData[accountAddress] + if (newNftKey && accountNftsData) { + accountNftsData[newNftKey] = { isHidden: true } + } + } + } + + newState.favorites = { + ...state.favorites, + nftsData, + } + delete newState.favorites.hiddenNfts + return newState + }, + + 44: function removeProviders(state: any) { + const newState = { ...state } + + delete newState.providers + + return newState + }, + + 45: function addTokensData(state: any) { + const newState = { ...state } + + newState.favorites = { + ...state.favorites, + tokensVisibility: {}, + } + return newState + }, + + 46: function deleteRTKQuerySlices(state: any) { + const newState = { ...state } + + delete newState.ENS + delete newState.ens + delete newState.gasApi + delete newState.onChainBalanceApi + delete newState.routingApi + delete newState.trmApi + + return newState + }, + + 47: function resetActiveChains(state: any) { + const newState = { ...state } + + newState.chains.byChainId = { + '1': { isActive: true }, + '10': { isActive: true }, + '56': { isActive: true }, + '137': { isActive: true }, + '8453': { isActive: true }, + '42161': { isActive: true }, + } + + return newState + }, + + 48: function addTweakStartingState(state: any) { + const newState = { ...state } + + newState.tweaks = {} + + return newState + }, + + 49: function addSwapProtectionSetting(state: any) { + const newState = { ...state } + newState.wallet.settings = { + ...state.wallet.settings, + swapProtection: SwapProtectionSetting.On, + } + return newState + }, + + 50: function deleteChainsSlice(state: any) { + const newState = { ...state } + delete newState.chains + return newState + }, + + 51: function addLanguageSettings(state: any) { + return { + ...state, + languageSettings: initialLanguageState, + } + }, + + 52: function addFiatCurrencySettings(state: any) { + return { + ...state, + fiatCurrencySettings: initialFiatCurrencyState, + } + }, + + 53: function updateLanguageSettings(state: any) { + return { + ...state, + languageSettings: initialLanguageState, + } + }, + + 54: function addWalletIsFunded(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + walletIsFunded: false, + } + + return newState + }, + + 55: function addBehaviorHistory(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + hasViewedReviewScreen: false, + hasSubmittedHoldToSwap: false, + } + + return newState + }, + + 56: function addAllowAnalyticsSwitch(state: any) { + const newState = { ...state } + + newState.telemetry = { + ...state.telemetry, + allowAnalytics: true, + lastHeartbeat: 0, + } + + return newState + }, + + 57: function moveSettingStateToGlobal(state: any) { + const newState = { ...state } + + // get old accounts + const accounts = newState?.wallet?.accounts ?? {} + const firstAccountKey = Object.keys(accounts)[0] + + // Read setting from the first wallet, or assign default value + const hideSmallBalances = firstAccountKey ? !accounts[firstAccountKey].showSmallBalances : true // default to true + const hideSpamTokens = firstAccountKey ? !accounts[firstAccountKey].showSpamTokens : true // default to true + + newState.wallet.settings.hideSmallBalances = hideSmallBalances + newState.wallet.settings.hideSpamTokens = hideSpamTokens + + // delete old account specific state + const accountKeys = Object.keys(accounts ?? {}) + for (const accountKey of accountKeys) { + delete accounts[accountKey].showSmallBalances + delete accounts[accountKey].showSpamTokens + } + + return newState + }, + + 58: function addSkippedUnitagBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasSkippedUnitagPrompt: false, + } + + return newState + }, + + 59: function addCompletedUnitagsIntroBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasCompletedUnitagsIntroModal: false, + } + + return newState + }, + + 60: function addUniconV2IntroModalBoolean(state: any) { + const newState = { ...state } + + newState.behaviorHistory = { + ...state.behaviorHistory, + hasViewedUniconV2IntroModal: false, + } + + return newState + }, +} diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx new file mode 100644 index 0000000..9a009e1 --- /dev/null +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.test.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { AccountSwitcher } from 'src/app/modals/AccountSwitcherModal' +import { preloadedMobileState, preloadedModalsState } from 'src/test/fixtures' +import { cleanup, render } from 'src/test/test-utils' +import { ModalName } from 'wallet/src/telemetry/constants' +import { ACCOUNT } from 'wallet/src/test/fixtures' +import { noOpFunction } from 'wallet/src/test/mocks' + +const preloadedState = preloadedMobileState({ + account: ACCOUNT, + modals: preloadedModalsState({ + [ModalName.AccountSwitcher]: { isOpen: true }, + }), +}) + +// TODO [MOB-259]: Figure out how to do snapshot tests when there is a BottomSheetModal +describe(AccountSwitcher, () => { + it('renders correctly', async () => { + const tree = render(, { preloadedState }) + + expect(tree.toJSON()).toMatchSnapshot() + cleanup() + }) +}) diff --git a/apps/mobile/src/app/modals/AccountSwitcherModal.tsx b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx new file mode 100644 index 0000000..cb059e3 --- /dev/null +++ b/apps/mobile/src/app/modals/AccountSwitcherModal.tsx @@ -0,0 +1,329 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { Action } from 'redux' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { AccountList } from 'src/components/accounts/AccountList' +import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { useSagaStatus } from 'src/utils/useSagaStatus' +import { + Button, + Flex, + Icons, + Text, + TouchableArea, + useDeviceDimensions, + useDeviceInsets, + useSporeColors, +} from 'ui/src' +import { spacing } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { ActionSheetModal, MenuItemProp } from 'wallet/src/components/modals/ActionSheetModal' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { + createAccountActions, + createAccountSagaName, +} from 'wallet/src/features/wallet/create/createAccountSaga' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { selectAllAccountsSorted } from 'wallet/src/features/wallet/selectors' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { openSettings } from 'wallet/src/utils/linking' + +export function AccountSwitcherModal(): JSX.Element { + const dispatch = useAppDispatch() + const colors = useSporeColors() + + return ( + dispatch(closeModal({ name: ModalName.AccountSwitcher }))}> + + { + dispatch(closeModal({ name: ModalName.AccountSwitcher })) + }} + /> + + + ) +} + +/** + * Exported for testing only. + * TODO [MOB-259] Once testing works with the BottomSheetModal stop exporting this component. + */ +export function AccountSwitcher({ onClose }: { onClose: () => void }): JSX.Element | null { + const insets = useDeviceInsets() + const dimensions = useDeviceDimensions() + const { t } = useTranslation() + const activeAccountAddress = useActiveAccountAddress() + const dispatch = useAppDispatch() + const hasImportedSeedPhrase = useNativeAccountExists() + const modalState = useAppSelector(selectModalState(ModalName.AccountSwitcher)) + const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) + const onCompleteOnboarding = useCompleteOnboardingCallback({ + entryPoint: OnboardingEntryPoint.Sidebar, + importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, + }) + + const [showAddWalletModal, setShowAddWalletModal] = useState(false) + const [createdAdditionalAccount, setCreatedAdditionalAccount] = useState(false) + + const accounts = useAppSelector(selectAllAccountsSorted) + + const onPressAccount = useCallback( + (address: Address) => { + onClose() + // allow close modal logic to finish in JS thread before `setAccountAsActive` logic kicks in + setImmediate(() => { + dispatch(setAccountAsActive(address)) + }) + }, + [dispatch, onClose] + ) + + const onPressAddWallet = (): void => { + setShowAddWalletModal(true) + } + + const onCloseAddWallet = (): void => { + setShowAddWalletModal(false) + } + + const onManageWallet = (): void => { + if (!activeAccountAddress) { + return + } + + dispatch(closeModal({ name: ModalName.AccountSwitcher })) + navigate(Screens.SettingsStack, { + screen: Screens.SettingsWallet, + params: { address: activeAccountAddress }, + }) + } + + // Pick up account creation and activate + useSagaStatus(createAccountSagaName, async () => { + if (createdAdditionalAccount) { + setCreatedAdditionalAccount(false) + await onCompleteOnboarding() + } + }) + + const addWalletOptions = useMemo(() => { + const onPressCreateNewWallet = (): void => { + // Ensure no pending accounts + dispatch(pendingAccountActions.trigger(PendingAccountActions.ActivateOneAndDelete)) + dispatch(createAccountActions.trigger()) + + if (unitagsFeatureFlagEnabled) { + if (hasImportedSeedPhrase) { + setCreatedAdditionalAccount(true) + } else { + // create pending account and place into welcome flow + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.WelcomeWallet, + params: { + importType: ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.Sidebar, + }, + }) + } + } else { + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.EditName, + params: { + entryPoint: OnboardingEntryPoint.Sidebar, + importType: hasImportedSeedPhrase ? ImportType.CreateAdditional : ImportType.CreateNew, + }, + }) + } + + setShowAddWalletModal(false) + onClose() + } + + const onPressAddViewOnlyWallet = (): void => { + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.WatchWallet, + params: { + importType: ImportType.Watch, + entryPoint: OnboardingEntryPoint.Sidebar, + }, + }) + setShowAddWalletModal(false) + onClose() + } + + const onPressImportWallet = (): void => { + if (hasImportedSeedPhrase && activeAccountAddress) { + dispatch(openModal({ name: ModalName.RemoveWallet })) + } else { + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.SeedPhraseInput, + params: { importType: ImportType.SeedPhrase, entryPoint: OnboardingEntryPoint.Sidebar }, + }) + } + + setShowAddWalletModal(false) + onClose() + } + + const onPressRestore = async (): Promise => { + const cloudStorageAvailable = await isCloudStorageAvailable() + + if (!cloudStorageAvailable) { + Alert.alert( + isAndroid + ? t('account.cloud.error.unavailable.title.android') + : t('account.cloud.error.unavailable.title.ios'), + isAndroid + ? t('account.cloud.error.unavailable.message.android') + : t('account.cloud.error.unavailable.message.ios'), + [ + { + text: t('account.cloud.error.unavailable.button.settings'), + onPress: openSettings, + style: 'default', + }, + { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, + ] + ) + return + } + + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.RestoreCloudBackupLoading, + params: { importType: ImportType.Restore, entryPoint: OnboardingEntryPoint.Sidebar }, + }) + setShowAddWalletModal(false) + onClose() + } + + const options: MenuItemProp[] = [ + { + key: ElementName.CreateAccount, + onPress: onPressCreateNewWallet, + render: () => ( + + {t('account.wallet.button.create')} + + ), + }, + { + key: ElementName.AddViewOnlyWallet, + onPress: onPressAddViewOnlyWallet, + render: () => ( + + {t('account.wallet.button.addViewOnly')} + + ), + }, + { + key: ElementName.ImportAccount, + onPress: onPressImportWallet, + render: () => ( + + {t('account.wallet.button.import')} + + ), + }, + ] + + if (!hasImportedSeedPhrase) { + options.push({ + key: ElementName.RestoreFromCloud, + onPress: onPressRestore, + render: () => ( + + + {isAndroid + ? t('account.cloud.button.restore.android') + : t('account.cloud.button.restore.ios')} + + + ), + }) + } + + return options + }, [activeAccountAddress, dispatch, hasImportedSeedPhrase, onClose, t, unitagsFeatureFlagEnabled]) + + const accountsWithoutActive = accounts.filter((a) => a.address !== activeAccountAddress) + + const isViewOnly = + accounts.find((a) => a.address === activeAccountAddress)?.type === AccountType.Readonly + + if (!activeAccountAddress) { + return null + } + + const fullScreenContentHeight = + dimensions.fullHeight - insets.top - insets.bottom - spacing.spacing36 // approximate bottom sheet handle height + padding bottom + + return ( + + + + + + + + + + + + + + + {t('account.wallet.button.add')} + + + + + + ) +} diff --git a/apps/mobile/src/app/modals/AppModals.tsx b/apps/mobile/src/app/modals/AppModals.tsx new file mode 100644 index 0000000..f670de7 --- /dev/null +++ b/apps/mobile/src/app/modals/AppModals.tsx @@ -0,0 +1,98 @@ +import React from 'react' +import { AccountSwitcherModal } from 'src/app/modals/AccountSwitcherModal' +import { ExperimentsModal } from 'src/app/modals/ExperimentsModal' +import { ExploreModal } from 'src/app/modals/ExploreModal' +import { SwapModal } from 'src/app/modals/SwapModal' +import { TransferTokenModal } from 'src/app/modals/TransferTokenModal' +import { ViewOnlyExplainerModal } from 'src/app/modals/ViewOnlyExplainerModal' +import { LazyModalRenderer } from 'src/app/modals/utils' +import { RemoveWalletModal } from 'src/components/RemoveWallet/RemoveWalletModal' +import { RestoreWalletModal } from 'src/components/RestoreWalletModal/RestoreWalletModal' +import { WalletConnectModals } from 'src/components/WalletConnect/WalletConnectModals' +import { ForceUpgradeModal } from 'src/components/forceUpgrade/ForceUpgradeModal' +import { UnitagsIntroModal } from 'src/components/unitags/UnitagsIntroModal' +import { LockScreenModal } from 'src/features/authentication/LockScreenModal' +import { ExchangeTransferModal } from 'src/features/fiatOnRamp/ExchangeTransferModal' +import { FiatOnRampAggregatorModal } from 'src/features/fiatOnRamp/FiatOnRampAggregatorModal' +import { FiatOnRampModal } from 'src/features/fiatOnRamp/FiatOnRampModal' +import { ScantasticModal } from 'src/features/scantastic/ScantasticModal' +import { ReceiveCryptoModal } from 'src/screens/ReceiveCryptoModal' +import { SettingsFiatCurrencyModal } from 'src/screens/SettingsFiatCurrencyModal' +import { SettingsLanguageModal } from 'src/screens/SettingsLanguageModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function AppModals(): JSX.Element { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/app/modals/ExperimentsModal.tsx b/apps/mobile/src/app/modals/ExperimentsModal.tsx new file mode 100644 index 0000000..ca79561 --- /dev/null +++ b/apps/mobile/src/app/modals/ExperimentsModal.tsx @@ -0,0 +1,262 @@ +import { useApolloClient } from '@apollo/client' +import React, { useState } from 'react' +import { ScrollView } from 'react-native-gesture-handler' +import { Action } from 'redux' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectCustomEndpoint } from 'src/features/tweaks/selectors' +import { setCustomEndpoint } from 'src/features/tweaks/slice' +import { + ConfigResult, + Statsig, + useExperimentWithExposureLoggingDisabled, +} from 'statsig-react-native' +import { + Accordion, + Button, + Flex, + Icons, + Separator, + Text, + useDeviceInsets, + useSporeColors, +} from 'ui/src' +import { spacing } from 'ui/src/theme' +import { Switch } from 'wallet/src/components/buttons/Switch' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { + EXPERIMENT_NAMES, + EXPERIMENT_VALUES_BY_EXPERIMENT, + FEATURE_FLAGS, +} from 'wallet/src/features/experiments/constants' +import { useFeatureFlagWithExposureLoggingDisabled } from 'wallet/src/features/experiments/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function ExperimentsModal(): JSX.Element { + const insets = useDeviceInsets() + const dispatch = useAppDispatch() + const customEndpoint = useAppSelector(selectCustomEndpoint) + + const apollo = useApolloClient() + + const [url, setUrl] = useState(customEndpoint?.url || '') + const [key, setKey] = useState(customEndpoint?.key || '') + + const clearEndpoint = (): void => { + dispatch(setCustomEndpoint({})) + setUrl('') + setKey('') + } + + const setEndpoint = (): void => { + if (url && key) { + dispatch( + setCustomEndpoint({ + customEndpoint: { url, key }, + }) + ) + } else { + clearEndpoint() + } + } + + return ( + dispatch(closeModal({ name: ModalName.Experiments }))}> + + + + + + + + You will need to restart the application to pick up any changes in this section. + Beware of client side caching! + + + + URL + + + + + Key + + + + + + + + + + + + + + + + + + + + + + + + + Overridden feature flags are reset when the app is restarted + + + + {Object.values(FEATURE_FLAGS).map((featureFlag) => { + return + })} + + + + + + + + + + Overridden experiments are reset when the app is restarted + + + + {Object.values(EXPERIMENT_NAMES).map((experiment) => { + return + })} + + + + + + + ) +} + +function AccordionHeader({ title }: { title: React.ReactNode }): JSX.Element { + return ( + + + {({ open }: { open: boolean }): JSX.Element => ( + <> + + {title} + + + + )} + + + ) +} + +function FeatureFlagRow({ featureFlag }: { featureFlag: FEATURE_FLAGS }): JSX.Element { + const status = useFeatureFlagWithExposureLoggingDisabled(featureFlag) + + return ( + + {featureFlag} + { + Statsig.overrideGate(featureFlag, newValue) + }} + /> + + ) +} + +function ExperimentRow({ name }: { name: string }): JSX.Element { + const experiment = useExperimentWithExposureLoggingDisabled(name) + + const params = Object.entries(experiment.config.value).map(([key, value]) => ( + + {key} + + + )) + + return ( + <> + + + {name} + {params} + + + ) +} + +function ExperimentValueSwitch({ + experiment, + configValueContent, + configValueName, +}: { + experiment: ConfigResult + configValueContent: unknown + configValueName: string +}): JSX.Element { + const colors = useSporeColors() + const experimentName = experiment.config.getName() + + const onValueChange = (newValue: boolean | string): void => { + Statsig.overrideConfig(experimentName, { + ...experiment.config.value, + [configValueName]: newValue, + }) + } + + if (typeof configValueContent === 'boolean') { + return + } + + const variants = EXPERIMENT_VALUES_BY_EXPERIMENT[experimentName]?.[configValueName] + + if (variants && typeof configValueContent === 'string') { + return ( + + {Object.entries(variants).map(([_, value]) => ( + onValueChange(value)}> + + {value} + + + ))} + + ) + } + + return Unknown Variants +} diff --git a/apps/mobile/src/app/modals/ExploreModal.tsx b/apps/mobile/src/app/modals/ExploreModal.tsx new file mode 100644 index 0000000..b588ac0 --- /dev/null +++ b/apps/mobile/src/app/modals/ExploreModal.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { ExploreStackNavigator } from 'src/app/navigation/navigation' +import { closeModal } from 'src/features/modals/modalSlice' +import { useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function ExploreModal(): JSX.Element { + const colors = useSporeColors() + const appDispatch = useAppDispatch() + + const onClose = (): void => { + appDispatch(closeModal({ name: ModalName.Explore })) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/app/modals/ExploreModalState.tsx b/apps/mobile/src/app/modals/ExploreModalState.tsx new file mode 100644 index 0000000..7b7e76e --- /dev/null +++ b/apps/mobile/src/app/modals/ExploreModalState.tsx @@ -0,0 +1,12 @@ +import { ExploreStackParamList } from 'src/app/navigation/types' +import { Screens } from 'src/screens/Screens' + +type InnerExploreStackParamList = Omit + +// The ExploreModalState allows a Screen and its Params to be defined, except for the initial Explore screen. +// This workaround facilitates navigation to any screen within the ExploreStack from outside. +// Implementation of this lives inside screens/ExploreScreen + +export type ExploreModalState = { + [V in keyof InnerExploreStackParamList]: { screen: V; params: InnerExploreStackParamList[V] } +}[keyof InnerExploreStackParamList] diff --git a/apps/mobile/src/app/modals/SwapModal.tsx b/apps/mobile/src/app/modals/SwapModal.tsx new file mode 100644 index 0000000..64f8c06 --- /dev/null +++ b/apps/mobile/src/app/modals/SwapModal.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useMemo } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { BiometricsIcon } from 'src/components/icons/BiometricsIcon' +import { + useBiometricAppSettings, + useBiometricPrompt, + useOsBiometricAuthEnabled, +} from 'src/features/biometrics/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { getFocusOnCurrencyFieldFromInitialState } from 'src/features/transactions/swap/utils' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { SwapFormState } from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { SwapFlow } from 'wallet/src/features/transactions/swap/SwapFlow' +import { ModalName } from 'wallet/src/telemetry/constants' +import { updateSwapStartTimestamp } from 'wallet/src/telemetry/timing/slice' + +export function SwapModal(): JSX.Element { + const appDispatch = useAppDispatch() + const { initialState } = useAppSelector(selectModalState(ModalName.Swap)) + + const onClose = useCallback((): void => { + appDispatch(closeModal({ name: ModalName.Swap })) + }, [appDispatch]) + + // Update flow start timestamp every time modal is opened for logging + useEffect(() => { + appDispatch(updateSwapStartTimestamp({ timestamp: Date.now() })) + }, [appDispatch]) + + const { openWalletRestoreModal, walletNeedsRestore } = useWalletRestore() + + const swapPrefilledState = useMemo( + (): SwapFormState | undefined => + initialState + ? { + customSlippageTolerance: initialState.customSlippageTolerance, + exactAmountFiat: initialState.exactAmountFiat, + exactAmountToken: initialState.exactAmountToken, + exactCurrencyField: initialState.exactCurrencyField, + focusOnCurrencyField: getFocusOnCurrencyFieldFromInitialState(initialState), + input: initialState.input ?? undefined, + output: initialState.output ?? undefined, + selectingCurrencyField: initialState.selectingCurrencyField, + txId: initialState.txId, + isFiatMode: false, + isSubmitting: false, + } + : undefined, + [initialState] + ) + + const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() + const { trigger: biometricsTrigger } = useBiometricPrompt() + + return ( + } + authTrigger={requiresBiometrics ? biometricsTrigger : undefined} + openWalletRestoreModal={openWalletRestoreModal} + prefilledState={swapPrefilledState} + walletNeedsRestore={Boolean(walletNeedsRestore)} + onClose={onClose} + /> + ) +} + +function SwapBiometricsIcon(): JSX.Element | null { + const isBiometricAuthEnabled = useOsBiometricAuthEnabled() + const { requiredForTransactions } = useBiometricAppSettings() + + return isBiometricAuthEnabled && requiredForTransactions ? : null +} diff --git a/apps/mobile/src/app/modals/TransferTokenModal.tsx b/apps/mobile/src/app/modals/TransferTokenModal.tsx new file mode 100644 index 0000000..940d9e4 --- /dev/null +++ b/apps/mobile/src/app/modals/TransferTokenModal.tsx @@ -0,0 +1,39 @@ +import React, { useCallback } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { TransferFlow } from 'src/features/transactions/transfer/TransferFlow' +import { TransferFlow as TransferFlowRewrite } from 'src/features/transactions/transfer/transferRewrite/TransferFlow' +import { useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function TransferTokenModal(): JSX.Element { + const colors = useSporeColors() + const appDispatch = useAppDispatch() + const modalState = useAppSelector(selectModalState(ModalName.Send)) + + const onClose = useCallback((): void => { + appDispatch(closeModal({ name: ModalName.Send })) + }, [appDispatch]) + + const isSendRewriteEnabled = useFeatureFlag(FEATURE_FLAGS.SendRewrite) + + return isSendRewriteEnabled ? ( + + ) : ( + + + + ) +} diff --git a/apps/mobile/src/app/modals/UniconsV2Modal.tsx b/apps/mobile/src/app/modals/UniconsV2Modal.tsx new file mode 100644 index 0000000..d2aa5c0 --- /dev/null +++ b/apps/mobile/src/app/modals/UniconsV2Modal.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { + Button, + Flex, + Icons, + Text, + Unicon, + UniconV2, + useDeviceInsets, + useIsDarkMode, + useUniconColors, +} from 'ui/src' +import { spacing } from 'ui/src/theme' +import { UniconGradient } from 'wallet/src/components/accounts/AccountIcon' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { setHasViewedUniconV2IntroModal } from 'wallet/src/features/behaviorHistory/slice' +import { ModalName } from 'wallet/src/telemetry/constants' + +interface UniconsV2ModalProps { + address: string +} + +const UNICON_HEADER_SIZE = 52 +const UNICON_SIZE = 42 +const UNICON_PADDING = 14 + +export function UniconsV2Modal({ address }: UniconsV2ModalProps): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() + const { gradientEnd: uniconColor } = useUniconColors(address) + const { top } = useDeviceInsets() + + const onCloseModal = (): void => { + dispatch(setHasViewedUniconV2IntroModal(true)) + } + + return ( + <> + + + + + + + + + + + + {t('unicons.banner.title')} + + {t('unicons.banner.subtitle')} + + + + + + + + + + ) +} diff --git a/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx new file mode 100644 index 0000000..d648aed --- /dev/null +++ b/apps/mobile/src/app/modals/ViewOnlyExplainerModal.tsx @@ -0,0 +1,78 @@ +import { useTranslation } from 'react-i18next' +import { navigate } from 'src/app/navigation/rootNavigation' +import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { Button, Flex, Text, useIsDarkMode } from 'ui/src' +import ViewOnlyWalletDark from 'ui/src/assets/graphics/view-only-wallet-dark.svg' +import ViewOnlyWalletLight from 'ui/src/assets/graphics/view-only-wallet-light.svg' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { useActiveAccountAddress, useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { useAppDispatch } from 'wallet/src/state' +import { ModalName } from 'wallet/src/telemetry/constants' + +const WALLET_IMAGE_ASPECT_RATIO = 327 / 215 + +export function ViewOnlyExplainerModal(): JSX.Element { + const { t } = useTranslation() + const activeAccountAddress = useActiveAccountAddress() + const dispatch = useAppDispatch() + const hasImportedSeedPhrase = useNativeAccountExists() + const isDarkMode = useIsDarkMode() + + const onClose = (): void => { + dispatch(closeModal({ name: ModalName.ViewOnlyExplainer })) + } + + const onPressImportWallet = (): void => { + if (hasImportedSeedPhrase && activeAccountAddress) { + dispatch(openModal({ name: ModalName.RemoveWallet })) + } else { + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.SeedPhraseInput, + params: { importType: ImportType.SeedPhrase, entryPoint: OnboardingEntryPoint.Sidebar }, + }) + } + onClose() + } + + const WalletImage = isDarkMode ? ViewOnlyWalletDark : ViewOnlyWalletLight + + return ( + + + + + + + + {t('account.wallet.viewOnly.title')} + + {t('account.wallet.viewOnly.description')} + + + + + + + + + + ) +} diff --git a/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap new file mode 100644 index 0000000..0774854 --- /dev/null +++ b/apps/mobile/src/app/modals/__snapshots__/AccountSwitcherModal.test.tsx.snap @@ -0,0 +1,684 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountSwitcher renders correctly 1`] = ` + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + + Test Account + + + + + + + 0x​82D5...3Fa6 + + + + + + + + + + + + + + Manage wallet + + + + + + ExpoLinearGradient + } + animatedStyle={ + { + "value": {}, + } + } + bounces={false} + collapsable={false} + data={[]} + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + keyboardShouldPersistTaps="always" + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEnabled={false} + scrollEventThrottle={16} + sentry-label="VirtualizedList" + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + ExpoLinearGradient + + + + + + + + + + + + + Add wallet + + + + +`; diff --git a/apps/mobile/src/app/modals/utils.test.tsx b/apps/mobile/src/app/modals/utils.test.tsx new file mode 100644 index 0000000..86ada93 --- /dev/null +++ b/apps/mobile/src/app/modals/utils.test.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { Text } from 'react-native' +import { LazyModalRenderer } from 'src/app/modals/utils' +import { preloadedMobileState, preloadedModalsState } from 'src/test/fixtures' +import { renderWithProviders } from 'src/test/render' +import { ModalName } from 'wallet/src/telemetry/constants' + +describe(LazyModalRenderer, () => { + it('renders null when modal is not open', () => { + const tree = renderWithProviders( + + Rendered + , + { preloadedState: preloadedMobileState() } + ) + + expect(tree.toJSON()).toBeNull() + }) + + it('renders modal when modal is open', () => { + const tree = renderWithProviders( + + Rendered + , + { + preloadedState: preloadedMobileState({ + modals: preloadedModalsState({ + [ModalName.Experiments]: { isOpen: true }, + }), + }), + } + ) + + expect(tree.toJSON()).toMatchInlineSnapshot(` + + Rendered + + `) + }) +}) diff --git a/apps/mobile/src/app/modals/utils.tsx b/apps/mobile/src/app/modals/utils.tsx new file mode 100644 index 0000000..19d2b44 --- /dev/null +++ b/apps/mobile/src/app/modals/utils.tsx @@ -0,0 +1,25 @@ +import { useAppSelector } from 'src/app/hooks' +import { ModalsState } from 'src/features/modals/ModalsState' +import { selectModalState } from 'src/features/modals/selectModalState' + +/** + * Delays evaluating `children` until modal is open + * @param modalName name of the modal for which to track open state + * @param WrappedComponent react node to render once modal opens + */ +export function LazyModalRenderer({ + name, + children, +}: { + name: keyof ModalsState + children: JSX.Element +}): JSX.Element | null { + const modalState = useAppSelector(selectModalState(name)) + + if (!modalState.isOpen) { + // avoid doing any work until the modal needs to be open + return null + } + + return children +} diff --git a/apps/mobile/src/app/navigation/NavBar.tsx b/apps/mobile/src/app/navigation/NavBar.tsx new file mode 100644 index 0000000..2189785 --- /dev/null +++ b/apps/mobile/src/app/navigation/NavBar.tsx @@ -0,0 +1,275 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { BlurView } from 'expo-blur' +import { impactAsync } from 'expo-haptics' +import React, { memo, useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet } from 'react-native' +import { TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler' +import { + cancelAnimation, + runOnJS, + useAnimatedGestureHandler, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { pulseAnimation } from 'src/components/buttons/utils' +import { openModal } from 'src/features/modals/modalSlice' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { Screens } from 'src/screens/Screens' +import { + AnimatedFlex, + Flex, + FlexProps, + Icons, + LinearGradient, + Text, + TouchableArea, + useDeviceInsets, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import { borderRadii, fonts } from 'ui/src/theme' +import { isAndroid, isIOS } from 'uniswap/src/utils/platform' +import { useHighestBalanceNativeCurrencyId } from 'wallet/src/features/dataApi/balances' +import { prepareSwapFormState } from 'wallet/src/features/transactions/swap/utils' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { opacify } from 'wallet/src/utils/colors' + +export const NAV_BAR_HEIGHT_XS = 52 +export const NAV_BAR_HEIGHT_SM = 72 + +export const SWAP_BUTTON_HEIGHT = 56 +const SWAP_BUTTON_SHADOW_OFFSET = { width: 0, height: 4 } + +function sendSwapPressAnalyticsEvent(): void { + sendMobileAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + screen: Screens.Home, + element: ElementName.Swap, + }) +} + +export function NavBar(): JSX.Element { + const insets = useDeviceInsets() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + + return ( + <> + + + + + + + + + + + ) +} + +type SwapTabBarButtonProps = { + /** + * The value to scale to when the Pressable is being pressed. + * @default 0.96 + */ + activeScale?: number +} + +const SwapFAB = memo(function _SwapFAB({ activeScale = 0.96 }: SwapTabBarButtonProps) { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const isDarkMode = useIsDarkMode() + + const activeAccountAddress = useActiveAccountAddressWithThrow() + const inputCurrencyId = useHighestBalanceNativeCurrencyId(activeAccountAddress) + + const onPress = useCallback(async () => { + dispatch( + openModal({ + name: ModalName.Swap, + initialState: prepareSwapFormState({ inputCurrencyId }), + }) + ) + + await impactAsync() + }, [dispatch, inputCurrencyId]) + + const scale = useSharedValue(1) + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }), [scale]) + const onGestureEvent = useAnimatedGestureHandler({ + onStart: () => { + cancelAnimation(scale) + scale.value = pulseAnimation(activeScale) + }, + onEnd: () => { + runOnJS(onPress)() + runOnJS(sendSwapPressAnalyticsEvent)() + }, + }) + + return ( + + + + + + + + {t('common.button.swap')} + + + + + ) +}) + +type ExploreTabBarButtonProps = { + /** + * The value to scale to when the Pressable is being pressed. + * @default 0.98 + */ + activeScale?: number +} + +function ExploreTabBarButton({ activeScale = 0.98 }: ExploreTabBarButtonProps): JSX.Element { + const dispatch = useAppDispatch() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const { t } = useTranslation() + + const onPress = (): void => { + dispatch(openModal({ name: ModalName.Explore })) + } + const scale = useSharedValue(1) + + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }), [scale]) + const onGestureEvent = useAnimatedGestureHandler({ + onStart: () => { + cancelAnimation(scale) + scale.value = pulseAnimation(activeScale) + }, + onEnd: () => { + runOnJS(onPress)() + }, + }) + + const contentProps: FlexProps = isIOS + ? { + backgroundColor: '$surface2', + opacity: isDarkMode ? 0.6 : 0.8, + } + : { + backgroundColor: '$surface1', + style: { + borderWidth: 1, + borderColor: colors.surface3.val, + }, + } + + return ( + + + + + + + + {t('common.input.search')} + + + + + + + ) +} + +const styles = StyleSheet.create({ + searchBar: { + flexGrow: 1, + }, +}) diff --git a/apps/mobile/src/app/navigation/NavigationContainer.tsx b/apps/mobile/src/app/navigation/NavigationContainer.tsx new file mode 100644 index 0000000..5fa0e8f --- /dev/null +++ b/apps/mobile/src/app/navigation/NavigationContainer.tsx @@ -0,0 +1,104 @@ +import { + createNavigationContainerRef, + DefaultTheme, + NavigationContainer as NativeNavigationContainer, + NavigationContainerRefWithCurrent, +} from '@react-navigation/native' +import { SharedEventName } from '@uniswap/analytics-events' +import React, { FC, PropsWithChildren, useCallback, useState } from 'react' +import { Linking } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { RootParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { openDeepLink } from 'src/features/deepLinking/handleDeepLinkSaga' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { getEventParams } from 'src/features/telemetry/constants' +import { DIRECT_LOG_ONLY_SCREENS } from 'src/features/telemetry/directLogScreens' +import { processWidgetEvents } from 'src/features/widgets/widgets' +import { AppScreen } from 'src/screens/Screens' +import { useSporeColors } from 'ui/src' +import { useAsyncData } from 'utilities/src/react/hooks' +import { sleep } from 'utilities/src/time/timing' + +interface Props { + onReady: (navigationRef: NavigationContainerRefWithCurrent) => void +} + +export const navigationRef = createNavigationContainerRef() + +/** Wrapped `NavigationContainer` with telemetry tracing. */ +export const NavigationContainer: FC> = ({ + children, + onReady, +}: PropsWithChildren) => { + const colors = useSporeColors() + const [routeName, setRouteName] = useState() + const [routeParams, setRouteParams] = useState | undefined>() + const [logImpression, setLogImpression] = useState(false) + + useManageDeepLinks() + + return ( + { + onReady(navigationRef) + sendMobileAnalyticsEvent(SharedEventName.APP_LOADED) + // Process widget events on app load + processWidgetEvents().catch(() => undefined) + + // setting initial route name for telemetry + const initialRoute = navigationRef.getCurrentRoute()?.name as AppScreen + setRouteName(initialRoute) + }} + onStateChange={(): void => { + const previousRouteName = routeName + const currentRouteName: AppScreen = navigationRef.getCurrentRoute()?.name as AppScreen + + if ( + currentRouteName && + previousRouteName !== currentRouteName && + !DIRECT_LOG_ONLY_SCREENS.includes(currentRouteName) + ) { + const currentRouteParams = getEventParams( + currentRouteName, + navigationRef.getCurrentRoute()?.params as RootParamList[AppScreen] + ) + setLogImpression(true) + setRouteName(currentRouteName) + setRouteParams(currentRouteParams) + } else { + setLogImpression(false) + } + }}> + + {children} + + + ) +} + +export const useManageDeepLinks = (): void => { + const dispatch = useAppDispatch() + const manageDeepLinks = useCallback(async () => { + const url = await Linking.getInitialURL() + if (url) { + dispatch(openDeepLink({ url, coldStart: true })) + } + // we need to set an event listener for deep links, but we don't want to do it immediately on cold start, + // as then there is a change we dispatch `openDeepLink` action twice if app was lauched by a deep link + await sleep(2000) // 2000 was chosen imperically + const urlListener = Linking.addEventListener('url', (event: { url: string }) => + dispatch(openDeepLink({ url: event.url, coldStart: false })) + ) + + return urlListener.remove + }, [dispatch]) + + useAsyncData(manageDeepLinks) +} diff --git a/apps/mobile/src/app/navigation/hooks.ts b/apps/mobile/src/app/navigation/hooks.ts new file mode 100644 index 0000000..ea5cb48 --- /dev/null +++ b/apps/mobile/src/app/navigation/hooks.ts @@ -0,0 +1,104 @@ +import { NavigationContainerRefContext, NavigationContext } from '@react-navigation/core' +import { useCallback, useContext } from 'react' +import { navigate as rootNavigate } from 'src/app/navigation/rootNavigation' +import { useAppStackNavigation, useExploreStackNavigation } from 'src/app/navigation/types' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' +import { useTransactionListLazyQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' + +/** + * Utility hook to simplify navigating to Activity screen. + * Preloads query needed to render transaction list. + */ +export function useEagerActivityNavigation(): { + preload: (address: string) => Promise + navigate: () => void +} { + const navigation = useAppStackNavigation() + const [load] = useTransactionListLazyQuery() + + const preload = useCallback( + async (address: string) => { + await load({ + variables: { + address, + }, + }) + }, + [load] + ) + + const navigate = useCallback( + () => navigation.navigate(Screens.Home, { tab: HomeScreenTabIndex.Activity }), + [navigation] + ) + + return { preload, navigate } +} + +/** + * Utility hook to simplify navigating to Activity screen. + * Preloads query needed to render transaction list. + */ +export function useEagerExternalProfileNavigation(): { + preload: (address: string) => Promise + navigate: (address: string) => void +} { + const navigation = useExploreStackNavigation() + + const [load] = useTransactionListLazyQuery() + + const preload = useCallback( + async (address: string) => { + await load({ variables: { address } }) + }, + [load] + ) + + const navigate = useCallback( + (address: string) => { + navigation.navigate(Screens.ExternalProfile, { address }) + }, + [navigation] + ) + + return { preload, navigate } +} + +export function useEagerExternalProfileRootNavigation(): { + preload: (address: string) => Promise + navigate: (address: string, callback: () => void) => Promise +} { + const [load] = useTransactionListLazyQuery() + + const preload = useCallback( + async (address: string) => { + await load({ + variables: { + address, + }, + }) + }, + [load] + ) + + const navigate = useCallback(async (address: string, callback?: () => void) => { + await rootNavigate(Screens.ExternalProfile, { address }) + callback?.() + }, []) + + return { preload, navigate } +} + +/** + * Utility hook that checks if the caller is part of the navigation tree. + * + * Inspired by how the navigation library checks if the the navigation object exists. + * https://github.com/react-navigation/react-navigation/blob/d7032ba8bb6ae24030a47f0724b61b561132fca6/packages/core/src/useNavigation.tsx#L18 + */ +export function useIsPartOfNavigationTree(): boolean { + const root = useContext(NavigationContainerRefContext) + const navigation = useContext(NavigationContext) + + return navigation !== undefined || root !== undefined +} diff --git a/apps/mobile/src/app/navigation/navigation.tsx b/apps/mobile/src/app/navigation/navigation.tsx new file mode 100644 index 0000000..df5cc79 --- /dev/null +++ b/apps/mobile/src/app/navigation/navigation.tsx @@ -0,0 +1,388 @@ +import { NavigationContainer } from '@react-navigation/native' +import { createNativeStackNavigator } from '@react-navigation/native-stack' +import { createStackNavigator, TransitionPresets } from '@react-navigation/stack' +import React from 'react' +import { useAppSelector } from 'src/app/hooks' +import { + AppStackParamList, + AppStackScreenProp, + ExploreStackParamList, + FiatOnRampStackParamList, + OnboardingStackParamList, + SettingsStackParamList, + UnitagStackParamList, +} from 'src/app/navigation/types' +import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' +import { useBiometricCheck } from 'src/features/biometrics/useBiometricCheck' +import { FiatOnRampProvider } from 'src/features/fiatOnRamp/FiatOnRampContext' +import { ChooseProfilePictureScreen } from 'src/features/unitags/ChooseProfilePictureScreen' +import { ClaimUnitagScreen } from 'src/features/unitags/ClaimUnitagScreen' +import { EditUnitagProfileScreen } from 'src/features/unitags/EditUnitagProfileScreen' +import { UnitagConfirmationScreen } from 'src/features/unitags/UnitagConfirmationScreen' +import { DevScreen } from 'src/screens/DevScreen' +import { EducationScreen } from 'src/screens/EducationScreen' +import { ExploreScreen } from 'src/screens/ExploreScreen' +import { ExternalProfileScreen } from 'src/screens/ExternalProfileScreen' +import { FiatOnRampConnectingScreen } from 'src/screens/FiatOnRampConnecting' +import { FiatOnRampScreen } from 'src/screens/FiatOnRampScreen' +import { FiatOnRampServiceProvidersScreen } from 'src/screens/FiatOnRampServiceProviders' +import { HomeScreen } from 'src/screens/HomeScreen' +import { ImportMethodScreen } from 'src/screens/Import/ImportMethodScreen' +import { RestoreCloudBackupLoadingScreen } from 'src/screens/Import/RestoreCloudBackupLoadingScreen' +import { RestoreCloudBackupPasswordScreen } from 'src/screens/Import/RestoreCloudBackupPasswordScreen' +import { RestoreCloudBackupScreen } from 'src/screens/Import/RestoreCloudBackupScreen' +import { SeedPhraseInputScreen } from 'src/screens/Import/SeedPhraseInputScreen' +import { SeedPhraseInputScreenV2 } from 'src/screens/Import/SeedPhraseInputScreenV2' +import { SelectWalletScreen } from 'src/screens/Import/SelectWalletScreen' +import { WatchWalletScreen } from 'src/screens/Import/WatchWalletScreen' +import { NFTCollectionScreen } from 'src/screens/NFTCollectionScreen' +import { NFTItemScreen } from 'src/screens/NFTItemScreen' +import { BackupScreen } from 'src/screens/Onboarding/BackupScreen' +import { CloudBackupPasswordConfirmScreen } from 'src/screens/Onboarding/CloudBackupPasswordConfirmScreen' +import { CloudBackupPasswordCreateScreen } from 'src/screens/Onboarding/CloudBackupPasswordCreateScreen' +import { CloudBackupProcessingScreen } from 'src/screens/Onboarding/CloudBackupProcessingScreen' +import { EditNameScreen } from 'src/screens/Onboarding/EditNameScreen' +import { LandingScreen } from 'src/screens/Onboarding/LandingScreen' +import { ManualBackupScreen } from 'src/screens/Onboarding/ManualBackupScreen' +import { NotificationsSetupScreen } from 'src/screens/Onboarding/NotificationsSetupScreen' +import { SecuritySetupScreen } from 'src/screens/Onboarding/SecuritySetupScreen' +import { WelcomeWalletScreen } from 'src/screens/Onboarding/WelcomeWalletScreen' +import { FiatOnRampScreens, OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { SettingsAppearanceScreen } from 'src/screens/SettingsAppearanceScreen' +import { SettingsBiometricAuthScreen } from 'src/screens/SettingsBiometricAuthScreen' +import { SettingsCloudBackupPasswordConfirmScreen } from 'src/screens/SettingsCloudBackupPasswordConfirmScreen' +import { SettingsCloudBackupPasswordCreateScreen } from 'src/screens/SettingsCloudBackupPasswordCreateScreen' +import { SettingsCloudBackupProcessingScreen } from 'src/screens/SettingsCloudBackupProcessingScreen' +import { SettingsCloudBackupStatus } from 'src/screens/SettingsCloudBackupStatus' +import { SettingsPrivacyScreen } from 'src/screens/SettingsPrivacyScreen' +import { SettingsScreen } from 'src/screens/SettingsScreen' +import { SettingsViewSeedPhraseScreen } from 'src/screens/SettingsViewSeedPhraseScreen' +import { SettingsWallet } from 'src/screens/SettingsWallet' +import { SettingsWalletEdit } from 'src/screens/SettingsWalletEdit' +import { SettingsWalletManageConnection } from 'src/screens/SettingsWalletManageConnection' +import { TokenDetailsScreen } from 'src/screens/TokenDetailsScreen' +import { WebViewScreen } from 'src/screens/WebViewScreen' +import { Icons, useDeviceInsets, useSporeColors } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' +import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' + +const OnboardingStack = createStackNavigator() +const AppStack = createNativeStackNavigator() +const ExploreStack = createNativeStackNavigator() +const FiatOnRampStack = createNativeStackNavigator() +const SettingsStack = createNativeStackNavigator() +const UnitagStack = createStackNavigator() + +function SettingsStackGroup(): JSX.Element { + return ( + + + + + + + + + + + + + + + + + ) +} + +export function WrappedHomeScreen(props: AppStackScreenProp): JSX.Element { + const activeAccount = useActiveAccountWithThrow() + // Adding `key` forces a full re-render and re-mount when switching accounts + // to avoid issues with wrong cached data being shown in some memoized components that are already mounted. + return +} + +export function ExploreStackNavigator(): JSX.Element { + const colors = useSporeColors() + + return ( + + + + + + + {(props): JSX.Element => } + + + {(props): JSX.Element => } + + + + + + + ) +} + +export function FiatOnRampStackNavigator(): JSX.Element { + return ( + + + + + + + + + + + ) +} + +const renderHeaderBackImage = (): JSX.Element => ( + +) + +export function OnboardingStackNavigator(): JSX.Element { + const colors = useSporeColors() + const insets = useDeviceInsets() + const seedPhraseRefactorEnabled = useFeatureFlag(FEATURE_FLAGS.SeedPhraseRefactorNative) + const SeedPhraseInputComponent = seedPhraseRefactorEnabled + ? SeedPhraseInputScreenV2 + : SeedPhraseInputScreen + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export function UnitagStackNavigator(): JSX.Element { + const colors = useSporeColors() + const insets = useDeviceInsets() + + return ( + + + + + + + + + ) +} + +export function AppStackNavigator(): JSX.Element { + const finishedOnboarding = useAppSelector(selectFinishedOnboarding) + useBiometricCheck() + + return ( + + {finishedOnboarding && } + + + + + + + + + + + + + ) +} + +const navOptions = { + noHeader: { headerShown: false }, + presentationModal: { presentation: 'modal' }, +} as const diff --git a/apps/mobile/src/app/navigation/rootNavigation.ts b/apps/mobile/src/app/navigation/rootNavigation.ts new file mode 100644 index 0000000..2bcb36a --- /dev/null +++ b/apps/mobile/src/app/navigation/rootNavigation.ts @@ -0,0 +1,52 @@ +import { NavigationAction, NavigationState } from '@react-navigation/core' +import { navigationRef } from 'src/app/navigation/NavigationContainer' +import { RootParamList } from 'src/app/navigation/types' +import { logger } from 'utilities/src/logger/logger' + +export type RootNavigationArgs = + undefined extends RootParamList[RouteName] + ? [RouteName] | [RouteName, RootParamList[RouteName]] + : [RouteName, RootParamList[RouteName]] + +function isNavigationRefReady(): boolean { + if (!navigationRef.isReady()) { + logger.error(new Error('Navigator was called before it was initialized'), { + tags: { file: 'rootNavigation', function: 'navigate' }, + }) + return false + } + return true +} + +export function navigate( + ...args: RootNavigationArgs +): void { + const [routeName, params] = args + if (!isNavigationRefReady()) { + return + } + + // Type assignment to `never` is a workaround until we figure out how to + // type `createNavigationContainerRef` in a way that's compatible + navigationRef.navigate(routeName as never, params as never) +} + +export function goBack(): void { + if (!isNavigationRefReady()) { + return + } + + if (navigationRef.canGoBack()) { + navigationRef.goBack() + } +} + +export function dispatchNavigationAction( + action: NavigationAction | ((state: NavigationState) => NavigationAction) +): void { + if (!isNavigationRefReady()) { + return + } + + navigationRef.dispatch(action) +} diff --git a/apps/mobile/src/app/navigation/types.ts b/apps/mobile/src/app/navigation/types.ts new file mode 100644 index 0000000..bf53114 --- /dev/null +++ b/apps/mobile/src/app/navigation/types.ts @@ -0,0 +1,186 @@ +import { + CompositeNavigationProp, + CompositeScreenProps, + NavigatorScreenParams, + useNavigation, +} from '@react-navigation/native' +import { NativeStackNavigationProp, NativeStackScreenProps } from '@react-navigation/native-stack' +import { EducationContentType } from 'src/components/education' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { FiatOnRampScreens, OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { UnitagClaim } from 'uniswap/src/features/unitags/types' +import { NFTItem } from 'wallet/src/features/nfts/types' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' + +type NFTItemScreenParams = { + owner?: Address + address: string + tokenId: string + isSpam?: boolean + fallbackData?: NFTItem +} + +export type CloudBackupFormParams = { + address: Address + password: string +} + +export type ExploreStackParamList = { + [Screens.Explore]: undefined + [Screens.ExternalProfile]: { + address: string + } + [Screens.NFTItem]: NFTItemScreenParams + [Screens.NFTCollection]: { collectionAddress: string } + [Screens.TokenDetails]: { + currencyId: string + } +} + +export type FiatOnRampStackParamList = { + [FiatOnRampScreens.AmountInput]: undefined + [FiatOnRampScreens.ServiceProviders]: undefined + [FiatOnRampScreens.Connecting]: undefined +} + +export type SettingsStackParamList = { + [Screens.Dev]: undefined + [Screens.Settings]: undefined + [Screens.SettingsAppearance]: undefined + [Screens.SettingsBiometricAuth]: undefined + [Screens.SettingsCloudBackupPasswordConfirm]: CloudBackupFormParams + [Screens.SettingsCloudBackupPasswordCreate]: { address: Address } + [Screens.SettingsCloudBackupProcessing]: CloudBackupFormParams + [Screens.SettingsCloudBackupStatus]: { address: Address } + [Screens.SettingsHelpCenter]: undefined + [Screens.SettingsLanguage]: undefined + [Screens.SettingsPrivacy]: undefined + [Screens.SettingsViewSeedPhrase]: { address: Address; walletNeedsRestore?: boolean } + [Screens.SettingsWallet]: { address: Address } + [Screens.SettingsWalletEdit]: { address: Address } + [Screens.SettingsWalletManageConnection]: { address: Address } + [Screens.WebView]: { headerTitle: string; uriLink: string } +} + +export type OnboardingStackBaseParams = { + importType: ImportType + entryPoint: OnboardingEntryPoint + unitagClaim?: UnitagClaim +} + +export type UnitagEntryPoint = OnboardingScreens.Landing | Screens.Home | Screens.Settings + +export type SharedUnitagScreenParams = { + [UnitagScreens.ClaimUnitag]: { + entryPoint: UnitagEntryPoint + address?: Address + } + [UnitagScreens.ChooseProfilePicture]: { + entryPoint: UnitagEntryPoint + unitag: string + unitagFontSize: number + address: Address + } +} + +export type OnboardingStackParamList = { + [OnboardingScreens.BackupManual]: OnboardingStackBaseParams + [OnboardingScreens.BackupCloudPasswordCreate]: { + address: Address + } & OnboardingStackBaseParams + [OnboardingScreens.BackupCloudPasswordConfirm]: CloudBackupFormParams & OnboardingStackBaseParams + [OnboardingScreens.BackupCloudProcessing]: CloudBackupFormParams & OnboardingStackBaseParams + [OnboardingScreens.Backup]: OnboardingStackBaseParams + [OnboardingScreens.Landing]: OnboardingStackBaseParams + [OnboardingScreens.EditName]: OnboardingStackBaseParams + [OnboardingScreens.Notifications]: OnboardingStackBaseParams + [OnboardingScreens.WelcomeWallet]: OnboardingStackBaseParams + [OnboardingScreens.Security]: OnboardingStackBaseParams + + // import + [OnboardingScreens.ImportMethod]: OnboardingStackBaseParams + [OnboardingScreens.RestoreCloudBackupLoading]: OnboardingStackBaseParams + [OnboardingScreens.RestoreCloudBackup]: OnboardingStackBaseParams + [OnboardingScreens.RestoreCloudBackupPassword]: { + mnemonicId: string + } & OnboardingStackBaseParams + [OnboardingScreens.SeedPhraseInput]: OnboardingStackBaseParams + [OnboardingScreens.SelectWallet]: OnboardingStackBaseParams + [OnboardingScreens.WatchWallet]: OnboardingStackBaseParams +} & SharedUnitagScreenParams + +export type UnitagStackParamList = SharedUnitagScreenParams & { + [UnitagScreens.UnitagConfirmation]: { + unitag: string + address: Address + profilePictureUri?: string + } + [UnitagScreens.EditProfile]: { + address: Address + unitag: string + entryPoint: UnitagScreens.UnitagConfirmation | Screens.SettingsWallet + } +} + +export type AppStackParamList = { + [Screens.Education]: { + type: EducationContentType + } & OnboardingStackBaseParams + [Screens.Home]?: { tab?: HomeScreenTabIndex } + [Screens.OnboardingStack]: NavigatorScreenParams + [Screens.SettingsStack]: NavigatorScreenParams + [Screens.UnitagStack]: NavigatorScreenParams + [Screens.TokenDetails]: { + currencyId: string + } + [Screens.NFTItem]: NFTItemScreenParams + [Screens.NFTCollection]: { collectionAddress: string } + [Screens.ExternalProfile]: { + address: string + } + [Screens.WebView]: { headerTitle: string; uriLink: string } +} + +export type AppStackNavigationProp = NativeStackNavigationProp +export type AppStackScreenProps = NativeStackScreenProps +export type AppStackScreenProp = NativeStackScreenProps< + AppStackParamList, + Screen +> + +export type ExploreStackNavigationProp = CompositeNavigationProp< + NativeStackNavigationProp, + AppStackNavigationProp +> + +export type SettingsStackNavigationProp = CompositeNavigationProp< + NativeStackNavigationProp, + AppStackNavigationProp +> + +export type SettingsStackScreenProp = + CompositeScreenProps, AppStackScreenProps> + +export type OnboardingStackNavigationProp = CompositeNavigationProp< + NativeStackNavigationProp, + AppStackNavigationProp +> + +export type UnitagStackScreenProp = + NativeStackScreenProps + +export type RootParamList = AppStackParamList & + ExploreStackParamList & + OnboardingStackParamList & + SettingsStackParamList & + UnitagStackParamList & + FiatOnRampStackParamList + +export const useAppStackNavigation = (): AppStackNavigationProp => + useNavigation() +export const useExploreStackNavigation = (): ExploreStackNavigationProp => + useNavigation() +export const useSettingsStackNavigation = (): SettingsStackNavigationProp => + useNavigation() +export const useOnboardingStackNavigation = (): OnboardingStackNavigationProp => + useNavigation() diff --git a/apps/mobile/src/app/reducer.ts b/apps/mobile/src/app/reducer.ts new file mode 100644 index 0000000..c5ab1ed --- /dev/null +++ b/apps/mobile/src/app/reducer.ts @@ -0,0 +1,27 @@ +import { combineReducers } from '@reduxjs/toolkit' +import { cloudBackupReducer } from 'src/features/CloudBackup/cloudBackupSlice' +import { passwordLockoutReducer } from 'src/features/CloudBackup/passwordLockoutSlice' +import { biometricSettingsReducer } from 'src/features/biometrics/slice' +import { modalsReducer } from 'src/features/modals/modalSlice' +import { telemetryReducer } from 'src/features/telemetry/slice' +import { tweaksReducer } from 'src/features/tweaks/slice' +import { walletConnectReducer } from 'src/features/walletConnect/walletConnectSlice' +import { sharedReducers } from 'wallet/src/state/reducer' +import { monitoredSagaReducers } from './saga' + +const reducers = { + ...sharedReducers, + biometricSettings: biometricSettingsReducer, + cloudBackup: cloudBackupReducer, + modals: modalsReducer, + passwordLockout: passwordLockoutReducer, + saga: monitoredSagaReducers, + telemetry: telemetryReducer, + tweaks: tweaksReducer, + walletConnect: walletConnectReducer, +} as const + +export const mobileReducer = combineReducers(reducers) + +export type MobileState = ReturnType +export type ReducerNames = keyof typeof reducers diff --git a/apps/mobile/src/app/saga.ts b/apps/mobile/src/app/saga.ts new file mode 100644 index 0000000..3c7854e --- /dev/null +++ b/apps/mobile/src/app/saga.ts @@ -0,0 +1,125 @@ +import { PersistState } from 'redux-persist' +import { apolloClientRef } from 'src/data/usePersistedApolloClient' +import { appRatingWatcherSaga } from 'src/features/appRating/saga' +import { cloudBackupsManagerSaga } from 'src/features/CloudBackup/saga' +import { deepLinkWatcher } from 'src/features/deepLinking/handleDeepLinkSaga' +import { firebaseDataWatcher } from 'src/features/firebase/firebaseDataSaga' +import { modalWatcher } from 'src/features/modals/saga' +import { telemetrySaga } from 'src/features/telemetry/saga' +import { restoreMnemonicCompleteWatcher } from 'src/features/wallet/saga' +import { walletConnectSaga } from 'src/features/walletConnect/saga' +import { signWcRequestSaga } from 'src/features/walletConnect/signWcRequestSaga' +import { call, delay, select, spawn } from 'typed-redux-saga' +import { appLanguageWatcherSaga } from 'wallet/src/features/language/saga' +import { + swapActions, + swapReducer, + swapSaga, + swapSagaName, +} from 'wallet/src/features/transactions/swap/swapSaga' +import { + tokenWrapActions, + tokenWrapReducer, + tokenWrapSaga, + tokenWrapSagaName, +} from 'wallet/src/features/transactions/swap/wrapSaga' +import { transactionWatcher } from 'wallet/src/features/transactions/transactionWatcherSaga' +import { + editAccountActions, + editAccountReducer, + editAccountSaga, + editAccountSagaName, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { + createAccountActions, + createAccountReducer, + createAccountSaga, + createAccountSagaName, +} from 'wallet/src/features/wallet/create/createAccountSaga' +import { pendingAccountSaga } from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { + importAccountActions, + importAccountReducer, + importAccountSaga, + importAccountSagaName, +} from 'wallet/src/features/wallet/import/importAccountSaga' +import { getMonitoredSagaReducers, MonitoredSaga } from 'wallet/src/state/saga' + +const REHYDRATION_STATUS_POLLING_INTERVAL = 50 + +// All regular sagas must be included here +const sagas = [ + appLanguageWatcherSaga, + appRatingWatcherSaga, + cloudBackupsManagerSaga, + deepLinkWatcher, + firebaseDataWatcher, + modalWatcher, + pendingAccountSaga, + restoreMnemonicCompleteWatcher, + signWcRequestSaga, + telemetrySaga, + walletConnectSaga, +] + +// All monitored sagas must be included here +export const monitoredSagas: Record = { + [createAccountSagaName]: { + name: createAccountSagaName, + wrappedSaga: createAccountSaga, + reducer: createAccountReducer, + actions: createAccountActions, + }, + [editAccountSagaName]: { + name: editAccountSagaName, + wrappedSaga: editAccountSaga, + reducer: editAccountReducer, + actions: editAccountActions, + }, + [importAccountSagaName]: { + name: importAccountSagaName, + wrappedSaga: importAccountSaga, + reducer: importAccountReducer, + actions: importAccountActions, + }, + [swapSagaName]: { + name: swapSagaName, + wrappedSaga: swapSaga, + reducer: swapReducer, + actions: swapActions, + }, + [tokenWrapSagaName]: { + name: tokenWrapSagaName, + wrappedSaga: tokenWrapSaga, + reducer: tokenWrapReducer, + actions: tokenWrapActions, + }, +} + +export const monitoredSagaReducers = getMonitoredSagaReducers(monitoredSagas) + +export function* mobileSaga() { + // wait until redux-persist has finished rehydration + while (true) { + if ( + yield* select( + (state: { _persist?: PersistState }): boolean | undefined => state._persist?.rehydrated + ) + ) { + break + } + yield* delay(REHYDRATION_STATUS_POLLING_INTERVAL) + } + + for (const s of sagas) { + yield* spawn(s) + } + + const apolloClient = yield* call(apolloClientRef.onReady) + + yield* spawn(transactionWatcher, { apolloClient }) + + for (const m of Object.values(monitoredSagas)) { + yield* spawn(m.wrappedSaga) + } +} diff --git a/apps/mobile/src/app/schema.ts b/apps/mobile/src/app/schema.ts new file mode 100644 index 0000000..50f3362 --- /dev/null +++ b/apps/mobile/src/app/schema.ts @@ -0,0 +1,455 @@ +import { initialFiatCurrencyState } from 'wallet/src/features/fiatCurrency/slice' +import { initialLanguageState } from 'wallet/src/features/language/slice' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { ModalName } from 'wallet/src/telemetry/constants' + +// only add fields that are persisted +export const initialSchema = { + balances: { + byChainId: {}, + }, + chains: { + byChainId: { + '1': { isActive: true }, + '10': { isActive: true }, + '137': { isActive: true }, + '42161': { isActive: true }, + }, + }, + favorites: { + tokens: [], + followedAddresses: [], + }, + notifications: { + notificationQueue: [], + notificationCount: {}, + }, + providers: { + isInitialized: false, + }, + saga: {}, + telemetry: { + lastBalancesReport: 0, + lastBalancesReportValue: 0, + }, + tokenLists: { + lastInitializedDefaultListOfLists: [], + byUrl: [], + activeListUrls: [], + }, + tokens: { + watchedTokens: {}, + customTokens: {}, + tokenPairs: {}, + dismissedWarningTokens: {}, + }, + transactions: { + byChainId: {}, + lastTxHistoryUpdate: {}, + }, + wallet: { + accounts: {}, + activeAccountAddress: null, + bluetooth: false, + flashbotsEnabled: false, + hardwareDevices: [], + isUnlocked: false, + }, + walletConnect: { + byAccount: {}, + pendingRequests: [], + modalState: 0, + }, +} + +export const v0Schema = { + ...initialSchema, + transactions: {}, + notifications: { + ...initialSchema.notifications, + lastTxNotificationUpdate: {}, + }, +} + +export const v1Schema = { + ...v0Schema, + walletConnect: { + byAccount: {}, + pendingRequests: [], + }, + modals: { + [ModalName.WalletConnectScan]: { + isOpen: false, + initialState: 0, + }, + [ModalName.Swap]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Send]: { + isOpen: false, + initialState: undefined, + }, + }, +} + +export const v2Schema = { + ...v1Schema, + favorites: { + ...v1Schema.favorites, + followedAddresses: undefined, + watchedAddresses: [], + }, +} + +export const v3Schema = { + ...v2Schema, + searchHistory: { + results: [], + }, +} + +export const v4Schema = { + ...v3Schema, +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { balances, ...restV4Schema } = v4Schema +delete restV4Schema.favorites.followedAddresses + +// adding in missed properties +export const v5Schema = { ...restV4Schema } + +const v5IntermediateSchema = { + ...v5Schema, + wallet: { + ...v5Schema.wallet, + bluetooth: undefined, + }, +} + +delete v5IntermediateSchema.wallet.bluetooth + +export const v6Schema = { + ...v5IntermediateSchema, + walletConnect: { ...v5IntermediateSchema.walletConnect, pendingSession: null }, + wallet: { + ...v5IntermediateSchema.wallet, + settings: {}, + }, +} + +export const v7Schema = { ...v6Schema } + +export const v8Schema = { + ...v7Schema, + cloudBackup: { + backupsFound: [], + }, +} +// schema did not change, but we removed private key wallets +export const v9Schema = { ...v8Schema } + +// schema did not change, removed the demo account +export const v10Schema = { ...v9Schema } + +export const v11Schema = { + ...v10Schema, + biometricSettings: { requiredForAppAccess: false, requiredForTransactions: false }, +} + +// schema did not change, added `pushNotificationsEnabled` prop to the Account type +export const v12Schema = { ...v11Schema } + +export const v13Schema = { ...v12Schema, ens: { ensForAddress: {} } } + +export const v14Schema = { ...v13Schema } + +export const v15Schema = { ...v14Schema } + +export const v16Schema = { ...v15Schema } + +export const v17Schema = { ...v16Schema } + +export const v18Schema = { ...v17Schema } + +export const v19Schema = { ...v18Schema } + +export const v20Schema = { ...v19Schema } + +export const v21Schema = { ...v20Schema, experiments: { experiments: {}, featureFlags: {} } } + +export const v22Schema = { ...v21Schema } + +// schema did not change, updated the types of `wallet.settings.tokensOrderBy` and `wallet.settings.tokensMetadataDisplayType` +export const v23Schema = { ...v22Schema } + +export const v24Schema = { + ...v23Schema, + notifications: { + notificationQueue: [], + notificationStatus: {}, + lastTxNotificationUpdate: {}, + }, +} + +export const v25Schema = { ...v24Schema, passwordLockout: { passwordAttempts: 0 } } + +export const v26Schema = { ...v25Schema } + +export const v27Schema = { ...v26Schema } + +export const v28Schema = { ...v27Schema } + +export const v29Schema = { ...v28Schema } + +export const v30Schema = { ...v29Schema } + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { tokenLists, ...v31SchemaIntermediate } = { ...v30Schema } +export const v31Schema = v31SchemaIntermediate + +export const v32Schema = { ...v31Schema } + +export const v33Schema = { + ...v32Schema, + wallet: { + ...v32Schema.wallet, + replaceAccountOptions: { + isReplacingAccount: false, + skipToSeedPhrase: false, + }, + }, +} + +export const v34Schema = { + ...v33Schema, + telemetry: { + lastBalancesReport: 0, + }, +} + +export const v35Schema = { + ...v34Schema, + appearanceSettings: { + selectedAppearanceSettings: 'system', + }, +} + +export const v36Schema = { + ...v35Schema, + favorites: { + ...v35Schema.favorites, + hiddenNfts: {}, + }, +} + +export const v37Schema = { ...v36Schema } + +const v37SchemaIntermediate = { + ...v37Schema, + wallet: { + ...v37Schema.wallet, + replaceAccountOptions: undefined, + }, +} +delete v37SchemaIntermediate.wallet.replaceAccountOptions + +export const v38Schema = { ...v37SchemaIntermediate } + +const v38SchemaIntermediate = { + ...v38Schema, + experiments: undefined, +} +delete v38SchemaIntermediate.experiments + +export const v39Schema = { ...v38SchemaIntermediate } + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const { walletConnect, ...v39SchemaIntermediate } = { ...v39Schema } + +export const v40Schema = { ...v39SchemaIntermediate } + +export const v41Schema = { + ...v40Schema, + telemetry: { + ...v40Schema.telemetry, + lastBalancesReportValue: 0, + }, +} + +export const v42Schema = { + ...v41Schema, + wallet: { ...v41Schema.wallet, flashbotsEnabled: undefined }, +} +delete v42Schema.wallet.flashbotsEnabled + +export const v43Schema = { + ...v42Schema, + favorites: { + ...v42Schema.favorites, + nftsData: {}, + hiddenNfts: undefined, + }, +} +delete v43Schema.favorites.hiddenNfts + +export const { providers, ...v44Schema } = { + ...v43Schema, +} + +export const v45Schema = { + ...v44Schema, + favorites: { + ...v44Schema.favorites, + tokensVisibility: {}, + }, +} + +const v45SchemaIntermediate = { + ...v44Schema, + ENS: undefined, + ens: undefined, + gasApi: undefined, + onChainBalanceApi: undefined, + routingApi: undefined, + trmApi: undefined, +} + +delete v45SchemaIntermediate.ENS +delete v45SchemaIntermediate.ens +delete v45SchemaIntermediate.gasApi +delete v45SchemaIntermediate.onChainBalanceApi +delete v45SchemaIntermediate.routingApi +delete v45SchemaIntermediate.trmApi + +export const v46Schema = { ...v45SchemaIntermediate } + +// Remove reliance on env var config.activeChains +export const v47Schema = { + ...v46Schema, + chains: { + byChainId: { + '1': { isActive: true }, + '10': { isActive: true }, + '56': { isActive: true }, + '137': { isActive: true }, + '8453': { isActive: true }, + '42161': { isActive: true }, + }, + }, +} + +export const v48Schema = { ...v46Schema, tweaks: {} } + +export const v49Schema = { + ...v48Schema, + wallet: { + ...v48Schema.wallet, + settings: { + ...v48Schema.wallet.settings, + swapProtection: SwapProtectionSetting.On, + }, + }, +} + +const v50SchemaIntermediate = { ...v49Schema, chains: undefined } +delete v50SchemaIntermediate.chains +export const v50Schema = { ...v50SchemaIntermediate } + +export const v51Schema = { + ...v50Schema, + modals: { + ...v50Schema.modals, + ['language-selector']: { + isOpen: false, + initialState: undefined, + }, + }, + languageSettings: initialLanguageState, +} + +export const v52Schema = { + ...v51Schema, + modals: { + ...v51Schema.modals, + [ModalName.FiatCurrencySelector]: { + isOpen: false, + initialState: undefined, + }, + }, + fiatCurrencySettings: initialFiatCurrencyState, +} + +const v53SchemaIntermediate = { + ...v52Schema, + languageSettings: initialLanguageState, + modals: { ...v52Schema.modals, ['language-selector']: undefined }, +} +delete v53SchemaIntermediate.modals['language-selector'] + +export const v53Schema = v53SchemaIntermediate + +export const v54Schema = { + ...v53Schema, + telemetry: { + ...v53Schema.telemetry, + walletIsFunded: false, + }, +} + +export const v55Schema = { + ...v54Schema, + behaviorHistory: { + hasViewedReviewScreen: false, + hasSubmittedHoldToSwap: false, + }, +} + +export const v56Schema = { + ...v55Schema, + telemetry: { + ...v55Schema.telemetry, + allowAnalytics: true, + lastHeartbeat: 0, + }, +} + +export const v57Schema = { + ...v56Schema, + wallet: { + ...v56Schema.wallet, + settings: { + ...v56Schema.wallet.settings, + hideSmallBalances: true, + hideSpamTokens: true, + }, + }, +} + +export const v58Schema = { + ...v57Schema, + behaviorHistory: { + ...v57Schema.behaviorHistory, + hasSkippedUnitagPrompt: false, + }, +} + +export const v59Schema = { + ...v58Schema, + behaviorHistory: { + ...v58Schema.behaviorHistory, + hasCompletedUnitagsIntroModal: false, + }, +} + +export const v60Schema = { + ...v59Schema, + behaviorHistory: { + ...v59Schema.behaviorHistory, + hasViewedUniconV2IntroModal: false, + }, +} +// TODO: [MOB-201] use function with typed output when API reducers are removed from rootReducer +// export const getSchema = (): RootState => v0Schema +export const getSchema = (): typeof v59Schema => v59Schema diff --git a/apps/mobile/src/app/store.test.ts b/apps/mobile/src/app/store.test.ts new file mode 100644 index 0000000..d58a2be --- /dev/null +++ b/apps/mobile/src/app/store.test.ts @@ -0,0 +1,13 @@ +import { migrations } from 'src/app/migrations' +import { persistConfig } from 'src/app/store' + +describe('Redux persist config', () => { + it('has a version that is in sync with migrations', () => { + const migrationKeys = Object.keys(migrations) + .map((version) => parseInt(version, 10)) + .sort((a, b) => a - b) + + const lastMigrationKey = migrationKeys.pop() + expect(persistConfig.version).toEqual(lastMigrationKey) + }) +}) diff --git a/apps/mobile/src/app/store.ts b/apps/mobile/src/app/store.ts new file mode 100644 index 0000000..635ff1c --- /dev/null +++ b/apps/mobile/src/app/store.ts @@ -0,0 +1,127 @@ +import type { Middleware, PayloadAction, PreloadedState } from '@reduxjs/toolkit' +import { isRejectedWithValue } from '@reduxjs/toolkit' +import * as Sentry from '@sentry/react' +import { MMKV } from 'react-native-mmkv' +import { Storage, persistReducer, persistStore } from 'redux-persist' +import createMigrate from 'src/app/createMigrate' +import { migrations } from 'src/app/migrations' +import { isNonJestDev } from 'utilities/src/environment' +import { logger } from 'utilities/src/logger/logger' +import { fiatOnRampAggregatorApi, fiatOnRampApi } from 'wallet/src/features/fiatOnRamp/api' +import { importAccountSagaName } from 'wallet/src/features/wallet/import/importAccountSaga' +import { createStore } from 'wallet/src/state' +import { RootReducerNames } from 'wallet/src/state/reducer' +import { MobileState, ReducerNames, mobileReducer } from './reducer' +import { mobileSaga } from './saga' + +const storage = new MMKV() + +export const reduxStorage: Storage = { + setItem: (key, value) => { + storage.set(key, value) + return Promise.resolve(true) + }, + getItem: (key) => { + const value = storage.getString(key) + return Promise.resolve(value) + }, + removeItem: (key) => { + storage.delete(key) + return Promise.resolve() + }, +} + +const rtkQueryErrorLogger: Middleware = () => (next) => (action: PayloadAction) => { + if (!isRejectedWithValue(action)) { + return next(action) + } + + logger.error(action.error, { + tags: { + file: 'store', + function: 'rtkQueryErrorLogger', + }, + extra: { + type: action.type, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + endpointName: (action.meta as any)?.arg?.endpointName, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + status: (action.payload as any)?.status, + }, + }) + + return next(action) +} + +const whitelist: Array = [ + 'appearanceSettings', + 'behaviorHistory', + 'biometricSettings', + 'favorites', + 'notifications', + 'passwordLockout', + 'searchHistory', + 'telemetry', + 'tokens', + 'transactions', + 'tweaks', + 'wallet', + 'cloudBackup', + 'languageSettings', + 'fiatCurrencySettings', +] + +export const persistConfig = { + key: 'root', + storage: reduxStorage, + whitelist, + version: 60, + migrate: createMigrate(migrations), +} + +export const persistedReducer = persistReducer(persistConfig, mobileReducer) + +const sentryReduxEnhancer = Sentry.createReduxEnhancer({ + // Add any restrictions here for when the enhancer should not be used + actionTransformer: (action) => { + if (action.type === `${importAccountSagaName}/trigger`) { + // Return null in the case of importing an account, as the payload could contain the mnemonic + return null + } + + return action + }, + stateTransformer: (state: MobileState): Maybe => { + // Do not log the state if a user has opted out of analytics. + if (state.telemetry.allowAnalytics) { + return state + } else { + return null + } + }, +}) + +const middlewares: Middleware[] = [fiatOnRampApi.middleware, fiatOnRampAggregatorApi.middleware] +if (isNonJestDev) { + const createDebugger = require('redux-flipper').default + middlewares.push(createDebugger()) +} + +export const setupStore = ( + preloadedState?: PreloadedState + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type +) => { + return createStore({ + reducer: persistedReducer, + preloadedState, + additionalSagas: [mobileSaga], + middlewareAfter: [rtkQueryErrorLogger, ...middlewares], + enhancers: [sentryReduxEnhancer], + }) +} +export const store = setupStore() + +export const persistor = persistStore(store) + +export type AppDispatch = typeof store.dispatch +export type AppStore = typeof store diff --git a/apps/mobile/src/assets/fonts/Basel-Book.ttf b/apps/mobile/src/assets/fonts/Basel-Book.ttf new file mode 100644 index 0000000..136d64d Binary files /dev/null and b/apps/mobile/src/assets/fonts/Basel-Book.ttf differ diff --git a/apps/mobile/src/assets/fonts/Basel-Medium.ttf b/apps/mobile/src/assets/fonts/Basel-Medium.ttf new file mode 100644 index 0000000..6e5bf68 Binary files /dev/null and b/apps/mobile/src/assets/fonts/Basel-Medium.ttf differ diff --git a/apps/mobile/src/assets/fonts/InputMono-Regular.ttf b/apps/mobile/src/assets/fonts/InputMono-Regular.ttf new file mode 100644 index 0000000..1da5604 Binary files /dev/null and b/apps/mobile/src/assets/fonts/InputMono-Regular.ttf differ diff --git a/apps/mobile/src/assets/unicons/Container/1.svg b/apps/mobile/src/assets/unicons/Container/1.svg new file mode 100644 index 0000000..bb7d1d6 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/1.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/10.svg b/apps/mobile/src/assets/unicons/Container/10.svg new file mode 100644 index 0000000..e546257 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/10.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/11.svg b/apps/mobile/src/assets/unicons/Container/11.svg new file mode 100644 index 0000000..c308672 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/11.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/12.svg b/apps/mobile/src/assets/unicons/Container/12.svg new file mode 100644 index 0000000..61b7274 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/12.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/13.svg b/apps/mobile/src/assets/unicons/Container/13.svg new file mode 100644 index 0000000..732a0e6 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/13.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/14.svg b/apps/mobile/src/assets/unicons/Container/14.svg new file mode 100644 index 0000000..6f0c95b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/14.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/15.svg b/apps/mobile/src/assets/unicons/Container/15.svg new file mode 100644 index 0000000..c89101b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/15.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/16.svg b/apps/mobile/src/assets/unicons/Container/16.svg new file mode 100644 index 0000000..46001e2 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/16.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/17.svg b/apps/mobile/src/assets/unicons/Container/17.svg new file mode 100644 index 0000000..ea8024d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/17.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/18.svg b/apps/mobile/src/assets/unicons/Container/18.svg new file mode 100644 index 0000000..859b5d3 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/18.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/19.svg b/apps/mobile/src/assets/unicons/Container/19.svg new file mode 100644 index 0000000..60e04f0 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/19.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/2.svg b/apps/mobile/src/assets/unicons/Container/2.svg new file mode 100644 index 0000000..0f12f3f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/2.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/20.svg b/apps/mobile/src/assets/unicons/Container/20.svg new file mode 100644 index 0000000..f3ba12b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/20.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/21.svg b/apps/mobile/src/assets/unicons/Container/21.svg new file mode 100644 index 0000000..9ee424d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/21.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/22.svg b/apps/mobile/src/assets/unicons/Container/22.svg new file mode 100644 index 0000000..32292b5 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/22.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/23.svg b/apps/mobile/src/assets/unicons/Container/23.svg new file mode 100644 index 0000000..9d9a815 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/23.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/24.svg b/apps/mobile/src/assets/unicons/Container/24.svg new file mode 100644 index 0000000..cd5d369 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/24.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/25.svg b/apps/mobile/src/assets/unicons/Container/25.svg new file mode 100644 index 0000000..a37d9ff --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/25.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/26.svg b/apps/mobile/src/assets/unicons/Container/26.svg new file mode 100644 index 0000000..687f8af --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/26.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/27.svg b/apps/mobile/src/assets/unicons/Container/27.svg new file mode 100644 index 0000000..b3bbbcd --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/27.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/28.svg b/apps/mobile/src/assets/unicons/Container/28.svg new file mode 100644 index 0000000..284408d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/28.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/29.svg b/apps/mobile/src/assets/unicons/Container/29.svg new file mode 100644 index 0000000..ad08724 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/29.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/3.svg b/apps/mobile/src/assets/unicons/Container/3.svg new file mode 100644 index 0000000..be0711d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/3.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/30.svg b/apps/mobile/src/assets/unicons/Container/30.svg new file mode 100644 index 0000000..5e08e0b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/30.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/31.svg b/apps/mobile/src/assets/unicons/Container/31.svg new file mode 100644 index 0000000..a1d83d6 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/31.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/32.svg b/apps/mobile/src/assets/unicons/Container/32.svg new file mode 100644 index 0000000..5f0a0a1 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/32.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/33.svg b/apps/mobile/src/assets/unicons/Container/33.svg new file mode 100644 index 0000000..76be5f2 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/33.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/34.svg b/apps/mobile/src/assets/unicons/Container/34.svg new file mode 100644 index 0000000..5b986d2 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/34.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/35.svg b/apps/mobile/src/assets/unicons/Container/35.svg new file mode 100644 index 0000000..1fec8d8 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/35.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/36.svg b/apps/mobile/src/assets/unicons/Container/36.svg new file mode 100644 index 0000000..12e4307 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/36.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/37.svg b/apps/mobile/src/assets/unicons/Container/37.svg new file mode 100644 index 0000000..4904d03 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/37.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/4.svg b/apps/mobile/src/assets/unicons/Container/4.svg new file mode 100644 index 0000000..0f12f3f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/4.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/5.svg b/apps/mobile/src/assets/unicons/Container/5.svg new file mode 100644 index 0000000..d1fcee2 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/5.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/6.svg b/apps/mobile/src/assets/unicons/Container/6.svg new file mode 100644 index 0000000..d0d482c --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/6.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/7.svg b/apps/mobile/src/assets/unicons/Container/7.svg new file mode 100644 index 0000000..08588e9 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/7.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/8.svg b/apps/mobile/src/assets/unicons/Container/8.svg new file mode 100644 index 0000000..0244d37 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/8.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Container/9.svg b/apps/mobile/src/assets/unicons/Container/9.svg new file mode 100644 index 0000000..4253e89 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Container/9.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/1.svg b/apps/mobile/src/assets/unicons/Emblem/1.svg new file mode 100644 index 0000000..a996d0a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/1.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/10.svg b/apps/mobile/src/assets/unicons/Emblem/10.svg new file mode 100644 index 0000000..6a9247b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/10.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/11.svg b/apps/mobile/src/assets/unicons/Emblem/11.svg new file mode 100644 index 0000000..1b8fc01 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/11.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/12.svg b/apps/mobile/src/assets/unicons/Emblem/12.svg new file mode 100644 index 0000000..d33d898 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/12.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/13.svg b/apps/mobile/src/assets/unicons/Emblem/13.svg new file mode 100644 index 0000000..8496668 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/13.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/14.svg b/apps/mobile/src/assets/unicons/Emblem/14.svg new file mode 100644 index 0000000..d706fce --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/14.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/15.svg b/apps/mobile/src/assets/unicons/Emblem/15.svg new file mode 100644 index 0000000..3945fe8 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/15.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/16.svg b/apps/mobile/src/assets/unicons/Emblem/16.svg new file mode 100644 index 0000000..05bbcb6 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/16.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/17.svg b/apps/mobile/src/assets/unicons/Emblem/17.svg new file mode 100644 index 0000000..1eb62d0 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/17.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/18.svg b/apps/mobile/src/assets/unicons/Emblem/18.svg new file mode 100644 index 0000000..5439a15 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/18.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/19.svg b/apps/mobile/src/assets/unicons/Emblem/19.svg new file mode 100644 index 0000000..30f949b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/19.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/2.svg b/apps/mobile/src/assets/unicons/Emblem/2.svg new file mode 100644 index 0000000..ea30ad6 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/2.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/20.svg b/apps/mobile/src/assets/unicons/Emblem/20.svg new file mode 100644 index 0000000..47ae7cd --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/20.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/21.svg b/apps/mobile/src/assets/unicons/Emblem/21.svg new file mode 100644 index 0000000..ebc25fc --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/21.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/22.svg b/apps/mobile/src/assets/unicons/Emblem/22.svg new file mode 100644 index 0000000..51da6d5 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/22.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/23.svg b/apps/mobile/src/assets/unicons/Emblem/23.svg new file mode 100644 index 0000000..4554798 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/23.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/24.svg b/apps/mobile/src/assets/unicons/Emblem/24.svg new file mode 100644 index 0000000..c6c115a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/24.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/25.svg b/apps/mobile/src/assets/unicons/Emblem/25.svg new file mode 100644 index 0000000..c8d95d9 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/25.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/26.svg b/apps/mobile/src/assets/unicons/Emblem/26.svg new file mode 100644 index 0000000..ff9e0f8 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/26.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/27.svg b/apps/mobile/src/assets/unicons/Emblem/27.svg new file mode 100644 index 0000000..aa3871a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/27.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/28.svg b/apps/mobile/src/assets/unicons/Emblem/28.svg new file mode 100644 index 0000000..d0816e1 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/28.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/29.svg b/apps/mobile/src/assets/unicons/Emblem/29.svg new file mode 100644 index 0000000..3357513 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/29.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/3.svg b/apps/mobile/src/assets/unicons/Emblem/3.svg new file mode 100644 index 0000000..f947233 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/3.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/30.svg b/apps/mobile/src/assets/unicons/Emblem/30.svg new file mode 100644 index 0000000..33eec60 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/30.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/31.svg b/apps/mobile/src/assets/unicons/Emblem/31.svg new file mode 100644 index 0000000..f6bb3ab --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/31.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/32.svg b/apps/mobile/src/assets/unicons/Emblem/32.svg new file mode 100644 index 0000000..5d8ee19 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/32.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/33.svg b/apps/mobile/src/assets/unicons/Emblem/33.svg new file mode 100644 index 0000000..facc996 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/33.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/34.svg b/apps/mobile/src/assets/unicons/Emblem/34.svg new file mode 100644 index 0000000..9e57669 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/34.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/35.svg b/apps/mobile/src/assets/unicons/Emblem/35.svg new file mode 100644 index 0000000..73a633b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/35.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/36.svg b/apps/mobile/src/assets/unicons/Emblem/36.svg new file mode 100644 index 0000000..eb7bb2f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/36.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/37.svg b/apps/mobile/src/assets/unicons/Emblem/37.svg new file mode 100644 index 0000000..7622c44 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/37.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/38.svg b/apps/mobile/src/assets/unicons/Emblem/38.svg new file mode 100644 index 0000000..6967df4 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/38.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/39.svg b/apps/mobile/src/assets/unicons/Emblem/39.svg new file mode 100644 index 0000000..356b257 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/39.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/4.svg b/apps/mobile/src/assets/unicons/Emblem/4.svg new file mode 100644 index 0000000..53a1e1a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/4.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/40.svg b/apps/mobile/src/assets/unicons/Emblem/40.svg new file mode 100644 index 0000000..111449d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/40.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/41.svg b/apps/mobile/src/assets/unicons/Emblem/41.svg new file mode 100644 index 0000000..df1b720 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/41.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/42.svg b/apps/mobile/src/assets/unicons/Emblem/42.svg new file mode 100644 index 0000000..a0ebfc3 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/42.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/43.svg b/apps/mobile/src/assets/unicons/Emblem/43.svg new file mode 100644 index 0000000..d6a07b7 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/43.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/44.svg b/apps/mobile/src/assets/unicons/Emblem/44.svg new file mode 100644 index 0000000..8718905 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/44.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/45.svg b/apps/mobile/src/assets/unicons/Emblem/45.svg new file mode 100644 index 0000000..0dba11f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/45.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/46.svg b/apps/mobile/src/assets/unicons/Emblem/46.svg new file mode 100644 index 0000000..cadc31a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/46.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/47.svg b/apps/mobile/src/assets/unicons/Emblem/47.svg new file mode 100644 index 0000000..c288b0f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/47.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/48.svg b/apps/mobile/src/assets/unicons/Emblem/48.svg new file mode 100644 index 0000000..7835a13 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/48.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/49.svg b/apps/mobile/src/assets/unicons/Emblem/49.svg new file mode 100644 index 0000000..90a7c08 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/49.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/5.svg b/apps/mobile/src/assets/unicons/Emblem/5.svg new file mode 100644 index 0000000..89fa50b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/5.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/50.svg b/apps/mobile/src/assets/unicons/Emblem/50.svg new file mode 100644 index 0000000..cc2c298 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/50.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/51.svg b/apps/mobile/src/assets/unicons/Emblem/51.svg new file mode 100644 index 0000000..612f629 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/51.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/52.svg b/apps/mobile/src/assets/unicons/Emblem/52.svg new file mode 100644 index 0000000..46ae714 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/52.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/53.svg b/apps/mobile/src/assets/unicons/Emblem/53.svg new file mode 100644 index 0000000..1af5478 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/53.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/54.svg b/apps/mobile/src/assets/unicons/Emblem/54.svg new file mode 100644 index 0000000..a99eceb --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/54.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/55.svg b/apps/mobile/src/assets/unicons/Emblem/55.svg new file mode 100644 index 0000000..a7ac2d3 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/55.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/56.svg b/apps/mobile/src/assets/unicons/Emblem/56.svg new file mode 100644 index 0000000..6f51ad9 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/56.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/57.svg b/apps/mobile/src/assets/unicons/Emblem/57.svg new file mode 100644 index 0000000..26fc942 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/57.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/58.svg b/apps/mobile/src/assets/unicons/Emblem/58.svg new file mode 100644 index 0000000..19aec8c --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/58.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/59.svg b/apps/mobile/src/assets/unicons/Emblem/59.svg new file mode 100644 index 0000000..c2b734a --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/59.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/6.svg b/apps/mobile/src/assets/unicons/Emblem/6.svg new file mode 100644 index 0000000..1321f6c --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/6.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/60.svg b/apps/mobile/src/assets/unicons/Emblem/60.svg new file mode 100644 index 0000000..14a31a9 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/60.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/61.svg b/apps/mobile/src/assets/unicons/Emblem/61.svg new file mode 100644 index 0000000..7c74f7c --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/61.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/62.svg b/apps/mobile/src/assets/unicons/Emblem/62.svg new file mode 100644 index 0000000..d201632 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/62.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/63.svg b/apps/mobile/src/assets/unicons/Emblem/63.svg new file mode 100644 index 0000000..501c53e --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/63.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/64.svg b/apps/mobile/src/assets/unicons/Emblem/64.svg new file mode 100644 index 0000000..50e3297 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/64.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/65.svg b/apps/mobile/src/assets/unicons/Emblem/65.svg new file mode 100644 index 0000000..90ced29 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/65.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/66.svg b/apps/mobile/src/assets/unicons/Emblem/66.svg new file mode 100644 index 0000000..3d7a479 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/66.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/67.svg b/apps/mobile/src/assets/unicons/Emblem/67.svg new file mode 100644 index 0000000..4d3a860 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/67.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/68.svg b/apps/mobile/src/assets/unicons/Emblem/68.svg new file mode 100644 index 0000000..2f89f6d --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/68.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/69.svg b/apps/mobile/src/assets/unicons/Emblem/69.svg new file mode 100644 index 0000000..aa5f088 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/69.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/7.svg b/apps/mobile/src/assets/unicons/Emblem/7.svg new file mode 100644 index 0000000..4b64e40 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/7.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/70.svg b/apps/mobile/src/assets/unicons/Emblem/70.svg new file mode 100644 index 0000000..de0d26b --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/70.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/71.svg b/apps/mobile/src/assets/unicons/Emblem/71.svg new file mode 100644 index 0000000..d5c7c0f --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/71.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/72.svg b/apps/mobile/src/assets/unicons/Emblem/72.svg new file mode 100644 index 0000000..18ff3cf --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/72.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/73.svg b/apps/mobile/src/assets/unicons/Emblem/73.svg new file mode 100644 index 0000000..eca90a9 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/73.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/74.svg b/apps/mobile/src/assets/unicons/Emblem/74.svg new file mode 100644 index 0000000..90f26e0 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/74.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/75.svg b/apps/mobile/src/assets/unicons/Emblem/75.svg new file mode 100644 index 0000000..bdff434 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/75.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/8.svg b/apps/mobile/src/assets/unicons/Emblem/8.svg new file mode 100644 index 0000000..e24b5c8 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/8.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/assets/unicons/Emblem/9.svg b/apps/mobile/src/assets/unicons/Emblem/9.svg new file mode 100644 index 0000000..8493ce4 --- /dev/null +++ b/apps/mobile/src/assets/unicons/Emblem/9.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/mobile/src/components/AnimatedNumber.tsx b/apps/mobile/src/components/AnimatedNumber.tsx new file mode 100644 index 0000000..6f247a8 --- /dev/null +++ b/apps/mobile/src/components/AnimatedNumber.tsx @@ -0,0 +1,355 @@ +import { SCREEN_WIDTH } from '@gorhom/bottom-sheet' +import React, { useEffect, useState } from 'react' +import { I18nManager, LayoutChangeEvent, StyleSheet } from 'react-native' +import Animated, { + FadeIn, + FadeOut, + Layout, + useAnimatedStyle, + useSharedValue, + withDelay, + withSequence, + withTiming, +} from 'react-native-reanimated' +import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg' +import { AnimatedFlex, Flex, Shine, useSporeColors } from 'ui/src' +import { TextLoaderWrapper } from 'ui/src/components/text/Text' +import { fonts } from 'ui/src/theme' +import { usePrevious } from 'utilities/src/react/hooks' + +export const NUMBER_ARRAY = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'] +export const NUMBER_WIDTH_ARRAY = [29, 20, 29, 29, 29, 29, 29, 29, 29, 29] // width of digits in a font +export const DIGIT_HEIGHT = 44 +export const ADDITIONAL_WIDTH_FOR_ANIMATIONS = 8 + +// TODO: remove need to manually define width of each character +const NUMBER_WIDTH_ARRAY_SCALED = NUMBER_WIDTH_ARRAY.map( + (width) => width * (fonts.heading2.fontSize / fonts.heading1.fontSize) +) + +const isRTL = I18nManager.isRTL + +const margin = { + // add negative margin to the correct side of each character + marginRight: isRTL ? 0 : -ADDITIONAL_WIDTH_FOR_ANIMATIONS, + marginLeft: isRTL ? -ADDITIONAL_WIDTH_FOR_ANIMATIONS : 0, +} + +const RollNumber = ({ + digit, + nextColor, + index, + chars, + commonPrefixLength, + shouldFadeDecimals, +}: { + chars: string[] + digit?: string + nextColor?: string + index: number + commonPrefixLength: number + shouldFadeDecimals: boolean +}): JSX.Element => { + const colors = useSporeColors() + const fontColor = useSharedValue( + nextColor || + (shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val) + ) + const yOffset = useSharedValue(digit && Number(digit) >= 0 ? DIGIT_HEIGHT * -digit : 0) + + useEffect(() => { + const finishColor = + shouldFadeDecimals && index > chars.length - 4 ? colors.neutral3.val : colors.neutral1.val + if (nextColor && index > commonPrefixLength - 1) { + fontColor.value = withSequence( + withTiming(nextColor, { duration: 250 }), + withDelay(50, withTiming(finishColor, { duration: 310 })) + ) + } else { + fontColor.value = finishColor + } + }, [ + digit, + nextColor, + colors.neutral3, + index, + chars.length, + colors.neutral1, + commonPrefixLength, + fontColor, + shouldFadeDecimals, + ]) + + const animatedFontStyle = useAnimatedStyle(() => { + return { + color: fontColor.value, + } + }) + + const numbers = NUMBER_ARRAY.map((char, idx) => { + return ( + + {char} + + ) + }) + + useEffect(() => { + if (digit && Number(digit) >= 0) { + yOffset.value = withTiming(DIGIT_HEIGHT * -digit) + } + }) + + const animatedWrapperStyle = useAnimatedStyle(() => { + return { + transform: [{ translateY: yOffset.value }], + } + }) + if (digit && !Number.isNaN(parseFloat(digit)) && Number(digit) >= 0) { + return ( + + {numbers} + + ) + } else { + return ( + + {digit} + + ) + } +} + +const Char = ({ + index, + chars, + nextColor, + commonPrefixLength, + shouldFadeDecimals, +}: { + index: number + chars: string[] + nextColor?: string + commonPrefixLength: number + shouldFadeDecimals: boolean +}): JSX.Element => { + return ( + + + + ) +} + +function longestCommonPrefix(a: string, b: string): string { + let i = 0 + while (a[i] && b[i] && a[i] === b[i]) { + i++ + } + return a.substr(0, i) +} + +export const TopAndBottomGradient = (): JSX.Element => { + const colors = useSporeColors() + + return ( + + + + + + + + + + + + + + + ) +} + +const SCREEN_WIDTH_BUFFER = 50 + +// Used for initial layout larger than all screen sizes +const MAX_DEVICE_WIDTH = 1000 + +const AnimatedNumber = ({ + value, + loading = false, + loadingPlaceholderText, + colorIndicationDuration, + shouldFadeDecimals, + warmLoading, +}: { + loadingPlaceholderText: string + loading: boolean | 'no-shimmer' + value?: string + colorIndicationDuration: number + shouldFadeDecimals: boolean + warmLoading: boolean +}): JSX.Element => { + const prevValue = usePrevious(value) + const [chars, setChars] = useState() + const [commonPrefixLength, setCommonPrefixLength] = useState(0) + const [nextColor, setNextColor] = useState() + const scale = useSharedValue(1) + const offset = useSharedValue(0) + + const colors = useSporeColors() + + const scaleWraper = useAnimatedStyle(() => { + return { + transform: [ + { translateX: -SCREEN_WIDTH / 2 }, + { scale: scale.value }, + { translateX: SCREEN_WIDTH / 2 }, + ], + } + }) + + const fitBalanceOnLayout = (e: LayoutChangeEvent): void => { + const newScale = (SCREEN_WIDTH - SCREEN_WIDTH_BUFFER) / e.nativeEvent.layout.width + + if (newScale < 1) { + const newOffset = (e.nativeEvent.layout.width - e.nativeEvent.layout.width * newScale) / 2 + scale.value = withTiming(newScale) + offset.value = withTiming(-newOffset) + } else if (scale.value < 1) { + scale.value = withTiming(1) + offset.value = withTiming(0) + } + } + + useEffect(() => { + if (value && prevValue !== value) { + if (prevValue && value > prevValue) { + setNextColor(colors.statusSuccess.val) + } else if (prevValue && value < prevValue) { + setNextColor(colors.neutral2.val) + } else { + setNextColor(undefined) + } + const newChars = value.split('') + setChars(newChars) + setCommonPrefixLength(longestCommonPrefix(prevValue ?? '', value).length) + setTimeout(() => { + setNextColor(undefined) + }, colorIndicationDuration) + } + }, [colorIndicationDuration, colors.neutral2, colors.statusSuccess.val, prevValue, value]) + + if (loading) { + const placeholderChars = [...loadingPlaceholderText] + + return ( + + + {placeholderChars.map((_, index) => ( + + ))} + + + ) + } + + return ( + + + + + + {chars?.map((_, index) => ( + + ))} + + + + {value} + + + + ) +} + +export default AnimatedNumber + +export const AnimatedNumberStyles = StyleSheet.create({ + gradientStyle: { + position: 'absolute', + zIndex: 100, + }, +}) + +export const AnimatedCharStyles = StyleSheet.create({ + wrapperStyle: { + overflow: 'hidden', + }, +}) + +export const AnimatedFontStyles = StyleSheet.create({ + fontStyle: { + fontFamily: fonts.heading2.family, + fontSize: fonts.heading2.fontSize, + // special case for the home screen balance, instead of using the heading2 font weight + fontWeight: '500', + lineHeight: fonts.heading2.lineHeight, + top: 1, + }, + invisible: { + opacity: 0, + position: 'absolute', + }, +}) diff --git a/apps/mobile/src/components/DevelopmentOnly/DevelopmentOnly.tsx b/apps/mobile/src/components/DevelopmentOnly/DevelopmentOnly.tsx new file mode 100644 index 0000000..57673b7 --- /dev/null +++ b/apps/mobile/src/components/DevelopmentOnly/DevelopmentOnly.tsx @@ -0,0 +1,9 @@ +import React, { PropsWithChildren } from 'react' + +export function DevelopmentOnly({ children }: PropsWithChildren): JSX.Element | null { + if (!__DEV__ || !children) { + return null + } + + return <>{children} +} diff --git a/apps/mobile/src/components/DevelopmentOnly/UniconSampleSheet.tsx b/apps/mobile/src/components/DevelopmentOnly/UniconSampleSheet.tsx new file mode 100644 index 0000000..6a6d709 --- /dev/null +++ b/apps/mobile/src/components/DevelopmentOnly/UniconSampleSheet.tsx @@ -0,0 +1,34 @@ +import { Flex, UniconV2 } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +const generateRandomEthereumAddresses = (numberOfAddresses: number): string[] => { + const addresses = [] + for (let i = 0; i < numberOfAddresses; i++) { + const randomHex = [...Array(40)].map(() => Math.floor(Math.random() * 16).toString(16)).join('') + addresses.push(`0x${randomHex}`) + } + return addresses +} + +export const UniconSampleSheet = ({ onClose }: { onClose: () => void }): JSX.Element => { + return ( + + + + {generateRandomEthereumAddresses(80).map((address) => { + return ( + + + + ) + })} + + + + ) +} diff --git a/apps/mobile/src/components/DevelopmentOnly/utils.ts b/apps/mobile/src/components/DevelopmentOnly/utils.ts new file mode 100644 index 0000000..13162d0 --- /dev/null +++ b/apps/mobile/src/components/DevelopmentOnly/utils.ts @@ -0,0 +1,87 @@ +import { useEffect, useState } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' + +export const exampleDisconnectedNotification = { + type: 2, + // address: '0x...', + dappName: 'Uniswap Interface', + event: 1, + imageUrl: 'https://app.uniswap.org/favicon.png', + hideDelay: 3000, +} + +export const exampleSwapConfirmation = { + type: 7, + chainId: 42161, + hideDelay: 2000, +} + +export const exampleSwapSuccess = { + txStatus: 'failed', + chainId: 42161, + txHash: '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + // address: '0x...', + // txId: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', + type: 3, + txType: 'swap', + inputCurrencyId: '42161-0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + outputCurrencyId: '42161-0xf97f4df75117a78c1A5a0DBb814Af92458539FB4', + inputCurrencyAmountRaw: '10000000000000000', + outputCurrencyAmountRaw: '1356219232855702996', + tradeType: 0, + hideDelay: 3000, +} + +// easiest to use inside NotificationToastWrapper before any returns +export const useMockNotification = (ms?: number): void => { + const [sent, setSent] = useState(false) + const dispatch = useAppDispatch() + const activeAddress = useActiveAccountAddressWithThrow() + + useEffect(() => { + setSent(false) + }, [ms]) + + useEffect(() => { + if (!sent && activeAddress) { + dispatch( + pushNotification({ + ...exampleSwapSuccess, + hideDelay: ms ?? exampleSwapSuccess.hideDelay, + address: activeAddress, + }) + ) + setSent(true) + } + }, [activeAddress, dispatch, ms, sent]) +} + +const generateRandomId = (): string => { + let randomId = '0x' + for (let i = 0; i < 40; i++) { + randomId += Math.floor(Math.random() * 16).toString(16) + } + return randomId +} + +const generateRandomDate = (): number => { + const start = new Date(2023, 4, 12) + const end = new Date() + return Math.floor( + new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).getTime() / 1000 + ) +} + +export const useMockCloudBackups = (numberOfBackups?: number): CloudStorageMnemonicBackup[] => { + const number = numberOfBackups ?? 1 + + const mockBackups = Array.from({ length: number }, () => ({ + mnemonicId: generateRandomId(), + createdAt: generateRandomDate(), + })) + + return mockBackups +} diff --git a/apps/mobile/src/components/NFT/NftView.tsx b/apps/mobile/src/components/NFT/NftView.tsx new file mode 100644 index 0000000..d986757 --- /dev/null +++ b/apps/mobile/src/components/NFT/NftView.tsx @@ -0,0 +1,68 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import ContextMenu from 'react-native-context-menu-view' +import { useNFTMenu } from 'src/features/nfts/hooks' +import { Flex, TouchableArea } from 'ui/src' +import { borderRadii } from 'ui/src/theme' +import noop from 'utilities/src/react/noop' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { + ESTIMATED_NFT_LIST_ITEM_SIZE, + MAX_NFT_IMAGE_SIZE, +} from 'wallet/src/features/nfts/constants' +import { NFTItem } from 'wallet/src/features/nfts/types' + +export function NftView({ + owner, + item, + onPress, +}: { + owner: Address + item: NFTItem + onPress: () => void +}): JSX.Element { + const { menuActions, onContextMenuPress } = useNFTMenu({ + contractAddress: item.contractAddress, + tokenId: item.tokenId, + owner, + isSpam: item.isSpam, + }) + + return ( + + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx b/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx new file mode 100644 index 0000000..8635bb3 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/AnimatedDecimalNumber.tsx @@ -0,0 +1,108 @@ +import React, { memo, useMemo } from 'react' +import { useWindowDimensions } from 'react-native' +import { useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' +import { AnimatedText } from 'src/components/text/AnimatedText' +import { Flex, useDeviceDimensions, useSporeColors } from 'ui/src' +import { TextVariantTokens, fonts } from 'ui/src/theme' +import { ValueAndFormatted } from './usePrice' + +type AnimatedDecimalNumberProps = { + number: ValueAndFormatted + separator: string + variant: TextVariantTokens + wholePartColor?: string + decimalPartColor?: string + decimalThreshold?: number // below this value (not including) decimal part would have wholePartColor too + testID?: string + maxWidth?: number + maxCharPixelWidth?: number +} + +/** + * TODO(MOB-1948): AnimatePresence should be able to do this: + * + * Example: https://gist.github.com/natew/e773fa3bdc99f75a3b28f21db168a449 + * + */ + +// Utility component to display decimal numbers where the decimal part +// is dimmed using AnimatedText +export const AnimatedDecimalNumber = memo(function AnimatedDecimalNumber( + props: AnimatedDecimalNumberProps +): JSX.Element { + const colors = useSporeColors() + const { fullWidth } = useDeviceDimensions() + const { fontScale } = useWindowDimensions() + + const { + number, + separator, + variant, + wholePartColor = colors.neutral1.val, + decimalPartColor = colors.neutral3.val, + decimalThreshold = 1, + testID, + maxWidth = fullWidth, + maxCharPixelWidth: maxCharPixelWidthProp, + } = props + + const wholePart = useDerivedValue( + () => number.formatted.value.split(separator)[0] || '', + [number, separator] + ) + const decimalPart = useDerivedValue( + () => separator + (number.formatted.value.split(separator)[1] || ''), + [number, separator] + ) + + const wholeStyle = useMemo(() => { + return { + color: wholePartColor, + } + }, [wholePartColor]) + + const decimalStyle = useAnimatedStyle(() => { + return { + color: number.value.value < decimalThreshold ? wholePartColor : decimalPartColor, + } + }, [number.value, decimalThreshold, wholePartColor, decimalPartColor]) + + const fontSize = fonts[variant].fontSize * fontScale + // Choose the arbitrary value that looks good for the font used + const maxCharPixelWidth = maxCharPixelWidthProp ?? (2 / 3) * fontSize + + const adjustedFontSize = useDerivedValue(() => { + const value = number.formatted.value + const approxWidth = value.length * maxCharPixelWidth + + if (approxWidth <= maxWidth) { + return fontSize + } + + const scale = Math.min(1, maxWidth / approxWidth) + return fontSize * scale + }) + + const animatedStyle = useAnimatedStyle(() => ({ + fontSize: adjustedFontSize.value, + })) + + return ( + + + {decimalPart.value !== separator && ( + + )} + + ) +}) diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx new file mode 100644 index 0000000..45357d3 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorer.tsx @@ -0,0 +1,200 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import { memo, useMemo } from 'react' +import { I18nManager } from 'react-native' +import { SharedValue, useDerivedValue } from 'react-native-reanimated' +import { LineChart, LineChartProvider } from 'react-native-wagmi-charts' +import PriceExplorerAnimatedNumber from 'src/components/PriceExplorer/PriceExplorerAnimatedNumber' +import { PriceExplorerError } from 'src/components/PriceExplorer/PriceExplorerError' +import { DatetimeText, RelativeChangeText } from 'src/components/PriceExplorer/Text' +import { TimeRangeGroup } from 'src/components/PriceExplorer/TimeRangeGroup' +import { CURSOR_INNER_SIZE, CURSOR_SIZE } from 'src/components/PriceExplorer/constants' +import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' +import { useLineChartPrice } from 'src/components/PriceExplorer/usePrice' +import { Loader } from 'src/components/loading' +import { invokeImpact } from 'src/utils/haptic' +import { Flex } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { CurrencyId } from 'wallet/src/utils/currencyId' +import { PriceNumberOfDigits, TokenSpotData, useTokenPriceHistory } from './usePriceHistory' + +type PriceTextProps = { + loading: boolean + relativeChange?: SharedValue + numberOfDigits: PriceNumberOfDigits + spotPrice?: SharedValue +} + +function PriceTextSection({ loading, numberOfDigits, spotPrice }: PriceTextProps): JSX.Element { + const price = useLineChartPrice(spotPrice) + const currency = useAppFiatCurrencyInfo() + const mx = spacing.spacing12 + + return ( + + + + + + + + ) +} + +export type LineChartPriceAndDateTimeTextProps = { + currencyId: CurrencyId +} + +export const PriceExplorer = memo(function PriceExplorer({ + currencyId, + tokenColor, + forcePlaceholder, + onRetry, +}: { + currencyId: string + tokenColor?: string + forcePlaceholder?: boolean + onRetry: () => void +}): JSX.Element { + const { data, loading, error, refetch, setDuration, selectedDuration, numberOfDigits } = + useTokenPriceHistory(currencyId) + + const { convertFiatAmount } = useLocalizationContext() + const conversionRate = convertFiatAmount().amount + const shouldShowAnimatedDot = + selectedDuration === HistoryDuration.Day || selectedDuration === HistoryDuration.Hour + const additionalPadding = shouldShowAnimatedDot ? 40 : 0 + + const { lastPricePoint, convertedPriceHistory } = useMemo(() => { + const priceHistory = data?.priceHistory?.map((point) => { + return { ...point, value: point.value * conversionRate } + }) + + const lastPoint = priceHistory ? priceHistory.length - 1 : 0 + + return { lastPricePoint: lastPoint, convertedPriceHistory: priceHistory } + }, [data, conversionRate]) + + const convertedSpotValue = useDerivedValue(() => conversionRate * (data?.spot?.value?.value ?? 0)) + const convertedSpot = useMemo((): TokenSpotData | undefined => { + return ( + data?.spot && { + ...data?.spot, + value: convertedSpotValue, + } + ) + }, [data, convertedSpotValue]) + + if ( + !loading && + (!convertedPriceHistory || (!convertedSpot && selectedDuration === HistoryDuration.Day)) + ) { + // Propagate retry up while refetching, if available + const refetchAndRetry = (): void => { + if (refetch) { + refetch() + } + onRetry() + } + return + } + + let content: JSX.Element | null + if (forcePlaceholder) { + content = + } else if (convertedPriceHistory?.length) { + content = ( + // TODO(MOB-2308): add better loading state + + + + ) + } else { + content = + } + + return ( + + + + {content} + + + + ) +}) + +function PriceExplorerPlaceholder(): JSX.Element { + return ( + + + + ) +} + +function PriceExplorerChart({ + tokenColor, + additionalPadding, + shouldShowAnimatedDot, + lastPricePoint, +}: { + tokenColor?: string + additionalPadding: number + shouldShowAnimatedDot: boolean + lastPricePoint: number +}): JSX.Element { + const { chartHeight, chartWidth } = useChartDimensions() + const isRTL = I18nManager.isRTL + + return ( + // TODO(MOB-2166): remove forced LTR direction + scaleX horizontal flip technique once react-native-wagmi-charts fixes this: https://github.com/coinjar/react-native-wagmi-charts/issues/136 + + + + {shouldShowAnimatedDot && ( + + )} + + + + + + ) +} diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx new file mode 100644 index 0000000..10626f4 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorerAnimatedNumber.tsx @@ -0,0 +1,385 @@ +import _ from 'lodash' +import React, { useEffect, useState } from 'react' +import { StyleSheet, Text, View } from 'react-native' +import Animated, { + SharedValue, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withSpring, + withTiming, +} from 'react-native-reanimated' +import { + ADDITIONAL_WIDTH_FOR_ANIMATIONS, + AnimatedCharStyles, + DIGIT_HEIGHT, + NUMBER_ARRAY, + NUMBER_WIDTH_ARRAY, + TopAndBottomGradient, +} from 'src/components/AnimatedNumber' +import { ValueAndFormattedWithAnimation } from 'src/components/PriceExplorer/usePrice' +import { PriceNumberOfDigits } from 'src/components/PriceExplorer/usePriceHistory' +import { useSporeColors } from 'ui/src' +import { TextLoaderWrapper } from 'ui/src/components/text/Text' +import { fonts } from 'ui/src/theme' +import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' + +// if price per token has > 3 numbers before the decimal, start showing decimals in neutral3 +// otherwise, show entire price in neutral1 +const DEEMPHASIZED_DECIMALS_THRESHOLD = 3 + +const getEmphasizedNumberColor = ( + index: number, + commaIndex: number, + emphasizedColor: string, + deemphasizedColor: string +): string => { + if (index >= commaIndex && commaIndex > DEEMPHASIZED_DECIMALS_THRESHOLD) { + return deemphasizedColor + } + return emphasizedColor +} + +const shouldUseSeparator = ( + index: number, + commaIndex: number, + decimalPlaceIndex: number +): boolean => { + 'worklet' + return ( + (index - commaIndex) % 4 === 0 && + index - commaIndex < 0 && + index > commaIndex - decimalPlaceIndex + ) +} + +const NumbersMain = ({ + color, + backgroundColor, + hidePlaceholder, +}: { + color: string + backgroundColor: string + hidePlaceholder(): void +}): JSX.Element | null => { + const [showNumbers, setShowNumbers] = useState(false) + const hideNumbers = useSharedValue(true) + + const animatedTextStyle = useAnimatedStyle(() => { + return { + opacity: hideNumbers.value ? 0 : 1, + } + }) + + useEffect(() => { + setTimeout(() => { + setShowNumbers(true) + }, 200) + }, []) + + const onLayout = (): void => { + hidePlaceholder() + hideNumbers.value = false + } + + if (showNumbers) { + return ( + + {NUMBER_ARRAY} + + ) + } + + return null +} + +const MemoizedNumbers = React.memo(NumbersMain) + +const RollNumber = ({ + chars, + index, + shouldAnimate, + decimalPlace, + hidePlaceholder, + commaIndex, + currency, +}: { + chars: SharedValue + index: number + shouldAnimate: SharedValue + decimalPlace: SharedValue + hidePlaceholder(): void + commaIndex: number + currency: FiatCurrencyInfo +}): JSX.Element => { + const colors = useSporeColors() + const numberColor = getEmphasizedNumberColor( + index, + commaIndex, + colors.neutral1.val, + colors.neutral3.val + ) + + const animatedDigit = useDerivedValue(() => { + const char = chars.value[index - (commaIndex - decimalPlace.value)] + const number = char ? parseFloat(char) : undefined + return Number.isNaN(number) ? undefined : number + }, [chars, commaIndex, decimalPlace, index]) + + const animatedFontStyle = useAnimatedStyle(() => { + return { + color: numberColor, + } + }) + + const transformY = useDerivedValue(() => { + const endValue = animatedDigit.value !== undefined ? DIGIT_HEIGHT * -animatedDigit.value : 0 + + return shouldAnimate.value + ? withSpring(endValue, { + mass: 1, + damping: 29, + stiffness: 164, + overshootClamping: false, + restDisplacementThreshold: 0.01, + restSpeedThreshold: 2, + }) + : endValue + }, [animatedDigit, shouldAnimate]) + + const animatedWrapperStyle = useAnimatedStyle(() => { + const digitWidth = + animatedDigit.value !== undefined ? NUMBER_WIDTH_ARRAY[animatedDigit.value] ?? 0 : 0 + const rowWidth = digitWidth + ADDITIONAL_WIDTH_FOR_ANIMATIONS - 7 + + return { + transform: [ + { + translateY: transformY.value, + }, + ], + width: shouldAnimate.value ? withTiming(rowWidth) : rowWidth, + } + }) + + // need it in case the current value is eg $999.00 but maximum value in chart is more than $1,000.00 + // so it can hide the comma to avoid something like $,999.00 + const animatedWrapperSeparatorStyle = useAnimatedStyle(() => { + if (!shouldUseSeparator(index, commaIndex, decimalPlace.value)) { + return { + width: withTiming(0), + } + } + + const digitWidth = + chars.value[index - (commaIndex - decimalPlace.value)] === currency.groupingSeparator ? 8 : 0 + + const rowWidth = Math.max(digitWidth, 0) + + return { + transform: [ + { + translateY: transformY.value, + }, + ], + width: shouldAnimate.value ? withTiming(rowWidth) : rowWidth, + } + }) + + if (index === commaIndex) { + return ( + + {currency.decimalSeparator} + + ) + } + + if ((index - commaIndex) % 4 === 0 && index - commaIndex < 0) { + return ( + + + {currency.groupingSeparator} + + + ) + } + + return ( + + + + ) +} + +const Numbers = ({ + price, + hidePlaceholder, + numberOfDigits, + currency, +}: { + price: ValueAndFormattedWithAnimation + hidePlaceholder(): void + numberOfDigits: PriceNumberOfDigits + currency: FiatCurrencyInfo +}): JSX.Element[] => { + const chars = useDerivedValue(() => { + return price.formatted.value + }, [price]) + + const decimalPlace = useDerivedValue(() => { + return price.formatted.value.indexOf(currency.decimalSeparator) + }, [currency.decimalSeparator, price.formatted]) + + const commaIndex = numberOfDigits.left + Math.floor((numberOfDigits.left - 1) / 3) + + return _.times( + numberOfDigits.left + numberOfDigits.right + Math.floor((numberOfDigits.left - 1) / 3) + 1, + (index) => ( + + + + ) + ) +} + +const LoadingWrapper = (): JSX.Element | null => { + return ( + + + + ) +} + +const PriceExplorerAnimatedNumber = ({ + price, + numberOfDigits, + currency, +}: { + price: ValueAndFormattedWithAnimation + numberOfDigits: PriceNumberOfDigits + currency: FiatCurrencyInfo +}): JSX.Element => { + const colors = useSporeColors() + const hideShimmer = useSharedValue(false) + const animatedWrapperStyle = useAnimatedStyle(() => { + return { + opacity: price.value.value > 0 && hideShimmer.value ? 0 : 1, + position: 'absolute', + zIndex: 1000, + backgroundColor: colors.surface1.val, + } + }) + + const lessThanStyle = useAnimatedStyle(() => { + return { + width: price.formatted.value[0] === '<' ? withTiming(22) : withTiming(0), + } + }) + + const hidePlaceholder = (): void => { + hideShimmer.value = true + } + + const currencySymbol = ( + + {currency.fullSymbol} + + ) + + const lessThanSymbol = ( + + {'<'} + + ) + + return ( + <> + + + + + + {lessThanSymbol} + {currency.symbolAtFront && currencySymbol} + {Numbers({ price, hidePlaceholder, numberOfDigits, currency })} + {!currency.symbolAtFront && currencySymbol} + + + ) +} + +export default PriceExplorerAnimatedNumber + +export const RowWrapper = StyleSheet.create({ + wrapperStyle: { + flexDirection: 'row', + }, +}) + +export const Shimmer = StyleSheet.create({ + shimmerSize: { + height: DIGIT_HEIGHT, + width: 200, + }, +}) + +const AnimatedFontStyles = StyleSheet.create({ + fontStyle: { + fontFamily: fonts.heading2.family, + fontSize: fonts.heading2.fontSize, + fontWeight: fonts.heading2.fontWeight, + lineHeight: fonts.heading2.lineHeight, + }, +}) diff --git a/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx new file mode 100644 index 0000000..a724068 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/PriceExplorerError.tsx @@ -0,0 +1,40 @@ +import React, { ComponentProps } from 'react' +import { useTranslation } from 'react-i18next' +import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' +import { Flex, Text } from 'ui/src' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' + +export function PriceExplorerError({ + showRetry, + onRetry, +}: Pick, 'onRetry'> & { + showRetry: boolean +}): JSX.Element { + const { t } = useTranslation() + const { chartHeight } = useChartDimensions() + + return ( + + + + { + '\u2013' // em dash + } + + + + + + + ) +} diff --git a/apps/mobile/src/components/PriceExplorer/Text.test.tsx b/apps/mobile/src/components/PriceExplorer/Text.test.tsx new file mode 100644 index 0000000..0b85c64 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/Text.test.tsx @@ -0,0 +1,115 @@ +import React from 'react' +import * as charts from 'react-native-wagmi-charts' +import { DatetimeText, PriceText, RelativeChangeText } from 'src/components/PriceExplorer/Text' +import { render, within } from 'src/test/test-utils' +import { amounts } from 'wallet/src/test/fixtures' + +jest.mock('react-native-wagmi-charts') +const mockedUseLineChartPrice = charts.useLineChartPrice as jest.Mock +const mockedUseLineChart = charts.useLineChart as jest.Mock +const mockedUseLineChartDatetime = charts.useLineChartDatetime as jest.Mock + +describe(PriceText, () => { + it('renders without error', () => { + mockedUseLineChartPrice.mockReturnValue({ value: '' }) + mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.md().value }] }) + + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('renders without error less than a dollar', () => { + mockedUseLineChartPrice.mockReturnValue({ value: '' }) + mockedUseLineChart.mockReturnValue({ data: [{ timestamp: 0, value: amounts.xs().value }] }) + + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('renders loading state', () => { + mockedUseLineChartPrice.mockReturnValue({ value: '' }) + mockedUseLineChart.mockReturnValue({ data: [] }) + + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('shows active price when scrubbing', async () => { + mockedUseLineChartPrice.mockReturnValue({ + value: { value: amounts.sm().value.toString() }, + }) + + const tree = render() + + const animatedText = await tree.findByTestId('price-text') + const wholePart = await within(animatedText).findByTestId('wholePart') + const decimalPart = await within(animatedText).findByTestId('decimalPart') + + expect(wholePart.props.text).toBe(`$${amounts.sm().value}`) + expect(decimalPart.props.text).toBe(`.00`) + }) +}) + +describe(RelativeChangeText, () => { + it('renders without error', () => { + mockedUseLineChart.mockReturnValue({ + isActive: { value: false }, + data: [{ value: 10 }, { value: 9 }], + currentIndex: { value: 1 }, + }) + + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('renders loading state', () => { + mockedUseLineChart.mockReturnValue({ + isActive: { value: false }, + data: [{ value: 10 }, { value: 9 }], + currentIndex: { value: 1 }, + }) + + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('shows active relative change when scrubbing', async () => { + mockedUseLineChart.mockReturnValue({ + isActive: { value: true }, + data: [{ value: 10 }, { value: 9 }], + currentIndex: { value: 1 }, + }) + + const tree = render() + + const text = await tree.findByTestId('relative-change-text') + expect(text.props.value).toBe(`10.00%`) + }) +}) + +describe(DatetimeText, () => { + it('renders without error', () => { + mockedUseLineChartDatetime.mockReturnValue({ + value: { value: '123' }, + formatted: { value: 'Thursday, November 1st, 2023' }, + }) + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('renders loading state', () => { + mockedUseLineChartDatetime.mockReturnValue({ + value: { value: '123' }, + formatted: { value: 'Thursday, November 1st, 2023' }, + }) + const tree = render() + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/components/PriceExplorer/Text.tsx b/apps/mobile/src/components/PriceExplorer/Text.tsx new file mode 100644 index 0000000..03f7c68 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/Text.tsx @@ -0,0 +1,97 @@ +import React from 'react' +import { useAnimatedStyle } from 'react-native-reanimated' +import { useLineChartDatetime } from 'react-native-wagmi-charts' +import { AnimatedText } from 'src/components/text/AnimatedText' +import { Flex, Icons, useSporeColors } from 'ui/src' +import { isAndroid } from 'uniswap/src/utils/platform' +import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' +import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useCurrentLocale } from 'wallet/src/features/language/hooks' +import { AnimatedDecimalNumber } from './AnimatedDecimalNumber' +import { useLineChartPrice, useLineChartRelativeChange } from './usePrice' + +export function PriceText({ maxWidth }: { loading: boolean; maxWidth?: number }): JSX.Element { + const price = useLineChartPrice() + const colors = useSporeColors() + const currency = useAppFiatCurrency() + const { decimalSeparator, symbolAtFront } = useAppFiatCurrencyInfo() + + // TODO gary re-enabling this for USD/Euros only, replace with more scalable approach + const shouldFadePortfolioDecimals = + (currency === FiatCurrency.UnitedStatesDollar || currency === FiatCurrency.Euro) && + symbolAtFront + + // TODO(MOB-2308): re-enable this when we have a better solution for handling the loading state + // if (loading) { + // return + // } + + return ( + + ) +} + +export function RelativeChangeText({ loading }: { loading: boolean }): JSX.Element { + const colors = useSporeColors() + + const relativeChange = useLineChartRelativeChange() + + const styles = useAnimatedStyle(() => ({ + color: relativeChange.value.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val, + })) + const caretStyle = useAnimatedStyle(() => ({ + color: relativeChange.value.value > 0 ? colors.statusSuccess.val : colors.statusCritical.val, + transform: [{ rotate: relativeChange.value.value > 0 ? '180deg' : '0deg' }], + })) + + if (loading) { + return ( + + + + ) + } + + return ( + + 0 ? -1 : 1 }, + ]} + /> + + + ) +} + +export function DatetimeText({ loading }: { loading: boolean }): JSX.Element | null { + const locale = useCurrentLocale() + // `datetime` when scrubbing the chart + const datetime = useLineChartDatetime({ locale }) + + if (loading) { + return null + } + + return +} diff --git a/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx new file mode 100644 index 0000000..89124d2 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/TimeRangeGroup.tsx @@ -0,0 +1,126 @@ +import React, { useState } from 'react' +import { I18nManager, StyleSheet, View } from 'react-native' +import { + SharedValue, + interpolateColor, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated' +import { TIME_RANGES } from 'src/components/PriceExplorer/constants' +import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' +import Trace from 'src/components/Trace/Trace' +import { AnimatedFlex, AnimatedText, Flex, TouchableArea, useSporeColors } from 'ui/src' +import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' + +interface Props { + label: string + index: number + selectedIndex: SharedValue + transition: SharedValue +} + +export function TimeRangeLabel({ index, label, selectedIndex, transition }: Props): JSX.Element { + const colors = useSporeColors() + + const style = useAnimatedStyle(() => { + const selected = index === selectedIndex.value + + if (!selected) { + return { color: colors.neutral2.val } + } + + const color = interpolateColor( + transition.value, + [0, 1], + [colors.neutral2.val, colors.neutral1.val] + ) + + return { color } + }) + + return ( + + {label} + + ) +} + +export function TimeRangeGroup({ + setDuration, +}: { + setDuration: (newDuration: HistoryDuration) => void +}): JSX.Element { + const { chartWidth, buttonWidth, labelWidth } = useChartDimensions() + const transition = useSharedValue(1) + const previousIndex = useSharedValue(1) + const currentIndex = useSharedValue(1) + const [adjustedLabelWidth, setAdjustedLabelWidth] = useState(labelWidth) + + const isRTL = I18nManager.isRTL + + // animates slider (time range label background) on press + const sliderStyle = useAnimatedStyle( + () => ({ + transform: [ + { + translateX: + (buttonWidth * currentIndex.value + (buttonWidth - adjustedLabelWidth) / 2) * + // left if RTL, right if LTR + (isRTL ? -1 : 1), + }, + ], + }), + [adjustedLabelWidth, buttonWidth, currentIndex, isRTL] + ) + + return ( + + + + + {TIME_RANGES.map(([duration, label, element], index) => { + return ( + + { + setDuration(duration) + + previousIndex.value = currentIndex.value + transition.value = 0 + currentIndex.value = index + transition.value = 1 + }}> + { + if (width > adjustedLabelWidth) { + setAdjustedLabelWidth(width) + } + }}> + + + + + ) + })} + + ) +} diff --git a/apps/mobile/src/components/PriceExplorer/TokenPriceHistory.graphql b/apps/mobile/src/components/PriceExplorer/TokenPriceHistory.graphql new file mode 100644 index 0000000..5490f0b --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/TokenPriceHistory.graphql @@ -0,0 +1,42 @@ +query TokenPriceHistory( + $contract: ContractInput! + $duration: HistoryDuration = DAY +) { + tokenProjects(contracts: [$contract]) { + id + name + markets(currencies: [USD]) { + id + price { + value + } + pricePercentChange24h { + value + } + priceHistory(duration: $duration) { + timestamp + value + } + } + tokens { + id + chain + address + symbol + decimals + market(currency: USD) { + id + price { + value + } + pricePercentChange24h: pricePercentChange(duration: DAY) { + value + } + priceHistory(duration: $duration) { + timestamp + value + } + } + } + } +} diff --git a/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap new file mode 100644 index 0000000..2272247 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/__snapshots__/Text.test.tsx.snap @@ -0,0 +1,439 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatetimeText renders loading state 1`] = `null`; + +exports[`DatetimeText renders without error 1`] = ` + +`; + +exports[`PriceText renders loading state 1`] = ` + + + +`; + +exports[`PriceText renders without error 1`] = ` + + + + +`; + +exports[`PriceText renders without error less than a dollar 1`] = ` + + + + +`; + +exports[`RelativeChangeText renders loading state 1`] = ` + + + + + + + + + 00.00% + + + + + + + + +`; + +exports[`RelativeChangeText renders without error 1`] = ` + + + + + + + + +`; diff --git a/apps/mobile/src/components/PriceExplorer/constants.ts b/apps/mobile/src/components/PriceExplorer/constants.ts new file mode 100644 index 0000000..01b920a --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/constants.ts @@ -0,0 +1,35 @@ +import { HistoryDuration } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import i18n from 'wallet/src/i18n/i18n' +import { ElementName } from 'wallet/src/telemetry/constants' + +export const NUM_GRAPHS = 5 + +export const BUTTON_PADDING = 20 + +export const CURSOR_INNER_SIZE = 12 +export const CURSOR_SIZE = CURSOR_INNER_SIZE + 6 +export const LINE_WIDTH = 1 + +export const TIME_RANGES = [ + [ + HistoryDuration.Hour, + i18n.t('token.priceExplorer.timeRangeLabel.hour'), + ElementName.TimeFrame1H, + ], + [HistoryDuration.Day, i18n.t('token.priceExplorer.timeRangeLabel.day'), ElementName.TimeFrame1D], + [ + HistoryDuration.Week, + i18n.t('token.priceExplorer.timeRangeLabel.week'), + ElementName.TimeFrame1W, + ], + [ + HistoryDuration.Month, + i18n.t('token.priceExplorer.timeRangeLabel.month'), + ElementName.TimeFrame1M, + ], + [ + HistoryDuration.Year, + i18n.t('token.priceExplorer.timeRangeLabel.year'), + ElementName.TimeFrame1Y, + ], +] as const diff --git a/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts b/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts new file mode 100644 index 0000000..463fca4 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/useChartDimensions.test.ts @@ -0,0 +1,41 @@ +import { renderHook } from '@testing-library/react-native' +import { Dimensions } from 'react-native' +import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' +import { heightBreakpoints } from 'ui/src/theme' + +const sharedDimensions = { + height: 1000, + width: 1000, + scale: 1, + fontScale: 1, +} + +describe(useChartDimensions, () => { + it('returns small chart height for small screens', () => { + jest + .spyOn(Dimensions, 'get') + .mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short - 1 }) + const { result } = renderHook(() => useChartDimensions()) + + expect(result.current).toEqual({ + chartHeight: 130, + chartWidth: 1000, + buttonWidth: expect.any(Number), + labelWidth: expect.any(Number), + }) + }) + + it('returns large chart height for large screens', () => { + jest + .spyOn(Dimensions, 'get') + .mockReturnValue({ ...sharedDimensions, height: heightBreakpoints.short }) + const { result } = renderHook(() => useChartDimensions()) + + expect(result.current).toEqual({ + chartHeight: 215, + chartWidth: 1000, + buttonWidth: expect.any(Number), + labelWidth: expect.any(Number), + }) + }) +}) diff --git a/apps/mobile/src/components/PriceExplorer/useChartDimensions.ts b/apps/mobile/src/components/PriceExplorer/useChartDimensions.ts new file mode 100644 index 0000000..acc1357 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/useChartDimensions.ts @@ -0,0 +1,29 @@ +import { useDeviceDimensions } from 'ui/src' +import { heightBreakpoints } from 'ui/src/theme' +import { BUTTON_PADDING, NUM_GRAPHS } from './constants' + +export type ChartDimensions = { + chartHeight: number + chartWidth: number + buttonWidth: number + labelWidth: number +} + +// TODO (MOB-1387): account for height in a more dynamic way to ensure +// that "Your balance" section will always show above the fold +export function useChartDimensions(): ChartDimensions { + const { fullHeight, fullWidth } = useDeviceDimensions() + + const chartHeight = fullHeight < heightBreakpoints.short ? 130 : 215 + const chartWidth = fullWidth + + const buttonWidth = chartWidth / NUM_GRAPHS + const labelWidth = buttonWidth - BUTTON_PADDING * 2 + + return { + chartHeight, + chartWidth, + buttonWidth, + labelWidth, + } +} diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.test.ts b/apps/mobile/src/components/PriceExplorer/usePrice.test.ts new file mode 100644 index 0000000..34e96e4 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/usePrice.test.ts @@ -0,0 +1,310 @@ +import { waitFor } from '@testing-library/react-native' +import { makeMutable } from 'react-native-reanimated' +import { + TLineChartData, + useLineChart, + useLineChartPrice as useRNWagmiChartLineChartPrice, +} from 'react-native-wagmi-charts' +import { act } from 'react-test-renderer' +import { renderHookWithProviders } from 'src/test/render' +import { useLineChartPrice, useLineChartRelativeChange } from './usePrice' + +jest.mock('react-native-wagmi-charts') + +const cursorValue = makeMutable('') +const cursorFormattedValue = makeMutable('-') + +const currentIndex = makeMutable(0) +const isActive = makeMutable(false) + +const mockData = ( + args: { data?: TLineChartData; currentIndex?: number; isActive?: boolean } = {} +): void => { + currentIndex.value = args.currentIndex ?? 0 + isActive.value = args.isActive ?? false + // react-native-wagmi-charts is mocked so we can mock the return + // of useLineChart + const mockedFunction = useLineChart as ReturnType + mockedFunction.mockReturnValue({ + data: args.data ?? [], + currentIndex, + isActive, + }) +} + +const mockCursorPrice = (value?: string): void => { + cursorValue.value = value ?? '' + cursorFormattedValue.value = value ? `$${value}` : '-' + + // react-native-wagmi-charts is mocked so we can mock the return + // of useLineChartPrice + const mockedFunction = useRNWagmiChartLineChartPrice as ReturnType + mockedFunction.mockReturnValue({ + value: cursorValue, + formatted: cursorFormattedValue, + }) +} + +describe(useLineChartPrice, () => { + beforeEach(() => { + const originalModule = jest.requireActual('react-native-wagmi-charts') + ;(useLineChart as ReturnType).mockImplementation(originalModule.useLineChart) + ;(useRNWagmiChartLineChartPrice as ReturnType).mockImplementation( + originalModule.useLineChartPrice + ) + }) + + afterAll(() => { + jest.resetAllMocks() + }) + + it('returns correct initial values', () => { + const { result } = renderHookWithProviders(useLineChartPrice) + + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 0 }), + formatted: expect.objectContaining({ value: '-' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + }) + + describe('when there is no active cursor price', () => { + beforeEach(() => { + // Mock data before all test to show that the currentSpot has higher + // priority than the last value from data + mockData({ + data: [ + { value: 1, timestamp: 1 }, + { value: 2, timestamp: 2 }, + ], + }) + }) + + it('returns last value from data if currentSpot is not provided', async () => { + const { result, rerender } = renderHookWithProviders(useLineChartPrice) + + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 2 }), + formatted: expect.objectContaining({ value: '$2.00' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + + // Update data + mockData({ + data: [ + { value: 1, timestamp: 1 }, + { value: 2, timestamp: 2 }, + { value: 3, timestamp: 3 }, + ], + }) + // Re-render to trigger the update (normally the useLineChart hook + // would trigger re-render) + await act(() => rerender()) + + await waitFor(() => { + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 3 }), + formatted: expect.objectContaining({ value: '$3.00' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + }) + }) + + it('returns currentSpot if it is provided', async () => { + const spotPrice = makeMutable(1) + const { result } = renderHookWithProviders(useLineChartPrice, { + initialProps: [spotPrice], + }) + + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 1 }), + formatted: expect.objectContaining({ value: '$1.00' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + + spotPrice.value = 2 + + await waitFor(() => { + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 2 }), + formatted: expect.objectContaining({ value: '$2.00' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + }) + }) + }) + + describe('when there is an active cursor price', () => { + beforeEach(() => { + // Mock data before all test to show that the currentSpot has higher + // priority than the last value from data + mockData({ + data: [ + { value: 1, timestamp: 1 }, + { value: 2, timestamp: 2 }, + ], + }) + }) + + it('returns active cursor price even if currentSpot and data are provided', async () => { + mockCursorPrice('3') + const { result } = renderHookWithProviders(useLineChartPrice, { + initialProps: [makeMutable(4)], + }) + + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 3 }), + formatted: expect.objectContaining({ value: '$3.00' }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + }) + + it('updates returned active cursor price when it changes', async () => { + mockCursorPrice('1') + const { result } = renderHookWithProviders(useLineChartPrice, { + initialProps: [makeMutable(4)], + }) + + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 1 }), + formatted: expect.objectContaining({ value: '$1.00' }), + }) + ) + + mockCursorPrice('2') // updates shared values + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 2 }), + formatted: expect.objectContaining({ value: '$2.00' }), + }) + ) + }) + }) + + it('sets shouldAnimate to false when cursor price changes', async () => { + mockCursorPrice() // uze mocked value and formatted value + const { result } = renderHookWithProviders(useLineChartPrice) + + // first update (previous value will be null as it's the first update after initial render) + mockCursorPrice('1') + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 1 }), + shouldAnimate: expect.objectContaining({ value: true }), + }) + ) + }) + + // second update (shouldAnimate should be false when the chart is + // scrubbed and the cursor price changes) + mockCursorPrice('2') + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 2 }), + shouldAnimate: expect.objectContaining({ value: false }), + }) + ) + }) + }) + }) +}) + +describe(useLineChartRelativeChange, () => { + const chartData1 = [ + { timestamp: 1, value: 1 }, + { timestamp: 2, value: 0.1 }, + { timestamp: 3, value: 10 }, + { timestamp: 4, value: 5 }, + ] + const chartData2 = [ + { timestamp: 1, value: 1 }, + { timestamp: 2, value: 0.1 }, + { timestamp: 3, value: 10 }, + { timestamp: 4, value: 20 }, + ] + + beforeAll(() => { + mockData() + }) + + it('returns correct initial values', () => { + const { result } = renderHookWithProviders(() => useLineChartRelativeChange()) + + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 0 }), + formatted: expect.objectContaining({ value: '0.00%' }), + }) + }) + + describe('when spotRelativeChange is not provided', () => { + it('calculates relative change based on the open and close price values', () => { + mockData({ data: chartData1 }) + const { result } = renderHookWithProviders(() => useLineChartRelativeChange()) + + // 1 -> 5 (+400%) + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 400 }), + formatted: expect.objectContaining({ value: '400.00%' }), + }) + }) + + it('updates the relative change when the currentIndex changes when active', async () => { + mockData({ data: chartData1 }) + const { result } = renderHookWithProviders(() => useLineChartRelativeChange()) + + // 1 -> 5 (+400%) + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 400 }), + formatted: expect.objectContaining({ value: '400.00%' }), + }) + ) + + currentIndex.value = 2 + isActive.value = true + + // 1 -> 10 (+900%) + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + value: expect.objectContaining({ value: 900 }), + formatted: expect.objectContaining({ value: '900.00%' }), + }) + ) + }) + }) + + it('updates the relative change when the data changes', async () => { + mockData({ data: chartData1 }) + const { result, rerender } = renderHookWithProviders(() => useLineChartRelativeChange()) + + // 1 -> 5 (+400%) + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 400 }), + formatted: expect.objectContaining({ value: '400.00%' }), + }) + + await act(() => { + mockData({ data: chartData2 }) + // Trigger rerender (it will be normally triggered when the data + // returned from the useLineChart hook changes) + rerender() + }) + + // 1 -> 20 (+1900%) + await waitFor(() => { + expect(result.current).toEqual({ + value: expect.objectContaining({ value: 1900 }), + formatted: expect.objectContaining({ value: '1900.00%' }), + }) + }) + }) + }) +}) diff --git a/apps/mobile/src/components/PriceExplorer/usePrice.tsx b/apps/mobile/src/components/PriceExplorer/usePrice.tsx new file mode 100644 index 0000000..28fc16a --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/usePrice.tsx @@ -0,0 +1,116 @@ +import { useMemo } from 'react' +import { + SharedValue, + useAnimatedReaction, + useDerivedValue, + useSharedValue, +} from 'react-native-reanimated' +import { + useLineChart, + useLineChartPrice as useRNWagmiChartLineChartPrice, +} from 'react-native-wagmi-charts' +import { numberToLocaleStringWorklet, numberToPercentWorklet } from 'src/utils/reanimated' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useCurrentLocale } from 'wallet/src/features/language/hooks' + +export type ValueAndFormatted = { + value: Readonly> + formatted: Readonly> +} + +export type ValueAndFormattedWithAnimation = ValueAndFormatted & { + shouldAnimate: Readonly> +} + +/** + * Wrapper around react-native-wagmi-chart#useLineChartPrice + * @returns latest price when not scrubbing and active price when scrubbing + */ +export function useLineChartPrice( + currentSpot?: SharedValue +): ValueAndFormattedWithAnimation { + const { value: activeCursorPrice } = useRNWagmiChartLineChartPrice({ + // do not round + precision: 18, + }) + const { data } = useLineChart() + const shouldAnimate = useSharedValue(true) + + useAnimatedReaction( + () => { + return activeCursorPrice.value + }, + (currentValue, previousValue) => { + if (previousValue && currentValue && shouldAnimate.value) { + shouldAnimate.value = false + } + } + ) + const currencyInfo = useAppFiatCurrencyInfo() + const locale = useCurrentLocale() + + const price = useDerivedValue(() => { + if (activeCursorPrice.value) { + // active price when scrubbing the chart + return Number(activeCursorPrice.value) + } + + shouldAnimate.value = true + // show spot price when chart not scrubbing, or if not available, show the last price in the chart + return currentSpot?.value ?? data[data.length - 1]?.value ?? 0 + }) + const priceFormatted = useDerivedValue(() => { + const { symbol, code } = currencyInfo + return numberToLocaleStringWorklet( + price.value, + locale, + { + style: 'currency', + currency: code, + }, + symbol + ) + }) + + return useMemo( + () => ({ + value: price, + formatted: priceFormatted, + shouldAnimate, + }), + [price, priceFormatted, shouldAnimate] + ) +} + +/** + * @returns % change for the active history duration when not scrubbing and % + * change between active index and period start when scrubbing + */ +export function useLineChartRelativeChange(): ValueAndFormatted { + const { currentIndex, data, isActive } = useLineChart() + + const relativeChange = useDerivedValue(() => { + // when scrubbing, compute relative change from open price + const openPrice = data[0]?.value + + // scrubbing: close price is active price + // not scrubbing: close price is period end price + const closePrice = isActive.value + ? data[currentIndex.value]?.value + : data[data.length - 1]?.value + + if (openPrice === undefined || closePrice === undefined || openPrice === 0) { + return 0 + } + + const change = ((closePrice - openPrice) / openPrice) * 100 + + return change + }) + + const relativeChangeFormatted = useDerivedValue(() => { + return numberToPercentWorklet(relativeChange.value, { precision: 2, absolute: true }) + }) + + return { value: relativeChange, formatted: relativeChangeFormatted } +} diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts new file mode 100644 index 0000000..013ac35 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.test.ts @@ -0,0 +1,383 @@ +import { waitFor } from '@testing-library/react-native' +import { act } from 'react-test-renderer' +import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' +import { renderHookWithProviders } from 'src/test/render' +import { + HistoryDuration, + TimestampedAmount, + TokenProject as TokenProjectType, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { + SAMPLE_CURRENCY_ID_1, + getLatestPrice, + priceHistory, + timestampedAmount, + token, + tokenMarket, + tokenProject, + tokenProjectMarket, + usdcTokenProject, +} from 'wallet/src/test/fixtures' +import { queryResolvers } from 'wallet/src/test/utils' + +const mockTokenProjectsQuery = (historyPrices: number[]) => (): TokenProjectType[] => { + const history = historyPrices.map((value) => timestampedAmount({ value })) + + return [ + tokenProject({ + markets: [ + tokenProjectMarket({ + priceHistory: history, + price: getLatestPrice(history), + }), + ], + }), + ] +} + +const formatPriceHistory = (history: TimestampedAmount[]): Omit[] => + history.map(({ timestamp, value }) => ({ value, timestamp: timestamp * 1000 })) + +describe(useTokenPriceHistory, () => { + it('returns correct initial values', async () => { + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1)) + + expect(result.current.loading).toBe(true) + expect(result.current.error).toBe(false) + expect(result.current.data).toEqual({ + priceHistory: undefined, + spot: undefined, + }) + expect(result.current.selectedDuration).toBe(HistoryDuration.Day) // default initial duration + expect(result.current.numberOfDigits).toEqual({ + left: 0, + right: 0, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + }) + + it('returns on-chain spot price if off-chain spot price is not available', async () => { + const market = tokenMarket() + const { resolvers } = queryResolvers({ + tokenProjects: () => [usdcTokenProject({ markets: null, tokens: [token({ market })] })], + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.data?.spot).toEqual({ + value: { value: market.price?.value }, + relativeChange: { value: market.pricePercentChange?.value }, + }) + }) + + describe('correct number of digits', () => { + it('for max price greater than 1', async () => { + const { resolvers } = queryResolvers({ + tokenProjects: mockTokenProjectsQuery([0.00001, 1, 111_111_111.1111]), + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.numberOfDigits).toEqual({ + left: 9, + right: 2, + }) + }) + + it('for max price less than 1', async () => { + const { resolvers } = queryResolvers({ + tokenProjects: mockTokenProjectsQuery([0.001, 0.002]), + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.numberOfDigits).toEqual({ + left: 1, + right: 10, + }) + }) + + it('for max price equal to 1', async () => { + const { resolvers } = queryResolvers({ tokenProjects: mockTokenProjectsQuery([0.1, 1]) }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.numberOfDigits).toEqual({ + left: 1, + right: 2, + }) + }) + }) + + describe('correct price history', () => { + it('properly formats price history entries', async () => { + const history = priceHistory() + const { resolvers } = queryResolvers({ + tokenProjects: () => [usdcTokenProject({ priceHistory: history })], + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.data?.priceHistory).toEqual(formatPriceHistory(history)) + }) + + it('filters out invalid price history entries', async () => { + const { resolvers } = queryResolvers({ + tokenProjects: () => [ + usdcTokenProject({ + priceHistory: [ + null, + timestampedAmount({ value: 1 }), + null, + timestampedAmount({ value: 2 }), + ], + }), + ], + }) + const { result } = renderHookWithProviders(() => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), { + resolvers, + }) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(false) + }) + + expect(result.current.data?.priceHistory).toEqual([ + { + timestamp: expect.any(Number), + value: 1, + }, + { + timestamp: expect.any(Number), + value: 2, + }, + ]) + }) + }) + + describe('different durations', () => { + const dayPriceHistory = priceHistory({ duration: HistoryDuration.Day }) + const weekPriceHistory = priceHistory({ duration: HistoryDuration.Week }) + const monthPriceHistory = priceHistory({ duration: HistoryDuration.Month }) + const yearPriceHistory = priceHistory({ duration: HistoryDuration.Year }) + + const dayTokenProject = usdcTokenProject({ priceHistory: dayPriceHistory }) + const weekTokenProject = usdcTokenProject({ priceHistory: weekPriceHistory }) + const monthTokenProject = usdcTokenProject({ priceHistory: monthPriceHistory }) + const yearTokenProject = usdcTokenProject({ priceHistory: yearPriceHistory }) + + const { resolvers } = queryResolvers({ + tokenProjects: (parent, args, context, info) => { + switch (info.variableValues.duration) { + case HistoryDuration.Day: + return [dayTokenProject] + case HistoryDuration.Week: + return [weekTokenProject] + case HistoryDuration.Month: + return [monthTokenProject] + case HistoryDuration.Year: + return [yearTokenProject] + default: + return [dayTokenProject] + } + }, + }) + + describe('when duration is set to default value (day)', () => { + it('returns correct price history', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } + ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + data: { + priceHistory: formatPriceHistory(dayPriceHistory), + spot: expect.anything(), + }, + selectedDuration: HistoryDuration.Day, + }) + ) + }) + }) + + it('returns correct spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } + ) + + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: { value: dayTokenProject.markets[0]?.price.value }, + relativeChange: { value: dayTokenProject.markets[0]?.pricePercentChange24h.value }, + }) + }) + }) + }) + + describe('when duration is set to non-default value (year)', () => { + it('returns correct price history', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), + { resolvers } + ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + data: { + priceHistory: formatPriceHistory(yearPriceHistory), + spot: expect.anything(), + }, + selectedDuration: HistoryDuration.Year, + }) + ) + }) + }) + + it('returns correct spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, jest.fn(), HistoryDuration.Year), + { resolvers } + ) + await waitFor(() => { + expect(result.current.data?.spot).toEqual({ + value: { value: yearTokenProject.markets[0]?.price?.value }, + relativeChange: { value: yearTokenProject.markets[0]?.pricePercentChange24h?.value }, + }) + }) + }) + }) + + describe('when duration is changed', () => { + it('re-fetches data', async () => { + const onCompleted = jest.fn() + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1, onCompleted), + { resolvers } + ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + loading: false, + error: false, + selectedDuration: HistoryDuration.Day, + }) + ) + }) + + expect(onCompleted).toHaveBeenCalledTimes(1) + + // Change duration + await act(() => { + result.current.setDuration(HistoryDuration.Week) + }) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + loading: false, + error: false, + selectedDuration: HistoryDuration.Week, + }) + ) + }) + + expect(onCompleted).toHaveBeenCalledTimes(2) + }) + + it('returns new price history and spot price', async () => { + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers } + ) + + await waitFor(() => { + expect(result.current.data).toEqual({ + priceHistory: formatPriceHistory(dayPriceHistory), + spot: { + value: { value: dayTokenProject.markets[0]?.price.value }, + relativeChange: { value: dayTokenProject.markets[0]?.pricePercentChange24h.value }, + }, + }) + }) + + // Change duration + await act(() => { + result.current.setDuration(HistoryDuration.Week) + }) + + await waitFor(() => { + expect(result.current.data).toEqual({ + priceHistory: formatPriceHistory(weekPriceHistory), + spot: { + value: { value: weekTokenProject.markets[0]?.price?.value }, + relativeChange: { + value: weekTokenProject.markets[0]?.pricePercentChange24h?.value, + }, + }, + }) + }) + }) + }) + + describe('error handling', () => { + it('returns error if query has no data and there is no loading state', async () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined) + const { resolvers: errorResolvers } = queryResolvers({ + tokenProjects: () => { + throw new Error('error') + }, + }) + const { result } = renderHookWithProviders( + () => useTokenPriceHistory(SAMPLE_CURRENCY_ID_1), + { resolvers: errorResolvers } + ) + + await waitFor(() => { + expect(result.current.loading).toBe(false) + expect(result.current.error).toBe(true) + }) + }) + }) + }) +}) diff --git a/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts new file mode 100644 index 0000000..7199861 --- /dev/null +++ b/apps/mobile/src/components/PriceExplorer/usePriceHistory.ts @@ -0,0 +1,144 @@ +import { maxBy } from 'lodash' +import { Dispatch, SetStateAction, useCallback, useMemo, useRef, useState } from 'react' +import { SharedValue } from 'react-native-reanimated' +import { TLineChartData } from 'react-native-wagmi-charts' +import { + HistoryDuration, + TimestampedAmount, + useTokenPriceHistoryQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { GqlResult } from 'uniswap/src/data/types' +import { PollingInterval } from 'wallet/src/constants/misc' +import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' + +export type TokenSpotData = { + value: SharedValue + relativeChange: SharedValue +} + +export type PriceNumberOfDigits = { + left: number + right: number +} + +/** + * @returns Token price history for requested duration + */ +export function useTokenPriceHistory( + currencyId: string, + onCompleted?: () => void, + initialDuration: HistoryDuration = HistoryDuration.Day +): Omit< + GqlResult<{ + priceHistory?: TLineChartData + spot?: TokenSpotData + }>, + 'error' +> & { + setDuration: Dispatch> + selectedDuration: HistoryDuration + error: boolean + numberOfDigits: PriceNumberOfDigits +} { + const lastPrice = useRef(undefined) + const lastNumberOfDigits = useRef({ + left: 0, + right: 0, + }) + const [duration, setDuration] = useState(initialDuration) + const { convertFiatAmount } = useLocalizationContext() + + const { + data: priceData, + refetch, + networkStatus, + } = useTokenPriceHistoryQuery({ + variables: { + contract: currencyIdToContractInput(currencyId), + duration, + }, + notifyOnNetworkStatusChange: true, + pollInterval: PollingInterval.Normal, + onCompleted, + // TODO(MOB-2308): maybe update to network-only once we have a better loading state + fetchPolicy: 'cache-and-network', + }) + + const offChainData = priceData?.tokenProjects?.[0]?.markets?.[0] + const onChainData = priceData?.tokenProjects?.[0]?.tokens?.[0]?.market + + const price = offChainData?.price?.value ?? onChainData?.price?.value ?? lastPrice.current + lastPrice.current = price + const priceHistory = offChainData?.priceHistory ?? onChainData?.priceHistory + const pricePercentChange24h = + offChainData?.pricePercentChange24h?.value ?? onChainData?.pricePercentChange24h?.value ?? 0 + + const spot = useMemo( + () => + price !== undefined + ? { + value: { value: price }, + relativeChange: { value: pricePercentChange24h }, + } + : undefined, + [price, pricePercentChange24h] + ) + + const formattedPriceHistory = useMemo(() => { + const formatted = priceHistory + ?.filter((x): x is TimestampedAmount => Boolean(x)) + .map((x) => ({ timestamp: x.timestamp * 1000, value: x.value })) + + return formatted + }, [priceHistory]) + + const numberOfDigits = useMemo(() => { + const maxPriceInHistory = maxBy(priceHistory, 'value')?.value + // If there is neither max price in history nor current price, return last number of digits + if (!maxPriceInHistory && price === undefined) { + return lastNumberOfDigits.current + } + const maxPrice = Math.max(maxPriceInHistory || 0, price || 0) + const convertedMaxValue = convertFiatAmount(maxPrice).amount + + const newNumberOfDigits = { + left: String(convertedMaxValue).split('.')[0]?.length || 10, + right: Number(String(convertedMaxValue.toFixed(10)).split('.')[0]) > 0 ? 2 : 10, + } + lastNumberOfDigits.current = newNumberOfDigits + + return newNumberOfDigits + }, [convertFiatAmount, priceHistory, price]) + + const retry = useCallback(async () => { + await refetch({ contract: currencyIdToContractInput(currencyId) }) + }, [refetch, currencyId]) + + return useMemo( + () => ({ + data: { + priceHistory: formattedPriceHistory, + spot, + }, + loading: isNonPollingRequestInFlight(networkStatus), + error: isError(networkStatus, !!priceData), + refetch: retry, + setDuration, + selectedDuration: duration, + numberOfDigits, + onCompleted, + }), + [ + duration, + formattedPriceHistory, + networkStatus, + priceData, + retry, + spot, + onCompleted, + numberOfDigits, + ] + ) +} diff --git a/apps/mobile/src/components/QRCodeScanner/QRCode.tsx b/apps/mobile/src/components/QRCodeScanner/QRCode.tsx new file mode 100644 index 0000000..2168ba1 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/QRCode.tsx @@ -0,0 +1,229 @@ +import React, { memo, useMemo } from 'react' +import { ImageSourcePropType } from 'react-native' +import QRCode from 'src/components/QRCodeScanner/custom-qr-code-generator' +import { + ColorTokens, + Flex, + getUniconV2Colors, + useIsDarkMode, + useSporeColors, + useUniconColors, +} from 'ui/src' + +import { borderRadii } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useAvatar } from 'wallet/src/features/wallet/hooks' +import { passesContrast, useExtractedColors } from 'wallet/src/utils/colors' + +type AvatarColors = { + primary: string + base: string + detail: string +} + +type ColorProps = { + smartColor: string + gradientProps: { + enableLinearGradient?: boolean + linearGradient?: string[] + gradientDirection?: string[] + color?: string + } +} + +const useColorProps = (address: Address, color?: string): ColorProps => { + const colors = useSporeColors() + const gradientData = useUniconColors(address) + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + const isDarkMode = useIsDarkMode() + const uniconV2Color = getUniconV2Colors(address, isDarkMode) as { color: string } + const { avatar, loading: avatarLoading } = useAvatar(address) + const { colors: avatarColors } = useExtractedColors(avatar) as { colors: AvatarColors } + const hasAvatar = !!avatar && !avatarLoading + + const smartColor: string = useMemo(() => { + const contrastThreshold = 3 // WCAG AA standard for contrast + const backgroundColor = colors.surface2.val // replace with your actual background color + + if (hasAvatar && avatarColors && avatarColors.primary) { + if (passesContrast(avatarColors.primary, backgroundColor, contrastThreshold)) { + return avatarColors.primary + } + if (passesContrast(avatarColors.base, backgroundColor, contrastThreshold)) { + return avatarColors.base + } + if (passesContrast(avatarColors.detail, backgroundColor, contrastThreshold)) { + return avatarColors.detail + } + // Modify the color if it doesn't pass the contrast check + // Replace 'modifiedColor' with the actual color you want to use + return colors.neutral1.val as string + } + return isUniconsV2Enabled ? uniconV2Color.color : '$transparent' + }, [ + avatarColors, + hasAvatar, + isUniconsV2Enabled, + uniconV2Color.color, + colors.surface2.val, + colors.neutral1.val, + ]) + + const gradientProps = useMemo(() => { + let gradientPropsObject: { + enableLinearGradient?: boolean + linearGradient?: string[] + gradientDirection?: string[] + color?: string + } = {} + gradientPropsObject = { + enableLinearGradient: isUniconsV2Enabled ? false : true, + linearGradient: [gradientData.gradientStart, gradientData.gradientEnd], + color: isUniconsV2Enabled ? color : gradientData.gradientStart, + // TODO(MOB-2822): see if we can remove ternary + gradientDirection: ['0%', '0%', isAndroid ? '150%' : '100%', '0%'], + } + return gradientPropsObject + }, [gradientData.gradientEnd, gradientData.gradientStart, isUniconsV2Enabled, color]) + + return { smartColor, gradientProps } +} + +type AddressQRCodeProps = { + address: Address + errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H' + size: number + backgroundColor?: ColorTokens + color?: string + safeAreaSize?: number + safeAreaColor?: ColorTokens +} + +export const AddressQRCode = ({ + address, + errorCorrectionLevel, + size, + backgroundColor = '$surface1', + color, + safeAreaSize, + safeAreaColor, +}: AddressQRCodeProps): JSX.Element => { + const backgroundColorValue = backgroundColor + const { gradientProps } = useColorProps(address, color) + const colors = useSporeColors() + + const safeAreaProps = useMemo(() => { + let safeAreaPropsObject: { + logoSize?: number + logoMargin?: number + logo?: ImageSourcePropType + logoBackgroundColor?: string + logoBorderRadius?: number + } = {} + + if (safeAreaSize && safeAreaColor) { + safeAreaPropsObject = { + logoSize: safeAreaSize, + logo: { uri: '' }, + // this could eventually be set to an SVG version of the Unicon which would ensure it's perfectly centered, but for now we can just use an empty logo image to create a blank circle in the middle of the QR code + logoBackgroundColor: colors.surface1.val, + logoBorderRadius: borderRadii.roundedFull, + // note: this QR code library doesn't actually create a 'safe' space in the middle, it just adds the logo on top, so that's why ecl is set to H (high error correction level) by default to ensure the QR code is still readable even if the middle of the QR code is partially obscured + } + } + return safeAreaPropsObject + }, [safeAreaSize, safeAreaColor, colors]) + + return ( + + ) +} + +type QRCodeDisplayProps = { + address: Address + errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H' + size: number + backgroundColor?: ColorTokens + containerBackgroundColor?: ColorTokens + overlayColor?: ColorTokens + safeAreaColor?: ColorTokens + logoSize?: number + hideOutline?: boolean + displayShadow?: boolean + color?: string +} + +const _QRCodeDisplay = ({ + address, + errorCorrectionLevel = 'H', + size, + containerBackgroundColor, + color, + logoSize = 32, + safeAreaColor, + hideOutline = false, + displayShadow = false, +}: QRCodeDisplayProps): JSX.Element => { + const { avatar } = useAvatar(address) + const { smartColor } = useColorProps(address, color) + + return ( + + + + + + + + + ) +} + +export const QRCodeDisplay = memo(_QRCodeDisplay) diff --git a/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx new file mode 100644 index 0000000..b87abb6 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/QRCodeScanner.tsx @@ -0,0 +1,371 @@ +import { BarCodeScanner } from 'expo-barcode-scanner' +import { BarCodeScanningResult, Camera, CameraType } from 'expo-camera' +import { PermissionStatus } from 'expo-modules-core' +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert, LayoutChangeEvent, LayoutRectangle, StyleSheet } from 'react-native' +import { launchImageLibrary } from 'react-native-image-picker' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { Defs, LinearGradient, Path, Rect, Stop, Svg } from 'react-native-svg' +import { DevelopmentOnly } from 'src/components/DevelopmentOnly/DevelopmentOnly' +import { + AnimatedFlex, + Button, + Flex, + Icons, + Text, + useDeviceDimensions, + useSporeColors, +} from 'ui/src' +import CameraScan from 'ui/src/assets/icons/camera-scan.svg' +import { iconSizes, spacing } from 'ui/src/theme' +import { Sentry } from 'utilities/src/logger/Sentry' +import PasteButton from 'wallet/src/components/buttons/PasteButton' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { openSettings } from 'wallet/src/utils/linking' + +type QRCodeScannerProps = { + onScanCode: (data: string) => void + shouldFreezeCamera: boolean +} +interface WCScannerProps extends QRCodeScannerProps { + numConnections: number + onPressConnections: () => void +} + +function isWalletConnect(props: QRCodeScannerProps | WCScannerProps): props is WCScannerProps { + return 'numConnections' in props +} + +const CAMERA_ASPECT_RATIO = 4 / 3 +const SCAN_ICON_RADIUS_RATIO = 0.1 +const SCAN_ICON_WIDTH_RATIO = 0.7 +const SCAN_ICON_MASK_OFFSET_RATIO = 0.02 // used for mask to match spacing in CameraScan SVG +const LOADER_SIZE = iconSizes.icon40 + +export function QRCodeScanner(props: QRCodeScannerProps | WCScannerProps): JSX.Element { + const { onScanCode, shouldFreezeCamera } = props + const isWalletConnectModal = isWalletConnect(props) + + const { t } = useTranslation() + const colors = useSporeColors() + const dimensions = useDeviceDimensions() + + const [permissionResponse, requestPermissionResponse] = Camera.useCameraPermissions() + const permissionStatus = permissionResponse?.status + + const [isReadingImageFile, setIsReadingImageFile] = useState(false) + const [overlayLayout, setOverlayLayout] = useState() + const [infoLayout, setInfoLayout] = useState() + const [bottomLayout, setBottomLayout] = useState() + + const handleBarCodeScanned = useCallback( + (result: BarCodeScanningResult): void => { + if (shouldFreezeCamera) { + return + } + const data = result?.data + onScanCode(data) + setIsReadingImageFile(false) + }, + [onScanCode, shouldFreezeCamera] + ) + + const onPickImageFilePress = useCallback(async (): Promise => { + if (isReadingImageFile) { + return + } + + setIsReadingImageFile(true) + + const response = await launchImageLibrary({ + mediaType: 'photo', + selectionLimit: 1, + }) + + const uri = response.assets?.[0]?.uri + + if (!uri) { + setIsReadingImageFile(false) + return + } + + const result = ( + await BarCodeScanner.scanFromURLAsync(uri, [BarCodeScanner.Constants.BarCodeType.qr]) + )[0] + + if (!result) { + Alert.alert(t('qrScanner.error.none')) + setIsReadingImageFile(false) + return + } + + handleBarCodeScanned(result) + }, [handleBarCodeScanned, isReadingImageFile, t]) + + useEffect(() => { + Sentry.addBreadCrumb({ + level: 'info', + category: 'camera', + message: 'QRCodeScannera camera permission status', + data: { + permissionStatus, + }, + }) + + if (permissionStatus === PermissionStatus.UNDETERMINED) { + requestPermissionResponse().catch(() => {}) + } + + if (permissionStatus === PermissionStatus.DENIED) { + Alert.alert(t('qrScanner.error.camera.title'), t('qrScanner.error.camera.message'), [ + { text: t('common.navigation.systemSettings'), onPress: openSettings }, + { + text: t('common.button.notNow'), + }, + ]) + } + }, [permissionStatus, requestPermissionResponse, t]) + + const overlayWidth = (overlayLayout?.height ?? 0) / CAMERA_ASPECT_RATIO + const scannerSize = Math.min(overlayWidth, dimensions.fullWidth) * SCAN_ICON_WIDTH_RATIO + + return ( + + + + {permissionStatus === PermissionStatus.GRANTED && !isReadingImageFile && ( + + )} + + + + setOverlayLayout(event.nativeEvent.layout)}> + + setInfoLayout(event.nativeEvent.layout)}> + + {t('qrScanner.title')} + + + {!shouldFreezeCamera ? ( + // camera isn't frozen (after seeing barcode) — show the camera scan icon (the four white corners) + + ) : ( + // camera has been frozen (has seen a barcode) — show the loading spinner and "Connecting..." or "Loading..." + + + + + + + + {isWalletConnectModal + ? t('qrScanner.status.connecting') + : t('qrScanner.status.loading')} + + + + )} + + {/* when in development mode AND there's no camera (using iOS Simulator), add a paste button */} + {!shouldFreezeCamera ? ( + + + + This paste button will only show up in development mode + + + + + ) : null} + + + + setBottomLayout(event.nativeEvent.layout) + }> + + {isReadingImageFile ? ( + + ) : ( + + )} + + + {isWalletConnectModal && props.numConnections > 0 && ( + + )} + + + + + ) +} + +type GradientOverlayProps = { + shouldFreezeCamera: boolean + overlayWidth: number + scannerSize: number +} + +const GradientOverlay = memo(function GradientOverlay({ + shouldFreezeCamera, + overlayWidth, + scannerSize, +}: GradientOverlayProps): JSX.Element { + const colors = useSporeColors() + const dimensions = useDeviceDimensions() + const [size, setSize] = useState<{ width: number; height: number } | null>(null) + + const pathWithHole = useMemo(() => { + if (!size) { + return '' + } + const { width: W, height: H } = size + const iconMaskOffset = SCAN_ICON_MASK_OFFSET_RATIO * scannerSize + const paddingX = Math.max(0, (W - scannerSize) / 2) + iconMaskOffset + const paddingY = Math.max(0, (H - scannerSize) / 2) + iconMaskOffset + const r = scannerSize * SCAN_ICON_RADIUS_RATIO + const L = paddingX + const R = W - paddingX + const T = paddingY + const B = H - paddingY + return `M${L + r} ${T} ${R - r} ${T}C${R - r} ${T} ${R} ${T} ${R} ${T + r}L${R} ${B - r}C${R} ${ + B - r + } ${R} ${B} ${R - r} ${B}L${L + r} ${B}C${L + r} ${B} ${L} ${B} ${L} ${B - r}L${L} ${T + r} 0 ${ + T + r + } 0 ${H} ${W} ${H} ${W} 0 0 0 0 ${T + r} ${L} ${T + r}C${L} ${T + r} ${L} ${T} ${L + r} ${T}` + }, [size, scannerSize]) + + const onLayout = ({ + nativeEvent: { + layout: { width, height }, + }, + }: LayoutChangeEvent): void => { + setSize({ width, height }) + } + + const gradientOffset = (overlayWidth / dimensions.fullWidth - 1) / 2 + + return ( + + + + + + + + + + + + + {!shouldFreezeCamera ? ( + + ) : ( + + )} + {/* gradient from top of modal to top of QR code, of color DEP_background1 to transparent */} + + {/* gradient from bottom of modal to bottom of QR code, of color DEP_background1 to transparent */} + + + + ) +}) diff --git a/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx new file mode 100644 index 0000000..60340b2 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/WalletQRCode.tsx @@ -0,0 +1,99 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' +import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' +import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useMedia, useSporeColors } from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' +import { ALL_SUPPORTED_CHAIN_IDS } from 'wallet/src/constants/chains' +import { ModalName } from 'wallet/src/telemetry/constants' + +interface Props { + address?: Address +} + +export function WalletQRCode({ address }: Props): JSX.Element | null { + const colors = useSporeColors() + const { t } = useTranslation() + const [showModal, setShowModal] = useState(false) + + const media = useMedia() + + const QR_CODE_SIZE = media.short ? 220 : 240 + const UNICON_SIZE = QR_CODE_SIZE / 4 + + if (!address) { + return null + } + + return ( + <> + + + + + + {t('qrScanner.wallet.title')} + + setShowModal(true)}> + + + + + + + {showModal && ( + + } + modalName={ModalName.QRCodeNetworkInfo} + title={t('qrScanner.wallet.networks.title')} + onClose={(): void => setShowModal(false)}> + + + )} + + ) +} diff --git a/apps/mobile/src/components/QRCodeScanner/constants.ts b/apps/mobile/src/components/QRCodeScanner/constants.ts new file mode 100644 index 0000000..a746f15 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/constants.ts @@ -0,0 +1,5 @@ +export enum ScannerModalState { + ScanQr, + ConnectedDapps, + WalletQr, +} diff --git a/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.d.ts b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.d.ts new file mode 100644 index 0000000..e4e6625 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.d.ts @@ -0,0 +1,45 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React from 'react' +import { ImageSourcePropType } from 'react-native' + +declare class QRCode extends React.PureComponent {} + +export interface QRCodeProps { + /* what the qr code stands for */ + value?: string + /* the whole component size */ + size?: number + /* the color of the cell */ + color?: string + /* the color of the background */ + backgroundColor?: string + /* the color of the background */ + overlayColor?: string + /* an image source object. example {uri: 'base64string'} or {require('pathToImage')} */ + logo?: ImageSourcePropType + /* logo size in pixels */ + logoSize?: number + /* the logo gets a filled rectangular background with this color. Use 'transparent' + if your logo already has its own backdrop. Default = same as backgroundColor */ + logoBackgroundColor?: string + /* logo's distance to its wrapper */ + logoMargin?: number + /* the border-radius of logo image */ + logoBorderRadius?: number + /* quiet zone in pixels */ + quietZone?: number + /* enable linear gradient effect */ + enableLinearGradient?: boolean + /* linear gradient direction */ + gradientDirection?: string[] + /* linear gradient color */ + linearGradient?: string[] + /* get svg ref for further usage */ + getRef?: (c: any) => any + /* error correction level */ + ecl?: 'L' | 'M' | 'Q' | 'H' + /* error handler called when matrix fails to generate */ + onError?: () => void +} + +export default QRCode diff --git a/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.js b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.js new file mode 100644 index 0000000..c989421 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/index.js @@ -0,0 +1 @@ +export { default } from './src/index.js' diff --git a/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js new file mode 100644 index 0000000..5a1534c --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js @@ -0,0 +1,14 @@ +import QRCode from 'qrcode' + +export default (value, errorCorrectionLevel) => { + const arr = Array.prototype.slice.call( + QRCode.create(value, { errorCorrectionLevel }).modules.data, + 0 + ) + const sqrt = Math.sqrt(arr.length) + return arr.reduce( + (rows, key, index) => + (index % sqrt === 0 ? rows.push([key]) : rows[rows.length - 1].push(key)) && rows, + [] + ) +} diff --git a/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/index.js b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/index.js new file mode 100644 index 0000000..2d37a17 --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/index.js @@ -0,0 +1,133 @@ +// Component logic from: https://github.com/awesomejerry/react-native-qrcode-svg +// Custom matric renderer from: https://github.com/awesomejerry/react-native-qrcode-svg/pull/139/files + +import React, { useMemo } from 'react' +import Svg, { Defs, G, LinearGradient, Path, Rect, Stop } from 'react-native-svg' +import genMatrix from 'src/components/QRCodeScanner/custom-qr-code-generator/src/genMatrix.js' +import transformMatrixIntoPath from 'src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js' +import { useMedia } from 'ui/src' + +const QREyes = ({ x = -1, y = -1, fillColor, size }) => ( + + + + +) + +const QREyeBG = ({ x = -1, y = -1, size, backgroundColor }) => ( + + + +) + +const QREyeWrapper = ({ x = 0, y = 0, backgroundColor, overlayColor, fillColor, size }) => ( + <> + + + + +) + +const QRCode = ({ + value = 'Wallet QR code', + size = 190, + color, + backgroundColor, + overlayColor = '#FFFFFF', + borderRadius = 24, + quietZone = 8, + enableLinearGradient = false, + gradientDirection = ['0%', '0%', '100%', '100%'], + linearGradient = ['rgb(255,255,255)', 'rgb(0,255,255)'], + ecl = 'H', + getRef, + onError, +}) => { + const result = useMemo(() => { + try { + return transformMatrixIntoPath(genMatrix(value, ecl), size) + } catch (error) { + if (onError && typeof onError === 'function') { + onError(error) + } else { + // Pass the error when no handler presented + throw error + } + } + }, [value, size, ecl, onError]) + + const media = useMedia() + if (!result) { + return null + } + + const { path } = result + + const eyeSize = media.short ? 126 : 138 + + return ( + + + + + + + + + + + + + + + + + + + + ) +} + +export default QRCode diff --git a/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js new file mode 100644 index 0000000..ae957ae --- /dev/null +++ b/apps/mobile/src/components/QRCodeScanner/custom-qr-code-generator/src/transformMatrixIntoCirclePath.js @@ -0,0 +1,21 @@ +export default (matrix, size) => { + const cellSize = size / matrix.length + let path = '' + + matrix.forEach((row, i) => { + row.forEach((column, j) => { + if (column) { + path += ` + M ${cellSize * j + cellSize / 2} ${cellSize * i} + A ${cellSize / 2.2} 0 0 1 1 ${cellSize * j + cellSize / 2 - 0.0001} ${ + cellSize * i - 0.00001 + }` + } + }) + }) + + return { + cellSize, + path, + } +} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx new file mode 100644 index 0000000..cd9c9f8 --- /dev/null +++ b/apps/mobile/src/components/RecipientSelect/RecipientScanModal.tsx @@ -0,0 +1,114 @@ +import { selectionAsync } from 'expo-haptics' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' +import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode' +import { getSupportedURI, URIType } from 'src/components/WalletConnect/ScanSheet/util' +import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import Scan from 'ui/src/assets/icons/receive.svg' +import ScanQRIcon from 'ui/src/assets/icons/scan.svg' +import { iconSizes } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + onClose: () => void + onSelectRecipient: (address: string) => void +} + +export function RecipientScanModal({ onSelectRecipient, onClose }: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const activeAddress = useAppSelector(selectActiveAccountAddress) + const [currentScreenState, setCurrentScreenState] = useState( + ScannerModalState.ScanQr + ) + const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) + + const onScanCode = async (uri: string): Promise => { + // don't scan any QR codes if camera is frozen + if (shouldFreezeCamera) { + return + } + + await selectionAsync() + setShouldFreezeCamera(true) + const supportedURI = await getSupportedURI(uri) + + if (supportedURI?.type === URIType.Address) { + onSelectRecipient(supportedURI.value) + onClose() + } else { + Alert.alert(t('qrScanner.recipient.error.title'), t('qrScanner.recipient.error.message'), [ + { + text: t('common.button.tryAgain'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ]) + } + } + + const onPressBottomToggle = (): void => { + if (currentScreenState === ScannerModalState.ScanQr) { + setCurrentScreenState(ScannerModalState.WalletQr) + } else { + setCurrentScreenState(ScannerModalState.ScanQr) + } + } + const isDarkMode = useIsDarkMode() + + return ( + + {currentScreenState === ScannerModalState.ScanQr && ( + + )} + {currentScreenState === ScannerModalState.WalletQr && activeAddress && ( + + )} + + + + {currentScreenState === ScannerModalState.ScanQr ? ( + + ) : ( + + )} + + {currentScreenState === ScannerModalState.ScanQr + ? t('qrScanner.recipient.action.show') + : t('qrScanner.recipient.action.scan')} + + + + + + ) +} diff --git a/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx new file mode 100644 index 0000000..9d5d2f5 --- /dev/null +++ b/apps/mobile/src/components/RecipientSelect/RecipientSelect.tsx @@ -0,0 +1,114 @@ +import React, { memo, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { RecipientScanModal } from 'src/components/RecipientSelect/RecipientScanModal' +import { AnimatedFlex, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import ScanQRIcon from 'ui/src/assets/icons/scan.svg' +import { iconSizes } from 'ui/src/theme' +import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' +import { filterRecipientByNameAndAddress } from 'wallet/src/components/RecipientSearch/filter' +import { useRecipients } from 'wallet/src/components/RecipientSearch/hooks' +import { RecipientList } from 'wallet/src/components/RecipientSearch/RecipientList' +import { filterSections } from 'wallet/src/components/RecipientSearch/utils' +import { SearchBar } from 'wallet/src/features/search/SearchBar' +import { ElementName } from 'wallet/src/telemetry/constants' + +interface RecipientSelectProps { + onSelectRecipient: (newRecipientAddress: string) => void + onToggleShowRecipientSelector: () => void + recipient?: string +} + +function QRScannerIconButton({ onPress }: { onPress: () => void }): JSX.Element { + const colors = useSporeColors() + + return ( + + + + ) +} + +export function _RecipientSelect({ + onSelectRecipient, + onToggleShowRecipientSelector, + recipient, +}: RecipientSelectProps): JSX.Element { + const { t } = useTranslation() + const { isSheetReady } = useBottomSheetContext() + + const [showQRScanner, setShowQRScanner] = useState(false) + const { sections, searchableRecipientOptions, pattern, onChangePattern, loading } = + useRecipients() + + const filteredSections = useMemo(() => { + const filteredAddresses = filterRecipientByNameAndAddress( + pattern, + searchableRecipientOptions + ).map((item) => item.data.address) + return filterSections(sections, filteredAddresses) + }, [pattern, searchableRecipientOptions, sections]) + + const onPressQRScanner = useCallback(() => { + Keyboard.dismiss() + setShowQRScanner(true) + }, [setShowQRScanner]) + + const onCloseQRScanner = useCallback(() => { + setShowQRScanner(false) + }, [setShowQRScanner]) + + const noResults = pattern && pattern?.length > 0 && !loading && filteredSections.length === 0 + + return ( + <> + + + {t('qrScanner.recipient.label.send')} + + } + placeholder={t('qrScanner.recipient.input.placeholder')} + value={pattern ?? ''} + onBack={recipient ? onToggleShowRecipientSelector : undefined} + onChangeText={onChangePattern} + /> + {noResults ? ( + + {t('qrScanner.recipient.results.empty')} + + {t('qrScanner.recipient.results.error')} + + + ) : ( + // Show either suggested recipients or filtered sections based on query + isSheetReady && ( + + ) + )} + + {showQRScanner && ( + + )} + + ) +} + +export const RecipientSelect = memo(_RecipientSelect) diff --git a/apps/mobile/src/components/RecipientSelect/hooks.test.ts b/apps/mobile/src/components/RecipientSelect/hooks.test.ts new file mode 100644 index 0000000..06fcc30 --- /dev/null +++ b/apps/mobile/src/components/RecipientSelect/hooks.test.ts @@ -0,0 +1,434 @@ +import { PreloadedState } from '@reduxjs/toolkit' +import { waitFor } from '@testing-library/react-native' +import { toIncludeSameMembers } from 'jest-extended' +import { act } from 'react-test-renderer' +import { MobileState } from 'src/app/reducer' +import { renderHookWithProviders } from 'src/test/render' +import { useRecipients } from 'wallet/src/components/RecipientSearch/hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { SearchableRecipient } from 'wallet/src/features/address/types' +import { TransactionStateMap } from 'wallet/src/features/transactions/slice' +import { TransactionStatus } from 'wallet/src/features/transactions/types' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' +import { + SAMPLE_SEED_ADDRESS_1, + SAMPLE_SEED_ADDRESS_2, + sendTokenTransactionInfo, + signerMnemonicAccount, + transactionDetails, +} from 'wallet/src/test/fixtures' + +expect.extend({ toIncludeSameMembers }) + +const sendTxDetailsPending = transactionDetails({ + status: TransactionStatus.Pending, + typeInfo: sendTokenTransactionInfo(), + addedTime: 1487076708000, +}) +const sendTxDetailsConfirmed = transactionDetails({ + status: TransactionStatus.Success, + typeInfo: sendTokenTransactionInfo(), + addedTime: 1487076708000, +}) +const sendTxDetailsFailed = transactionDetails({ + status: TransactionStatus.Failed, + typeInfo: sendTokenTransactionInfo(), + addedTime: 1487076710000, +}) + +/** + * Tests interaction of mobile state with useRecipients hook + */ + +type PreloadedStateProps = { + watchedAddresses?: Address[] + hasInactiveAccounts?: boolean + transactions?: TransactionStateMap +} + +const getPreloadedState = (props?: PreloadedStateProps): PreloadedState => { + const { watchedAddresses = [], hasInactiveAccounts = false, transactions = {} } = props || {} + return { + favorites: { + watchedAddresses, + tokens: [], + tokensVisibility: {}, + nftsData: {}, + }, + wallet: { + accounts: { + [activeAccount.address]: activeAccount, + ...(hasInactiveAccounts && { [inactiveAccount.address]: inactiveAccount }), + }, + activeAccountAddress: activeAccount.address, + isUnlocked: true, + settings: { + swapProtection: SwapProtectionSetting.On, + hideSmallBalances: false, + hideSpamTokens: false, + }, + }, + transactions, + } +} + +const activeAccount = signerMnemonicAccount() +const inactiveAccount = signerMnemonicAccount() +const validatedAddressRecipient: SearchableRecipient = { + address: SAMPLE_SEED_ADDRESS_1, +} + +const watchedAddresses = [SAMPLE_SEED_ADDRESS_1, SAMPLE_SEED_ADDRESS_2] + +const searchSectionResult = { + title: 'Search results', + data: [validatedAddressRecipient], +} + +const recentRecipientsSectionResult = { + title: 'Recent', + data: [ + { + address: sendTxDetailsFailed.typeInfo.recipient, + name: '', + }, + { + address: sendTxDetailsConfirmed.typeInfo.recipient, + name: '', + }, + { + address: sendTxDetailsPending.typeInfo.recipient, + name: '', + }, + ], +} + +const recentRecipients = recentRecipientsSectionResult.data.map((recipient) => ({ + data: recipient, + key: recipient.address, +})) + +const inactiveWalletsSectionResult = { + title: 'Your wallets', + data: [inactiveAccount], +} + +const favoriteWalletsSectionResult = { + title: 'Favorite wallets', + data: [{ address: SAMPLE_SEED_ADDRESS_1 }, { address: SAMPLE_SEED_ADDRESS_2 }], +} + +describe(useRecipients, () => { + it('returns correct initial values', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + expect(result.current).toEqual({ + sections: [], + searchableRecipientOptions: [], + pattern: null, + onChangePattern: expect.any(Function), + loading: false, + }) + }) + + describe('Validated address recipient', () => { + it('result does not contain Search Results section if there is no pattern', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.not.arrayContaining([ + expect.objectContaining({ title: 'Search results' }), + ]), + }) + ) + }) + + it('result contains Search Results section if there is a pattern', async () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + // Set pattern + await act(() => { + result.current.onChangePattern(SAMPLE_SEED_ADDRESS_1) + }) + + await waitFor(() => { + expect(result.current.sections).toEqual(expect.arrayContaining([searchSectionResult])) + }) + }) + + it('searchableRecipientOptions contains validatedAddressRecipient', async () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + // Set pattern + await act(() => { + result.current.onChangePattern(SAMPLE_SEED_ADDRESS_1) + }) + + expect(result.current.searchableRecipientOptions).toEqual( + expect.arrayContaining([ + { + data: expect.objectContaining({ address: SAMPLE_SEED_ADDRESS_1 }), + key: SAMPLE_SEED_ADDRESS_1, + }, + ]) + ) + }) + }) + + describe('Recent recipients', () => { + it('result does not contain Recent section if there are no recent recipients', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.not.arrayContaining([expect.objectContaining({ title: 'Recent' })]), + }) + ) + }) + + it('result contains Recent section if there are recent recipients', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + transactions: { + [activeAccount.address]: { + [sendTxDetailsPending.chainId]: [sendTxDetailsPending], + }, + }, + }), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.arrayContaining([ + { + title: 'Recent', + data: [ + { + address: sendTxDetailsPending.typeInfo.recipient, + name: '', + }, + ], + }, + ]), + }) + ) + }) + + it('returns unique recipient addresses', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + transactions: { + [activeAccount.address]: { + [ChainId.Base as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [ChainId.Mainnet as ChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [ChainId.Bnb as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + }, + }, + }), + }) + + const section = result.current.sections[0]! + expect(section.title).toEqual('Recent') + // This method doesn't check the order of the elements + expect(section.data).toIncludeSameMembers([ + { + address: sendTxDetailsPending.typeInfo.recipient, + name: '', + }, + { + address: sendTxDetailsConfirmed.typeInfo.recipient, + name: '', + }, + { + address: sendTxDetailsFailed.typeInfo.recipient, + name: '', + }, + ]) + }) + + it('sorts recipients by most recent transaction', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + transactions: { + [activeAccount.address]: { + [sendTxDetailsPending.chainId]: [ + sendTxDetailsPending, + sendTxDetailsFailed, + sendTxDetailsConfirmed, + ], + }, + }, + }), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.arrayContaining([recentRecipientsSectionResult]), + }) + ) + }) + + it('searchableRecipientOptions contains recent recipients', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + transactions: { + [activeAccount.address]: { + [ChainId.Base as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [ChainId.Mainnet as ChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [ChainId.Bnb as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + }, + }, + }), + }) + + expect(result.current.searchableRecipientOptions).toEqual(recentRecipients) + }) + }) + + describe('Inactive local accounts', () => { + it('result does not contain Your wallets section if there are no inactive accounts', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.not.arrayContaining([ + expect.objectContaining({ title: 'Your wallets' }), + ]), + }) + ) + }) + + it('result contains Your wallets section if there are inactive accounts', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ hasInactiveAccounts: true }), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.arrayContaining([inactiveWalletsSectionResult]), + }) + ) + }) + + it('searchableRecipientOptions contains inactive accounts', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ hasInactiveAccounts: true }), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + searchableRecipientOptions: [{ data: inactiveAccount, key: inactiveAccount.address }], + }) + ) + }) + }) + + describe('Watched wallets', () => { + it('result does not contain Favorite Wallets section if there are no watched wallets', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState(), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.not.arrayContaining([ + expect.objectContaining({ title: 'Favorite wallets' }), + ]), + }) + ) + }) + + it('result contains Favorite Wallets section if there are watched wallets', () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + watchedAddresses, + }), + }) + + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.arrayContaining([favoriteWalletsSectionResult]), + }) + ) + }) + }) + + describe('multiple sections', () => { + it('result contains all sections', async () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + watchedAddresses, + hasInactiveAccounts: true, + transactions: { + [activeAccount.address]: { + [ChainId.Base as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [ChainId.Mainnet as ChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [ChainId.Bnb as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + }, + }, + }), + }) + + await act(() => { + result.current.onChangePattern(SAMPLE_SEED_ADDRESS_1) + }) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + sections: expect.arrayContaining([ + searchSectionResult, + recentRecipientsSectionResult, + inactiveWalletsSectionResult, + favoriteWalletsSectionResult, + ]), + }) + ) + }) + }) + + it('searchableRecipientOptions contains all unique recipients', async () => { + const { result } = renderHookWithProviders(useRecipients, { + preloadedState: getPreloadedState({ + watchedAddresses, + hasInactiveAccounts: true, + transactions: { + [activeAccount.address]: { + [ChainId.Base as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + [ChainId.Mainnet as ChainId]: [sendTxDetailsConfirmed, sendTxDetailsFailed], + [ChainId.Bnb as ChainId]: [sendTxDetailsPending, sendTxDetailsConfirmed], + }, + }, + }), + }) + + await act(() => { + result.current.onChangePattern(SAMPLE_SEED_ADDRESS_1) + }) + + await waitFor(() => { + expect(result.current.searchableRecipientOptions).toEqual([ + // Validated address recipient + { data: validatedAddressRecipient, key: validatedAddressRecipient.address }, + // Inactive local accounts + { data: inactiveAccount, key: inactiveAccount.address }, + // Recent recipients + ...recentRecipients, + ]) + }) + }) + }) +}) diff --git a/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx new file mode 100644 index 0000000..7675ca7 --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/AssociatedAccountsList.tsx @@ -0,0 +1,101 @@ +import React, { useMemo } from 'react' +import { ScrollView, StyleSheet } from 'react-native' +import { Flex, Text, useDeviceDimensions } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { AccountListQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NumberType } from 'utilities/src/format/types' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { useAccountList } from 'wallet/src/features/accounts/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { Account } from 'wallet/src/features/wallet/accounts/types' + +const ADDRESS_ROW_HEIGHT = 40 + +type Portfolio = NonNullable[0]>> + +function _AssociatedAccountsList({ accounts }: { accounts: Account[] }): JSX.Element { + const { fullHeight } = useDeviceDimensions() + const addresses = useMemo(() => accounts.map((account) => account.address), [accounts]) + const { data, loading } = useAccountList({ + addresses, + notifyOnNetworkStatusChange: true, + }) + + const sortedAddressesByBalance = (data?.portfolios ?? []) + .filter((portfolio): portfolio is Portfolio => Boolean(portfolio)) + .map((portfolio) => ({ + address: portfolio.ownerAddress, + balance: portfolio.tokensTotalDenominatedValue?.value, + })) + .sort((a, b) => (b.balance ?? 0) - (a.balance ?? 0)) + + // set max height to around 30% screen size, so we always cut the last visible element + // this way user is aware if there are more elements to see + const accountsScrollViewHeight = + Math.floor((fullHeight * 0.3) / ADDRESS_ROW_HEIGHT) * ADDRESS_ROW_HEIGHT + + ADDRESS_ROW_HEIGHT / 2 + + spacing.spacing12 // 12 is the ScrollView vertical padding + + return ( + + + {sortedAddressesByBalance.map(({ address, balance }, index) => ( + + ))} + + + ) +} + +export const AssociatedAccountsList = React.memo(_AssociatedAccountsList) + +function AssociatedAccountRow({ + index, + address, + balance, + totalCount, + loading, +}: { + index: number + address: string + balance: number | undefined + totalCount: number + loading: boolean +}): JSX.Element { + const { convertFiatAmountFormatted } = useLocalizationContext() + const balanceFormatted = convertFiatAmountFormatted(balance, NumberType.PortfolioBalance) + + return ( + + + + + + {balanceFormatted} + + + ) +} + +const styles = StyleSheet.create({ + accounts: { + paddingVertical: spacing.spacing12, + }, +}) diff --git a/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx new file mode 100644 index 0000000..d32cb60 --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/RemoveLastMnemonicWalletFooter.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, CheckBox, Flex, Text } from 'ui/src' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { ElementName } from 'wallet/src/telemetry/constants' + +export function RemoveLastMnemonicWalletFooter({ + onPress, + inProgress, +}: { + onPress: () => void + inProgress: boolean +}): JSX.Element { + const { t } = useTranslation() + + const [checkBoxAccepted, setCheckBoxAccepted] = useState(false) + const onCheckPressed = (): void => setCheckBoxAccepted(!checkBoxAccepted) + + return ( + <> + + + + {t('account.wallet.remove.check')} + + + } + onCheckPressed={onCheckPressed} + /> + + + + + + ) +} diff --git a/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx new file mode 100644 index 0000000..25fc43a --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/RemoveWalletModal.tsx @@ -0,0 +1,217 @@ +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAnimatedStyle, withTiming } from 'react-native-reanimated' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { Delay } from 'src/components/layout/Delayed' +import { AssociatedAccountsList } from 'src/components/RemoveWallet/AssociatedAccountsList' +import { RemoveLastMnemonicWalletFooter } from 'src/components/RemoveWallet/RemoveLastMnemonicWalletFooter' +import { RemoveWalletStep, useModalContent } from 'src/components/RemoveWallet/useModalContent' +import { navigateToOnboardingImportMethod } from 'src/components/RemoveWallet/utils' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { AnimatedFlex, Button, ColorTokens, Flex, Text, ThemeKeys, useSporeColors } from 'ui/src' +import { iconSizes, opacify } from 'ui/src/theme' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { selectSignerMnemonicAccounts } from 'wallet/src/features/wallet/selectors' +import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +export function RemoveWalletModal(): JSX.Element | null { + const { t } = useTranslation() + const colors = useSporeColors() + const dispatch = useAppDispatch() + + const addressToAccount = useAccounts() + const associatedAccounts = useAppSelector(selectSignerMnemonicAccounts) + + const { initialState } = useAppSelector(selectModalState(ModalName.RemoveWallet)) + const address = initialState?.address + + const account = (address && addressToAccount[address]) || undefined + // If address was not provided, it means we need to remove all mnemonics. + // This happens when user wants to replace mnemonic with a new one + const isReplacing = !address + + const isRemovingMnemonic = Boolean(associatedAccounts.find((acc) => address === acc.address)) + const isRemovingLastMnemonic = isRemovingMnemonic && associatedAccounts.length === 1 + const isRemovingRecoveryPhrase = isReplacing || isRemovingLastMnemonic + + const hasAccountsLeft = + Object.keys(addressToAccount).length > (isReplacing ? associatedAccounts.length : 1) + + const [inProgress, setInProgress] = useState(false) + const [currentStep, setCurrentStep] = useState( + isRemovingRecoveryPhrase ? RemoveWalletStep.Warning : RemoveWalletStep.Final + ) + + const onClose = useCallback((): void => { + dispatch(closeModal({ name: ModalName.RemoveWallet })) + }, [dispatch]) + + const onRemoveWallet = useCallback((): void => { + if (!hasAccountsLeft) { + // user has no accounts left, so we bring onboarding back + dispatch(setFinishedOnboarding({ finishedOnboarding: false })) + navigateToOnboardingImportMethod() + } else if (isReplacing) { + // there are account left and it's replacing, user has view-only accounts left + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.ImportMethod, + params: { + importType: ImportType.NotYetSelected, + entryPoint: OnboardingEntryPoint.Sidebar, + }, + }) + } + const accountsToRemove = isReplacing ? associatedAccounts : account ? [account] : [] + accountsToRemove.forEach(({ address: accAddress, pushNotificationsEnabled }) => { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.Remove, + address: accAddress, + notificationsEnabled: !!pushNotificationsEnabled, + }) + ) + }) + + onClose() + setInProgress(false) + }, [account, associatedAccounts, dispatch, isReplacing, hasAccountsLeft, onClose]) + + const { trigger } = useBiometricPrompt( + () => { + onRemoveWallet() + }, + () => { + setInProgress(false) + } + ) + + const { + requiredForAppAccess: biometricAuthRequiredForAppAccess, + requiredForTransactions: biometricAuthRequiredForTransactions, + } = useBiometricAppSettings() + + const onRemoveWalletPress = async (): Promise => { + if (biometricAuthRequiredForAppAccess || biometricAuthRequiredForTransactions) { + await trigger() + } else { + onRemoveWallet() + } + } + + const onPress = async (): Promise => { + // we want to call onRemoveWallet only once + if (inProgress) { + return + } + if (currentStep === RemoveWalletStep.Warning) { + setCurrentStep(RemoveWalletStep.Final) + } else if (currentStep === RemoveWalletStep.Final) { + setInProgress(true) + await onRemoveWalletPress() + } + } + + const modalContent = useModalContent({ + account, + isReplacing, + currentStep, + isRemovingRecoveryPhrase, + associatedAccounts, + }) + + // we want to nicely squeeze the cancel button when user presses remove + const animatedCancelButtonSpanStyles = useAnimatedStyle(() => { + return { + flexGrow: withTiming(inProgress ? 0 : 1, { duration: Delay.Short / 2 }), + } + }) + + if (!modalContent) { + return null + } + + const { title, description, Icon, iconColorLabel, actionButtonTheme, actionButtonLabel } = + modalContent + + // TODO(MOB-1420): clean up types + const labelColor: ThemeKeys = iconColorLabel + + return ( + + + + + + + + + {title} + + + {description} + + + + + {currentStep === RemoveWalletStep.Final && isRemovingRecoveryPhrase ? ( + <> + + + + ) : ( + + {inProgress ? ( + + ) : ( + + )} + + + )} + + + + ) +} diff --git a/apps/mobile/src/components/RemoveWallet/RemoveWalletModalState.tsx b/apps/mobile/src/components/RemoveWallet/RemoveWalletModalState.tsx new file mode 100644 index 0000000..7c63eff --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/RemoveWalletModalState.tsx @@ -0,0 +1,3 @@ +export interface RemoveWalletModalState { + address?: Address +} diff --git a/apps/mobile/src/components/RemoveWallet/useModalContent.tsx b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx new file mode 100644 index 0000000..8c4f626 --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/useModalContent.tsx @@ -0,0 +1,172 @@ +import React, { useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { SvgProps } from 'react-native-svg' +import { concatListOfAccountNames } from 'src/components/RemoveWallet/utils' +import { Text, ThemeKeys } from 'ui/src' +import AlertTriangleIcon from 'ui/src/assets/icons/alert-triangle.svg' +import TrashIcon from 'ui/src/assets/icons/trash.svg' +import WalletIcon from 'ui/src/assets/icons/wallet-filled.svg' +import { ThemeNames } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' + +export enum RemoveWalletStep { + Warning = 'warning', + Final = 'final', +} + +interface ModalContentParams { + account: Account | undefined + isReplacing: boolean + currentStep: string + isRemovingRecoveryPhrase: boolean + associatedAccounts: Account[] +} + +interface ModalContentResult { + title: React.ReactNode + description: React.ReactNode + Icon: React.ComponentType + iconColorLabel: ThemeKeys + actionButtonLabel?: string + actionButtonTheme?: ThemeNames +} + +export const useModalContent = ({ + account, + isReplacing, + currentStep, + isRemovingRecoveryPhrase, + associatedAccounts, +}: ModalContentParams): ModalContentResult | undefined => { + const { t } = useTranslation() + + const displayName = useDisplayName(account?.address, { includeUnitagSuffix: true }) + + return useMemo(() => { + // 1st speed bump when removing recovery phrase + if (isRemovingRecoveryPhrase && !isReplacing && currentStep === RemoveWalletStep.Warning) { + return { + title: ( + + }} + i18nKey="account.recoveryPhrase.remove.initial.title" + values={{ walletName: displayName?.name }} + /> + + ), + description: t('account.recoveryPhrase.remove.initial.description'), + Icon: TrashIcon, + iconColorLabel: 'statusCritical', + actionButtonLabel: t('common.button.continue'), + actionButtonTheme: 'detrimental', + } + } + + // 1st speed bump when replacing recovery phrase + if (isRemovingRecoveryPhrase && isReplacing && currentStep === RemoveWalletStep.Warning) { + return { + title: t('account.wallet.button.import'), + description: t('account.recoveryPhrase.remove.import.description'), + Icon: WalletIcon, + iconColorLabel: 'neutral2', + actionButtonLabel: t('common.button.continue'), + actionButtonTheme: 'secondary', + } + } + + // 2nd and final speed bump when removing or replacing recovery phrase + if (isRemovingRecoveryPhrase && currentStep === RemoveWalletStep.Final) { + return { + title: ( + + }} + i18nKey="account.recoveryPhrase.remove.final.title" + /> + + ), + description: ( + + ), + }} + i18nKey="account.recoveryPhrase.remove.final.description" + values={{ cloudProviderName: getCloudProviderName() }} + /> + ), + Icon: AlertTriangleIcon, + iconColorLabel: 'statusCritical', + } + } + + // removing mnemonic account + if (account?.type === AccountType.SignerMnemonic && currentStep === RemoveWalletStep.Final) { + const associatedAccountNames = concatListOfAccountNames( + associatedAccounts.filter((aa) => aa.address !== account?.address), + ', ' + ) + + return { + title: ( + + , + }} + i18nKey="account.recoveryPhrase.remove.initial.title" + values={{ walletName: displayName?.name }} + /> + + ), + description: ( + , + }} + i18nKey="account.recoveryPhrase.remove.mnemonic.description" + values={{ walletNames: associatedAccountNames }} + /> + ), + Icon: TrashIcon, + iconColorLabel: 'statusCritical', + actionButtonLabel: t('common.button.remove'), + actionButtonTheme: 'detrimental', + } + } + + // removing view-only account + if (account?.type === AccountType.Readonly && currentStep === RemoveWalletStep.Final) { + return { + title: ( + + , + }} + i18nKey="account.recoveryPhrase.remove.initial.title" + values={{ walletName: displayName?.name }} + /> + + ), + description: t('account.wallet.remove.viewOnly'), + Icon: TrashIcon, + iconColorLabel: 'neutral2', + actionButtonLabel: t('common.button.remove'), + actionButtonTheme: 'secondary', + } + } + }, [ + account, + associatedAccounts, + currentStep, + displayName, + isRemovingRecoveryPhrase, + isReplacing, + t, + ]) +} diff --git a/apps/mobile/src/components/RemoveWallet/utils.test.ts b/apps/mobile/src/components/RemoveWallet/utils.test.ts new file mode 100644 index 0000000..538eab0 --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/utils.test.ts @@ -0,0 +1,31 @@ +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { concatListOfAccountNames } from './utils' + +it('formats no account', () => { + expect(concatListOfAccountNames([], 'and')).toEqual('') +}) + +it('formats 1 account', () => { + expect(concatListOfAccountNames([{ name: '1' }] as Account[], 'and')).toEqual('1') +}) + +it('formats 2 accounts', () => { + expect(concatListOfAccountNames([{ name: '1' }, { name: '2' }] as Account[], 'and')).toEqual( + '1 and 2' + ) +}) + +it('formats 3 accounts', () => { + expect( + concatListOfAccountNames([{ name: '1' }, { name: '2' }, { name: '3' }] as Account[], 'and') + ).toEqual('1, 2 and 3') +}) + +it('formats more than 3 accounts', () => { + expect( + concatListOfAccountNames( + [{ name: '1' }, { name: '2' }, { name: '3' }, { name: '4' }] as Account[], + 'and' + ) + ).toEqual('1, 2, 3 and 4') +}) diff --git a/apps/mobile/src/components/RemoveWallet/utils.ts b/apps/mobile/src/components/RemoveWallet/utils.ts new file mode 100644 index 0000000..1fa4f22 --- /dev/null +++ b/apps/mobile/src/components/RemoveWallet/utils.ts @@ -0,0 +1,51 @@ +import { CommonActions } from '@react-navigation/core' +import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { Account } from 'wallet/src/features/wallet/accounts/types' + +export function concatListOfAccountNames(accounts: Account[], endAdornmentText: string): string { + let result = accounts.map((a) => a.name).join(', ') + // replacing last comman with ' and' + const lastCommaIndex = result.lastIndexOf(',') + if (lastCommaIndex !== -1) { + const before = result.slice(0, lastCommaIndex) + const after = result.slice(lastCommaIndex + 1) + result = before + ' ' + endAdornmentText + after + } + return result +} + +// This fast-forwards user to the same app state as if +// they have pressed "Get Started" on Landing and should now see import method view +export function navigateToOnboardingImportMethod(): void { + dispatchNavigationAction( + CommonActions.reset({ + index: 0, + routes: [ + { + name: Screens.OnboardingStack, + state: { + index: 1, + routes: [ + { + name: OnboardingScreens.Landing, + params: { + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + importType: ImportType.NotYetSelected, + }, + }, + { + name: OnboardingScreens.ImportMethod, + params: { + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + importType: ImportType.NotYetSelected, + }, + }, + ], + }, + }, + ], + }) + ) +} diff --git a/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx new file mode 100644 index 0000000..e6c44f4 --- /dev/null +++ b/apps/mobile/src/components/RestoreWalletModal/RestoreWalletModal.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { closeAllModals, closeModal } from 'src/features/modals/modalSlice' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { Button, Flex, Text, useSporeColors } from 'ui/src' +import LockIcon from 'ui/src/assets/icons/lock.svg' +import { iconSizes, opacify } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +export function RestoreWalletModal(): JSX.Element | null { + const { t } = useTranslation() + const colors = useSporeColors() + const dispatch = useAppDispatch() + + const onDismiss = (): void => { + dispatch(closeModal({ name: ModalName.RestoreWallet })) + } + + const onRestore = (): void => { + dispatch(closeAllModals()) + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.RestoreCloudBackupLoading, + params: { + entryPoint: OnboardingEntryPoint.Sidebar, + importType: ImportType.RestoreMnemonic, + }, + }) + } + + return ( + + + + + + + {t('account.wallet.button.restore')} + + + {t('account.wallet.restore.description')} + + + + + + + + ) +} diff --git a/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx new file mode 100644 index 0000000..a6aac9e --- /dev/null +++ b/apps/mobile/src/components/Settings/BiometricAuthWarningModal.tsx @@ -0,0 +1,41 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useBiometricName } from 'src/features/biometrics/hooks' +import { isAndroid } from 'uniswap/src/utils/platform' +import { + WarningModal, + WarningModalProps, +} from 'wallet/src/components/modals/WarningModal/WarningModal' +import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' +import { ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + isTouchIdDevice: boolean + onConfirm: WarningModalProps['onConfirm'] + onClose: WarningModalProps['onClose'] +} + +export function BiometricAuthWarningModal({ + isTouchIdDevice, + onConfirm, + onClose, +}: Props): JSX.Element { + const { t } = useTranslation() + const biometricsMethod = useBiometricName(isTouchIdDevice) + return ( + + ) +} diff --git a/apps/mobile/src/components/Settings/SettingsRow.tsx b/apps/mobile/src/components/Settings/SettingsRow.tsx new file mode 100644 index 0000000..156a215 --- /dev/null +++ b/apps/mobile/src/components/Settings/SettingsRow.tsx @@ -0,0 +1,141 @@ +import { NavigatorScreenParams } from '@react-navigation/core' +import React from 'react' +import { ValueOf } from 'react-native-gesture-handler/lib/typescript/typeUtils' +import { + OnboardingStackNavigationProp, + OnboardingStackParamList, + SettingsStackNavigationProp, + SettingsStackParamList, +} from 'src/app/navigation/types' +import { openModal } from 'src/features/modals/modalSlice' +import { Screens } from 'src/screens/Screens' +import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { Switch } from 'wallet/src/components/buttons/Switch' +import { Arrow } from 'wallet/src/components/icons/Arrow' +import { useAppDispatch } from 'wallet/src/state' +import { ModalName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' + +export interface SettingsSection { + subTitle: string + data: (SettingsSectionItem | SettingsSectionItemComponent)[] + isHidden?: boolean +} + +export interface SettingsSectionItemComponent { + component: JSX.Element + isHidden?: boolean +} +type SettingsModal = typeof ModalName.FiatCurrencySelector | typeof ModalName.LanguageSelector + +export interface SettingsSectionItem { + screen?: keyof SettingsStackParamList | typeof Screens.OnboardingStack + modal?: SettingsModal + screenProps?: ValueOf | NavigatorScreenParams + externalLink?: string + action?: JSX.Element + text: string + subText?: string + icon: JSX.Element + isHidden?: boolean + currentSetting?: string + onToggle?: () => void + isToggleEnabled?: boolean +} + +interface SettingsRowProps { + page: SettingsSectionItem + navigation: SettingsStackNavigationProp & OnboardingStackNavigationProp +} + +export function SettingsRow({ + page: { + screen, + modal, + screenProps, + externalLink, + action, + icon, + text, + subText, + currentSetting, + onToggle, + isToggleEnabled, + }, + navigation, +}: SettingsRowProps): JSX.Element { + const colors = useSporeColors() + const dispatch = useAppDispatch() + + const handleRow = async (): Promise => { + if (onToggle) { + return + } else if (screen) { + navigation.navigate(screen, screenProps) + } else if (modal) { + dispatch(openModal({ name: modal })) + } else if (externalLink) { + await openUri(externalLink) + } + } + + if (onToggle && isToggleEnabled === undefined) { + throw new Error('Should pass valid isToggleEnabled prop when onToggle is passed') + } + + return ( + + + + + {icon} + + + + {text} + + {subText && ( + + {subText} + + )} + + + {onToggle && typeof isToggleEnabled === 'boolean' ? ( + + ) : screen || modal ? ( + + {currentSetting ? ( + + + {currentSetting} + + + ) : null} + + + ) : externalLink ? ( + + ) : ( + action + )} + + + ) +} diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx new file mode 100644 index 0000000..a7cab10 --- /dev/null +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceItemContextMenu.tsx @@ -0,0 +1,30 @@ +import React, { memo, useMemo } from 'react' +import ContextMenu from 'react-native-context-menu-view' +import { useTokenContextMenu } from 'src/features/balances/hooks' +import { borderRadii } from 'ui/src/theme' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' + +export const TokenBalanceItemContextMenu = memo(function _TokenBalanceItem({ + portfolioBalance, + children, +}: { + portfolioBalance: PortfolioBalance + children: React.ReactNode +}) { + const { menuActions, onContextMenuPress } = useTokenContextMenu({ + currencyId: portfolioBalance.currencyInfo.currencyId, + portfolioBalance, + }) + + const style = useMemo(() => ({ borderRadius: borderRadii.rounded16 }), []) + + return ( + + {children} + + ) +}) diff --git a/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx new file mode 100644 index 0000000..a1e577d --- /dev/null +++ b/apps/mobile/src/components/TokenBalanceList/TokenBalanceList.tsx @@ -0,0 +1,301 @@ +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import { useFocusEffect } from '@react-navigation/core' +import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' +import React, { forwardRef, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, RefreshControl } from 'react-native' +import Animated, { FadeInDown, FadeOut } from 'react-native-reanimated' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { TokenBalanceItemContextMenu } from 'src/components/TokenBalanceList/TokenBalanceItemContextMenu' +import { useAdaptiveFooter } from 'src/components/home/hooks' +import { + TAB_BAR_HEIGHT, + TAB_VIEW_SCROLL_THROTTLE, + TabProps, +} from 'src/components/layout/TabHelpers' +import { Screens } from 'src/screens/Screens' +import { + AnimatedFlex, + Flex, + Loader, + useDeviceDimensions, + useDeviceInsets, + useSporeColors, +} from 'ui/src' +import { zIndices } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { HiddenTokensRow } from 'wallet/src/features/portfolio/HiddenTokensRow' +import { TokenBalanceItem } from 'wallet/src/features/portfolio/TokenBalanceItem' +import { + HIDDEN_TOKEN_BALANCES_ROW, + TokenBalanceListContextProvider, + TokenBalanceListRow, + useTokenBalanceListContext, +} from 'wallet/src/features/portfolio/TokenBalanceListContext' +import { CurrencyId } from 'wallet/src/utils/currencyId' + +type TokenBalanceListProps = TabProps & { + empty?: JSX.Element | null + onPressToken: (currencyId: CurrencyId) => void + isExternalProfile?: boolean +} + +const ESTIMATED_TOKEN_ITEM_HEIGHT = 64 + +export const TokenBalanceList = forwardRef, TokenBalanceListProps>( + function _TokenBalanceList( + { owner, onPressToken, isExternalProfile = false, ...rest }, + ref + ): JSX.Element { + return ( + + + + ) + } +) + +export const TokenBalanceListInner = forwardRef< + FlatList, + TokenBalanceListProps +>(function _TokenBalanceListInner( + { + empty, + containerProps, + scrollHandler, + isExternalProfile = false, + renderedInModal = false, + refreshing, + headerHeight = 0, + onRefresh, + }, + ref +) { + const { t } = useTranslation() + const colors = useSporeColors() + const insets = useDeviceInsets() + + const { rows, balancesById, networkStatus, refetch } = useTokenBalanceListContext() + + const { onContentSizeChange, adaptiveFooter, footerHeight } = useAdaptiveFooter( + containerProps?.contentContainerStyle + ) + + // The following logic is meant to speed up the screen transition from the token details screen back to the home screen. + // When we call `navigation.goBack()`, a re-render is triggered *before* the animation begins. + // In order for that first re-render to be fast, we use `cachedData` so that it renders a memoized `FlatList` of tokens, + // (this `FlatList` is the most expensive component on this screen). + // After the transition ends, we set focus to `true` to trigger a re-render using the latest `data`. + + const [isFocused, setIsFocused] = useState(true) + const [cachedRows, setCachedRows] = useState(null) + + const rowsRef = useRef(rows) + rowsRef.current = rows + + useFocusEffect( + useCallback(() => { + return (): void => { + // We save the cached data to avoid a re-render when the user navigates back to it. + // This speeds up the animation while preserving the scroll position. + setCachedRows(rowsRef.current) + setIsFocused(false) + } + }, []) + ) + + const navigation = useAppStackNavigation() + + useEffect(() => { + // We use this instead of relying on react-navigation's `useIsFocused` because we want to speed up the screen transition + // when the user goes from the token details screen back to the home screen, so we want this state to change *after* the animation is done instead of *before*. + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', (e) => { + if (!e.data.closing) { + setIsFocused(true) + } + }) + + return (): void => unsubscribeTransitionEnd() + }, [navigation]) + + const refreshControl = useMemo(() => { + return ( + + ) + }, [insets.top, headerHeight, refreshing, colors.neutral3, onRefresh]) + + // In order to avoid unnecessary re-renders of the entire FlatList, the `renderItem` function should never change. + // That's why we use a context provider so that each row can read from there instead of passing down new props every time the data changes. + const renderItem = useCallback( + ({ item }: { item: TokenBalanceListRow }): JSX.Element => { + return + }, + [footerHeight] + ) + + const ListEmptyComponent = useMemo(() => { + return ( + + {empty} + + ) + }, [containerProps?.emptyContainerStyle, empty]) + + const hasError = isError(networkStatus, !!balancesById) + + const ListHeaderComponent = useMemo(() => { + return hasError ? ( + + + + ) : null + }, [hasError, refetch, t]) + + // add negative z index to prevent footer from covering hidden tokens row when minimized + const ListFooterComponentStyle = useMemo(() => ({ zIndex: zIndices.negative }), []) + + const List = renderedInModal + ? BottomSheetFlatList + : Animated.FlatList + + const getItemLayout = useCallback( + ( + _: Maybe, + index: number + ): { length: number; offset: number; index: number } => ({ + length: ESTIMATED_TOKEN_ITEM_HEIGHT, + offset: ESTIMATED_TOKEN_ITEM_HEIGHT * index, + index, + }), + [] + ) + + // Note: `PerformanceView` must wrap the entire return statement to properly track interactive states. + return ( + + {!balancesById ? ( + isNonPollingRequestInFlight(networkStatus) ? ( + + + + ) : ( + + refetch?.()} + /> + + ) + ) : ( + + )} + + ) +}) + +const TokenBalanceItemRow = memo(function TokenBalanceItemRow({ + item, + footerHeight, +}: { + item: TokenBalanceListRow + footerHeight: Animated.SharedValue +}) { + const { fullHeight } = useDeviceDimensions() + + const { + balancesById, + hiddenTokensCount, + hiddenTokensExpanded, + isWarmLoading, + onPressToken, + setHiddenTokensExpanded, + } = useTokenBalanceListContext() + + if (item === HIDDEN_TOKEN_BALANCES_ROW) { + return ( + { + if (hiddenTokensExpanded) { + footerHeight.value = fullHeight + } + setHiddenTokensExpanded(!hiddenTokensExpanded) + }} + /> + ) + } + + const portfolioBalance = balancesById?.[item] + + if (!portfolioBalance) { + // This can happen when the view is out of focus and the user sells/sends 100% of a token's balance. + // In that case, the token is removed from the `balancesById` object, but the FlatList is still using the cached array of IDs until the view comes back into focus. + // As soon as the view comes back into focus, the FlatList will re-render with the latest data, so users won't really see this Skeleton for more than a few milliseconds when this happens. + return ( + + + + ) + } + + return ( + + + + ) +}) diff --git a/apps/mobile/src/components/TokenDetails/LinkButton.tsx b/apps/mobile/src/components/TokenDetails/LinkButton.tsx new file mode 100644 index 0000000..b701842 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/LinkButton.tsx @@ -0,0 +1,83 @@ +import React from 'react' +import { SvgProps } from 'react-native-svg' +import { useAppDispatch } from 'src/app/hooks' +import Trace from 'src/components/Trace/Trace' +import { Flex, IconProps, Text, TouchableArea, useSporeColors } from 'ui/src' +import CopyIcon from 'ui/src/assets/icons/copy-sheets.svg' +import { iconSizes } from 'ui/src/theme' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { ElementNameType } from 'wallet/src/telemetry/constants' +import { setClipboard } from 'wallet/src/utils/clipboard' +import { openUri } from 'wallet/src/utils/linking' + +export enum LinkButtonType { + Copy = 'copy', + Link = 'link', +} + +export function LinkButton({ + buttonType, + label, + Icon, + element, + openExternalBrowser = false, + isSafeUri = false, + value, +}: { + buttonType: LinkButtonType + label: string + Icon?: React.FC + element: ElementNameType + openExternalBrowser?: boolean + isSafeUri?: boolean + value: string +}): JSX.Element { + const dispatch = useAppDispatch() + const colors = useSporeColors() + + const copyValue = async (): Promise => { + await setClipboard(value) + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.Address, + }) + ) + } + + const onPress = async (): Promise => { + if (buttonType === LinkButtonType.Link) { + await openUri(value, openExternalBrowser, isSafeUri) + } else { + await copyValue() + } + } + + return ( + + + + {Icon && } + + {label} + + {buttonType === LinkButtonType.Copy && ( + + )} + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/SendButton.tsx b/apps/mobile/src/components/TokenDetails/SendButton.tsx new file mode 100644 index 0000000..40bc2d5 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/SendButton.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { Flex, TouchableArea } from 'ui/src' +import SendIcon from 'ui/src/assets/icons/send-action.svg' +import { iconSizes } from 'ui/src/theme' +import { ElementName } from 'wallet/src/telemetry/constants' + +type Props = { + onPress: () => void + size?: number + color?: string +} + +export function SendButton({ onPress, color, size = iconSizes.icon24 }: Props): JSX.Element { + return ( + + + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenBalances.tsx b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx new file mode 100644 index 0000000..a864686 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenBalances.tsx @@ -0,0 +1,173 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import Trace from 'src/components/Trace/Trace' +import { MobileEventName } from 'src/features/telemetry/constants' +import { Flex, Separator, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import { InlineNetworkPill } from 'wallet/src/components/network/NetworkPill' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccount, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' +import { CurrencyId } from 'wallet/src/utils/currencyId' +import { SendButton } from './SendButton' + +/** + * Renders token balances for current chain (if any) and other chains (if any). + * If user has no balance at all, it renders nothing. + */ +export function TokenBalances({ + currentChainBalance, + otherChainBalances, + onPressSend, +}: { + currentChainBalance: PortfolioBalance | null + otherChainBalances: PortfolioBalance[] | null + onPressSend: () => void +}): JSX.Element | null { + const { t } = useTranslation() + + const activeAccount = useActiveAccount() + const accountType = activeAccount?.type + const displayName = useDisplayName(activeAccount?.address, { includeUnitagSuffix: true })?.name + const isReadonly = accountType === AccountType.Readonly + + const hasCurrentChainBalances = Boolean(currentChainBalance) + const hasOtherChainBalances = Boolean(otherChainBalances && otherChainBalances.length > 0) + + const { preload, navigateWithPop } = useTokenDetailsNavigation() + const navigateToCurrency = useCallback( + (currencyId: CurrencyId) => { + preload(currencyId) + navigateWithPop(currencyId) + }, + [navigateWithPop, preload] + ) + + if (!hasCurrentChainBalances && !hasOtherChainBalances) { + return null + } + + return ( + + {currentChainBalance && ( + + + + + )} + {hasOtherChainBalances && otherChainBalances ? ( + + + {t('token.balances.other')} + + + {otherChainBalances.map((balance) => { + return ( + + ) + })} + + + ) : null} + + ) +} + +export function CurrentChainBalance({ + balance, + isReadonly, + displayName, + onPressSend, +}: { + balance: PortfolioBalance + isReadonly: boolean + displayName?: string + onPressSend: () => void +}): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() + + return ( + + + + {isReadonly + ? t('token.balances.viewOnly', { ownerAddress: displayName }) + : t('token.balances.main')} + + + + {convertFiatAmountFormatted(balance.balanceUSD, NumberType.FiatTokenDetails)} + + + {formatNumberOrString({ value: balance.quantity, type: NumberType.TokenNonTx })}{' '} + {getSymbolDisplayText(balance.currencyInfo.currency.symbol)} + + + + + + + + ) +} + +function OtherChainBalance({ + balance, + navigate, +}: { + balance: PortfolioBalance + navigate: (currencyId: CurrencyId) => void +}): JSX.Element { + const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() + + return ( + + navigate(balance.currencyInfo.currencyId)}> + + + + + + {convertFiatAmountFormatted(balance.balanceUSD, NumberType.FiatTokenDetails)} + + + + + + {formatNumberOrString({ + value: balance.quantity, + type: NumberType.TokenNonTx, + })}{' '} + {getSymbolDisplayText(balance.currencyInfo.currency.symbol)} + + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx new file mode 100644 index 0000000..f5cfd16 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsActionButtons.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import Trace from 'src/components/Trace/Trace' +import { Button, Flex } from 'ui/src' +import { validColor } from 'ui/src/theme' +import { ElementName, ElementNameType, SectionName } from 'wallet/src/telemetry/constants' +import { getContrastPassingTextColor } from 'wallet/src/utils/colors' + +function CTAButton({ + title, + element, + onPress, + tokenColor, +}: { + title: string + element: ElementNameType + onPress: () => void + tokenColor?: Maybe +}): JSX.Element { + return ( + + + + ) +} + +export function TokenDetailsActionButtons({ + onPressBuy, + onPressSell, + tokenColor, +}: { + onPressBuy: () => void + onPressSell: () => void + tokenColor?: Maybe +}): JSX.Element { + const { t } = useTranslation() + + return ( + + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsFavoriteButton.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsFavoriteButton.tsx new file mode 100644 index 0000000..47d91e4 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsFavoriteButton.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { useAppSelector } from 'src/app/hooks' +import { Favorite } from 'src/components/icons/Favorite' +import { useToggleFavoriteCallback } from 'src/features/favorites/hooks' +import { TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' + +export function TokenDetailsFavoriteButton({ currencyId }: { currencyId: string }): JSX.Element { + const id = currencyId.toLowerCase() + const isFavoriteToken = useAppSelector(selectFavoriteTokens).indexOf(id) !== -1 + const onFavoritePress = useToggleFavoriteCallback(id, isFavoriteToken) + return ( + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx new file mode 100644 index 0000000..361c9d7 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsHeader.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { Flex, flexStyles, Text, TouchableArea } from 'ui/src' +import { iconSizes, imageSizes } from 'ui/src/theme' +import { + SafetyLevel, + TokenDetailsScreenQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import WarningIcon from 'wallet/src/components/icons/WarningIcon' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' + +export interface TokenDetailsHeaderProps { + data?: TokenDetailsScreenQuery + loading?: boolean + onPressWarningIcon: () => void +} + +export function TokenDetailsHeader({ + data, + loading = false, + onPressWarningIcon, +}: TokenDetailsHeaderProps): JSX.Element { + const token = data?.token + const tokenProject = token?.project + + return ( + + + + + {tokenProject?.name ?? '—'} + + {/* Suppress warning icon on low warning level */} + {(tokenProject?.safetyLevel === SafetyLevel.StrongWarning || + tokenProject?.safetyLevel === SafetyLevel.Blocked) && ( + + + + )} + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx new file mode 100644 index 0000000..af0eedf --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsLinks.tsx @@ -0,0 +1,80 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView, View } from 'react-native' +import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' +import { Flex, Text } from 'ui/src' +import GlobeIcon from 'ui/src/assets/icons/globe-filled.svg' +import TwitterIcon from 'ui/src/assets/icons/x-twitter.svg' +import { TokenDetailsScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ElementName } from 'wallet/src/telemetry/constants' +import { + currencyIdToAddress, + currencyIdToChain, + isDefaultNativeAddress, +} from 'wallet/src/utils/currencyId' +import { ExplorerDataType, getExplorerLink, getTwitterLink } from 'wallet/src/utils/linking' +import { LinkButton, LinkButtonType } from './LinkButton' + +export function TokenDetailsLinks({ + currencyId, + data, +}: { + currencyId: string + data: TokenDetailsScreenQuery | undefined +}): JSX.Element { + const { t } = useTranslation() + + const { homepageUrl, twitterName } = data?.token?.project ?? {} + const chainId = currencyIdToChain(currencyId) ?? ChainId.Mainnet + const address = currencyIdToAddress(currencyId) + const explorerLink = getExplorerLink(chainId, address, ExplorerDataType.TOKEN) + const explorerName = CHAIN_INFO[chainId].explorer.name + + return ( + + + + {t('token.links.title')} + + + + + {homepageUrl && ( + + )} + {twitterName && ( + + )} + {!isDefaultNativeAddress(address) && ( + + )} + + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx new file mode 100644 index 0000000..9fc9e31 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/TokenDetailsStats.tsx @@ -0,0 +1,215 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import Animated, { FadeIn, FadeOut } from 'react-native-reanimated' +import { LongText } from 'src/components/text/LongText' +import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { TokenDetailsScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NumberType } from 'utilities/src/format/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { Language } from 'wallet/src/features/language/constants' +import { useCurrentLanguage, useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' + +function StatsRow({ + label, + children, + statsIcon, +}: { + label: string + children: JSX.Element + statsIcon: JSX.Element +}): JSX.Element { + return ( + + + {statsIcon} + + {label} + + + {children} + + ) +} + +export function TokenDetailsMarketData({ + fullyDilutedValuation, + marketCap, + volume, + priceLow52W, + priceHight52W, + isLoading = false, + tokenColor, +}: { + fullyDilutedValuation?: number + marketCap?: number + volume?: number + priceLow52W?: number + priceHight52W?: number + isLoading?: boolean + tokenColor?: Nullable +}): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const defaultTokenColor = colors.neutral3.get() + const { convertFiatAmountFormatted } = useLocalizationContext() + + return ( + + + }> + + {convertFiatAmountFormatted(marketCap, NumberType.FiatTokenStats)} + + + + }> + + {convertFiatAmountFormatted(fullyDilutedValuation, NumberType.FiatTokenStats)} + + + + }> + + {convertFiatAmountFormatted(volume, NumberType.FiatTokenStats)} + + + + }> + + {convertFiatAmountFormatted(priceHight52W, NumberType.FiatTokenDetails)} + + + + }> + + {convertFiatAmountFormatted(priceLow52W, NumberType.FiatTokenDetails)} + + + + ) +} + +export function TokenDetailsStats({ + data, + tokenColor, +}: { + data: TokenDetailsScreenQuery | undefined + tokenColor?: Maybe +}): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const currentLanguage = useCurrentLanguage() + const currentLanguageInfo = useCurrentLanguageInfo() + + const [showTranslation, setShowTranslation] = useState(false) + + const onChainData = data?.token + const offChainData = data?.token?.project + + const description = offChainData?.description + const translatedDescription = + offChainData?.descriptionTranslations?.descriptionEsEs || + offChainData?.descriptionTranslations?.descriptionFrFr || + offChainData?.descriptionTranslations?.descriptionJaJp || + offChainData?.descriptionTranslations?.descriptionPtPt || + offChainData?.descriptionTranslations?.descriptionZhHans || + offChainData?.descriptionTranslations?.descriptionZhHant + const name = offChainData?.name ?? onChainData?.name + const marketCap = offChainData?.markets?.[0]?.marketCap?.value + const volume = onChainData?.market?.volume?.value + const priceHight52W = + offChainData?.markets?.[0]?.priceHigh52W?.value ?? onChainData?.market?.priceHigh52W?.value + const priceLow52W = + offChainData?.markets?.[0]?.priceLow52W?.value ?? onChainData?.market?.priceLow52W?.value + const fullyDilutedValuation = offChainData?.markets?.[0]?.fullyDilutedValuation?.value + const currentDescription = + showTranslation && translatedDescription ? translatedDescription : description + + return ( + + {currentDescription && ( + + {name && ( + + {t('token.stats.section.about', { token: name })} + + )} + + + + {currentLanguage !== Language.English && !!translatedDescription && ( + setShowTranslation(!showTranslation)}> + + {showTranslation ? ( + + + + + {currentLanguageInfo.displayName} + + + + {t('token.stats.translation.original')} + + + ) : ( + + + + + {t('token.stats.translation.translate', { + language: currentLanguageInfo.displayName, + })} + + + + )} + + + )} + + )} + + + {t('token.stats.title')} + + + + + ) +} diff --git a/apps/mobile/src/components/TokenDetails/hooks.test.ts b/apps/mobile/src/components/TokenDetails/hooks.test.ts new file mode 100644 index 0000000..a432787 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/hooks.test.ts @@ -0,0 +1,190 @@ +import { useCrossChainBalances, useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { Screens } from 'src/screens/Screens' +import { preloadedMobileState } from 'src/test/fixtures' +import { act, renderHook, waitFor } from 'src/test/test-utils' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { + SAMPLE_CURRENCY_ID_1, + portfolio, + portfolioBalances, + tokenBalance, + usdcArbitrumToken, + usdcBaseToken, +} from 'wallet/src/test/fixtures' +import { queryResolvers } from 'wallet/src/test/utils' + +const mockedNavigation = { + navigate: jest.fn(), + canGoBack: jest.fn(), + pop: jest.fn(), + push: jest.fn(), +} + +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native') + return { + ...actualNav, + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + useNavigation: () => mockedNavigation, + } +}) + +describe(useCrossChainBalances, () => { + describe('currentChainBalance', () => { + it('returns null if there are no balances for the specified currency', async () => { + const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), { + preloadedState: preloadedMobileState(), + }) + + await act(() => undefined) + + expect(result.current).toEqual( + expect.objectContaining({ + currentChainBalance: null, + }) + ) + }) + + it('returns balance if there is at least one for the specified currency', async () => { + const Portfolio = portfolio() + const currentChainBalance = portfolioBalances({ portfolio: Portfolio })[0]! + const { resolvers } = queryResolvers({ + portfolios: () => [Portfolio], + }) + const { result } = renderHook( + () => useCrossChainBalances(currentChainBalance.currencyInfo.currencyId, null), + { + preloadedState: preloadedMobileState(), + resolvers, + } + ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ + currentChainBalance, + }) + ) + }) + }) + }) + + describe('otherChainBalances', () => { + it('returns null if there are no bridged currencies', async () => { + const { result } = renderHook(() => useCrossChainBalances(SAMPLE_CURRENCY_ID_1, null), { + preloadedState: preloadedMobileState(), + }) + + await act(() => undefined) + + expect(result.current).toEqual( + expect.objectContaining({ + otherChainBalances: null, + }) + ) + }) + + it('does not include current chain balance in other chain balances', async () => { + const tokenBalances = [ + tokenBalance({ token: usdcBaseToken() }), + tokenBalance({ token: usdcArbitrumToken() }), + ] + + const bridgeInfo = tokenBalances.map((balance) => ({ + chain: balance.token.chain, + address: balance.token?.address, + })) + const Portfolio = portfolio({ tokenBalances }) + const [currentChainBalance, ...otherChainBalances] = portfolioBalances({ + portfolio: Portfolio, + }) + const { resolvers } = queryResolvers({ + portfolios: () => [Portfolio], + }) + + const { result } = renderHook( + () => useCrossChainBalances(currentChainBalance!.currencyInfo.currencyId, bridgeInfo), + { + preloadedState: preloadedMobileState(), + resolvers, + } + ) + + await waitFor(() => { + expect(result.current).toEqual( + expect.objectContaining({ currentChainBalance, otherChainBalances }) + ) + }) + }) + }) +}) + +describe(useTokenDetailsNavigation, () => { + afterEach(() => { + jest.resetAllMocks() + }) + + it('returns correct result', () => { + const { result } = renderHook(() => useTokenDetailsNavigation()) + + expect(result.current).toEqual({ + preload: expect.any(Function), + navigate: expect.any(Function), + navigateWithPop: expect.any(Function), + }) + }) + + it('preloads token details when preload function is called', async () => { + const queryResolver = jest.fn() + const { resolvers } = queryResolvers({ + token: queryResolver, + }) + const { result } = renderHook(() => useTokenDetailsNavigation(), { + resolvers, + }) + + await act(() => result.current.preload(SAMPLE_CURRENCY_ID_1)) + + expect(queryResolver).toHaveBeenCalledTimes(1) + expect(queryResolver.mock.calls[0][1]).toEqual(currencyIdToContractInput(SAMPLE_CURRENCY_ID_1)) + }) + + it('navigates to token details when navigate function is called', async () => { + const { result } = renderHook(() => useTokenDetailsNavigation()) + + await act(() => result.current.navigate(SAMPLE_CURRENCY_ID_1)) + + expect(mockedNavigation.navigate).toHaveBeenCalledTimes(1) + expect(mockedNavigation.navigate).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { + currencyId: SAMPLE_CURRENCY_ID_1, + }) + }) + + describe('navigationWithPop', () => { + it('pops the last screen from the stack and navigates to token details if can go back', async () => { + mockedNavigation.canGoBack.mockReturnValueOnce(true) + const { result } = renderHook(() => useTokenDetailsNavigation()) + + await act(() => result.current.navigateWithPop(SAMPLE_CURRENCY_ID_1)) + + expect(mockedNavigation.pop).toHaveBeenCalledTimes(1) + expect(mockedNavigation.push).toHaveBeenCalledTimes(1) + expect(mockedNavigation.push).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { + currencyId: SAMPLE_CURRENCY_ID_1, + }) + }) + + it('pushes token details screen to the stack without popping if there is no previous screen', async () => { + mockedNavigation.canGoBack.mockReturnValueOnce(false) + const { result } = renderHook(() => useTokenDetailsNavigation()) + + await act(() => result.current.navigateWithPop(SAMPLE_CURRENCY_ID_1)) + + expect(mockedNavigation.pop).not.toHaveBeenCalled() + expect(mockedNavigation.push).toHaveBeenCalledTimes(1) + expect(mockedNavigation.push).toHaveBeenNthCalledWith(1, Screens.TokenDetails, { + currencyId: SAMPLE_CURRENCY_ID_1, + }) + }) + }) +}) diff --git a/apps/mobile/src/components/TokenDetails/hooks.ts b/apps/mobile/src/components/TokenDetails/hooks.ts new file mode 100644 index 0000000..151d040 --- /dev/null +++ b/apps/mobile/src/components/TokenDetails/hooks.ts @@ -0,0 +1,99 @@ +import { useCallback, useMemo } from 'react' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { useBalances } from 'src/features/dataApi/balances' +import { Screens } from 'src/screens/Screens' +import { + Chain, + useTokenDetailsScreenLazyQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { + CurrencyId, + buildCurrencyId, + buildNativeCurrencyId, + currencyIdToChain, +} from 'wallet/src/utils/currencyId' + +/** Helper hook to retrieve balances across chains for a given currency, for the active account. */ +export function useCrossChainBalances( + currencyId: string, + bridgeInfo: Maybe<{ chain: Chain; address?: Maybe }[]> +): { + currentChainBalance: PortfolioBalance | null + otherChainBalances: PortfolioBalance[] | null +} { + const currentChainBalance = useBalances([currencyId])?.[0] ?? null + const currentChainId = currencyIdToChain(currencyId) + + const bridgedCurrencyIds = useMemo( + () => + bridgeInfo + ?.map(({ chain, address }) => { + const chainId = fromGraphQLChain(chain) + if (!chainId || chainId === currentChainId) { + return null + } + if (!address) { + return buildNativeCurrencyId(chainId) + } + return buildCurrencyId(chainId, address) + }) + .filter((b): b is string => !!b), + + [bridgeInfo, currentChainId] + ) + const otherChainBalances = useBalances(bridgedCurrencyIds) + + return { + currentChainBalance, + otherChainBalances, + } +} + +/** Utility hook to simplify navigating to token details screen */ +export function useTokenDetailsNavigation(): { + preload: (currencyId: CurrencyId) => void + navigate: (currencyId: CurrencyId) => void + navigateWithPop: (currencyId: CurrencyId) => void +} { + const navigation = useAppStackNavigation() + const [load] = useTokenDetailsScreenLazyQuery() + + const preload = useCallback( + async (currencyId: CurrencyId): Promise => { + await load({ + variables: currencyIdToContractInput(currencyId), + }) + }, + [load] + ) + + // the desired behavior is to push the new token details screen onto the stack instead of replacing it + // however, `push` could create an infinitely deep navigation stack that is hard to get out of + // for that reason, we first `pop` token details from the stack, and then push it. + // + // Use whenever we want to avoid nested token details screens in the nav stack. + const navigateWithPop = useCallback( + (currencyId: CurrencyId): void => { + if (navigation.canGoBack()) { + navigation.pop() + } + navigation.push(Screens.TokenDetails, { currencyId }) + }, + [navigation] + ) + + const navigate = useCallback( + (currencyId: CurrencyId): void => { + navigation.navigate(Screens.TokenDetails, { currencyId }) + }, + [navigation] + ) + + return useMemo( + () => ({ preload, navigate, navigateWithPop }), + [navigate, navigateWithPop, preload] + ) +} diff --git a/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx new file mode 100644 index 0000000..940ac17 --- /dev/null +++ b/apps/mobile/src/components/TokenSelector/TokenFiatOnRampList.tsx @@ -0,0 +1,107 @@ +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import React, { memo, useCallback, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItemInfo } from 'react-native' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { Flex, Inset, Loader } from 'ui/src' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { TokenOptionItem } from 'wallet/src/components/TokenSelector/TokenOptionItem' +import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { CurrencyId } from 'wallet/src/utils/currencyId' + +interface Props { + onSelectCurrency: (currency: FiatOnRampCurrency) => void + onRetry: () => void + error: boolean + loading: boolean + list: FiatOnRampCurrency[] | undefined +} + +function TokenOptionItemWrapper({ + currency, + onSelectCurrency, +}: { + currency: FiatOnRampCurrency + onSelectCurrency: (currency: FiatOnRampCurrency) => void +}): JSX.Element | null { + const { currencyInfo } = currency + + const option = useMemo( + // we need to convert to TokenOption without quantity and balanceUSD + // to use in Token Selector + () => (currencyInfo ? { currencyInfo, quantity: 0, balanceUSD: 0 } : null), + [currencyInfo] + ) + const onPress = useCallback(() => onSelectCurrency?.(currency), [currency, onSelectCurrency]) + + if (!option) { + return null + } + + return ( + + ) +} + +function _TokenFiatOnRampList({ + onSelectCurrency, + error, + onRetry, + list, + loading, +}: Props): JSX.Element { + const { t } = useTranslation() + + const flatListRef = useRef(null) + + const renderItem = useCallback( + ({ item: currency }: ListRenderItemInfo) => { + return + }, + [onSelectCurrency] + ) + + if (error) { + return ( + + + + ) + } + + if (loading) { + return + } + + return ( + } + ListFooterComponent={} + data={list} + focusHook={useBottomSheetFocusHook} + keyExtractor={key} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="always" + renderItem={renderItem} + showsVerticalScrollIndicator={false} + windowSize={5} + /> + ) +} + +function key(item: FiatOnRampCurrency): CurrencyId { + return item.currencyInfo?.currencyId ?? '' +} + +export const TokenFiatOnRampList = memo(_TokenFiatOnRampList) diff --git a/apps/mobile/src/components/Trace/Trace.tsx b/apps/mobile/src/components/Trace/Trace.tsx new file mode 100644 index 0000000..7d218a9 --- /dev/null +++ b/apps/mobile/src/components/Trace/Trace.tsx @@ -0,0 +1,28 @@ +import { memo, PropsWithChildren } from 'react' +import { ManualPageViewScreen, MobileEventName } from 'src/features/telemetry/constants' +import { AppScreen } from 'src/screens/Screens' +import { TraceProps, Trace as UntypedTrace } from 'utilities/src/telemetry/trace/Trace' +import { ElementNameType, ModalNameType, SectionNameType } from 'wallet/src/telemetry/constants' + +// Mobile specific version of ITraceContext +interface MobileTraceContext { + screen?: AppScreen | ManualPageViewScreen + section?: SectionNameType + modal?: ModalNameType + element?: ElementNameType +} + +interface MobileTracePropsOverrides { + pressEvent?: MobileEventName +} + +type MobileTraceProps = MobileTraceContext & + Omit & + MobileTracePropsOverrides + +function _Trace({ children, ...rest }: PropsWithChildren): JSX.Element { + return {children} +} + +const Trace = memo(_Trace) +export default Trace diff --git a/apps/mobile/src/components/Trace/TraceTabView.tsx b/apps/mobile/src/components/Trace/TraceTabView.tsx new file mode 100644 index 0000000..eb10e59 --- /dev/null +++ b/apps/mobile/src/components/Trace/TraceTabView.tsx @@ -0,0 +1,25 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import React from 'react' +import { Route, TabView, TabViewProps } from 'react-native-tab-view' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { Screens } from 'src/screens/Screens' +import { SectionNameType } from 'wallet/src/telemetry/constants' + +type TraceRouteProps = { key: SectionNameType } & Route + +export default function TraceTabView({ + onIndexChange, + navigationState, + screenName, + ...rest +}: TabViewProps & { screenName: Screens }): JSX.Element { + const onIndexChangeTrace = (index: number): void => { + sendMobileAnalyticsEvent(SharedEventName.PAGE_VIEWED, { + section: navigationState.routes[index]?.key, + screen: screenName, + }) + onIndexChange(index) + } + + return +} diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx new file mode 100644 index 0000000..cf7dd03 --- /dev/null +++ b/apps/mobile/src/components/Trace/TraceUserProperties.test.tsx @@ -0,0 +1,165 @@ +import React from 'react' +import { useColorScheme } from 'react-native' +import renderer, { act } from 'react-test-renderer' +import * as appHooks from 'src/app/hooks' +import { TraceUserProperties } from 'src/components/Trace/TraceUserProperties' +import * as biometricHooks from 'src/features/biometrics/hooks' +import { AuthMethod, UserPropertyName } from 'src/features/telemetry/constants' +import * as versionUtils from 'src/utils/version' +import * as useIsDarkModeFile from 'ui/src/hooks/useIsDarkMode' +import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' +import * as fiatCurrencyHooks from 'wallet/src/features/fiatCurrency/hooks' +import * as languageHooks from 'wallet/src/features/language/hooks' +import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' +import * as walletHooks from 'wallet/src/features/wallet/hooks' +import { SwapProtectionSetting } from 'wallet/src/features/wallet/slice' + +// `any` is the actual type used by `jest.spyOn` +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function mockFn(module: any, func: string, returnValue: any): jest.SpyInstance { + return jest.spyOn(module, func).mockImplementation(() => returnValue) +} + +jest.mock('react-native/Libraries/Utilities/useColorScheme') + +const address1 = '0x168fA52Da8A45cEb01318E72B299b2d6A17167BF' +const address2 = '0x168fA52Da8A45cEb01318E72B299b2d6A17167BD' +const address3 = '0x168fA52Da8A45cEb01318E72B299b2d6A17167BE' + +const signerAccount1 = { + type: AccountType.SignerMnemonic, + address: address1, + timeImportedMs: 100000, +} + +const signerAccount2 = { + type: AccountType.SignerMnemonic, + address: address2, + timeImportedMs: 100000, +} + +const signerAccount3 = { + type: AccountType.SignerMnemonic, + address: address3, + timeImportedMs: 100000, +} + +describe('TraceUserProperties', () => { + afterEach(() => { + jest.clearAllMocks() + }) + + it('sets user properties with active account', async () => { + mockFn(versionUtils, 'getFullAppVersion', '1.0.0.345') + // Hooks mocks + const mockedUsedColorScheme = useColorScheme as jest.Mock + mockedUsedColorScheme.mockReturnValue('dark') + mockFn(walletHooks, 'useActiveAccount', { + address: 'address', + type: AccountType.SignerMnemonic, + backups: [BackupType.Cloud], + pushNotificationsEnabled: true, + }) + mockFn(walletHooks, 'useViewOnlyAccounts', ['address1', 'address2']) + mockFn(walletHooks, 'useSwapProtectionSetting', SwapProtectionSetting.On) + mockFn(walletHooks, 'useNonPendingSignerAccounts', [ + signerAccount1, + signerAccount2, + signerAccount3, + ]) + mockFn(walletHooks, 'useHideSpamTokensSetting', true) + mockFn(walletHooks, 'useHideSmallBalancesSetting', false) + mockFn(biometricHooks, 'useBiometricAppSettings', { + requiredForAppAccess: true, + requiredForTransactions: true, + }) + mockFn(biometricHooks, 'useDeviceSupportsBiometricAuth', { + touchId: false, + faceId: true, + }) + mockFn(useIsDarkModeFile, 'useIsDarkMode', true) + mockFn(fiatCurrencyHooks, 'useAppFiatCurrency', FiatCurrency.UnitedStatesDollar) + mockFn(languageHooks, 'useCurrentLanguageInfo', { loggingName: 'English' }) + mockFn(appHooks, 'useAppSelector', { enabled: true }) + + // mock setUserProperty + const mocked = mockFn(analytics, 'setUserProperty', undefined) + + // Execute useEffects + // https://reactjs.org/docs/test-renderer.html#testrendereract + await act(() => { + renderer.create() + }) + + // Check setUserProperty calls with correct values + expect(mocked).toHaveBeenCalledWith(UserPropertyName.AppVersion, '1.0.0.345') + expect(mocked).toHaveBeenCalledWith(UserPropertyName.DarkMode, true) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.ActiveWalletAddress, 'address') + expect(mocked).toHaveBeenCalledWith( + UserPropertyName.ActiveWalletType, + AccountType.SignerMnemonic + ) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.IsCloudBackedUp, true) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.IsPushEnabled, true) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.IsHideSmallBalancesEnabled, false) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.IsHideSpamTokensEnabled, true) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.WalletViewOnlyCount, 2) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.WalletSignerCount, 3) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.WalletSignerAccounts, [ + address1, + address2, + address3, + ]) + expect(mocked).toHaveBeenCalledWith( + UserPropertyName.WalletSwapProtectionSetting, + SwapProtectionSetting.On + ) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.AppOpenAuthMethod, AuthMethod.FaceId) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.TransactionAuthMethod, AuthMethod.FaceId) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.Language, 'English') + expect(mocked).toHaveBeenCalledWith(UserPropertyName.Currency, 'USD') + + expect(mocked).toHaveBeenCalledTimes(16) + }) + + it('sets user properties without active account', async () => { + mockFn(versionUtils, 'getFullAppVersion', '1.0.0.345') + // Hooks mocks + const mockedUsedColorScheme = useColorScheme as jest.Mock + mockedUsedColorScheme.mockReturnValue('dark') + mockFn(walletHooks, 'useActiveAccount', null) + mockFn(walletHooks, 'useViewOnlyAccounts', []) + mockFn(walletHooks, 'useSwapProtectionSetting', SwapProtectionSetting.On) + mockFn(walletHooks, 'useNonPendingSignerAccounts', []) + mockFn(biometricHooks, 'useBiometricAppSettings', { + requiredForAppAccess: false, + requiredForTransactions: false, + }) + mockFn(biometricHooks, 'useDeviceSupportsBiometricAuth', { + touchId: false, + faceId: false, + }) + mockFn(useIsDarkModeFile, 'useIsDarkMode', true) + mockFn(fiatCurrencyHooks, 'useAppFiatCurrency', FiatCurrency.UnitedStatesDollar) + mockFn(languageHooks, 'useCurrentLanguageInfo', { loggingName: 'English' }) + + // mock setUserProperty + const mocked = mockFn(analytics, 'setUserProperty', undefined) + + // Execute useEffects + await act(() => { + renderer.create() + }) + + // Check setUserProperty calls with correct values + expect(mocked).toHaveBeenCalledWith(UserPropertyName.AppVersion, '1.0.0.345') + expect(mocked).toHaveBeenCalledWith(UserPropertyName.DarkMode, true) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.WalletViewOnlyCount, 0) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.WalletSignerCount, 0) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.AppOpenAuthMethod, AuthMethod.None) + expect(mocked).toHaveBeenCalledWith(UserPropertyName.TransactionAuthMethod, AuthMethod.None) + + expect(mocked).toHaveBeenCalledTimes(10) + }) +}) diff --git a/apps/mobile/src/components/Trace/TraceUserProperties.tsx b/apps/mobile/src/components/Trace/TraceUserProperties.tsx new file mode 100644 index 0000000..83e03d0 --- /dev/null +++ b/apps/mobile/src/components/Trace/TraceUserProperties.tsx @@ -0,0 +1,112 @@ +import { useEffect } from 'react' +import { NativeModules } from 'react-native' +import { useAppSelector } from 'src/app/hooks' +import { + useBiometricAppSettings, + useDeviceSupportsBiometricAuth, +} from 'src/features/biometrics/hooks' +import { setUserProperty } from 'src/features/telemetry' +import { UserPropertyName, getAuthMethod } from 'src/features/telemetry/constants' +import { selectAllowAnalytics } from 'src/features/telemetry/selectors' +import { getFullAppVersion } from 'src/utils/version' +import { useIsDarkMode } from 'ui/src' +import { isAndroid } from 'uniswap/src/utils/platform' +import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { useAppFiatCurrency } from 'wallet/src/features/fiatCurrency/hooks' +import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' +import { BackupType } from 'wallet/src/features/wallet/accounts/types' +import { + useActiveAccount, + useHideSmallBalancesSetting, + useHideSpamTokensSetting, + useNonPendingSignerAccounts, + useSwapProtectionSetting, + useViewOnlyAccounts, +} from 'wallet/src/features/wallet/hooks' + +/** Component that tracks UserProperties during the lifetime of the app */ +export function TraceUserProperties(): null { + const isDarkMode = useIsDarkMode() + const viewOnlyAccounts = useViewOnlyAccounts() + const activeAccount = useActiveAccount() + const signerAccounts = useNonPendingSignerAccounts() + const biometricsAppSettingsState = useBiometricAppSettings() + const { touchId, faceId } = useDeviceSupportsBiometricAuth() + const swapProtectionSetting = useSwapProtectionSetting() + const currentLanguage = useCurrentLanguageInfo().loggingName + const currentFiatCurrency = useAppFiatCurrency() + const hideSpamTokens = useHideSpamTokensSetting() + const hideSmallBalances = useHideSmallBalancesSetting() + + // Effects must check this and ensure they are setting properties for when analytics is reenabled + const allowAnalytics = useAppSelector(selectAllowAnalytics) + + useEffect(() => { + setUserProperty(UserPropertyName.AppVersion, getFullAppVersion()) + if (isAndroid) { + NativeModules.AndroidDeviceModule.getPerformanceClass().then((perfClass: number) => { + setUserProperty(UserPropertyName.AndroidPerfClass, perfClass) + }) + } + return () => { + analytics.flushEvents() + } + }, [allowAnalytics]) + + useEffect(() => { + setUserProperty(UserPropertyName.WalletSwapProtectionSetting, swapProtectionSetting) + }, [allowAnalytics, swapProtectionSetting]) + + useEffect(() => { + setUserProperty(UserPropertyName.DarkMode, isDarkMode) + }, [allowAnalytics, isDarkMode]) + + useEffect(() => { + setUserProperty(UserPropertyName.WalletSignerCount, signerAccounts.length) + setUserProperty( + UserPropertyName.WalletSignerAccounts, + signerAccounts.map((account) => account.address) + ) + }, [allowAnalytics, signerAccounts]) + + useEffect(() => { + setUserProperty(UserPropertyName.WalletViewOnlyCount, viewOnlyAccounts.length) + }, [allowAnalytics, viewOnlyAccounts]) + + useEffect(() => { + if (!activeAccount) { + return + } + setUserProperty(UserPropertyName.ActiveWalletAddress, activeAccount.address) + setUserProperty(UserPropertyName.ActiveWalletType, activeAccount.type) + setUserProperty( + UserPropertyName.IsCloudBackedUp, + Boolean(activeAccount.backups?.includes(BackupType.Cloud)) + ) + setUserProperty(UserPropertyName.IsPushEnabled, Boolean(activeAccount.pushNotificationsEnabled)) + + setUserProperty(UserPropertyName.IsHideSmallBalancesEnabled, hideSmallBalances) + setUserProperty(UserPropertyName.IsHideSpamTokensEnabled, hideSpamTokens) + }, [allowAnalytics, activeAccount, hideSmallBalances, hideSpamTokens]) + + useEffect(() => { + setUserProperty( + UserPropertyName.AppOpenAuthMethod, + getAuthMethod(biometricsAppSettingsState.requiredForAppAccess, touchId, faceId) + ) + setUserProperty( + UserPropertyName.TransactionAuthMethod, + getAuthMethod(biometricsAppSettingsState.requiredForTransactions, touchId, faceId) + ) + }, [allowAnalytics, biometricsAppSettingsState, touchId, faceId]) + + useEffect(() => { + setUserProperty(UserPropertyName.Language, currentLanguage) + }, [allowAnalytics, currentLanguage]) + + useEffect(() => { + setUserProperty(UserPropertyName.Currency, currentFiatCurrency) + }, [allowAnalytics, currentFiatCurrency]) + + return null +} diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx new file mode 100644 index 0000000..c39cd46 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/ConnectedDappsList.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, StyleSheet } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { BackButton } from 'src/components/buttons/BackButton' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { DappConnectedNetworkModal } from 'src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal' +import { DappConnectionItem } from 'src/components/WalletConnect/ConnectedDapps/DappConnectionItem' +import { openModal } from 'src/features/modals/modalSlice' +import { + removePendingSession, + WalletConnectSession, +} from 'src/features/walletConnect/walletConnectSlice' +import { AnimatedFlex, Flex, Icons, Text, TouchableArea, useDeviceDimensions } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { ModalName } from 'wallet/src/telemetry/constants' + +type ConnectedDappsProps = { + sessions: WalletConnectSession[] + backButton?: JSX.Element +} + +export function ConnectedDappsList({ backButton, sessions }: ConnectedDappsProps): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + const { fullHeight } = useDeviceDimensions() + const [isEditing, setIsEditing] = useState(false) + const [selectedSession, setSelectedSession] = useState() + + const onPressScan = useCallback(() => { + // in case we received a pending session from a previous scan after closing modal + dispatch(removePendingSession()) + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }) + ) + }, [dispatch]) + + return ( + <> + + + + {backButton ?? } + + + + {t('walletConnect.dapps.manage.title')} + + + + {sessions.length > 0 ? ( + { + setIsEditing(!isEditing) + }}> + {isEditing ? ( + + ) : ( + + )} + + ) : ( + + + + )} + + + + {sessions.length > 0 ? ( + item.id} + numColumns={2} + renderItem={({ item }): JSX.Element => ( + setSelectedSession(session)} + /> + )} + /> + ) : ( + + + {t('walletConnect.dapps.manage.empty.title')} + + + {t('walletConnect.dapps.empty.description')} + + + )} + + {selectedSession && ( + setSelectedSession(undefined)} + /> + )} + + ) +} + +const ColumnStyle = StyleSheet.create({ + base: { + justifyContent: 'space-between', + }, +}) diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx new file mode 100644 index 0000000..aa086c0 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectedNetworksModal.tsx @@ -0,0 +1,116 @@ +import { getSdkError } from '@walletconnect/utils' +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' +import { Button, Flex, Text } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { CHAIN_INFO } from 'wallet/src/constants/chains' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' +import { ModalName } from 'wallet/src/telemetry/constants' +interface DappConnectedNetworkModalProps { + session: WalletConnectSession + onClose: () => void +} + +export function DappConnectedNetworkModal({ + session, + onClose, +}: DappConnectedNetworkModalProps): JSX.Element { + const { t } = useTranslation() + const address = useActiveAccountAddressWithThrow() + const dispatch = useAppDispatch() + const { dapp, id } = session + + const onDisconnect = async (): Promise => { + try { + dispatch(removeSession({ account: address, sessionId: id })) + // Explicitly verify that WalletConnect has this session id as an active session + // It's possible that the session was already disconnected on WC but wasn't updated locally in redux + const sessions = wcWeb3Wallet.getActiveSessions() + if (sessions[session.id]) { + await wcWeb3Wallet.disconnectSession({ + topic: session.id, + reason: getSdkError('USER_DISCONNECTED'), + }) + } + dispatch( + pushNotification({ + type: AppNotificationType.WalletConnect, + address, + dappName: dapp.name, + event: WalletConnectEvent.Disconnected, + imageUrl: dapp.icon, + hideDelay: 3 * ONE_SECOND_MS, + }) + ) + onClose() + } catch (error) { + logger.error(error, { tags: { file: 'DappConnectedNetworkModal', function: 'onDisconnect' } }) + } + } + + return ( + + + + + + }} + i18nKey="walletConnect.dapps.connection" + values={{ dappNameOrUrl: dapp.name || dapp.url }} + /> + + + {dapp.url} + + + + + {session.chains.map((chainId) => ( + + + + {CHAIN_INFO[chainId].label} + + + + + + ))} + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx new file mode 100644 index 0000000..d218902 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ConnectedDapps/DappConnectionItem.tsx @@ -0,0 +1,148 @@ +import { getSdkError } from '@walletconnect/utils' +import { ImpactFeedbackStyle } from 'expo-haptics' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, StyleSheet } from 'react-native' +import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import 'react-native-reanimated' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { WalletConnectSession, removeSession } from 'src/features/walletConnect/walletConnectSlice' +import { disableOnPress } from 'src/utils/disableOnPress' +import { AnimatedTouchableArea, Flex, Text, TouchableArea } from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' +import { ElementName } from 'wallet/src/telemetry/constants' + +export function DappConnectionItem({ + session, + isEditing, + onPressChangeNetwork, +}: { + session: WalletConnectSession + isEditing: boolean + onPressChangeNetwork: (session: WalletConnectSession) => void +}): JSX.Element { + const { t } = useTranslation() + const { dapp } = session + const dispatch = useAppDispatch() + const address = useActiveAccountAddressWithThrow() + + const onDisconnect = async (): Promise => { + try { + dispatch(removeSession({ account: address, sessionId: session.id })) + // Explicitly verify that WalletConnect has this session id as an active session + // It's possible that the session was already disconnected on WC but wasn't updated locally in redux + const sessions = wcWeb3Wallet.getActiveSessions() + if (sessions[session.id]) { + await wcWeb3Wallet.disconnectSession({ + topic: session.id, + reason: getSdkError('USER_DISCONNECTED'), + }) + } + dispatch( + pushNotification({ + type: AppNotificationType.WalletConnect, + address, + dappName: dapp.name, + event: WalletConnectEvent.Disconnected, + imageUrl: dapp.icon, + hideDelay: 3 * ONE_SECOND_MS, + }) + ) + } catch (error) { + logger.error(error, { tags: { file: 'DappConnectionItem', function: 'onDisconnect' } }) + } + } + + const menuActions = [ + { title: t('common.button.disconnect'), systemIcon: 'trash', destructive: true }, + ] + + const onPress = async (e: NativeSyntheticEvent): Promise => { + if (e.nativeEvent.index === 0) { + await onDisconnect() + } + } + + return ( + + + + {isEditing ? ( + + + + ) : ( + + )} + + + + + {dapp.name || dapp.url} + + + {dapp.url} + + + + onPressChangeNetwork(session)}> + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + width: '48%', + }, +}) diff --git a/apps/mobile/src/components/WalletConnect/DappHeaderIcon.tsx b/apps/mobile/src/components/WalletConnect/DappHeaderIcon.tsx new file mode 100644 index 0000000..4f7d71a --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/DappHeaderIcon.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { StyleSheet } from 'react-native' +import { Flex } from 'ui/src' +import { borderRadii, iconSizes } from 'ui/src/theme' +import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' +import { DappIconPlaceholder } from 'wallet/src/components/WalletConnect/DappIconPlaceholder' +import { CurrencyInfo } from 'wallet/src/features/dataApi/types' +import { ImageUri } from 'wallet/src/features/images/ImageUri' +import { DappInfo } from 'wallet/src/features/walletConnect/types' + +export function DappHeaderIcon({ + dapp, + permitCurrencyInfo, +}: { + dapp: DappInfo + permitCurrencyInfo?: CurrencyInfo | null +}): JSX.Element { + if (permitCurrencyInfo) { + return + } + + const fallback = + + return ( + + {dapp.icon ? ( + + ) : ( + fallback + )} + + ) +} + +const DappIconPlaceholderStyles = StyleSheet.create({ + icon: { borderRadius: borderRadii.rounded4, height: iconSizes.icon40, width: iconSizes.icon40 }, + loading: { borderRadius: borderRadii.roundedFull, overflow: 'hidden' }, +}) diff --git a/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx b/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx new file mode 100644 index 0000000..63fe433 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/NetworkLogos.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import 'react-native-reanimated' +import { Flex, FlexProps, Text } from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' +import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' + +export type NetworkLogosProps = { + chains: ChainId[] + showFirstChainLabel?: boolean + negativeGap?: boolean + size?: number +} & FlexProps + +export function NetworkLogos({ + negativeGap, + chains, + showFirstChainLabel, + size = iconSizes.icon20, + ...rest +}: NetworkLogosProps): JSX.Element { + const firstChain = chains[0] + + return ( + + {chains.length === 1 && firstChain && showFirstChainLabel ? ( + + + + {CHAIN_INFO[firstChain].label} + + + + ) : ( + + {chains.map((chainId) => ( + + + + ))} + + )} + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx new file mode 100644 index 0000000..6ac9f8b --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/ClientDetails.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { LinkButton } from 'src/components/buttons/LinkButton' +import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { HeaderText } from 'src/components/WalletConnect/RequestModal/HeaderText' +import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice' +import { Flex, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' + +export interface PermitInfo { + currencyId: string + amount: number | undefined +} + +export function ClientDetails({ + request, + permitInfo, +}: { + request: WalletConnectRequest + permitInfo?: PermitInfo +}): JSX.Element { + const { dapp } = request + const colors = useSporeColors() + + const permitCurrencyInfo = useCurrencyInfo(permitInfo?.currencyId) + + return ( + + + + + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx new file mode 100644 index 0000000..3fd42bf --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/HeaderText.tsx @@ -0,0 +1,76 @@ +import { Currency } from '@uniswap/sdk-core' +import React from 'react' +import { Trans } from 'react-i18next' +import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util' +import { WalletConnectRequest } from 'src/features/walletConnect/walletConnectSlice' +import { Text } from 'ui/src' +import { EthMethod } from 'wallet/src/features/walletConnect/types' +import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' + +export function HeaderText({ + request, + permitAmount, + permitCurrency, +}: { + request: WalletConnectRequest + permitAmount?: number + permitCurrency?: Currency | null +}): JSX.Element { + const { dapp, type: method } = request + + if (permitCurrency) { + const readablePermitAmount = getCurrencyAmount({ + value: permitAmount?.toString(), + valueType: ValueType.Raw, + currency: permitCurrency, + })?.toExact() + + return readablePermitAmount ? ( + + }} + i18nKey="qrScanner.request.withAmount" + values={{ + dappName: dapp.name, + currencySymbol: permitCurrency?.symbol, + amount: readablePermitAmount, + }} + /> + + ) : ( + + }} + i18nKey="qrScanner.request.withoutAmount" + values={{ + dappName: dapp.name, + currencySymbol: permitCurrency?.symbol, + }} + /> + + ) + } + + const getReadableMethodName = (ethMethod: EthMethod, dappNameOrUrl: string): JSX.Element => { + switch (ethMethod) { + case EthMethod.PersonalSign: + case EthMethod.EthSign: + case EthMethod.SignTypedData: + return + case EthMethod.EthSendTransaction: + return + } + + return + } + + return ( + + {getReadableMethodName(method, truncateDappName(dapp.name || dapp.url))} + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx new file mode 100644 index 0000000..1ff797f --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/RequestDetails.tsx @@ -0,0 +1,230 @@ +import { BigNumber } from 'ethers' +import { Transaction, TransactionDescription } from 'no-yolo-signatures' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView } from 'react-native-gesture-handler' +import { LinkButton } from 'src/components/buttons/LinkButton' +import { SpendingDetails } from 'src/components/WalletConnect/RequestModal/SpendingDetails' +import { + isTransactionRequest, + SignRequest, + WalletConnectRequest, +} from 'src/features/walletConnect/walletConnectSlice' +import { useNoYoloParser } from 'src/utils/useNoYoloParser' +import { Flex, Text, useSporeColors } from 'ui/src' +import { iconSizes, TextVariantTokens } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { ChainId } from 'wallet/src/constants/chains' +import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { useENS } from 'wallet/src/features/ens/useENS' +import { EthMethod, EthTransaction } from 'wallet/src/features/walletConnect/types' +import { getValidAddress, shortenAddress } from 'wallet/src/utils/addresses' +import { ExplorerDataType, getExplorerLink } from 'wallet/src/utils/linking' + +const getStrMessage = (request: WalletConnectRequest): string => { + if (request.type === EthMethod.PersonalSign || request.type === EthMethod.EthSign) { + return request.message || request.rawMessage + } + + return '' +} + +type AddressButtonProps = { + address: string + chainId: number + textVariant?: TextVariantTokens +} + +const AddressButton = ({ address, chainId, ...rest }: AddressButtonProps): JSX.Element => { + const { name } = useENS(chainId, address, false) + const colors = useSporeColors() + const supportedChainId = toSupportedChainId(chainId) ?? ChainId.Mainnet + + return ( + + ) +} + +const MAX_TYPED_DATA_PARSE_DEPTH = 3 + +// recursively parses typed data objects and adds margin to left +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getParsedObjectDisplay = (chainId: number, obj: any, depth = 0): JSX.Element => { + if (depth === MAX_TYPED_DATA_PARSE_DEPTH + 1) { + return ... + } + + return ( + + {Object.keys(obj).map((objKey) => { + const childValue = obj[objKey] + + if (typeof childValue === 'object') { + return ( + + + {objKey} + + {getParsedObjectDisplay(chainId, childValue, depth + 1)} + + ) + } + + if (typeof childValue === 'string') { + return ( + + + {objKey} + + + {getValidAddress(childValue, true) ? ( + + ) : ( + + {childValue} + + )} + + + ) + } + + // TODO: [MOB-216] handle array child types + return null + })} + + ) +} + +function TransactionDetails({ + chainId, + transaction, +}: { + chainId: ChainId + transaction: EthTransaction +}): JSX.Element { + const { t } = useTranslation() + const parser = useNoYoloParser(chainId) + + const [isLoading, setIsLoading] = useState(true) + const [parsedData, setParsedData] = useState(undefined) + + const { from, to, value, data } = transaction + + useEffect(() => { + const parseResult = async (): Promise => { + // no-yolo-parser library expects these fields to be defined + if (!from || !to || !value || !data) { + return + } + return parser.parseAsResult(transaction as Transaction).then((result) => { + if (!result.transactionDescription.ok) { + throw result.transactionDescription.error + } + + return result.transactionDescription.result + }) + } + + parseResult() + .then((result) => { + setParsedData(result) + }) + .catch((error) => { + setParsedData(undefined) + logger.warn('RequestMessage', 'DecodedDataDetails', 'Could not parse data', error) + }) + .finally(() => { + setIsLoading(false) + }) + }, [data, from, parser, to, transaction, value]) + + return ( + + {value && !BigNumber.from(value).eq(0) ? ( + + ) : null} + {to ? ( + + + {t('walletConnect.request.details.label.recipient')} + + + + ) : null} + + + {t('walletConnect.request.details.label.function')} + + + + {parsedData ? parsedData.name : t('common.text.unknown')} + + + + + ) +} + +type Props = { + request: WalletConnectRequest +} + +function isSignTypedDataRequest(request: WalletConnectRequest): request is SignRequest { + return request.type === EthMethod.SignTypedData || request.type === EthMethod.SignTypedDataV4 +} + +function RequestDetailsContent({ request }: Props): JSX.Element { + const { t } = useTranslation() + + if (isSignTypedDataRequest(request)) { + try { + const data = JSON.parse(request.rawMessage) + return getParsedObjectDisplay(request.chainId, data.message, 0) + } catch (error) { + logger.error(error, { tags: { file: 'RequestDetails', function: 'RequestDetailsContent' } }) + return + } + } + + if (isTransactionRequest(request)) { + return + } + + const message = getStrMessage(request) + return message ? ( + {message} + ) : ( + + {t('qrScanner.request.message.unavailable')} + + ) +} + +export function RequestDetails({ request }: Props): JSX.Element { + return ( + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx new file mode 100644 index 0000000..e16309b --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/SpendingDetails.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Text } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' +import { ChainId } from 'wallet/src/constants/chains' +import { useUSDValue } from 'wallet/src/features/gas/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useNativeCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' +import { ValueType, getCurrencyAmount } from 'wallet/src/utils/getCurrencyAmount' + +export function SpendingDetails({ + value, + chainId, +}: { + value: string + chainId: ChainId +}): JSX.Element { + const { t } = useTranslation() + const { convertFiatAmountFormatted, formatCurrencyAmount } = useLocalizationContext() + + const nativeCurrencyInfo = useNativeCurrencyInfo(chainId) + const nativeCurrencyAmount = nativeCurrencyInfo + ? getCurrencyAmount({ + value, + valueType: ValueType.Raw, + currency: nativeCurrencyInfo.currency, + }) + : null + const usdValue = useUSDValue(chainId, value) + + const tokenAmountWithSymbol = + formatCurrencyAmount({ value: nativeCurrencyAmount, type: NumberType.TokenTx }) + + ' ' + + getSymbolDisplayText(nativeCurrencyInfo?.currency.symbol) + const fiatAmount = convertFiatAmountFormatted(usdValue, NumberType.FiatTokenPrice) + + return ( + + + {t('walletConnect.request.details.label.sending')} + + + + {tokenAmountWithSymbol} + + {fiatAmount} + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx new file mode 100644 index 0000000..d3e8496 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/WalletConnectRequestModal.tsx @@ -0,0 +1,420 @@ +import { useNetInfo } from '@react-native-community/netinfo' +import { getSdkError } from '@walletconnect/utils' +import { providers } from 'ethers' +import React, { PropsWithChildren, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleProp, ViewStyle } from 'react-native' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { ClientDetails, PermitInfo } from 'src/components/WalletConnect/RequestModal/ClientDetails' +import { useHasSufficientFunds } from 'src/components/WalletConnect/RequestModal/hooks' +import { RequestDetails } from 'src/components/WalletConnect/RequestModal/RequestDetails' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' +import { signWcRequestActions } from 'src/features/walletConnect/signWcRequestSaga' +import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect' +import { + isTransactionRequest, + SignRequest, + TransactionRequest, + WalletConnectRequest, +} from 'src/features/walletConnect/walletConnectSlice' +import { Button, Flex, Text, useSporeColors } from 'ui/src' +import AlertTriangle from 'ui/src/assets/icons/alert-triangle.svg' +import { iconSizes } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { NetworkFee } from 'wallet/src/components/network/NetworkFee' +import { NetworkPill } from 'wallet/src/components/network/NetworkPill' +import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' +import { GasSpeed } from 'wallet/src/features/gas/types' +import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { BlockedAddressWarning } from 'wallet/src/features/trm/BlockedAddressWarning' +import { useIsBlocked, useIsBlockedActiveAddress } from 'wallet/src/features/trm/hooks' +import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { + EthMethod, + isPrimaryTypePermit, + WCEventType, + WCRequestOutcome, +} from 'wallet/src/features/walletConnect/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' + +const MAX_MODAL_MESSAGE_HEIGHT = 200 + +interface Props { + onClose: () => void + request: SignRequest | TransactionRequest +} + +const isPotentiallyUnsafe = (request: WalletConnectRequest): boolean => + request.type !== EthMethod.PersonalSign + +const methodCostsGas = (request: WalletConnectRequest): request is TransactionRequest => + request.type === EthMethod.EthSendTransaction + +/** If the request is a permit then parse the relevant information otherwise return undefined. */ +const getPermitInfo = (request: WalletConnectRequest): PermitInfo | undefined => { + if (request.type !== EthMethod.SignTypedDataV4) { + return undefined + } + + try { + const message = JSON.parse(request.rawMessage) + if (!isPrimaryTypePermit(message)) { + return undefined + } + + const { domain, message: permitPayload } = message + const currencyId = buildCurrencyId(domain.chainId, domain.verifyingContract) + const amount = permitPayload.value + + return { currencyId, amount } + } catch (error) { + logger.error(error, { tags: { file: 'WalletConnectRequestModal', function: 'getPermitInfo' } }) + return undefined + } +} + +const VALID_REQUEST_TYPES = [ + EthMethod.PersonalSign, + EthMethod.SignTypedData, + EthMethod.SignTypedDataV4, + EthMethod.EthSign, + EthMethod.EthSendTransaction, +] + +function SectionContainer({ + children, + style, +}: PropsWithChildren<{ style?: StyleProp }>): JSX.Element | null { + return children ? ( + + {children} + + ) : null +} + +export function WalletConnectRequestModal({ onClose, request }: Props): JSX.Element | null { + const colors = useSporeColors() + const netInfo = useNetInfo() + const didOpenFromDeepLink = useAppSelector(selectDidOpenFromDeepLink) + const chainId = request.chainId + + const tx: providers.TransactionRequest | null = useMemo(() => { + if (!isTransactionRequest(request)) { + return null + } + + return { ...request.transaction, chainId } + }, [chainId, request]) + + const signerAccounts = useSignerAccounts() + const signerAccount = signerAccounts.find((account) => + areAddressesEqual(account.address, request.account) + ) + const gasFee = useTransactionGasFee(tx, GasSpeed.Urgent) + const hasSufficientFunds = useHasSufficientFunds({ + account: request.account, + chainId, + gasFee, + value: isTransactionRequest(request) ? request.transaction.value : undefined, + }) + + const { isBlocked: isSenderBlocked, isBlockedLoading: isSenderBlockedLoading } = + useIsBlockedActiveAddress() + + const { isBlocked: isRecipientBlocked, isBlockedLoading: isRecipientBlockedLoading } = + useIsBlocked(tx?.to) + + const isBlocked = isSenderBlocked ?? isRecipientBlocked + const isBlockedLoading = isSenderBlockedLoading || isRecipientBlockedLoading + + const checkConfirmEnabled = (): boolean => { + if (!netInfo.isInternetReachable) { + return false + } + + if (!signerAccount) { + return false + } + + if (isBlocked || isBlockedLoading) { + return false + } + + if (methodCostsGas(request)) { + return !!(tx && hasSufficientFunds && gasFee.value) + } + + if (isTransactionRequest(request)) { + return !!tx + } + + return true + } + + const confirmEnabled = checkConfirmEnabled() + + const { t } = useTranslation() + const dispatch = useAppDispatch() + /** + * TODO: [MOB-239] implement this behavior in a less janky way. Ideally if we can distinguish between `onClose` being called programmatically and `onClose` as a results of a user dismissing the modal then we can determine what this value should be without this class variable. + * Indicates that the modal can reject the request when the modal happens. This will be false when the modal closes as a result of the user explicitly confirming or rejecting a request and true otherwise. + */ + const rejectOnCloseRef = useRef(true) + + const onReject = async (): Promise => { + if (request.dapp.source === 'walletconnect') { + await wcWeb3Wallet.respondSessionRequest({ + topic: request.sessionId, + response: { + id: Number(request.internalId), + jsonrpc: '2.0', + error: getSdkError('USER_REJECTED'), + }, + }) + } + + rejectOnCloseRef.current = false + + sendMobileAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { + request_type: isTransactionRequest(request) + ? WCEventType.TransactionRequest + : WCEventType.SignRequest, + eth_method: request.type, + dapp_url: request.dapp.url, + dapp_name: request.dapp.name, + wc_version: '2', + chain_id: chainId, + outcome: WCRequestOutcome.Reject, + }) + + onClose() + if (didOpenFromDeepLink) { + returnToPreviousApp() + } + } + + const onConfirm = async (): Promise => { + if (!confirmEnabled || !signerAccount) { + return + } + if (request.type === EthMethod.EthSendTransaction) { + if (!gasFee.params) { + return + } // appeasing typescript + dispatch( + signWcRequestActions.trigger({ + sessionId: request.sessionId, + requestInternalId: request.internalId, + method: request.type, + transaction: { ...tx, ...gasFee.params }, + account: signerAccount, + dapp: request.dapp, + chainId, + }) + ) + } else { + dispatch( + signWcRequestActions.trigger({ + sessionId: request.sessionId, + requestInternalId: request.internalId, + // this is EthSignMessage type + method: request.type, + message: request.message || request.rawMessage, + account: signerAccount, + dapp: request.dapp, + chainId, + }) + ) + } + + rejectOnCloseRef.current = false + + sendMobileAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { + request_type: isTransactionRequest(request) + ? WCEventType.TransactionRequest + : WCEventType.SignRequest, + eth_method: request.type, + dapp_url: request.dapp.url, + dapp_name: request.dapp.name, + wc_version: '2', + chain_id: chainId, + outcome: WCRequestOutcome.Confirm, + }) + + onClose() + if (didOpenFromDeepLink) { + returnToPreviousApp() + } + } + + const { trigger: actionButtonTrigger } = useBiometricPrompt(onConfirm) + const { requiredForTransactions } = useBiometricAppSettings() + + if (!VALID_REQUEST_TYPES.includes(request.type)) { + return null + } + + const handleClose = async (): Promise => { + if (rejectOnCloseRef.current) { + await onReject() + } else { + onClose() + } + } + + const nativeCurrency = chainId && NativeCurrency.onChain(chainId) + const permitInfo = getPermitInfo(request) + + return ( + <> + + + + + + {!permitInfo && ( + + + + + + )} + + {methodCostsGas(request) ? ( + + ) : ( + + + {t('walletConnect.request.label.network')} + + + + )} + + + + + {!hasSufficientFunds && ( + + {t('walletConnect.request.error.insufficientFunds', { + currencySymbol: nativeCurrency?.symbol, + })} + + )} + + + {!netInfo.isInternetReachable ? ( + + } + textColor="$DEP_accentWarning" + title={t('walletConnect.request.error.network')} + /> + ) : ( + + )} + + + + + + + + + ) +} + +function WarningSection({ + request, + showUnsafeWarning, + isBlockedAddress, +}: { + request: WalletConnectRequest + showUnsafeWarning: boolean + isBlockedAddress: boolean +}): JSX.Element | null { + const colors = useSporeColors() + const { t } = useTranslation() + + if (!showUnsafeWarning && !isBlockedAddress) { + return null + } + + if (isBlockedAddress) { + return + } + + return ( + + + + {isTransactionRequest(request) + ? t('walletConnect.request.warning.general.transaction') + : t('walletConnect.request.warning.general.message')} + + + ) +} + +const requestMessageStyle: StyleProp = { + // need a fixed height here or else modal gets confused about total height + maxHeight: MAX_MODAL_MESSAGE_HEIGHT, + overflow: 'hidden', +} diff --git a/apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts b/apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts new file mode 100644 index 0000000..508ea8a --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/RequestModal/hooks.ts @@ -0,0 +1,42 @@ +import { useMemo } from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import { GasFeeResult } from 'wallet/src/features/gas/types' +import { useOnChainNativeCurrencyBalance } from 'wallet/src/features/portfolio/api' +import { NativeCurrency } from 'wallet/src/features/tokens/NativeCurrency' +import { hasSufficientFundsIncludingGas } from 'wallet/src/features/transactions/utils' +import { getCurrencyAmount, ValueType } from 'wallet/src/utils/getCurrencyAmount' + +export function useHasSufficientFunds({ + account, + chainId, + gasFee, + value, +}: { + account?: string + chainId?: ChainId + gasFee: GasFeeResult + value?: string +}): boolean { + const nativeCurrency = NativeCurrency.onChain(chainId || ChainId.Mainnet) + const { balance: nativeBalance } = useOnChainNativeCurrencyBalance( + chainId ?? ChainId.Mainnet, + account + ) + + const hasSufficientFunds = useMemo(() => { + const transactionAmount = + getCurrencyAmount({ + value, + valueType: ValueType.Raw, + currency: nativeCurrency, + }) ?? undefined + + return hasSufficientFundsIncludingGas({ + transactionAmount, + gasFee: gasFee.value, + nativeCurrencyBalance: nativeBalance, + }) + }, [value, nativeCurrency, gasFee.value, nativeBalance]) + + return hasSufficientFunds +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx new file mode 100644 index 0000000..3734fe0 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionModal.tsx @@ -0,0 +1,312 @@ +import { getSdkError } from '@walletconnect/utils' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { LinkButton } from 'src/components/buttons/LinkButton' +import { DappHeaderIcon } from 'src/components/WalletConnect/DappHeaderIcon' +import { NetworkLogos } from 'src/components/WalletConnect/NetworkLogos' +import { PendingConnectionSwitchAccountModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal' +import { truncateDappName } from 'src/components/WalletConnect/ScanSheet/util' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { selectDidOpenFromDeepLink } from 'src/features/walletConnect/selectors' +import { getSessionNamespaces } from 'src/features/walletConnect/utils' +import { returnToPreviousApp } from 'src/features/walletConnect/WalletConnect' +import { + addSession, + removePendingSession, + WalletConnectPendingSession, +} from 'src/features/walletConnect/walletConnectSlice' +import { AnimatedFlex, Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ChainId } from 'wallet/src/constants/chains' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { + useActiveAccountAddressWithThrow, + useActiveAccountWithThrow, + useSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import { + WalletConnectEvent, + WCEventType, + WCRequestOutcome, +} from 'wallet/src/features/walletConnect/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + pendingSession: WalletConnectPendingSession + onClose: () => void +} + +enum PendingConnectionModalState { + Hidden, + SwitchNetwork, + SwitchAccount, +} + +const SitePermissions = (): JSX.Element => { + const { t } = useTranslation() + + const normalInfoTextSize = 'body2' + const shortInfoTextSize = 'body3' + + return ( + + + {t('walletConnect.permissions.title')} + + + + + {t('walletConnect.permissions.option.viewWalletAddress')} + + + + + + {t('walletConnect.permissions.option.viewTokenBalances')} + + + + + + {t('walletConnect.permissions.option.transferAssets')} + + + + ) +} + +const NetworksRow = ({ chains }: { chains: ChainId[] }): JSX.Element => { + const { t } = useTranslation() + + return ( + + + {t('walletConnect.permissions.networks')} + + + + ) +} + +type SwitchAccountProps = { + activeAddress: string + setModalState: (state: PendingConnectionModalState.SwitchAccount) => void +} + +const SwitchAccountRow = ({ activeAddress, setModalState }: SwitchAccountProps): JSX.Element => { + const signerAccounts = useSignerAccounts() + const accountIsSwitchable = signerAccounts.length > 1 + + const onPress = useCallback(() => { + setModalState(PendingConnectionModalState.SwitchAccount) + }, [setModalState]) + + return ( + + + + ) +} + +export const PendingConnectionModal = ({ pendingSession, onClose }: Props): JSX.Element => { + const { t } = useTranslation() + const colors = useSporeColors() + const activeAddress = useActiveAccountAddressWithThrow() + const dispatch = useAppDispatch() + const activeAccount = useActiveAccountWithThrow() + const didOpenFromDeepLink = useAppSelector(selectDidOpenFromDeepLink) + + const [modalState, setModalState] = useState( + PendingConnectionModalState.Hidden + ) + + const onPressSettleConnection = useCallback( + async (approved: boolean) => { + sendMobileAnalyticsEvent(MobileEventName.WalletConnectSheetCompleted, { + request_type: WCEventType.SessionPending, + dapp_url: pendingSession.dapp.url, + dapp_name: pendingSession.dapp.name, + wc_version: '2', + connection_chain_ids: pendingSession.chains, + outcome: approved ? WCRequestOutcome.Confirm : WCRequestOutcome.Reject, + }) + + // Handle WC 2.0 session request + if (approved) { + const namespaces = getSessionNamespaces(activeAddress, pendingSession.proposalNamespaces) + + const session = await wcWeb3Wallet.approveSession({ + id: Number(pendingSession.id), + namespaces, + }) + + dispatch( + addSession({ + wcSession: { + id: session.topic, + dapp: { + name: session.peer.metadata.name, + url: session.peer.metadata.url, + icon: session.peer.metadata.icons[0] ?? null, + source: 'walletconnect', + }, + chains: pendingSession.chains, + namespaces, + }, + account: activeAddress, + }) + ) + + dispatch( + pushNotification({ + type: AppNotificationType.WalletConnect, + address: activeAddress, + event: WalletConnectEvent.Connected, + dappName: session.peer.metadata.name, + imageUrl: session.peer.metadata.icons[0] ?? null, + hideDelay: 3 * ONE_SECOND_MS, + }) + ) + } else { + await wcWeb3Wallet.rejectSession({ + id: Number(pendingSession.id), + reason: getSdkError('USER_REJECTED'), + }) + dispatch(removePendingSession()) + } + + onClose() + if (didOpenFromDeepLink) { + returnToPreviousApp() + } + }, + [activeAddress, dispatch, onClose, pendingSession, didOpenFromDeepLink] + ) + const dappName = pendingSession.dapp.name || pendingSession.dapp.url || '' + + return ( + + + + + + {t('walletConnect.pending.title', { + dappName: truncateDappName(dappName), + })}{' '} + + + + + + + + + + + + + + {modalState === PendingConnectionModalState.SwitchAccount && ( + setModalState(PendingConnectionModalState.Hidden)} + onPressAccount={(account): void => { + dispatch(setAccountAsActive(account.address)) + setModalState(PendingConnectionModalState.Hidden) + }} + /> + )} + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx new file mode 100644 index 0000000..971e792 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchAccountModal.tsx @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { SwitchAccountOption } from 'src/components/WalletConnect/ScanSheet/SwitchAccountOption' +import { Flex, Text } from 'ui/src' +import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { useSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + activeAccount: Account | null + onPressAccount: (account: Account) => void + onClose: () => void +} + +export const PendingConnectionSwitchAccountModal = ({ + activeAccount, + onPressAccount, + onClose, +}: Props): JSX.Element => { + const { t } = useTranslation() + const signerAccounts = useSignerAccounts() + + const options = useMemo( + () => + signerAccounts.map((account) => { + return { + key: `${ElementName.AccountCard}-${account.address}`, + onPress: () => onPressAccount(account), + render: () => , + } + }), + [signerAccounts, activeAccount, onPressAccount] + ) + + return ( + + {t('walletConnect.pending.switchAccount')} + + } + isVisible={true} + name={ModalName.AccountEdit} + options={options} + onClose={onClose} + /> + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx new file mode 100644 index 0000000..a80ebf5 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/PendingConnectionSwitchNetworkModal.tsx @@ -0,0 +1,75 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Separator, Text, useSporeColors } from 'ui/src' +import Check from 'ui/src/assets/icons/check.svg' +import { iconSizes } from 'ui/src/theme' +import { NetworkLogo } from 'wallet/src/components/CurrencyLogo/NetworkLogo' +import { ActionSheetModal } from 'wallet/src/components/modals/ActionSheetModal' +import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + selectedChainId: ChainId + onPressChain: (chainId: ChainId) => void + onClose: () => void +} + +export const PendingConnectionSwitchNetworkModal = ({ + selectedChainId, + onPressChain, + onClose, +}: Props): JSX.Element => { + const colors = useSporeColors() + const { t } = useTranslation() + + const options = useMemo( + () => + ALL_SUPPORTED_CHAIN_IDS.map((chainId) => { + const info = CHAIN_INFO[chainId] + return { + key: `${ElementName.NetworkButton}-${chainId}`, + onPress: () => onPressChain(chainId), + render: () => ( + <> + + + + + {info.label} + + + {chainId === selectedChainId && ( + + )} + + + + ), + } + }), + [selectedChainId, onPressChain, colors.accent1] + ) + + return ( + + {t('walletConnect.pending.switchNetwork')} + + } + isVisible={true} + name={ModalName.NetworkSelector} + options={options} + onClose={onClose} + /> + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx new file mode 100644 index 0000000..7f44047 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/SwitchAccountOption.tsx @@ -0,0 +1,49 @@ +import React from 'react' +import { Flex, Separator, Text, Unicon, UniconV2, useSporeColors } from 'ui/src' +import Check from 'ui/src/assets/icons/check.svg' +import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' +import { shortenAddress } from 'wallet/src/utils/addresses' + +type Props = { + account: Account + activeAccount: Account | null +} + +const ICON_SIZE = 24 + +export const SwitchAccountOption = ({ account, activeAccount }: Props): JSX.Element => { + const colors = useSporeColors() + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + + const displayName = useDisplayName(account.address) + return ( + <> + + + {isUniconsV2Enabled ? ( + + ) : ( + + )} + + + + {shortenAddress(account.address)} + + + + {activeAccount?.address === account.address && ( + + )} + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx new file mode 100644 index 0000000..549bba0 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/WalletConnectModal.tsx @@ -0,0 +1,337 @@ +import { selectionAsync } from 'expo-haptics' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import 'react-native-reanimated' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useEagerExternalProfileRootNavigation } from 'src/app/navigation/hooks' +import { BackButtonView } from 'src/components/layout/BackButtonView' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { QRCodeScanner } from 'src/components/QRCodeScanner/QRCodeScanner' +import { WalletQRCode } from 'src/components/QRCodeScanner/WalletQRCode' +import Trace from 'src/components/Trace/Trace' +import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList' +import { + getSupportedURI, + isAllowedUwULinkRequest, + parseScantasticParams, + URIType, + UWULINK_PREFIX, +} from 'src/components/WalletConnect/ScanSheet/util' +import { closeAllModals, openModal } from 'src/features/modals/modalSlice' +import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' +import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' +import { addRequest } from 'src/features/walletConnect/walletConnectSlice' +import { Flex, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import Scan from 'ui/src/assets/icons/receive.svg' +import ScanQRIcon from 'ui/src/assets/icons/scan.svg' +import { iconSizes } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' +import { EthMethod, UwULinkRequest } from 'wallet/src/features/walletConnect/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + initialScreenState?: ScannerModalState + onClose: () => void +} + +export function WalletConnectModal({ + initialScreenState = ScannerModalState.ScanQr, + onClose, +}: Props): JSX.Element | null { + const { t } = useTranslation() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const activeAddress = useAppSelector(selectActiveAccountAddress) + const { sessions, hasPendingSessionError } = useWalletConnect(activeAddress) + const [currentScreenState, setCurrentScreenState] = + useState(initialScreenState) + const [shouldFreezeCamera, setShouldFreezeCamera] = useState(false) + const { preload, navigate } = useEagerExternalProfileRootNavigation() + const dispatch = useAppDispatch() + const isUwULinkEnabled = useFeatureFlag(FEATURE_FLAGS.UwULink) + const isScantasticEnabled = useFeatureFlag(FEATURE_FLAGS.Scantastic) + + // Update QR scanner states when pending session error alert is shown from WCv2 saga event channel + useEffect(() => { + if (hasPendingSessionError) { + // Cancels the pending connection state in QRCodeScanner + setShouldFreezeCamera(false) + } + }, [hasPendingSessionError, setShouldFreezeCamera]) + + const onScanCode = useCallback( + async (uri: string) => { + // don't scan any QR codes if there is an error popup open or camera is frozen + if (!activeAddress || hasPendingSessionError || shouldFreezeCamera) { + return + } + await selectionAsync() + + const supportedURI = await getSupportedURI(uri, { isUwULinkEnabled, isScantasticEnabled }) + if (!supportedURI) { + setShouldFreezeCamera(true) + Alert.alert( + t('walletConnect.error.unsupported.title'), + // TODO(EXT-495): Add Scantastic product name here when ready + t('walletConnect.error.unsupported.message'), + [ + { + text: t('common.button.tryAgain'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ] + ) + + return + } + + if (supportedURI.type === URIType.Address) { + await preload(supportedURI.value) + await navigate(supportedURI.value, onClose) + return + } + + if (supportedURI.type === URIType.WalletConnectURL) { + setShouldFreezeCamera(true) + Alert.alert( + t('walletConnect.error.unsupportedV1.title'), + t('walletConnect.error.unsupportedV1.message'), + [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ] + ) + return + } + + if (supportedURI.type === URIType.WalletConnectV2URL) { + setShouldFreezeCamera(true) + try { + await pairWithWalletConnectURI(supportedURI.value) + } catch (error) { + logger.error(error, { tags: { file: 'WalletConnectModal', function: 'onScanCode' } }) + Alert.alert( + t('walletConnect.error.general.title'), + t('walletConnect.error.general.message'), + [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ] + ) + } + } + + if (supportedURI.type === URIType.Scantastic) { + const params = parseScantasticParams(supportedURI.value) + + if (!params) { + setShouldFreezeCamera(true) + Alert.alert( + t('walletConnect.error.scantastic.title'), + t('walletConnect.error.scantastic.message'), + [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ] + ) + return + } + + setShouldFreezeCamera(true) + dispatch(closeAllModals()) + dispatch( + openModal({ + name: ModalName.Scantastic, + initialState: { + params, + }, + }) + ) + + return + } + + if (supportedURI.type === URIType.UwULink) { + setShouldFreezeCamera(true) + try { + const parsedUwulinkRequest: UwULinkRequest = JSON.parse(supportedURI.value) + const isAllowed = isAllowedUwULinkRequest(parsedUwulinkRequest) + + if (!isAllowed) { + Alert.alert( + t('walletConnect.error.uwu.title'), + t('walletConnect.error.uwu.unsupported'), + [ + { + text: t('common.button.ok'), + onPress: (): void => { + setShouldFreezeCamera(false) + }, + }, + ] + ) + return + } + + dispatch( + addRequest({ + account: activeAddress, + request: { + type: EthMethod.EthSendTransaction, + transaction: { from: activeAddress, ...parsedUwulinkRequest.value }, + sessionId: UWULINK_PREFIX, // session/internalId is WalletConnect specific, but not needed here + internalId: UWULINK_PREFIX, + account: activeAddress, + dapp: { + ...parsedUwulinkRequest.dapp, + source: UWULINK_PREFIX, + chain_id: parsedUwulinkRequest.chainId, + webhook: parsedUwulinkRequest.webhook, + }, + chainId: parsedUwulinkRequest.chainId, + }, + }) + ) + onClose() + } catch (_) { + setShouldFreezeCamera(false) + Alert.alert(t('walletConnect.error.uwu.title'), t('walletConnect.error.uwu.scan')) + } + } + + if (supportedURI.type === URIType.EasterEgg) { + setShouldFreezeCamera(true) + Alert.alert('Have you tried full-sending lately?', 'Highly recommend it', [ + { + text: 'Bye', + onPress: (): void => { + setShouldFreezeCamera(true) + onClose() + }, + }, + ]) + } + }, + [ + activeAddress, + navigate, + onClose, + preload, + setShouldFreezeCamera, + shouldFreezeCamera, + hasPendingSessionError, + isUwULinkEnabled, + isScantasticEnabled, + t, + dispatch, + ] + ) + + const onPressBottomToggle = (): void => { + if (currentScreenState === ScannerModalState.ScanQr) { + setCurrentScreenState(ScannerModalState.WalletQr) + } else { + setCurrentScreenState(ScannerModalState.ScanQr) + } + } + + const onPressShowConnectedDapps = (): void => { + setCurrentScreenState(ScannerModalState.ConnectedDapps) + } + + const onPressShowScanQr = (): void => { + setCurrentScreenState(ScannerModalState.ScanQr) + } + + if (!activeAddress) { + return null + } + + return ( + + <> + {currentScreenState === ScannerModalState.ConnectedDapps && ( + + + + } + sessions={sessions} + /> + )} + {currentScreenState === ScannerModalState.ScanQr && ( + + + + )} + {currentScreenState === ScannerModalState.WalletQr && ( + + + + )} + + + + {currentScreenState === ScannerModalState.ScanQr ? ( + + ) : ( + + )} + + {currentScreenState === ScannerModalState.ScanQr + ? t('qrScanner.recipient.action.show') + : t('qrScanner.recipient.action.scan')} + + + + + + + ) +} diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts b/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts new file mode 100644 index 0000000..94f314a --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/util.test.ts @@ -0,0 +1,124 @@ +import * as wcUtils from '@walletconnect/utils' +import { + wcAsParamInUniwapScheme, + wcInUniwapScheme, + wcUniversalLinkUrl, +} from 'src/features/deepLinking/handleDeepLinkSaga.test' +import { CUSTOM_UNI_QR_CODE_PREFIX, URIType, getSupportedURI } from './util' + +const VALID_WC_V1_URI = 'validWcV1Uri@1?relay-protocol=irn&symKey=51e' +const VALID_WC_V2_URI = 'validWcV2Uri@2?relay-protocol=irn&symKey=51e' + +function getWcVersion(uri: string): number { + switch (uri) { + case VALID_WC_V1_URI: + return 1 + case VALID_WC_V2_URI: + return 2 + default: + return -1 + } +} + +jest.spyOn(wcUtils, 'parseUri').mockImplementation((uri) => { + return { + version: getWcVersion(uri), + protocol: '', + topic: '', + symKey: '', + relay: { + protocol: '', + }, + } +}) + +describe('getSupportedURI', () => { + it('should return undefined for empty URIs', async () => { + expect(await getSupportedURI('')).toBeUndefined() + }) + + it('should return undefined for invalid URIs', async () => { + expect(await getSupportedURI('invalid_uri')).toBeUndefined() + }) + + it('should return undefined for hello_uniwallet v1 URI', async () => { + const result = await getSupportedURI(CUSTOM_UNI_QR_CODE_PREFIX + VALID_WC_V1_URI) + expect(result).toBeUndefined() + }) + + it('should return correct values for hello_uniwallet v2 URI', async () => { + const result = await getSupportedURI('hello_uniwallet:' + VALID_WC_V2_URI) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should return undefined for uniswap scheme v1 URI with wc URI as query param', async () => { + const result = await getSupportedURI(wcAsParamInUniwapScheme + VALID_WC_V1_URI) + expect(result).toBeUndefined() + }) + + it('should return correct values for uniswap scheme v2 URI with wc URI as query param', async () => { + const result = await getSupportedURI('uniswap://wc?uri=' + VALID_WC_V2_URI) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should return undefined for uniswap scheme v1 URI', async () => { + const result = await getSupportedURI(wcInUniwapScheme + VALID_WC_V1_URI) + expect(result).toBeUndefined() + }) + + it('should return correct values for uniswap scheme v2 URI', async () => { + const result = await getSupportedURI('uniswap://' + VALID_WC_V2_URI) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should return undefined for uniswap scheme deep link URI', async () => { + const result = await getSupportedURI('uniswap://widget/' + VALID_WC_V2_URI) + expect(result).toBeUndefined() + }) + + it('should return undefined for uniswap app URL v1 URI', async () => { + const result = await getSupportedURI(wcUniversalLinkUrl + VALID_WC_V1_URI) + expect(result).toBeUndefined() + }) + + it('should return correct values for uniswap app URL v2 URI', async () => { + const result = await getSupportedURI('https://uniswap.org/app/wc?uri=' + VALID_WC_V2_URI) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should return correct values for valid v1 URIs', async () => { + const result = await getSupportedURI(VALID_WC_V1_URI) + expect(result).toEqual({ type: URIType.WalletConnectURL, value: VALID_WC_V1_URI }) + }) + + it('should return correct values for valid v2 URIs', async () => { + const result = await getSupportedURI(VALID_WC_V2_URI) + expect(result).toEqual({ type: URIType.WalletConnectV2URL, value: VALID_WC_V2_URI }) + }) + + it('should extract correct address from address URI', async () => { + const validUri = 'address:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + const result = await getSupportedURI(validUri) + expect(result).toEqual({ + type: URIType.Address, + value: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }) + }) + + it('should return undefined for invalid address URI', async () => { + expect(await getSupportedURI('address:invalid_address')).toBeUndefined() + }) + + it('should extract correct address from metamask URI', async () => { + const validUri = 'ethereum:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' + const result = await getSupportedURI(validUri) + expect(result).toEqual({ + type: URIType.Address, + value: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + }) + }) + + it('should return undefined for invalid metamask address', async () => { + expect(await getSupportedURI('ethereum:invalid_address')).toBeUndefined() + }) +}) diff --git a/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts new file mode 100644 index 0000000..1e89586 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/ScanSheet/util.ts @@ -0,0 +1,220 @@ +import { parseUri } from '@walletconnect/utils' +import { parseEther } from 'ethers/lib/utils' +import { + UNISWAP_URL_SCHEME, + UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM, + UNISWAP_WALLETCONNECT_URL, +} from 'src/features/deepLinking/handleDeepLinkSaga' +import { logger } from 'utilities/src/logger/logger' +import { ScantasticParams, ScantasticParamsSchema } from 'wallet/src/features/scantastic/types' +import { UwULinkRequest } from 'wallet/src/features/walletConnect/types' +import { getValidAddress } from 'wallet/src/utils/addresses' + +export enum URIType { + WalletConnectURL = 'walletconnect', + WalletConnectV2URL = 'walletconnect-v2', + Address = 'address', + EasterEgg = 'easter-egg', + Scantastic = 'scantastic', + UwULink = 'uwu-link', +} + +export type URIFormat = { + type: URIType + value: string +} + +interface EnabledFeatureFlags { + isUwULinkEnabled: boolean + isScantasticEnabled: boolean +} + +const UNISNAP_CONTRACT_ADDRESS = '0xFd2308677A0eb48e2d0c4038c12AA7DCb703e8DC' +const UWULINK_CONTRACT_ALLOWLIST = [UNISNAP_CONTRACT_ADDRESS] +const UWULINK_MAX_TXN_VALUE = '0.001' + +const EASTER_EGG_QR_CODE = 'DO_NOT_SCAN_OR_ELSE_YOU_WILL_GO_TO_MOBILE_TEAM_JAIL' +export const CUSTOM_UNI_QR_CODE_PREFIX = 'hello_uniwallet:' +export const UWULINK_PREFIX = 'uwulink' +const MAX_DAPP_NAME_LENGTH = 60 + +export function truncateDappName(name: string): string { + return name && name.length > MAX_DAPP_NAME_LENGTH + ? `${name.slice(0, MAX_DAPP_NAME_LENGTH)}...` + : name +} + +export async function getSupportedURI( + uri: string, + enabledFeatureFlags?: EnabledFeatureFlags +): Promise { + if (!uri) { + return undefined + } + + const maybeAddress = getValidAddress(uri, /*withChecksum=*/ true, /*log=*/ false) + if (maybeAddress) { + return { type: URIType.Address, value: maybeAddress } + } + + const maybeMetamaskAddress = getMetamaskAddress(uri) + if (maybeMetamaskAddress) { + return { type: URIType.Address, value: maybeMetamaskAddress } + } + + const maybeScantasticAddress = getScantasticAddress(uri) + if (enabledFeatureFlags?.isScantasticEnabled && maybeScantasticAddress) { + return { type: URIType.Scantastic, value: maybeScantasticAddress } + } + + // The check for custom prefixes must be before the parseUri version 2 check because + // parseUri(hello_uniwallet:[valid_wc_uri]) also returns version 2 + const { uri: maybeCustomWcUri, type } = + (await getWcUriWithCustomPrefix(uri, CUSTOM_UNI_QR_CODE_PREFIX)) || + (await getWcUriWithCustomPrefix(uri, UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM)) || + (await getWcUriWithCustomPrefix(uri, UNISWAP_URL_SCHEME)) || + (await getWcUriWithCustomPrefix(decodeURIComponent(uri), UNISWAP_WALLETCONNECT_URL)) || + {} + if (maybeCustomWcUri && type) { + return { type, value: maybeCustomWcUri } + } + + const wctUriVersion = parseUri(uri).version + if (wctUriVersion === 1) { + return { type: URIType.WalletConnectURL, value: uri } + } + + if (wctUriVersion === 2) { + return { type: URIType.WalletConnectV2URL, value: uri } + } + + if (uri === EASTER_EGG_QR_CODE) { + return { type: URIType.EasterEgg, value: uri } + } + + if (enabledFeatureFlags?.isUwULinkEnabled && isUwULink(uri)) { + return { type: URIType.UwULink, value: uri.slice(UWULINK_PREFIX.length) } + } +} + +async function getWcUriWithCustomPrefix( + uri: string, + prefix: string +): Promise<{ uri: string; type: URIType } | null> { + if (uri.indexOf(prefix) !== 0) { + return null + } + + const maybeWcUri = uri.slice(prefix.length) + + if (parseUri(maybeWcUri).version === 2) { + return { uri: maybeWcUri, type: URIType.WalletConnectV2URL } + } + + return null +} + +function isUwULink(uri: string): boolean { + // Note the trailing `{` char is required for UwULink. See spec: + // https://github.com/ethereum/EIPs/pull/7253/files#diff-ec1218463dc29af4f2826e540d30abe987ab4c5b7152e1f6c567a0f71938a293R30 + return uri.startsWith(`${UWULINK_PREFIX}{`) +} + +/** + * Util function to check if a UwULinkRequest is valid. + * + * Current testing conditions requires: + * 1. The to address is in the UWULINK_CONTRACT_ALLOWLIST + * 2. The value is less than or equal to UWULINK_MAX_TXN_VALUE + * + * @param request parsed UwULinkRequest + * @returns boolean for whether the UwULinkRequest is allowed + */ +export function isAllowedUwULinkRequest(request: UwULinkRequest): boolean { + const { to, value } = request.value + const belowMaximumValue = + value && parseFloat(value) <= parseEther(UWULINK_MAX_TXN_VALUE).toNumber() + const isAllowedContractAddress = to && UWULINK_CONTRACT_ALLOWLIST.includes(to) + + if (!belowMaximumValue || !isAllowedContractAddress) { + return false + } + + return true +} + +// metamask QR code values have the format "ethereum:

" +function getMetamaskAddress(uri: string): Nullable { + const uriParts = uri.split(':') + if (uriParts.length < 2) { + return null + } + + return getValidAddress(uriParts[1], /*withChecksum=*/ true, /*log=*/ false) +} + +// format is scantastic:// +function getScantasticAddress(uri: string): Nullable { + if (!uri.startsWith('scantastic://')) { + return null + } + + const uriParts = uri.split('://') + + if (uriParts.length < 2) { + return null + } + + return uriParts[1] || null +} + +const PARAM_PUB_KEY = 'pubKey' +const PARAM_UUID = 'uuid' +const PARAM_VENDOR = 'vendor' +const PARAM_MODEL = 'model' +const PARAM_BROWSER = 'browser' + +/** parses scantastic params for a valid scantastic URI. */ +export function parseScantasticParams(uri: string): ScantasticParams | undefined { + const uriParams = new URLSearchParams(uri) + const paramKeys = [PARAM_PUB_KEY, PARAM_UUID, PARAM_VENDOR, PARAM_MODEL, PARAM_BROWSER] + + // Validate all keys are unique for security + for (const paramKey of paramKeys) { + if (uriParams.getAll(paramKey).length > 1) { + logger.error(new Error('Invalid scantastic params due to duplicate keys'), { + tags: { + file: 'util.ts', + function: 'parseScantasticParams', + }, + extra: { uri }, + }) + return + } + } + + const publicKey = uriParams.get(PARAM_PUB_KEY) + const uuid = uriParams.get(PARAM_UUID) + const vendor = uriParams.get(PARAM_VENDOR) + const model = uriParams.get(PARAM_MODEL) + const browser = uriParams.get(PARAM_BROWSER) + + try { + return ScantasticParamsSchema.parse({ + publicKey: publicKey ? JSON.parse(publicKey) : undefined, + uuid: uuid ? decodeURIComponent(uuid) : undefined, + vendor: vendor ? decodeURIComponent(vendor) : undefined, + model: model ? decodeURIComponent(model) : undefined, + browser: browser ? decodeURIComponent(browser) : undefined, + }) + } catch (e) { + const wrappedError = new Error('Invalid scantastic params') + wrappedError.cause = e + logger.error(wrappedError, { + tags: { + file: 'util.ts', + function: 'parseScantasticParams', + }, + }) + } +} diff --git a/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx new file mode 100644 index 0000000..2be7ff4 --- /dev/null +++ b/apps/mobile/src/components/WalletConnect/WalletConnectModals.tsx @@ -0,0 +1,134 @@ +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { WalletConnectRequestModal } from 'src/components/WalletConnect/RequestModal/WalletConnectRequestModal' +import { PendingConnectionModal } from 'src/components/WalletConnect/ScanSheet/PendingConnectionModal' +import { WalletConnectModal } from 'src/components/WalletConnect/ScanSheet/WalletConnectModal' +import { closeModal } from 'src/features/modals/modalSlice' +import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' +import { + removePendingSession, + removeRequest, + setDidOpenFromDeepLink, + WalletConnectRequest, +} from 'src/features/walletConnect/walletConnectSlice' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' +import { Flex, useSporeColors } from 'ui/src' +import EyeIcon from 'ui/src/assets/icons/eye.svg' +import { iconSizes } from 'ui/src/theme' +import { AccountDetails } from 'wallet/src/components/accounts/AccountDetails' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' +import { + useActiveAccount, + useActiveAccountAddressWithThrow, + useSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { areAddressesEqual } from 'wallet/src/utils/addresses' + +export function WalletConnectModals(): JSX.Element { + const activeAccount = useActiveAccount() + const dispatch = useAppDispatch() + + const { pendingRequests, modalState, pendingSession } = useWalletConnect(activeAccount?.address) + + /* + * Reset didOpenFromDeepLink state when app is backgrounded, since we only want + * to call `returnToPreviousApp` when the app was deep linked to from another app. + * Handles case where user opens app via WalletConnect deep link, backgrounds app, then + * opens Uniswap app via Spotlight search – we don't want `returnToPreviousApp` to return + * to Spotlight search. + * */ + useAppStateTrigger('active', 'inactive', () => { + dispatch(setDidOpenFromDeepLink(undefined)) + }) + + const currRequest = pendingRequests[0] ?? null + + const onCloseWCModal = (): void => { + dispatch(closeModal({ name: ModalName.WalletConnectScan })) + } + + // TODO: Move returnToPreviousApp() call to onClose but ensure it is not called twice + const onClosePendingConnection = (): void => { + dispatch(removePendingSession()) + } + + // When WalletConnectModal is open and a WC QR code is scanned to add a pendingSession, + // dismiss the scan modal in favor of showing PendingConnectionModal + useEffect(() => { + if (modalState.isOpen && pendingSession) { + dispatch(closeModal({ name: ModalName.WalletConnectScan })) + } + }, [modalState.isOpen, pendingSession, dispatch]) + + return ( + <> + {modalState.isOpen && ( + + )} + {pendingSession ? ( + + ) : null} + {currRequest ? : null} + + ) +} + +type RequestModalProps = { + currRequest: WalletConnectRequest +} + +function RequestModal({ currRequest }: RequestModalProps): JSX.Element { + const signerAccounts = useSignerAccounts() + const activeAccountAddress = useActiveAccountAddressWithThrow() + const { t } = useTranslation() + const dispatch = useAppDispatch() + const colors = useSporeColors() + + // TODO: Move returnToPreviousApp() call to onClose but ensure it is not called twice + const onClose = (): void => { + dispatch( + removeRequest({ requestInternalId: currRequest.internalId, account: activeAccountAddress }) + ) + } + + const isRequestFromSignerAccount = signerAccounts.some((account) => + areAddressesEqual(account.address, currRequest.account) + ) + + if (!isRequestFromSignerAccount) { + return ( + + } + modalName={ModalName.WCViewOnlyWarning} + severity={WarningSeverity.None} + title={t('walletConnect.request.warning.title')} + onCancel={onClose} + onClose={onClose}> + + + + + ) + } + + return +} diff --git a/apps/mobile/src/components/accounts/AccountCardItem.test.tsx b/apps/mobile/src/components/accounts/AccountCardItem.test.tsx new file mode 100644 index 0000000..11900b1 --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountCardItem.test.tsx @@ -0,0 +1,123 @@ +import { fireEvent, render, screen, waitFor } from 'src/test/test-utils' +import * as hooks from 'wallet/src/features/accounts/hooks' +import { + ON_PRESS_EVENT_PAYLOAD, + SAMPLE_SEED_ADDRESS_1, + amount, + portfolio, +} from 'wallet/src/test/fixtures' +import { queryResolvers } from 'wallet/src/test/utils' +import { AccountCardItem } from './AccountCardItem' + +describe(AccountCardItem, () => { + beforeEach(() => { + jest.spyOn(hooks, 'useAccountList').mockReturnValue({ + data: undefined, + loading: false, + networkStatus: 7, + refetch: jest.fn(), + startPolling: jest.fn(), + stopPolling: jest.fn(), + }) + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + const defaultProps = { + address: SAMPLE_SEED_ADDRESS_1, + isPortfolioValueLoading: false, + portfolioValue: 100, + isViewOnly: false, + onPress: jest.fn(), + } + + it('renders correctly', () => { + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('calls onPress when address is pressed', () => { + const onPress = jest.fn() + render() + + const address = screen.getByTestId(`account-item/${SAMPLE_SEED_ADDRESS_1}`) + fireEvent.press(address, ON_PRESS_EVENT_PAYLOAD) + + expect(onPress).toHaveBeenCalledTimes(1) + }) + + describe('portfolio value', () => { + it('displays loading shimmmer when portfolio value is loading', () => { + const { rerender } = render( + + ) + + // Select shimmer placeholder because the actual shimmer is rendered after onLayout + // is fired and this logic is not a part of this test + expect(screen.queryByTestId('shimmer-placeholder')).toBeTruthy() + + rerender( + + ) + + expect(screen.queryByTestId('shimmer-placeholder')).toBeFalsy() + }) + + it('shows current portfolio value when available', () => { + render() + + expect(screen.queryByText('$100.00')).toBeTruthy() + }) + + it('shows placeholder text when portfolio value is not available', () => { + render() + + expect(screen.queryByText('N/A')).toBeTruthy() + }) + + it('shows cached portfolio value when not provided explicitly in props', async () => { + // We don't want to use the mocked query response for this test as we want to + // test if the cached value (returned by the query) is used when value is not provided + jest.restoreAllMocks() + const { resolvers: resolversWithPortfolioValue } = queryResolvers({ + portfolios: () => [portfolio({ tokensTotalDenominatedValue: amount({ value: 200 }) })], + }) + render(, { + resolvers: resolversWithPortfolioValue, + }) + + await waitFor(() => { + expect(screen.queryByText('$200.00')).toBeTruthy() + }) + }) + }) + + describe('view only accounts', () => { + it('renders view only badge when account is view only', () => { + render() + + const badge = screen.queryByTestId('account-icon/view-only-badge') + + expect(badge).toBeTruthy() + }) + + it('does not render view only badge when account is not view only', () => { + render() + + const badge = screen.queryByTestId('account-icon/view-only-badge') + + expect(badge).toBeFalsy() + }) + }) +}) diff --git a/apps/mobile/src/components/accounts/AccountCardItem.tsx b/apps/mobile/src/components/accounts/AccountCardItem.tsx new file mode 100644 index 0000000..bef5dab --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountCardItem.tsx @@ -0,0 +1,162 @@ +import { impactAsync } from 'expo-haptics' +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { NotificationBadge } from 'src/components/notifications/Badge' +import { closeModal, openModal } from 'src/features/modals/modalSlice' +import { Screens } from 'src/screens/Screens' +import { disableOnPress } from 'src/utils/disableOnPress' +import { Flex, Text, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { useAccountList } from 'wallet/src/features/accounts/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { ModalName } from 'wallet/src/telemetry/constants' +import { setClipboard } from 'wallet/src/utils/clipboard' + +type AccountCardItemProps = { + address: Address + isViewOnly: boolean + onPress: (address: Address) => void +} & PortfolioValueProps + +type PortfolioValueProps = { + address: Address + isPortfolioValueLoading: boolean + portfolioValue: number | undefined +} + +function PortfolioValue({ + address, + isPortfolioValueLoading, + portfolioValue: providedPortfolioValue, +}: PortfolioValueProps): JSX.Element { + const { t } = useTranslation() + const { convertFiatAmountFormatted } = useLocalizationContext() + + // When we add a new wallet, we'll make a new network request to fetch all accounts as a single request. + // Since we're adding a new wallet address to the `ownerAddresses` array, this will be a brand new query, which won't be cached. + // To avoid all wallets showing a "loading" state, we read directly from cache while we wait for the other query to complete. + + const { data } = useAccountList({ + fetchPolicy: 'cache-first', + addresses: address, + }) + + const cachedPortfolioValue = data?.portfolios?.[0]?.tokensTotalDenominatedValue?.value + + const portfolioValue = providedPortfolioValue ?? cachedPortfolioValue + + const isLoading = isPortfolioValueLoading && portfolioValue === undefined + + return ( + + {portfolioValue === undefined + ? t('common.text.notAvailable') + : convertFiatAmountFormatted(portfolioValue, NumberType.PortfolioBalance)} + + ) +} + +export function AccountCardItem({ + address, + isViewOnly, + isPortfolioValueLoading, + portfolioValue, + onPress, +}: AccountCardItemProps): JSX.Element { + const { t } = useTranslation() + + const dispatch = useAppDispatch() + + const onPressCopyAddress = async (): Promise => { + await impactAsync() + await setClipboard(address) + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.Address, + }) + ) + } + + const onPressWalletSettings = (): void => { + dispatch(closeModal({ name: ModalName.AccountSwitcher })) + navigate(Screens.SettingsStack, { + screen: Screens.SettingsWallet, + params: { address }, + }) + } + + const onPressRemoveWallet = (): void => { + dispatch(closeModal({ name: ModalName.AccountSwitcher })) + dispatch(openModal({ name: ModalName.RemoveWallet, initialState: { address } })) + } + + const menuActions = useMemo(() => { + return [ + { title: t('account.wallet.action.copy'), systemIcon: 'doc.on.doc' }, + { title: t('account.wallet.action.settings'), systemIcon: 'gearshape' }, + { title: t('account.wallet.button.remove'), systemIcon: 'trash', destructive: true }, + ] + }, [t]) + + return ( + => { + // Emitted index based on order of menu action array + // Copy address + if (e.nativeEvent.index === 0) { + await onPressCopyAddress() + } + // Navigate to settings + if (e.nativeEvent.index === 1) { + onPressWalletSettings() + } + // Remove wallet + if (e.nativeEvent.index === 2) { + onPressRemoveWallet() + } + }}> + onPress(address)}> + + + + + + + + + ) +} + +const NotificationsBadgeContainer = ({ + children, + address, +}: { + children: React.ReactNode + address: string +}): JSX.Element => {children} diff --git a/apps/mobile/src/components/accounts/AccountHeader.test.tsx b/apps/mobile/src/components/accounts/AccountHeader.test.tsx new file mode 100644 index 0000000..6167270 --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountHeader.test.tsx @@ -0,0 +1,115 @@ +import * as ExpoClipboard from 'expo-clipboard' +import { navigationRef } from 'src/app/navigation/NavigationContainer' +import { MobileState } from 'src/app/reducer' +import { AccountHeader } from 'src/components/accounts/AccountHeader' +import { Screens } from 'src/screens/Screens' +import { fireEvent, render, screen, waitFor, within } from 'src/test/test-utils' +import { ModalName } from 'wallet/src/telemetry/constants' +import { + ACCOUNT, + ON_PRESS_EVENT_PAYLOAD, + preloadedSharedState, + signerMnemonicAccount, +} from 'wallet/src/test/fixtures' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +const preloadedState = preloadedSharedState({ account: ACCOUNT }) +const address = ACCOUNT.address +const shortenedAddress = sanitizeAddressText(shortenAddress(address))! + +const isModalOpen = (state: MobileState): boolean => { + const modalState = state.modals[ModalName.AccountSwitcher] + return modalState.isOpen +} + +describe(AccountHeader, () => { + it('renders correctly', () => { + const tree = render(, { preloadedState }) + + expect(tree.toJSON()).toMatchSnapshot() + }) + + describe('when wallet has no display name', () => { + const accountWithoutName = signerMnemonicAccount({ name: undefined, address }) + const stateWithoutName = preloadedSharedState({ + account: accountWithoutName, + }) + + it('renders shortened address within section address without name section', () => { + render(, { preloadedState: stateWithoutName }) + + const addressSection = screen.getByTestId('account-header/address-only') + const addressText = within(addressSection).queryByText(shortenedAddress) + + expect(addressText).toBeTruthy() + }) + + it('copies wallet address to clipboard when address section is pressed', async () => { + const setStringAsync = jest.fn() + jest.spyOn(ExpoClipboard, 'setStringAsync').mockImplementation(setStringAsync) + render(, { preloadedState: stateWithoutName }) + + const addressSection = screen.getByTestId('account-header/address-only') + fireEvent.press(addressSection, ON_PRESS_EVENT_PAYLOAD) + + await waitFor(() => { + expect(setStringAsync).toHaveBeenCalledTimes(1) + expect(setStringAsync).toHaveBeenCalledWith(address) + }) + }) + }) + + describe('when wallet has a display name', () => { + it('renders section with display name and address', () => { + render(, { preloadedState }) + + const displayNameSection = screen.getByTestId('account-header/display-name') + const displayNameText = within(displayNameSection).queryByText(ACCOUNT.name) + const addressText = within(displayNameSection).queryByText(shortenedAddress) + + expect(displayNameText).toBeTruthy() + expect(addressText).toBeTruthy() + }) + + it('opens account switcher modal when account name is pressed', () => { + const { store } = render(, { preloadedState }) + + const displayNameText = within(screen.getByTestId('account-header/display-name')).getByText( + ACCOUNT.name + ) + + expect(isModalOpen(store.getState())).toBe(false) + + fireEvent.press(displayNameText, ON_PRESS_EVENT_PAYLOAD) + + expect(isModalOpen(store.getState())).toBe(true) + }) + }) + + it('opens account switcher modal when account avatar is pressed', () => { + const { store } = render(, { preloadedState }) + + const avatar = screen.getByTestId('account-icon') + + expect(isModalOpen(store.getState())).toBe(false) + + fireEvent.press(avatar, ON_PRESS_EVENT_PAYLOAD) + + expect(isModalOpen(store.getState())).toBe(true) + }) + + it('opens settings screen when settings button is pressed', async () => { + const navigate = jest.fn() + jest.spyOn(navigationRef, 'isReady').mockImplementation(() => true) + jest.spyOn(navigationRef, 'navigate').mockImplementation(navigate) + render(, { preloadedState }) + + const settingsButton = screen.getByTestId('account-header/settings-button') + fireEvent.press(settingsButton, ON_PRESS_EVENT_PAYLOAD) + + await waitFor(() => { + expect(navigate).toHaveBeenCalledTimes(1) + expect(navigate).toHaveBeenCalledWith(Screens.SettingsStack, { screen: Screens.Settings }) + }) + }) +}) diff --git a/apps/mobile/src/components/accounts/AccountHeader.tsx b/apps/mobile/src/components/accounts/AccountHeader.tsx new file mode 100644 index 0000000..0444443 --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountHeader.tsx @@ -0,0 +1,144 @@ +import { impactAsync, ImpactFeedbackStyle, selectionAsync } from 'expo-haptics' +import React, { useCallback, useEffect } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { openModal } from 'src/features/modals/modalSlice' +import { setUserProperty } from 'src/features/telemetry' +import { UserPropertyName } from 'src/features/telemetry/constants' +import { Screens } from 'src/screens/Screens' +import { isDevBuild } from 'src/utils/version' +import { Flex, Icons, Text, TouchableArea } from 'ui/src' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { AnimatedUnitagDisplayName } from 'wallet/src/components/accounts/AnimatedUnitagDisplayName' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { + selectActiveAccount, + selectActiveAccountAddress, +} from 'wallet/src/features/wallet/selectors' +import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' +import { setClipboard } from 'wallet/src/utils/clipboard' + +export function AccountHeader(): JSX.Element { + const activeAddress = useAppSelector(selectActiveAccountAddress) + const account = useAppSelector(selectActiveAccount) + const dispatch = useAppDispatch() + + const { avatar } = useAvatar(activeAddress) + const displayName = useDisplayName(activeAddress) + + // Log ENS and Unitag ownership for user usage stats + useEffect(() => { + switch (displayName?.type) { + case DisplayNameType.ENS: + setUserProperty(UserPropertyName.HasLoadedENS, true) + return + case DisplayNameType.Unitag: + setUserProperty(UserPropertyName.HasLoadedUnitag, true) + return + default: + return + } + }, [displayName?.type]) + + const onPressAccountHeader = useCallback(() => { + dispatch(openModal({ name: ModalName.AccountSwitcher })) + }, [dispatch]) + + const onPressSettings = (): void => { + navigate(Screens.SettingsStack, { screen: Screens.Settings }) + } + + const onPressCopyAddress = async (): Promise => { + if (activeAddress) { + await impactAsync() + await setClipboard(activeAddress) + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.Address, + }) + ) + } + } + + const walletHasName = displayName && displayName?.type !== DisplayNameType.Address + const iconSize = 52 + + return ( + + {activeAddress && ( + + + => { + if (isDevBuild()) { + await selectionAsync() + dispatch(openModal({ name: ModalName.Experiments })) + } + }} + onPress={onPressAccountHeader}> + + + + + + + {walletHasName ? ( + + + + + + ) : ( + + + + {sanitizeAddressText(shortenAddress(activeAddress))} + + + + + )} + + )} + + ) +} diff --git a/apps/mobile/src/components/accounts/AccountList.graphql b/apps/mobile/src/components/accounts/AccountList.graphql new file mode 100644 index 0000000..0af77a1 --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountList.graphql @@ -0,0 +1,16 @@ +query AccountList( + $addresses: [String!]! + $valueModifiers: [PortfolioValueModifier!] +) { + portfolios( + ownerAddresses: $addresses + chains: [ETHEREUM, POLYGON, ARBITRUM, OPTIMISM, BASE, BNB] + valueModifiers: $valueModifiers + ) { + id + ownerAddress + tokensTotalDenominatedValue { + value + } + } +} diff --git a/apps/mobile/src/components/accounts/AccountList.test.tsx b/apps/mobile/src/components/accounts/AccountList.test.tsx new file mode 100644 index 0000000..cf238fe --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountList.test.tsx @@ -0,0 +1,109 @@ +import { AccountList } from 'src/components/accounts/AccountList' +import { cleanup, fireEvent, render, screen } from 'src/test/test-utils' +import { NumberType } from 'utilities/src/format/types' +import { + ACCOUNT, + ON_PRESS_EVENT_PAYLOAD, + amounts, + portfolio, + readOnlyAccount, + signerMnemonicAccount, +} from 'wallet/src/test/fixtures' +import { mockLocalizedFormatter } from 'wallet/src/test/mocks' +import { createArray, queryResolvers } from 'wallet/src/test/utils' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +const tokensTotalDenominatedValue = amounts.md() +const { resolvers } = queryResolvers({ + portfolios: () => [portfolio({ tokensTotalDenominatedValue })], +}) + +describe(AccountList, () => { + afterEach(cleanup) + + it('renders without error', async () => { + const tree = render(, { resolvers }) + + expect( + await screen.findByText( + mockLocalizedFormatter.formatNumberOrString({ + value: tokensTotalDenominatedValue.value, + type: NumberType.PortfolioBalance, + currencyCode: 'usd', + }) + ) + ).toBeDefined() + expect(tree.toJSON()).toMatchSnapshot() + }) + + it('handles press on card items', async () => { + const onPressSpy = jest.fn() + render(, { + resolvers, + }) + // go to success state + expect( + await screen.findByText( + mockLocalizedFormatter.formatNumberOrString({ + value: tokensTotalDenominatedValue.value, + type: NumberType.PortfolioBalance, + currencyCode: 'usd', + }) + ) + ).toBeDefined() + + fireEvent.press(screen.getByTestId(`account-item/${ACCOUNT.address}`), ON_PRESS_EVENT_PAYLOAD) + + expect(onPressSpy).toHaveBeenCalledTimes(1) + }) + + describe('signer accounts', () => { + it('renders signer accounts section if there are signer accounts', () => { + const signerAccounts = createArray(3, signerMnemonicAccount) + render(, { resolvers }) + + expect(screen.queryByText('Your other wallets')).toBeTruthy() + + signerAccounts.forEach((account) => { + const address = sanitizeAddressText(shortenAddress(account.address)) + if (address) { + expect(screen.queryByText(address)).toBeTruthy() + } + }) + cleanup() + }) + + it('does not render signer accounts section if there are no signer accounts', () => { + render(, { resolvers }) + + expect(screen.queryByText('Your other wallets')).toBeFalsy() + cleanup() + }) + }) + + describe('view only accounts', () => { + it('renders view only accounts section if there are view only accounts', () => { + const viewOnlyAccounts = createArray(3, readOnlyAccount) + render(, { resolvers }) + + expect(screen.queryByText('View only wallets')).toBeTruthy() + + viewOnlyAccounts.forEach((account) => { + const address = sanitizeAddressText(shortenAddress(account.address)) + if (address) { + expect(screen.queryByText(address)).toBeTruthy() + } + }) + cleanup() + }) + + it('does not render view only accounts section if there are no view only accounts', () => { + render(, { + resolvers, + }) + + expect(screen.queryByText('View only wallets')).toBeFalsy() + // cleanup() + }) + }) +}) diff --git a/apps/mobile/src/components/accounts/AccountList.tsx b/apps/mobile/src/components/accounts/AccountList.tsx new file mode 100644 index 0000000..508a59a --- /dev/null +++ b/apps/mobile/src/components/accounts/AccountList.tsx @@ -0,0 +1,166 @@ +import { LinearGradient } from 'expo-linear-gradient' +import { ComponentProps, default as React, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet } from 'react-native' +import { AccountCardItem } from 'src/components/accounts/AccountCardItem' +import { VirtualizedList } from 'src/components/layout/VirtualizedList' +import { Flex, Text, useSporeColors } from 'ui/src' +import { opacify, spacing } from 'ui/src/theme' +import { useAsyncData } from 'utilities/src/react/hooks' +import { PollingInterval } from 'wallet/src/constants/misc' +import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { useAccountList } from 'wallet/src/features/accounts/hooks' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' + +// Most screens can fit more but this is set conservatively +const MIN_ACCOUNTS_TO_ENABLE_SCROLL = 5 + +type AccountListProps = Pick, 'onPress'> & { + accounts: Account[] + isVisible?: boolean +} + +type AccountWithPortfolioValue = { + account: Account + isPortfolioValueLoading: boolean + portfolioValue: number | undefined +} + +const ViewOnlyHeader = (): JSX.Element => { + const { t } = useTranslation() + return ( + + + {t('account.wallet.header.viewOnly')} + + + ) +} + +const SignerHeader = (): JSX.Element => { + const { t } = useTranslation() + return ( + + + {t('account.wallet.header.other')} + + + ) +} + +export function AccountList({ accounts, onPress, isVisible }: AccountListProps): JSX.Element { + const colors = useSporeColors() + const addresses = useMemo(() => accounts.map((a) => a.address), [accounts]) + + const { data, networkStatus, refetch, startPolling, stopPolling } = useAccountList({ + addresses, + notifyOnNetworkStatusChange: true, + }) + + // Only poll account total values when the account list is visible + const controlPolling = useCallback(async () => { + if (isVisible) { + await refetch() + startPolling(PollingInterval.Fast) + } else { + stopPolling() + } + }, [isVisible, refetch, startPolling, stopPolling]) + + useAsyncData(controlPolling) + + const isPortfolioValueLoading = isNonPollingRequestInFlight(networkStatus) + + const accountsWithPortfolioValue: AccountWithPortfolioValue[] = useMemo(() => { + return accounts.map((account, i) => { + return { + account, + isPortfolioValueLoading, + portfolioValue: data?.portfolios?.[i]?.tokensTotalDenominatedValue?.value, + } + }) + }, [accounts, data, isPortfolioValueLoading]) + + const signerAccounts = useMemo(() => { + return accountsWithPortfolioValue.filter( + (account) => account.account.type === AccountType.SignerMnemonic + ) + }, [accountsWithPortfolioValue]) + + const hasSignerAccounts = signerAccounts.length > 0 + + const viewOnlyAccounts = useMemo(() => { + return accountsWithPortfolioValue.filter( + (account) => account.account.type === AccountType.Readonly + ) + }, [accountsWithPortfolioValue]) + + const hasViewOnlyAccounts = viewOnlyAccounts.length > 0 + + const renderAccountCardItem = useCallback( + (item: AccountWithPortfolioValue): JSX.Element => ( + + ), + [onPress] + ) + + return ( + + {/* TODO(MOB-646): attempt to switch gradients to react-native-svg#LinearGradient and avoid new clear color */} + + = MIN_ACCOUNTS_TO_ENABLE_SCROLL} + showsVerticalScrollIndicator={false}> + {hasSignerAccounts && ( + <> + + {signerAccounts.map(renderAccountCardItem)} + + )} + {hasViewOnlyAccounts && ( + <> + + {viewOnlyAccounts.map(renderAccountCardItem)} + + )} + + + + ) +} + +const ListSheet = StyleSheet.create({ + bottomGradient: { + bottom: 0, + height: spacing.spacing16, + left: 0, + position: 'absolute', + width: '100%', + }, + topGradient: { + height: spacing.spacing16, + left: 0, + position: 'absolute', + top: 0, + width: '100%', + zIndex: 1, + }, +}) diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap new file mode 100644 index 0000000..5e10f2e --- /dev/null +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountCardItem.test.tsx.snap @@ -0,0 +1,353 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountCardItem renders correctly 1`] = ` + + + + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + + + 0x​82D5...3Fa6 + + + + + + + + $100.00 + + + + +`; diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap new file mode 100644 index 0000000..8cfa773 --- /dev/null +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountHeader.test.tsx.snap @@ -0,0 +1,667 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountHeader renders correctly 1`] = ` + + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test Account + + + + + .uni.eth + + + + + + 0x​82D5...3Fa6 + + + + + + + + + + + + + + + +`; diff --git a/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap new file mode 100644 index 0000000..7c368a8 --- /dev/null +++ b/apps/mobile/src/components/accounts/__snapshots__/AccountList.test.tsx.snap @@ -0,0 +1,444 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`AccountList renders without error 1`] = ` + + ExpoLinearGradient + + + + + + + } + animatedStyle={ + { + "value": {}, + } + } + bounces={false} + collapsable={false} + data={[]} + getItem={[Function]} + getItemCount={[Function]} + keyExtractor={[Function]} + keyboardShouldPersistTaps="always" + onContentSizeChange={[Function]} + onLayout={[Function]} + onMomentumScrollBegin={[Function]} + onMomentumScrollEnd={[Function]} + onScroll={[Function]} + onScrollBeginDrag={[Function]} + onScrollEndDrag={[Function]} + removeClippedSubviews={false} + renderItem={[Function]} + scrollEnabled={false} + scrollEventThrottle={16} + sentry-label="VirtualizedList" + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + stickyHeaderIndices={[]} + viewabilityConfigCallbackPairs={[]} + > + + + + + Your other wallets + + + + + + + + + + + + + + + } + > + + + + + + + + + + + } + > + + + + + + + + + + + + + 0x​82D5...3Fa6 + + + + + + + + $55.00 + + + + + + + + ExpoLinearGradient + +`; diff --git a/apps/mobile/src/components/animation/AnimateInOrder.tsx b/apps/mobile/src/components/animation/AnimateInOrder.tsx new file mode 100644 index 0000000..61570f2 --- /dev/null +++ b/apps/mobile/src/components/animation/AnimateInOrder.tsx @@ -0,0 +1,54 @@ +import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' +import { PropsWithChildren, useEffect, useState } from 'react' +import { Flex, FlexProps } from 'ui/src' + +export const AnimateInOrder = ({ + children, + index, + animation = 'bouncy', + enterStyle = { opacity: 0, scale: 0.8 }, + exitStyle = { opacity: 0, scale: 0.8 }, + delayMs = 150, + hapticOnEnter, + ...rest +}: PropsWithChildren< + { + index: number + hapticOnEnter?: boolean + delayMs?: number + } & Pick & + FlexProps +>): JSX.Element => { + return ( + + + {children} + + + ) +} + +const Delay = ({ + children, + hapticOnEnter, + by, +}: PropsWithChildren<{ by: number; hapticOnEnter?: boolean }>): JSX.Element | null => { + const [done, setDone] = useState(false) + + useEffect(() => { + const showTimer = setTimeout(async () => { + if (hapticOnEnter) { + await impactAsync(ImpactFeedbackStyle.Light) + } + setDone(true) + }, by) + return () => clearTimeout(showTimer) + }, [by, hapticOnEnter]) + + return done ? <>{children} : null +} diff --git a/apps/mobile/src/components/banners/BottomBanner.tsx b/apps/mobile/src/components/banners/BottomBanner.tsx new file mode 100644 index 0000000..3fd53c9 --- /dev/null +++ b/apps/mobile/src/components/banners/BottomBanner.tsx @@ -0,0 +1,58 @@ +import React from 'react' +import { FadeIn, FadeOut, useAnimatedStyle, withTiming } from 'react-native-reanimated' +import { AnimatedFlex, ColorTokens, Text } from 'ui/src' + +export const BANNER_HEIGHT = 45 + +export type BottomBannerProps = { + text: string + icon?: JSX.Element + backgroundColor?: ColorTokens + translateY?: number +} + +export function BottomBanner({ + text, + icon, + backgroundColor, + translateY, +}: BottomBannerProps): JSX.Element { + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + // need to check for undefined since 0 is falsy + translateY: withTiming(translateY !== undefined ? -1 * translateY : -1 * BANNER_HEIGHT, { + duration: 200, + }), + }, + ], + })) + + return ( + + {icon} + {text} + + ) +} diff --git a/apps/mobile/src/components/banners/OfflineBanner.tsx b/apps/mobile/src/components/banners/OfflineBanner.tsx new file mode 100644 index 0000000..0589572 --- /dev/null +++ b/apps/mobile/src/components/banners/OfflineBanner.tsx @@ -0,0 +1,47 @@ +import { useNetInfo } from '@react-native-community/netinfo' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useAppSelector } from 'src/app/hooks' +import { BANNER_HEIGHT, BottomBanner } from 'src/components/banners/BottomBanner' +import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' +import { useSporeColors } from 'ui/src' +import InfoCircle from 'ui/src/assets/icons/info-circle.svg' +import { iconSizes } from 'ui/src/theme' +import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' + +const EXTRA_MARGIN = 5 + +export function OfflineBanner(): JSX.Element | null { + const { t } = useTranslation() + const colors = useSporeColors() + const netInfo = useNetInfo() + + // don't show the offline banner in onboarding + const finishedOnboarding = useAppSelector(selectFinishedOnboarding) + const isModalOpen = useAppSelector(selectSomeModalOpen) + + // Needs to explicity check for false since `netInfo.isConnected` may be null + const showBanner = netInfo.isConnected === false && finishedOnboarding && !isModalOpen + + if (__DEV__) { + // do not check in Dev mode since the simulator + // gets funky with the network state: + // https://github.com/react-native-netinfo/react-native-netinfo/issues/7 + return null + } + + return showBanner ? ( + + } + text={t('home.banner.offline')} + translateY={BANNER_HEIGHT - EXTRA_MARGIN} + /> + ) : null +} diff --git a/apps/mobile/src/components/buttons/BackButton.test.tsx b/apps/mobile/src/components/buttons/BackButton.test.tsx new file mode 100644 index 0000000..38f2a72 --- /dev/null +++ b/apps/mobile/src/components/buttons/BackButton.test.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { fireEvent, render, screen } from 'src/test/test-utils' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' +import { BackButton } from './BackButton' + +const mockedGoBack = jest.fn() +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native') + return { + ...actualNav, + useNavigation: (): void => ({ + ...actualNav.useNavigation, + goBack: mockedGoBack, + }), + } +}) + +describe(BackButton, () => { + it('renders without error', async () => { + const tree = render() + + expect(tree).toMatchSnapshot() + expect(await screen.findByText('Back')).toBeDefined() + }) + + it('calls goBack', async () => { + render() + + const button = await screen.findByText('Back') + fireEvent.press(button, ON_PRESS_EVENT_PAYLOAD) + + expect(mockedGoBack).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/mobile/src/components/buttons/BackButton.tsx b/apps/mobile/src/components/buttons/BackButton.tsx new file mode 100644 index 0000000..04c448e --- /dev/null +++ b/apps/mobile/src/components/buttons/BackButton.tsx @@ -0,0 +1,38 @@ +import { useNavigation } from '@react-navigation/native' +import React from 'react' +import { BackButtonView } from 'src/components/layout/BackButtonView' +import { ColorTokens, TouchableArea, TouchableAreaProps } from 'ui/src' + +type Props = { + size?: number + color?: ColorTokens + showButtonLabel?: boolean + onPressBack?: () => void +} & TouchableAreaProps + +export function BackButton({ + onPressBack, + size, + color, + showButtonLabel, + ...rest +}: Props): JSX.Element { + const navigation = useNavigation() + + const goBack = onPressBack + ? onPressBack + : (): void => { + navigation.goBack() + } + return ( + + + + ) +} diff --git a/apps/mobile/src/components/buttons/CloseButton.test.tsx b/apps/mobile/src/components/buttons/CloseButton.test.tsx new file mode 100644 index 0000000..99e7262 --- /dev/null +++ b/apps/mobile/src/components/buttons/CloseButton.test.tsx @@ -0,0 +1,21 @@ +import { fireEvent, render } from 'src/test/test-utils' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' +import { CloseButton } from './CloseButton' + +describe(CloseButton, () => { + it('renders without error', () => { + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('calls onPress when pressed', async () => { + const onPress = jest.fn() + const { getByTestId } = render() + + const button = getByTestId('buttons/close-button') + fireEvent.press(button, ON_PRESS_EVENT_PAYLOAD) + + expect(onPress).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/mobile/src/components/buttons/CloseButton.tsx b/apps/mobile/src/components/buttons/CloseButton.tsx new file mode 100644 index 0000000..ff3feee --- /dev/null +++ b/apps/mobile/src/components/buttons/CloseButton.tsx @@ -0,0 +1,17 @@ +import React from 'react' +import { ColorTokens, IconProps, Icons, TouchableArea, TouchableAreaProps } from 'ui/src' + +type Props = { + onPress: () => void + size?: IconProps['size'] + strokeWidth?: number + color?: ColorTokens +} & TouchableAreaProps + +export function CloseButton({ onPress, size, strokeWidth, color, ...rest }: Props): JSX.Element { + return ( + + + + ) +} diff --git a/apps/mobile/src/components/buttons/CopyTextButton.stories.tsx b/apps/mobile/src/components/buttons/CopyTextButton.stories.tsx new file mode 100644 index 0000000..97618bd --- /dev/null +++ b/apps/mobile/src/components/buttons/CopyTextButton.stories.tsx @@ -0,0 +1,12 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react' +import React from 'react' +import { CopyTextButton } from 'src/components/buttons/CopyTextButton' + +export default { + title: 'WIP/Button/Copy', + component: CopyTextButton, +} as ComponentMeta + +const Template: ComponentStory = (args) => + +export const Primary = Template.bind({}) diff --git a/apps/mobile/src/components/buttons/CopyTextButton.test.tsx b/apps/mobile/src/components/buttons/CopyTextButton.test.tsx new file mode 100644 index 0000000..2b52b5d --- /dev/null +++ b/apps/mobile/src/components/buttons/CopyTextButton.test.tsx @@ -0,0 +1,49 @@ +import { act, fireEvent, render } from 'src/test/test-utils' +import { setClipboard } from 'wallet/src/utils/clipboard' +import { CopyTextButton } from './CopyTextButton' + +jest.mock('wallet/src/utils/clipboard') + +describe(CopyTextButton, () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('renders without error', () => { + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('copies text when pressed', async () => { + const { getByText } = render() + + const button = getByText('Copy') + await act(async () => { + fireEvent.press(button) + }) + + expect(setClipboard).toHaveBeenCalledWith('copy text') + }) + + it('changes button text when text is copied and brings back original text after timeout', async () => { + const { queryByText, getByText } = render() + + const button = getByText('Copy') + await act(async () => { + fireEvent.press(button) + }) + + expect(queryByText('Copied')).toBeTruthy() + + await act(async () => { + jest.advanceTimersByTime(2000) + }) + + expect(queryByText('Copy')).toBeTruthy() + }) +}) diff --git a/apps/mobile/src/components/buttons/CopyTextButton.tsx b/apps/mobile/src/components/buttons/CopyTextButton.tsx new file mode 100644 index 0000000..ae61035 --- /dev/null +++ b/apps/mobile/src/components/buttons/CopyTextButton.tsx @@ -0,0 +1,48 @@ +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Button, useSporeColors } from 'ui/src' +import CheckCircle from 'ui/src/assets/icons/check-circle.svg' +import CopySheets from 'ui/src/assets/icons/copy-sheets.svg' +import { iconSizes } from 'ui/src/theme' +import { useTimeout } from 'utilities/src/time/timing' +import { setClipboard } from 'wallet/src/utils/clipboard' + +interface Props { + copyText?: string +} + +export function CopyTextButton({ copyText }: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + + const ICON_SIZE = iconSizes.icon20 + const RESET_COPY_STATE_DELAY = 1500 + + const [isCopied, setIsCopied] = useState(false) + + const copyIcon = + const copiedIcon = ( + + ) + + const onPress = async (): Promise => { + if (copyText) { + await setClipboard(copyText) + } + setIsCopied(true) + } + + const resetIsCopied = useCallback(() => { + if (isCopied) { + setIsCopied(false) + } + }, [isCopied]) + + useTimeout(resetIsCopied, RESET_COPY_STATE_DELAY) + + return ( + + ) +} diff --git a/apps/mobile/src/components/buttons/LinkButton.test.tsx b/apps/mobile/src/components/buttons/LinkButton.test.tsx new file mode 100644 index 0000000..9f4d7a9 --- /dev/null +++ b/apps/mobile/src/components/buttons/LinkButton.test.tsx @@ -0,0 +1,48 @@ +import { fireEvent, render } from 'src/test/test-utils' +import { ON_PRESS_EVENT_PAYLOAD } from 'wallet/src/test/fixtures' +import { LinkButton } from './LinkButton' + +jest.mock('wallet/src/utils/linking') + +describe(LinkButton, () => { + it('renders without error', () => { + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + it('renders button with specified label', async () => { + const { queryByText } = render() + + expect(queryByText('link text')).toBeDefined() + }) + + describe('when pressed', () => { + const cases = [ + { openExternalBrowser: false, isSafeUri: false }, + { openExternalBrowser: true, isSafeUri: false }, + { openExternalBrowser: false, isSafeUri: true }, + { openExternalBrowser: true, isSafeUri: true }, + ] + + it.each(cases)('calls openUri with %p', async ({ openExternalBrowser, isSafeUri }) => { + const { getByText } = render( + + ) + + const button = getByText('link text') + fireEvent.press(button, ON_PRESS_EVENT_PAYLOAD) + + expect(require('wallet/src/utils/linking').openUri).toHaveBeenCalledWith( + 'https://example.com', + openExternalBrowser, + isSafeUri + ) + }) + }) +}) diff --git a/apps/mobile/src/components/buttons/LinkButton.tsx b/apps/mobile/src/components/buttons/LinkButton.tsx new file mode 100644 index 0000000..1ab18c7 --- /dev/null +++ b/apps/mobile/src/components/buttons/LinkButton.tsx @@ -0,0 +1,55 @@ +import React, { useMemo } from 'react' +import { Flex, FlexProps, Text, TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' +import ExternalLinkIcon from 'ui/src/assets/icons/external-link.svg' +import { TextVariantTokens, iconSizes } from 'ui/src/theme' +import { openUri } from 'wallet/src/utils/linking' + +interface LinkButtonProps extends Omit { + label: string + url: string + openExternalBrowser?: boolean + isSafeUri?: boolean + color?: string + iconColor?: string + size?: number + textVariant?: TextVariantTokens +} + +export function LinkButton({ + url, + label, + textVariant, + color, + iconColor, + openExternalBrowser = false, + isSafeUri = false, + size = iconSizes.icon20, + justifyContent = 'center', + ...rest +}: LinkButtonProps & Pick): JSX.Element { + const colors = useSporeColors() + const colorStyles = useMemo(() => { + return color + ? { style: { color } } + : // if a hex color is not defined, don't give the Text component a style prop, because that will override its default behavior of using neutral1 when no color prop is defined + {} + }, [color]) + + return ( + => openUri(url, openExternalBrowser, isSafeUri)} + {...rest}> + + + {label} + + + + + ) +} diff --git a/apps/mobile/src/components/buttons/__snapshots__/BackButton.test.tsx.snap b/apps/mobile/src/components/buttons/__snapshots__/BackButton.test.tsx.snap new file mode 100644 index 0000000..63ff5e5 --- /dev/null +++ b/apps/mobile/src/components/buttons/__snapshots__/BackButton.test.tsx.snap @@ -0,0 +1,152 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BackButton renders without error 1`] = ` + + + + + + + + + + + Back + + + +`; diff --git a/apps/mobile/src/components/buttons/__snapshots__/CloseButton.test.tsx.snap b/apps/mobile/src/components/buttons/__snapshots__/CloseButton.test.tsx.snap new file mode 100644 index 0000000..e52eadd --- /dev/null +++ b/apps/mobile/src/components/buttons/__snapshots__/CloseButton.test.tsx.snap @@ -0,0 +1,86 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CloseButton renders without error 1`] = ` + + + + + + + +`; diff --git a/apps/mobile/src/components/buttons/__snapshots__/CopyTextButton.test.tsx.snap b/apps/mobile/src/components/buttons/__snapshots__/CopyTextButton.test.tsx.snap new file mode 100644 index 0000000..cf7d945 --- /dev/null +++ b/apps/mobile/src/components/buttons/__snapshots__/CopyTextButton.test.tsx.snap @@ -0,0 +1,68 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CopyTextButton renders without error 1`] = ` + + + + Copy + + +`; diff --git a/apps/mobile/src/components/buttons/__snapshots__/LinkButton.test.tsx.snap b/apps/mobile/src/components/buttons/__snapshots__/LinkButton.test.tsx.snap new file mode 100644 index 0000000..66ed118 --- /dev/null +++ b/apps/mobile/src/components/buttons/__snapshots__/LinkButton.test.tsx.snap @@ -0,0 +1,60 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LinkButton renders without error 1`] = ` + + + + link text + + + + +`; diff --git a/apps/mobile/src/components/buttons/utils.ts b/apps/mobile/src/components/buttons/utils.ts new file mode 100644 index 0000000..2f64ab7 --- /dev/null +++ b/apps/mobile/src/components/buttons/utils.ts @@ -0,0 +1,12 @@ +import { withSequence, withSpring, WithSpringConfig } from 'react-native-reanimated' + +export function pulseAnimation( + activeScale: number, + spingAnimationConfig: WithSpringConfig = { damping: 1, stiffness: 200 } +): number { + 'worklet' + return withSequence( + withSpring(activeScale, spingAnimationConfig), + withSpring(1, spingAnimationConfig) + ) +} diff --git a/apps/mobile/src/components/carousel/Carousel.tsx b/apps/mobile/src/components/carousel/Carousel.tsx new file mode 100644 index 0000000..dda9199 --- /dev/null +++ b/apps/mobile/src/components/carousel/Carousel.tsx @@ -0,0 +1,76 @@ +import React, { ComponentProps, createContext, ReactNode, useCallback, useRef } from 'react' +import { ListRenderItemInfo } from 'react-native' +import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' +import { AnimatedIndicator } from 'src/components/carousel/Indicator' +import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' +import { Flex, useDeviceDimensions } from 'ui/src' + +interface CarouselContextProps { + current: number + goToNext: () => void + goToPrev: () => void +} + +// Allows child components to control the carousel +export const CarouselContext = createContext({ + goToNext: () => undefined, + goToPrev: () => undefined, + current: 0, +}) + +type CarouselProps = { + slides: JSX.Element[] +} & Pick, 'scrollEnabled'> + +export function Carousel({ slides, ...flatListProps }: CarouselProps): JSX.Element { + const scroll = useSharedValue(0) + const { fullWidth } = useDeviceDimensions() + const myRef = useRef>(null) + + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scroll.value = event.contentOffset.x + }, + }) + + const goToNext = useCallback(() => { + // @ts-expect-error https://github.com/software-mansion/react-native-reanimated/issues/2976 + myRef.current?._listRef._scrollRef.scrollTo({ + x: Math.ceil(scroll.value / fullWidth + 0.5) * fullWidth, + }) + }, [fullWidth, scroll]) + + const goToPrev = useCallback(() => { + // @ts-expect-error https://github.com/software-mansion/react-native-reanimated/issues/2976 + myRef.current?._listRef._scrollRef.scrollTo({ + x: Math.floor(scroll.value / fullWidth - 0.5) * fullWidth, + }) + }, [fullWidth, scroll]) + + return ( + + + + ): JSX.Element => ( + + {item} + + )} + scrollEnabled={true} + scrollEventThrottle={32} + showsHorizontalScrollIndicator={false} + onScroll={scrollHandler} + /> + + + ) +} + +const key = (_: JSX.Element, index: number): string => index.toString() diff --git a/apps/mobile/src/components/carousel/Indicator.tsx b/apps/mobile/src/components/carousel/Indicator.tsx new file mode 100644 index 0000000..c0a00c1 --- /dev/null +++ b/apps/mobile/src/components/carousel/Indicator.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { Extrapolate, interpolate, SharedValue, useAnimatedStyle } from 'react-native-reanimated' +import { AnimatedFlex, Flex, useDeviceDimensions } from 'ui/src' + +export function Indicator({ + stepCount, + currentStep, +}: { + stepCount: number + currentStep: number +}): JSX.Element { + const { fullWidth } = useDeviceDimensions() + const indicatorWidth = (200 / 375) * fullWidth + + return ( + + {[...Array(stepCount)].map((_, i) => ( + + ))} + + ) +} + +export function AnimatedIndicator({ + scroll, + stepCount, +}: { + scroll: SharedValue + stepCount: number +}): JSX.Element { + return ( + + {[...Array(stepCount)].map((_, i) => ( + + ))} + + ) +} + +function AnimatedIndicatorPill({ + index, + scroll, +}: { + index: number + scroll: SharedValue +}): JSX.Element { + const { fullWidth } = useDeviceDimensions() + const style = useAnimatedStyle(() => { + const inputRange = [(index - 1) * fullWidth, index * fullWidth, (index + 1) * fullWidth] + return { + opacity: interpolate(scroll.value, inputRange, [0.2, 1, 0.2], Extrapolate.CLAMP), + } + }) + + return ( + + ) +} diff --git a/apps/mobile/src/components/education/SeedPhrase.tsx b/apps/mobile/src/components/education/SeedPhrase.tsx new file mode 100644 index 0000000..bbcf1ba --- /dev/null +++ b/apps/mobile/src/components/education/SeedPhrase.tsx @@ -0,0 +1,114 @@ +import React, { ComponentProps, ReactNode, useCallback, useContext, useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import { runOnJS } from 'react-native-reanimated' +import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types' +import { CloseButton } from 'src/components/buttons/CloseButton' +import { CarouselContext } from 'src/components/carousel/Carousel' +import { OnboardingScreens } from 'src/screens/Screens' +import { Flex, Text, useDeviceDimensions } from 'ui/src' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' + +function Page({ + text, + params, +}: { + text: ReactNode + params: OnboardingStackBaseParams +}): JSX.Element { + const { t } = useTranslation() + const { fullWidth } = useDeviceDimensions() + const { goToPrev, goToNext } = useContext(CarouselContext) + const navigation = useOnboardingStackNavigation() + + const onDismiss = useCallback((): void => { + navigation.navigate(OnboardingScreens.Backup, params) + }, [navigation, params]) + + const slideChangeGesture = useMemo( + () => + Gesture.Tap().onEnd(({ absoluteX }) => { + if (absoluteX < fullWidth * 0.33) { + runOnJS(goToPrev)() + } else { + runOnJS(goToNext)() + } + }), + [goToPrev, goToNext, fullWidth] + ) + + const dismissGesture = useMemo( + () => + Gesture.Tap().onEnd(() => { + runOnJS(onDismiss)() + }), + [onDismiss] + ) + + return ( + + + + + + {t('onboarding.tooltip.recoveryPhrase.trigger')} + + + undefined} /> + + + + + {text} + + + + + ) +} + +export const SeedPhraseEducationContent = (params: OnboardingStackBaseParams): JSX.Element[] => { + const cloudProviderName = getCloudProviderName() + const highlightComponent = + + const pageContentList = [ + , + , + , + , + , + , + ] + + return pageContentList.map((content) => ( + {content}} /> + )) +} + +function CustomHeadingText(props: ComponentProps): JSX.Element { + return +} diff --git a/apps/mobile/src/components/education/index.ts b/apps/mobile/src/components/education/index.ts new file mode 100644 index 0000000..a5f4808 --- /dev/null +++ b/apps/mobile/src/components/education/index.ts @@ -0,0 +1,9 @@ +import { SeedPhraseEducationContent } from 'src/components/education/SeedPhrase' + +export enum EducationContentType { + SeedPhrase, +} + +export const educationContent = { + [EducationContentType.SeedPhrase]: SeedPhraseEducationContent, +} diff --git a/apps/mobile/src/components/explore/ExploreSections.tsx b/apps/mobile/src/components/explore/ExploreSections.tsx new file mode 100644 index 0000000..8816dbd --- /dev/null +++ b/apps/mobile/src/components/explore/ExploreSections.tsx @@ -0,0 +1,276 @@ +import { NetworkStatus } from '@apollo/client' +import React, { useCallback, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItem, ListRenderItemInfo, StyleSheet, View } from 'react-native' +import { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { FavoriteTokensGrid } from 'src/components/explore/FavoriteTokensGrid' +import { FavoriteWalletsGrid } from 'src/components/explore/FavoriteWalletsGrid' +import { SortButton } from 'src/components/explore/SortButton' +import { TokenItem, TokenItemData } from 'src/components/explore/TokenItem' +import { AnimatedBottomSheetFlatList } from 'src/components/layout/AnimatedFlatList' +import { AutoScrollProps } from 'src/components/sortableGrid' +import { + getClientTokensOrderByCompareFn, + getTokenMetadataDisplayType, + getTokensOrderByValues, +} from 'src/features/explore/utils' +import { usePollOnFocusOnly } from 'src/utils/hooks' +import { Flex, Loader, Text, useDeviceInsets } from 'ui/src' +import { + Chain, + ExploreTokensTabQuery, + useExploreTokensTabQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { getWrappedNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { PollingInterval } from 'wallet/src/constants/misc' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { usePersistedError } from 'wallet/src/features/dataApi/utils' +import { + selectHasFavoriteTokens, + selectHasWatchedWallets, +} from 'wallet/src/features/favorites/selectors' +import { selectTokensOrderBy } from 'wallet/src/features/wallet/selectors' +import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' + +type ExploreSectionsProps = { + listRef: React.MutableRefObject +} + +export function ExploreSections({ listRef }: ExploreSectionsProps): JSX.Element { + const { t } = useTranslation() + const insets = useDeviceInsets() + const scrollY = useSharedValue(0) + const headerRef = useRef(null) + const visibleListHeight = useSharedValue(0) + + // Top tokens sorting + const orderBy = useAppSelector(selectTokensOrderBy) + const tokenMetadataDisplayType = getTokenMetadataDisplayType(orderBy) + const { clientOrderBy, serverOrderBy } = getTokensOrderByValues(orderBy) + + const { + data, + networkStatus, + loading: requestLoading, + error: requestError, + refetch, + startPolling, + stopPolling, + } = useExploreTokensTabQuery({ + variables: { + topTokensOrderBy: serverOrderBy, + }, + returnPartialData: true, + }) + + usePollOnFocusOnly(startPolling, stopPolling, PollingInterval.Fast) + + const topTokenItems = useMemo(() => { + if (!data || !data.topTokens) { + return + } + + // special case to replace weth with eth because the backend does not return eth data + // eth will be defined only if all the required data is available + // when eth data is not fully available, we do not replace weth with eth + const { eth } = data + const wethAddress = getWrappedNativeAddress(ChainId.Mainnet) + + const topTokens = data.topTokens + .map((token) => { + if (!token) { + return + } + + const isWeth = + areAddressesEqual(token.address, wethAddress) && token?.chain === Chain.Ethereum + + // manually replace weth with eth given backend only returns eth data as a proxy for eth + if (isWeth && eth) { + return gqlTokenToTokenItemData(eth) + } + + return gqlTokenToTokenItemData(token) + }) + .filter(Boolean) as TokenItemData[] + + if (!clientOrderBy) { + return topTokens + } + + // Apply client side sort order + const compareFn = getClientTokensOrderByCompareFn(clientOrderBy) + return topTokens.sort(compareFn) + }, [data, clientOrderBy]) + + const renderItem: ListRenderItem = useCallback( + ({ item, index }: ListRenderItemInfo) => { + return ( + + ) + }, + [tokenMetadataDisplayType] + ) + + // Don't want to show full screen loading state when changing tokens sort, which triggers NetworkStatus.setVariable request + const isLoading = + networkStatus === NetworkStatus.loading || networkStatus === NetworkStatus.refetch + const hasAllData = !!data?.topTokens + const error = usePersistedError(requestLoading, requestError) + + const onRetry = useCallback(async () => { + await refetch() + }, [refetch]) + + const scrollHandler = useAnimatedScrollHandler((e) => { + scrollY.value = e.contentOffset.y + }) + + // Use showLoading for showing full screen loading state + // Used in each section to ensure loading state layout matches loaded state + const showLoading = (!hasAllData && isLoading) || (!!error && isLoading) + + if (!hasAllData && error) { + return ( + + + + ) + } + + return ( + // Pass onLayout callback to the list wrapper component as it returned + // incorrect values when it was passed to the list itself + { + visibleListHeight.value = height + }}> + + + + } + ListHeaderComponent={ + + + + + {t('explore.tokens.top.title')} + + + + + + + } + ListHeaderComponentStyle={styles.foreground} + contentContainerStyle={{ paddingBottom: insets.bottom }} + data={showLoading ? undefined : topTokenItems} + keyExtractor={tokenKey} + renderItem={renderItem} + scrollEventThrottle={16} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + onScroll={scrollHandler} + /> + + ) +} + +const tokenKey = (token: TokenItemData): string => { + return token.address + ? buildCurrencyId(token.chainId, token.address) + : buildNativeCurrencyId(token.chainId) +} + +function gqlTokenToTokenItemData( + token: Maybe[0]>> +): TokenItemData | null { + if (!token || !token.project) { + return null + } + + const { symbol, address, chain, project, market } = token + const { logoUrl, markets, name } = project + const tokenProjectMarket = markets?.[0] + + const chainId = fromGraphQLChain(chain) + + return { + chainId, + address, + name, + symbol, + logoUrl, + price: tokenProjectMarket?.price?.value, + marketCap: tokenProjectMarket?.marketCap?.value, + pricePercentChange24h: tokenProjectMarket?.pricePercentChange24h?.value, + volume24h: market?.volume?.value, + totalValueLocked: market?.totalValueLocked?.value, + } as TokenItemData +} + +type FavoritesSectionProps = AutoScrollProps & { + showLoading: boolean +} + +function FavoritesSection(props: FavoritesSectionProps): JSX.Element | null { + const hasFavoritedTokens = useAppSelector(selectHasFavoriteTokens) + const hasFavoritedWallets = useAppSelector(selectHasWatchedWallets) + + if (!hasFavoritedTokens && !hasFavoritedWallets) { + return null + } + + return ( + + {hasFavoritedTokens && } + {hasFavoritedWallets && } + + ) +} + +const styles = StyleSheet.create({ + foreground: { + zIndex: 1, + }, +}) diff --git a/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx new file mode 100644 index 0000000..db3fc6e --- /dev/null +++ b/apps/mobile/src/components/explore/FavoriteHeaderRow.tsx @@ -0,0 +1,48 @@ +import { default as React } from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Icons, Text, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { ElementName } from 'wallet/src/telemetry/constants' + +export function FavoriteHeaderRow({ + title, + editingTitle, + isEditing, + onPress, +}: { + title: string + editingTitle: string + isEditing: boolean + onPress: () => void +}): JSX.Element { + const { t } = useTranslation() + return ( + + + {isEditing ? editingTitle : title} + + {!isEditing ? ( + + + + ) : ( + + + {t('common.button.done')} + + + )} + + ) +} diff --git a/apps/mobile/src/components/explore/FavoriteTokenCard.tsx b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx new file mode 100644 index 0000000..4ba438b --- /dev/null +++ b/apps/mobile/src/components/explore/FavoriteTokenCard.tsx @@ -0,0 +1,156 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { memo, useCallback } from 'react' +import { ViewProps } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { FadeIn, SharedValue } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import RemoveButton from 'src/components/explore/RemoveButton' +import { useAnimatedCardDragStyle, useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { Loader } from 'src/components/loading' +import { disableOnPress } from 'src/utils/disableOnPress' +import { usePollOnFocusOnly } from 'src/utils/hooks' +import { AnimatedFlex, AnimatedTouchableArea, Flex, Text } from 'ui/src' +import { borderRadii, imageSizes } from 'ui/src/theme' +import { useFavoriteTokenCardQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NumberType } from 'utilities/src/format/types' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import { RelativeChange } from 'wallet/src/components/text/RelativeChange' +import { ChainId } from 'wallet/src/constants/chains' +import { PollingInterval } from 'wallet/src/constants/misc' +import { isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { removeFavoriteToken } from 'wallet/src/features/favorites/slice' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { SectionName } from 'wallet/src/telemetry/constants' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' + +export const FAVORITE_TOKEN_CARD_LOADER_HEIGHT = 114 + +type FavoriteTokenCardProps = { + currencyId: string + isEditing?: boolean + isTouched: SharedValue + dragActivationProgress: SharedValue + setIsEditing: (update: boolean) => void +} & ViewProps + +function FavoriteTokenCard({ + currencyId, + isEditing, + isTouched, + dragActivationProgress, + setIsEditing, + ...rest +}: FavoriteTokenCardProps): JSX.Element { + const dispatch = useAppDispatch() + const tokenDetailsNavigation = useTokenDetailsNavigation() + const { convertFiatAmountFormatted } = useLocalizationContext() + + const { data, networkStatus, startPolling, stopPolling } = useFavoriteTokenCardQuery({ + variables: currencyIdToContractInput(currencyId), + // Rely on cache for fast favoriting UX, and poll for updates. + fetchPolicy: 'cache-first', + returnPartialData: true, + }) + + usePollOnFocusOnly(startPolling, stopPolling, PollingInterval.Fast) + + const token = data?.token + + // Mirror behavior in top tokens list, use first chain the token is on for the symbol + const chainId = fromGraphQLChain(token?.chain) ?? ChainId.Mainnet + + const price = convertFiatAmountFormatted( + token?.project?.markets?.[0]?.price?.value, + NumberType.FiatTokenPrice + ) + const pricePercentChange = token?.project?.markets?.[0]?.pricePercentChange24h?.value + + const onRemove = useCallback(() => { + if (currencyId) { + dispatch(removeFavoriteToken({ currencyId })) + } + }, [currencyId, dispatch]) + + const onEditFavorites = useCallback(() => { + setIsEditing(true) + }, [setIsEditing]) + + const { menuActions, onContextMenuPress } = useExploreTokenContextMenu({ + chainId, + currencyId, + analyticsSection: SectionName.ExploreFavoriteTokensSection, + onEditFavorites, + }) + + const onPress = (): void => { + if (isEditing || !currencyId) { + return + } + tokenDetailsNavigation.preload(currencyId) + tokenDetailsNavigation.navigate(currencyId) + } + + const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress) + + if (isNonPollingRequestInFlight(networkStatus)) { + return + } + + return ( + + + + + + + + + {getSymbolDisplayText(token?.symbol)} + + + + + + {price} + + + + + + + + + ) +} + +export default memo(FavoriteTokenCard) diff --git a/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx new file mode 100644 index 0000000..a2c22c6 --- /dev/null +++ b/apps/mobile/src/components/explore/FavoriteTokensGrid.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' +import FavoriteTokenCard, { + FAVORITE_TOKEN_CARD_LOADER_HEIGHT, +} from 'src/components/explore/FavoriteTokenCard' +import { Loader } from 'src/components/loading' +import { + AutoScrollProps, + SortableGrid, + SortableGridChangeEvent, + SortableGridRenderItem, +} from 'src/components/sortableGrid' +import { AnimatedFlex, Flex } from 'ui/src' +import { selectFavoriteTokens } from 'wallet/src/features/favorites/selectors' +import { setFavoriteTokens } from 'wallet/src/features/favorites/slice' +import { useAppDispatch } from 'wallet/src/state' + +const NUM_COLUMNS = 2 +const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } + +type FavoriteTokensGridProps = AutoScrollProps & { + showLoading: boolean +} + +/** Renders the favorite tokens section on the Explore tab */ +export function FavoriteTokensGrid({ + showLoading, + ...rest +}: FavoriteTokensGridProps): JSX.Element | null { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const [isEditing, setIsEditing] = useState(false) + const isTokenDragged = useSharedValue(false) + const favoriteCurrencyIds = useAppSelector(selectFavoriteTokens) + + // Reset edit mode when there are no favorite tokens + useEffect(() => { + if (favoriteCurrencyIds.length === 0) { + setIsEditing(false) + } + }, [favoriteCurrencyIds.length]) + + const handleOrderChange = useCallback( + ({ data }: SortableGridChangeEvent) => { + dispatch(setFavoriteTokens({ currencyIds: data })) + }, + [dispatch] + ) + + const renderItem = useCallback>( + ({ item: currencyId, isTouched, dragActivationProgress }): JSX.Element => ( + + ), + [isEditing] + ) + + const animatedStyle = useAnimatedStyle(() => ({ + zIndex: isTokenDragged.value ? 1 : 0, + })) + + return ( + + setIsEditing(!isEditing)} + /> + {showLoading ? ( + + ) : ( + { + isTokenDragged.value = false + }} + onDragStart={(): void => { + isTokenDragged.value = true + }} + /> + )} + + ) +} + +function FavoriteTokensGridLoader(): JSX.Element { + return ( + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/explore/FavoriteWalletCard.tsx b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx new file mode 100644 index 0000000..e8561db --- /dev/null +++ b/apps/mobile/src/components/explore/FavoriteWalletCard.tsx @@ -0,0 +1,116 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import { memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { ViewProps } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { SharedValue } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' +import { useAnimatedCardDragStyle } from 'src/components/explore/hooks' +import RemoveButton from 'src/components/explore/RemoveButton' +import { disableOnPress } from 'src/utils/disableOnPress' +import { AnimatedFlex, Flex, TouchableArea } from 'ui/src' +import { borderRadii, iconSizes } from 'ui/src/theme' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { removeWatchedAddress } from 'wallet/src/features/favorites/slice' +import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' + +type FavoriteWalletCardProps = { + address: Address + isEditing?: boolean + isTouched: SharedValue + dragActivationProgress: SharedValue + setIsEditing: (update: boolean) => void +} & ViewProps + +function FavoriteWalletCard({ + address, + isEditing, + isTouched, + dragActivationProgress, + setIsEditing, + ...rest +}: FavoriteWalletCardProps): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { preload, navigate } = useEagerExternalProfileNavigation() + + const displayName = useDisplayName(address) + const { avatar } = useAvatar(address) + + const icon = useMemo(() => { + return + }, [address, avatar]) + + const onRemove = useCallback(() => { + dispatch(removeWatchedAddress({ address })) + }, [address, dispatch]) + + /// Options for long press context menu + const menuActions = useMemo(() => { + return [ + { title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' }, + { title: t('explore.wallets.favorite.action.edit'), systemIcon: 'square.and.pencil' }, + ] + }, [t]) + + const animatedDragStyle = useAnimatedCardDragStyle(isTouched, dragActivationProgress) + + return ( + + { + // Emitted index based on order of menu action array + // remove favorite action + if (e.nativeEvent.index === 0) { + onRemove() + } + // Edit mode toggle action + if (e.nativeEvent.index === 1) { + setIsEditing(true) + } + }} + {...rest}> + { + navigate(address) + }} + onPressIn={async (): Promise => { + await preload(address) + }}> + + + + {icon} + + + + + + + + + ) +} + +export default memo(FavoriteWalletCard) diff --git a/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx new file mode 100644 index 0000000..b67b037 --- /dev/null +++ b/apps/mobile/src/components/explore/FavoriteWalletsGrid.tsx @@ -0,0 +1,113 @@ +import { default as React, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { FadeIn, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { FavoriteHeaderRow } from 'src/components/explore/FavoriteHeaderRow' +import FavoriteWalletCard from 'src/components/explore/FavoriteWalletCard' +import { Loader } from 'src/components/loading' +import { + AutoScrollProps, + SortableGrid, + SortableGridChangeEvent, + SortableGridRenderItem, +} from 'src/components/sortableGrid' +import { AnimatedFlex, Flex } from 'ui/src' +import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' +import { setFavoriteWallets } from 'wallet/src/features/favorites/slice' +import { useAppDispatch } from 'wallet/src/state' + +const NUM_COLUMNS = 2 +const ITEM_FLEX = { flex: 1 / NUM_COLUMNS } + +type FavoriteWalletsGridProps = AutoScrollProps & { + showLoading: boolean +} + +/** Renders the favorite wallets section on the Explore tab */ +export function FavoriteWalletsGrid({ + showLoading, + ...rest +}: FavoriteWalletsGridProps): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const [isEditing, setIsEditing] = useState(false) + const isTokenDragged = useSharedValue(false) + const watchedWalletsSet = useAppSelector(selectWatchedAddressSet) + const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet]) + + // Reset edit mode when there are no favorite wallets + useEffect(() => { + if (watchedWalletsSet.size === 0) { + setIsEditing(false) + } + }, [watchedWalletsSet.size]) + + const handleOrderChange = useCallback( + ({ data }: SortableGridChangeEvent) => { + dispatch(setFavoriteWallets({ addresses: data })) + }, + [dispatch] + ) + + const renderItem = useCallback>( + ({ item: address, isTouched, dragActivationProgress }): JSX.Element => ( + + ), + [isEditing] + ) + + const animatedStyle = useAnimatedStyle(() => ({ + zIndex: isTokenDragged.value ? 1 : 0, + })) + + return ( + + setIsEditing(!isEditing)} + /> + {showLoading ? ( + + ) : ( + { + isTokenDragged.value = false + }} + onDragStart={(): void => { + isTokenDragged.value = true + }} + /> + )} + + ) +} + +function FavoriteWalletsGridLoader(): JSX.Element { + return ( + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/explore/RemoveButton.tsx b/apps/mobile/src/components/explore/RemoveButton.tsx new file mode 100644 index 0000000..5d0d24c --- /dev/null +++ b/apps/mobile/src/components/explore/RemoveButton.tsx @@ -0,0 +1,29 @@ +import { useAnimatedStyle, withTiming } from 'react-native-reanimated' +import { AnimatedTouchableArea, Flex, TouchableAreaProps } from 'ui/src' +import { imageSizes } from 'ui/src/theme' + +type RemoveButtonProps = TouchableAreaProps & { + visible?: boolean +} + +export default function RemoveButton({ visible = true, ...rest }: RemoveButtonProps): JSX.Element { + const animatedVisibilityStyle = useAnimatedStyle(() => ({ + opacity: visible ? withTiming(1) : withTiming(0), + })) + + return ( + + + + ) +} diff --git a/apps/mobile/src/components/explore/SortButton.tsx b/apps/mobile/src/components/explore/SortButton.tsx new file mode 100644 index 0000000..7f8c770 --- /dev/null +++ b/apps/mobile/src/components/explore/SortButton.tsx @@ -0,0 +1,101 @@ +import React, { memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch } from 'src/app/hooks' +import { + getTokensOrderByMenuLabel, + getTokensOrderBySelectedLabel, +} from 'src/features/explore/utils' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { logger } from 'utilities/src/logger/logger' +import { setTokensOrderBy } from 'wallet/src/features/wallet/slice' +import { ClientTokensOrderBy, TokensOrderBy } from 'wallet/src/features/wallet/types' +interface FilterGroupProps { + orderBy: TokensOrderBy +} + +function _SortButton({ orderBy }: FilterGroupProps): JSX.Element { + const isDarkMode = useIsDarkMode() + const dispatch = useAppDispatch() + const { t } = useTranslation() + + const menuActions = useMemo(() => { + return [ + { + title: getTokensOrderByMenuLabel(TokenSortableField.Volume, t), + systemIcon: orderBy === TokenSortableField.Volume ? 'checkmark' : '', + orderBy: TokenSortableField.Volume, + }, + { + title: getTokensOrderByMenuLabel(TokenSortableField.TotalValueLocked, t), + systemIcon: orderBy === TokenSortableField.TotalValueLocked ? 'checkmark' : '', + orderBy: TokenSortableField.TotalValueLocked, + }, + { + title: getTokensOrderByMenuLabel(TokenSortableField.MarketCap, t), + systemIcon: orderBy === TokenSortableField.MarketCap ? 'checkmark' : '', + orderBy: TokenSortableField.MarketCap, + }, + { + title: getTokensOrderByMenuLabel(ClientTokensOrderBy.PriceChangePercentage24hDesc, t), + systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hDesc ? 'checkmark' : '', + orderBy: ClientTokensOrderBy.PriceChangePercentage24hDesc, + }, + { + title: getTokensOrderByMenuLabel(ClientTokensOrderBy.PriceChangePercentage24hAsc, t), + systemIcon: orderBy === ClientTokensOrderBy.PriceChangePercentage24hAsc ? 'checkmark' : '', + orderBy: ClientTokensOrderBy.PriceChangePercentage24hAsc, + }, + ] + }, [t, orderBy]) + + return ( + { + const selectedMenuAction = menuActions[e.nativeEvent.index] + // Handle switching selected sort option + if (!selectedMenuAction) { + logger.error(new Error('Unexpected context menu index selected'), { + tags: { file: 'SortButton', function: 'SortButtonContextMenu:onPress' }, + }) + return + } + + dispatch(setTokensOrderBy({ newTokensOrderBy: selectedMenuAction.orderBy })) + sendMobileAnalyticsEvent(MobileEventName.ExploreFilterSelected, { + filter_type: selectedMenuAction.orderBy, + }) + }}> + + + {orderBy === TokenSortableField.Volume || orderBy === TokenSortableField.TotalValueLocked} + + {getTokensOrderBySelectedLabel(orderBy, t)} + + + + + + ) +} + +export const SortButton = memo(_SortButton) diff --git a/apps/mobile/src/components/explore/TokenItem.tsx b/apps/mobile/src/components/explore/TokenItem.tsx new file mode 100644 index 0000000..8fc5bf5 --- /dev/null +++ b/apps/mobile/src/components/explore/TokenItem.tsx @@ -0,0 +1,145 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { memo } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { TokenMetadata } from 'src/components/tokens/TokenMetadata' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { AnimatedFlex, Flex, Text, TouchableArea } from 'ui/src' +import { NumberType } from 'utilities/src/format/types' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import { RelativeChange } from 'wallet/src/components/text/RelativeChange' +import { ChainId } from 'wallet/src/constants/chains' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { TokenMetadataDisplayType } from 'wallet/src/features/wallet/types' +import { SectionName } from 'wallet/src/telemetry/constants' +import { + buildCurrencyId, + buildNativeCurrencyId, + currencyIdToAddress, + currencyIdToChain, +} from 'wallet/src/utils/currencyId' + +export type TokenItemData = { + name: string + logoUrl: string + chainId: ChainId + address: Address | null + symbol: string + price?: number + marketCap?: number + pricePercentChange24h?: number + volume24h?: number + totalValueLocked?: number +} + +interface TokenItemProps { + tokenItemData: TokenItemData + index: number + metadataDisplayType?: TokenMetadataDisplayType +} + +export const TokenItem = memo(function _TokenItem({ + tokenItemData, + index, + metadataDisplayType, +}: TokenItemProps) { + const { t } = useTranslation() + const tokenDetailsNavigation = useTokenDetailsNavigation() + const { convertFiatAmountFormatted } = useLocalizationContext() + + const { + name, + logoUrl, + chainId, + address, + symbol, + price, + marketCap, + pricePercentChange24h, + volume24h, + totalValueLocked, + } = tokenItemData + const _currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId) + const marketCapFormatted = convertFiatAmountFormatted(marketCap, NumberType.FiatTokenDetails) + const volume24hFormatted = convertFiatAmountFormatted(volume24h, NumberType.FiatTokenDetails) + const totalValueLockedFormatted = convertFiatAmountFormatted( + totalValueLocked, + NumberType.FiatTokenDetails + ) + + const getMetadataSubtitle = (): string | undefined => { + switch (metadataDisplayType) { + case TokenMetadataDisplayType.MarketCap: + return t('explore.tokens.metadata.marketCap', { number: marketCapFormatted }) + case TokenMetadataDisplayType.Volume: + return t('explore.tokens.metadata.volume', { number: volume24hFormatted }) + case TokenMetadataDisplayType.TVL: + return t('explore.tokens.metadata.totalValueLocked', { + number: totalValueLockedFormatted, + }) + case TokenMetadataDisplayType.Symbol: + return symbol + } + } + + const onPress = (): void => { + tokenDetailsNavigation.preload(_currencyId) + tokenDetailsNavigation.navigate(_currencyId) + sendMobileAnalyticsEvent(MobileEventName.ExploreTokenItemSelected, { + address: currencyIdToAddress(_currencyId), + chain: currencyIdToChain(_currencyId) as number, + name, + position: index + 1, + }) + } + + const { menuActions, onContextMenuPress } = useExploreTokenContextMenu({ + chainId, + currencyId: _currencyId, + analyticsSection: SectionName.ExploreTopTokensSection, + }) + + return ( + + + + + {index !== undefined && ( + + + {index + 1} + + + )} + + + + + {name} + + + {getMetadataSubtitle()} + + + + + + {convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)} + + + + + + + + ) +}) diff --git a/apps/mobile/src/components/explore/hooks.test.ts b/apps/mobile/src/components/explore/hooks.test.ts new file mode 100644 index 0000000..46153e7 --- /dev/null +++ b/apps/mobile/src/components/explore/hooks.test.ts @@ -0,0 +1,271 @@ +import { NativeSyntheticEvent, Share } from 'react-native' +import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { act } from 'react-test-renderer' +import configureMockStore from 'redux-mock-store' +import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { renderHookWithProviders } from 'src/test/render' +import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { FavoritesState } from 'wallet/src/features/favorites/slice' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { SectionName } from 'wallet/src/telemetry/constants' +import { SAMPLE_SEED_ADDRESS_1 } from 'wallet/src/test/fixtures/constants' + +const tokenId = SAMPLE_SEED_ADDRESS_1 +const currencyId = `1-${tokenId}` + +const resolvers: Resolvers = { + Token: { + id: () => tokenId, + }, +} + +const mockStore = configureMockStore() + +describe(useExploreTokenContextMenu, () => { + const tokenMenuParams = { + currencyId, + chainId: 1, + analyticsSection: SectionName.CurrencyInputPanel, + } + + describe('editing favorite tokens', () => { + it('renders proper context menu items when onEditFavorites is not provided', async () => { + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu(tokenMenuParams), + { resolvers } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + expect(result.current.menuActions).toEqual([ + expect.objectContaining({ + title: 'Favorite token', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Swap', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Receive', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Share', + onPress: expect.any(Function), + }), + ]) + }) + + it('renders proper context menu items when onEditFavorites is provided', async () => { + const onEditFavorites = jest.fn() + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu({ ...tokenMenuParams, onEditFavorites }), + { resolvers } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + expect(result.current.menuActions).toEqual([ + expect.objectContaining({ + title: 'Favorite token', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Edit favorites', + onPress: onEditFavorites, + }), + expect.objectContaining({ + title: 'Swap', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Receive', + onPress: expect.any(Function), + }), + ]) + }) + + it('calls onEditFavorites when edit favorites is pressed', async () => { + const onEditFavorites = jest.fn() + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu({ ...tokenMenuParams, onEditFavorites }), + { resolvers } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + const editFavoritesActionIndex = result.current.menuActions.findIndex( + (action: ContextMenuAction) => action.title === 'Edit favorites' + ) + result.current.onContextMenuPress({ + nativeEvent: { index: editFavoritesActionIndex }, + } as NativeSyntheticEvent) + + expect(onEditFavorites).toHaveBeenCalledTimes(1) + }) + }) + + describe('adding / removing favorite tokens', () => { + it('renders proper context menu items when token is favorited', async () => { + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu(tokenMenuParams), + { + preloadedState: { + favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] } as FavoritesState, + }, + resolvers, + } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + expect(result.current.menuActions).toEqual([ + expect.objectContaining({ + title: 'Remove favorite', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Swap', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Receive', + onPress: expect.any(Function), + }), + expect.objectContaining({ + title: 'Share', + onPress: expect.any(Function), + }), + ]) + }) + + it("dispatches add to favorites redux action when 'Favorite token' is pressed", async () => { + const store = mockStore({ favorites: { tokens: [] }, appearance: { theme: 'system' } }) + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu(tokenMenuParams), + { resolvers, store } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + const favoriteTokenActionIndex = result.current.menuActions.findIndex( + (action: ContextMenuAction) => action.title === 'Favorite token' + ) + result.current.onContextMenuPress({ + nativeEvent: { index: favoriteTokenActionIndex }, + } as NativeSyntheticEvent) + + const dispatchedActions = store.getActions() + expect(dispatchedActions).toEqual([ + { + type: 'favorites/addFavoriteToken', + payload: { currencyId: tokenMenuParams.currencyId }, + }, + ]) + }) + + it("dispatches remove from favorites redux action when 'Remove favorite' is pressed", async () => { + const store = mockStore({ + favorites: { tokens: [tokenMenuParams.currencyId.toLowerCase()] }, + appearance: { theme: 'system' }, + }) + const { result } = renderHookWithProviders( + () => useExploreTokenContextMenu(tokenMenuParams), + { resolvers, store } + ) + + await act(async () => { + // Wait for the token query to resolve + }) + + const removeFavoriteTokenActionIndex = result.current.menuActions.findIndex( + (action: ContextMenuAction) => action.title === 'Remove favorite' + ) + result.current.onContextMenuPress({ + nativeEvent: { index: removeFavoriteTokenActionIndex }, + } as NativeSyntheticEvent) + + const dispatchedActions = store.getActions() + expect(dispatchedActions).toEqual([ + { + type: 'favorites/removeFavoriteToken', + payload: { currencyId: tokenMenuParams.currencyId }, + }, + ]) + }) + }) + + it('dispatches swap redux action when swap is pressed', async () => { + const store = mockStore({ + favorites: { tokens: [] }, + selectedAppearanceSettings: { theme: 'system' }, + }) + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { + store, + resolvers, + }) + + await act(async () => { + // Wait for the token query to resolve + }) + + const swapActionIndex = result.current.menuActions.findIndex( + (action: ContextMenuAction) => action.title === 'Swap' + ) + result.current.onContextMenuPress({ + nativeEvent: { index: swapActionIndex }, + } as NativeSyntheticEvent) + + const dispatchedActions = store.getActions() + expect(dispatchedActions).toEqual([ + { + type: 'modals/openModal', + payload: { + name: 'swap-modal', + initialState: { + exactAmountToken: '0', + exactCurrencyField: 'input', + [CurrencyField.INPUT]: null, + [CurrencyField.OUTPUT]: { + chainId: 1, + address: tokenId, + type: 'currency', + }, + }, + }, + }, + ]) + }) + + it('opens share modal when share is pressed', async () => { + const { result } = renderHookWithProviders(() => useExploreTokenContextMenu(tokenMenuParams), { + resolvers, + }) + + await act(async () => { + // Wait for the token query to resolve + }) + + jest.spyOn(Share, 'share') + + const shareActionIndex = result.current.menuActions.findIndex( + (action: ContextMenuAction) => action.title === 'Share' + ) + result.current.onContextMenuPress({ + nativeEvent: { index: shareActionIndex }, + } as NativeSyntheticEvent) + + expect(Share.share).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/mobile/src/components/explore/hooks.ts b/apps/mobile/src/components/explore/hooks.ts new file mode 100644 index 0000000..867313d --- /dev/null +++ b/apps/mobile/src/components/explore/hooks.ts @@ -0,0 +1,201 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, Share, ViewStyle } from 'react-native' +import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { + AnimateStyle, + SharedValue, + interpolate, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, +} from 'react-native-reanimated' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { useSelectHasTokenFavorited, useToggleFavoriteCallback } from 'src/features/favorites/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { logger } from 'utilities/src/logger/logger' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { useAppDispatch } from 'wallet/src/state' +import { ElementName, ModalName, SectionNameType } from 'wallet/src/telemetry/constants' +import { CurrencyId, currencyIdToAddress } from 'wallet/src/utils/currencyId' +import { getTokenUrl } from 'wallet/src/utils/linking' + +interface TokenMenuParams { + currencyId: CurrencyId + chainId: ChainId + analyticsSection: SectionNameType + // token, which are in favorite section would have it defined + onEditFavorites?: () => void +} + +// Provide context menu related data for token +export function useExploreTokenContextMenu({ + currencyId, + chainId, + analyticsSection, + onEditFavorites, +}: TokenMenuParams): { + menuActions: Array void }> + onContextMenuPress: (e: NativeSyntheticEvent) => void +} { + const { t } = useTranslation() + const isFavorited = useSelectHasTokenFavorited(currencyId) + const dispatch = useAppDispatch() + + // `address` is undefined for native currencies, so we want to extract it from + // currencyId, where we have hardcoded addresses for native currencies + const currencyAddress = currencyIdToAddress(currencyId) + + const onPressReceive = useCallback( + () => + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ), + [dispatch] + ) + + const onPressShare = useCallback(async () => { + const tokenUrl = getTokenUrl(currencyId) + if (!tokenUrl) { + return + } + try { + await Share.share({ + message: tokenUrl, + }) + sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, { + entity: ShareableEntity.Token, + url: tokenUrl, + }) + } catch (error) { + logger.error(error, { tags: { file: 'balances/hooks.ts', function: 'onPressShare' } }) + } + }, [currencyId]) + + const toggleFavoriteToken = useToggleFavoriteCallback(currencyId, isFavorited) + + const onPressSwap = useCallback(() => { + const swapFormState: TransactionState = { + exactCurrencyField: CurrencyField.INPUT, + exactAmountToken: '0', + [CurrencyField.INPUT]: null, + [CurrencyField.OUTPUT]: { + chainId, + address: currencyAddress, + type: AssetType.Currency, + }, + } + dispatch(openModal({ name: ModalName.Swap, initialState: swapFormState })) + sendMobileAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: ElementName.Swap, + section: analyticsSection, + }) + }, [analyticsSection, chainId, currencyAddress, dispatch]) + + const onPressToggleFavorite = useCallback(() => { + toggleFavoriteToken() + }, [toggleFavoriteToken]) + + const menuActions = useMemo( + () => [ + { + title: isFavorited + ? t('explore.tokens.favorite.action.remove') + : t('explore.tokens.favorite.action.add'), + systemIcon: isFavorited ? 'heart.fill' : 'heart', + onPress: onPressToggleFavorite, + }, + ...(onEditFavorites + ? [ + { + title: t('explore.tokens.favorite.action.edit'), + systemIcon: 'square.and.pencil', + onPress: onEditFavorites, + }, + ] + : []), + { title: t('common.button.swap'), systemIcon: 'arrow.2.squarepath', onPress: onPressSwap }, + { + title: t('common.button.receive'), + systemIcon: 'qrcode', + onPress: onPressReceive, + }, + ...(!onEditFavorites + ? [ + { + title: t('common.button.share'), + systemIcon: 'square.and.arrow.up', + onPress: onPressShare, + }, + ] + : []), + ], + [ + isFavorited, + t, + onPressToggleFavorite, + onEditFavorites, + onPressSwap, + onPressReceive, + onPressShare, + ] + ) + + const onContextMenuPress = useCallback( + async (e: NativeSyntheticEvent): Promise => { + await menuActions[e.nativeEvent.index]?.onPress?.() + }, + [menuActions] + ) + + return { menuActions, onContextMenuPress } +} + +export function useAnimatedCardDragStyle( + isTouched: SharedValue, + dragActivationProgress: SharedValue +): AnimateStyle { + const wasTouched = useSharedValue(false) + const dragAnimationProgress = useSharedValue(0) + + useAnimatedReaction( + () => dragActivationProgress.value, + (activationProgress, prev) => { + const prevActivationProgress = prev ?? 0 + // If the activation progress is increasing (the user is touching one of the cards) + if (activationProgress > prevActivationProgress) { + if (isTouched.value) { + // If the current card is the one being touched, reset the animation progress + wasTouched.value = true + dragAnimationProgress.value = 0 + } else { + // Otherwise, animate the card + wasTouched.value = false + dragAnimationProgress.value = activationProgress + } + } + // If the activation progress is decreasing (the user is no longer touching one of the cards) + else { + if (isTouched.value || wasTouched.value) { + // If the current card is the one that was being touched, reset the animation progress + dragAnimationProgress.value = 0 + } else { + // Otherwise, animate the card + dragAnimationProgress.value = activationProgress + } + } + } + ) + + return useAnimatedStyle(() => ({ + opacity: interpolate(dragAnimationProgress.value, [0, 1], [1, 0.5]), + })) +} diff --git a/apps/mobile/src/components/explore/search/SearchEmptySection.tsx b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx new file mode 100644 index 0000000..3e2fc41 --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchEmptySection.tsx @@ -0,0 +1,131 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { SearchPopularNFTCollections } from 'src/components/explore/search/SearchPopularNFTCollections' +import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens' +import { renderSearchItem } from 'src/components/explore/search/SearchResultsSection' +import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' +import { AnimatedFlex, Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import ClockIcon from 'ui/src/assets/icons/clock.svg' +import TrendArrowIcon from 'ui/src/assets/icons/trend-up.svg' +import { iconSizes } from 'ui/src/theme' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { clearSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { + SearchResult, + SearchResultType, + WalletSearchResult, +} from 'wallet/src/features/search/SearchResult' +import { selectSearchHistory } from 'wallet/src/features/search/selectSearchHistory' + +export const SUGGESTED_WALLETS: WalletSearchResult[] = [ + { + type: SearchResultType.ENSAddress, + address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', + ensName: 'vitalik.eth', + }, + { + type: SearchResultType.ENSAddress, + address: '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3', + ensName: 'hayden.eth', + }, +] + +export function SearchEmptySection(): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const searchHistory = useAppSelector(selectSearchHistory) + const unitagFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) + + const onPressClearSearchHistory = (): void => { + dispatch(clearSearchHistory()) + } + + const modifiedHistory: SearchResult[] = useMemo( + () => + searchHistory.map((historyItem: SearchResult) => { + if (!unitagFeatureFlagEnabled && historyItem.type === SearchResultType.Unitag) { + return { + type: SearchResultType.WalletByAddress, + address: historyItem.address, + searchId: historyItem.searchId, + } + } else { + return historyItem + } + }), + [searchHistory, unitagFeatureFlagEnabled] + ) + + // Show search history (if applicable), trending tokens, and wallets + return ( + + {searchHistory.length > 0 && ( + + + } + title={t('explore.search.section.recent')} + /> + + + {t('explore.search.action.clear')} + + + + } + data={modifiedHistory} + renderItem={(props): JSX.Element | null => + renderSearchItem({ ...props, searchContext: { isHistory: true } }) + } + /> + + )} + + } title={t('explore.search.section.popularTokens')} /> + + + + } title={t('explore.search.section.popularNFT')} /> + + + } + title={t('explore.search.section.suggestedWallets')} + /> + } + data={SUGGESTED_WALLETS} + keyExtractor={walletKey} + renderItem={renderSearchItem} + /> + + ) +} + +const walletKey = (wallet: WalletSearchResult): string => { + return wallet.address +} + +export const TrendIcon = (): JSX.Element => { + const colors = useSporeColors() + return +} + +export const RecentIcon = (): JSX.Element => { + const colors = useSporeColors() + return ( + + ) +} diff --git a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql new file mode 100644 index 0000000..6145531 --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.graphql @@ -0,0 +1,21 @@ +query SearchPopularNFTCollections { + topCollections(chains: [ETHEREUM], orderBy: VOLUME, duration: DAY, first: 2) { + edges { + node { + id + name + collectionId + isVerified + nftContracts { + id + chain + address + } + image { + id + url + } + } + } + } +} diff --git a/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx new file mode 100644 index 0000000..2428f98 --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchPopularNFTCollections.tsx @@ -0,0 +1,57 @@ +import React, { useMemo } from 'react' +import { FlatList, ListRenderItemInfo } from 'react-native' +import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem' +import { + getSearchResultId, + gqlNFTToNFTCollectionSearchResult, +} from 'src/components/explore/search/utils' +import { Inset, Loader } from 'ui/src' +import { useSearchPopularNftCollectionsQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { + NFTCollectionSearchResult, + SearchResultType, +} from 'wallet/src/features/search/SearchResult' + +function isNFTCollectionSearchResult( + result: NFTCollectionSearchResult | null +): result is NFTCollectionSearchResult { + return (result as NFTCollectionSearchResult).type === SearchResultType.NFTCollection +} + +export function SearchPopularNFTCollections(): JSX.Element { + // Load popular NFTs by top trading volume + const { data, loading } = useSearchPopularNftCollectionsQuery() + + const formattedItems = useMemo(() => { + if (!data?.topCollections?.edges) { + return + } + + const searchResults = data.topCollections.edges.map(({ node }) => + gqlNFTToNFTCollectionSearchResult(node) + ) + return searchResults.filter(isNFTCollectionSearchResult) + }, [data]) + + if (loading) { + return ( + + + + ) + } + + return ( + + ) +} + +const renderNFTCollectionItem = ({ + item, +}: ListRenderItemInfo): JSX.Element => ( + +) diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.graphql b/apps/mobile/src/components/explore/search/SearchPopularTokens.graphql new file mode 100644 index 0000000..7897abd --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.graphql @@ -0,0 +1,30 @@ +query SearchPopularTokens { + topTokens(chain: ETHEREUM, orderBy: VOLUME, page: 1, pageSize: 2) { + id + address + chain + symbol + decimals + project { + id + name + logoUrl + safetyLevel + } + } + # `topTokens` returns WETH rather than ETH + # here we retrieve ETH information to swap out in the UI + eth: tokens(contracts: [{ address: null, chain: ETHEREUM }]) { + id + address + chain + symbol + decimals + project { + id + name + logoUrl + safetyLevel + } + } +} diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx new file mode 100644 index 0000000..2f05238 --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.test.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { SearchPopularTokens } from 'src/components/explore/search/SearchPopularTokens' +import { render, screen } from 'src/test/test-utils' +import { ethToken, usdcToken, wethToken } from 'wallet/src/test/fixtures' +import { queryResolvers } from 'wallet/src/test/utils' + +const { resolvers } = queryResolvers({ + topTokens: () => [wethToken(), usdcToken()], + tokens: () => [ethToken({ address: null })], +}) + +describe(SearchPopularTokens, () => { + it('renders without error', async () => { + const tree = render(, { resolvers }) + + // Loading should show Token loader + expect(screen.getAllByText('Token Full Name')).toBeDefined() + expect(tree.toJSON()).toMatchSnapshot() + + // Success where WETH result in topTokens is replaced by ETH + expect(await screen.findByText('ETH')).toBeDefined() + expect(screen.getByText('USDC')).toBeDefined() + expect(tree.toJSON()).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx new file mode 100644 index 0000000..9701e28 --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchPopularTokens.tsx @@ -0,0 +1,56 @@ +import React, { useMemo } from 'react' +import { FlatList, ListRenderItemInfo } from 'react-native' +import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' +import { getSearchResultId } from 'src/components/explore/search/utils' +import { Inset, Loader } from 'ui/src' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' +import { TopToken, usePopularTokens } from 'wallet/src/features/tokens/hooks' + +function gqlTokenToTokenSearchResult(token: Maybe): TokenSearchResult | null { + if (!token || !token.project) { + return null + } + + const { chain, address, symbol, project } = token + const { name } = project + const chainId = fromGraphQLChain(chain) + if (!chainId || !symbol || !name) { + return null + } + + return { + type: SearchResultType.Token, + chainId, + address: address ?? null, + name, + symbol, + logoUrl: project?.logoUrl ?? null, + safetyLevel: project?.safetyLevel ?? null, + } +} + +export function SearchPopularTokens(): JSX.Element { + const { popularTokens, loading } = usePopularTokens() + const tokens = useMemo( + () => + popularTokens + ?.map(gqlTokenToTokenSearchResult) + .filter((t): t is TokenSearchResult => Boolean(t)), + [popularTokens] + ) + + if (loading) { + return ( + + + + ) + } + + return +} + +const renderTokenItem = ({ item }: ListRenderItemInfo): JSX.Element => ( + +) diff --git a/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx new file mode 100644 index 0000000..326559d --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchResultsLoader.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' +import { AnimatedFlex, Flex, Loader } from 'ui/src' + +export const SearchResultsLoader = (): JSX.Element => { + const { t } = useTranslation() + return ( + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/SearchResultsSection.tsx b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx new file mode 100644 index 0000000..4a5e76c --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchResultsSection.tsx @@ -0,0 +1,265 @@ +import React, { useCallback, useMemo } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { FlatList, ListRenderItemInfo } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { SearchResultsLoader } from 'src/components/explore/search/SearchResultsLoader' +import { SectionHeaderText } from 'src/components/explore/search/SearchSectionHeader' +import { useWalletSearchResults } from 'src/components/explore/search/hooks' +import { SearchENSAddressItem } from 'src/components/explore/search/items/SearchENSAddressItem' +import { SearchEtherscanItem } from 'src/components/explore/search/items/SearchEtherscanItem' +import { SearchNFTCollectionItem } from 'src/components/explore/search/items/SearchNFTCollectionItem' +import { SearchTokenItem } from 'src/components/explore/search/items/SearchTokenItem' +import { SearchUnitagItem } from 'src/components/explore/search/items/SearchUnitagItem' +import { SearchWalletByAddressItem } from 'src/components/explore/search/items/SearchWalletByAddressItem' +import { + formatNFTCollectionSearchResults, + formatTokenSearchResults, + getSearchResultId, +} from 'src/components/explore/search/utils' +import { AnimatedFlex, Flex, Text } from 'ui/src' +import { + SafetyLevel, + useExploreSearchQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { logger } from 'utilities/src/logger/logger' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { + NFTCollectionSearchResult, + SearchResultType, + TokenSearchResult, +} from 'wallet/src/features/search/SearchResult' +import i18n from 'wallet/src/i18n/i18n' +import { getValidAddress } from 'wallet/src/utils/addresses' +import { SEARCH_RESULT_HEADER_KEY } from './constants' +import { SearchResultOrHeader } from './types' + +const WalletHeaderItem: SearchResultOrHeader = { + type: SEARCH_RESULT_HEADER_KEY, + title: i18n.t('explore.search.section.wallets'), +} +const TokenHeaderItem: SearchResultOrHeader = { + type: SEARCH_RESULT_HEADER_KEY, + title: i18n.t('explore.search.section.tokens'), +} +const NFTHeaderItem: SearchResultOrHeader = { + type: SEARCH_RESULT_HEADER_KEY, + title: i18n.t('explore.search.section.nft'), +} +const EtherscanHeaderItem: SearchResultOrHeader = { + type: SEARCH_RESULT_HEADER_KEY, + title: i18n.t('explore.search.action.viewEtherscan', { + blockExplorerName: CHAIN_INFO[ChainId.Mainnet].explorer.name, + }), +} + +export function SearchResultsSection({ searchQuery }: { searchQuery: string }): JSX.Element { + const { t } = useTranslation() + + // Search for matching tokens + const { + data: searchResultsData, + loading: searchResultsLoading, + error, + refetch, + } = useExploreSearchQuery({ + variables: { searchQuery, nftCollectionsFilter: { nameQuery: searchQuery } }, + }) + + const onRetry = useCallback(async () => { + await refetch() + }, [refetch]) + + const tokenResults = useMemo(() => { + if (!searchResultsData || !searchResultsData.searchTokens) { + return + } + + return formatTokenSearchResults(searchResultsData.searchTokens, searchQuery) + }, [searchQuery, searchResultsData]) + + // Search for matching NFT collections + + const nftCollectionResults = useMemo(() => { + if (!searchResultsData || !searchResultsData.nftCollections) { + return + } + + return formatNFTCollectionSearchResults(searchResultsData.nftCollections) + }, [searchResultsData]) + + // Search for matching wallets + + const { + wallets: walletSearchResults, + loading: walletsLoading, + exactENSMatch, + exactUnitagMatch, + } = useWalletSearchResults(searchQuery) + + const validAddress: Address | undefined = useMemo( + () => getValidAddress(searchQuery, true, false) ?? undefined, + [searchQuery] + ) + + const countTokenResults = tokenResults?.length ?? 0 + const countNftCollectionResults = nftCollectionResults?.length ?? 0 + const countWalletResults = walletSearchResults.length + const countTotalResults = countTokenResults + countNftCollectionResults + countWalletResults + + const prefixTokenMatch = tokenResults?.find((res: TokenSearchResult) => + isPrefixTokenMatch(res, searchQuery) + ) + + const hasVerifiedTokenResults = Boolean( + tokenResults?.some( + (res) => + res.safetyLevel === SafetyLevel.Verified || res.safetyLevel === SafetyLevel.MediumWarning + ) + ) + + const hasVerifiedNFTResults = Boolean(nftCollectionResults?.some((res) => res.isVerified)) + + const showWalletSectionFirst = exactUnitagMatch || (exactENSMatch && !prefixTokenMatch) + const showNftCollectionsBeforeTokens = hasVerifiedNFTResults && !hasVerifiedTokenResults + + const sortedSearchResults: SearchResultOrHeader[] = useMemo(() => { + // Format results arrays with header, and handle empty results + const nftsWithHeader = nftCollectionResults?.length + ? [NFTHeaderItem, ...nftCollectionResults] + : [] + const tokensWithHeader = tokenResults?.length ? [TokenHeaderItem, ...tokenResults] : [] + const walletsWithHeader = + walletSearchResults.length > 0 ? [WalletHeaderItem, ...walletSearchResults] : [] + + // Rank token and nft results + const searchResultItems: SearchResultOrHeader[] = showNftCollectionsBeforeTokens + ? [...nftsWithHeader, ...tokensWithHeader] + : [...tokensWithHeader, ...nftsWithHeader] + + // Add wallet results at beginning or end + if (walletsWithHeader.length > 0) { + if (showWalletSectionFirst) { + searchResultItems.unshift(...walletsWithHeader) + } else { + searchResultItems.push(...walletsWithHeader) + } + } + + // Add etherscan items at end + if (validAddress) { + searchResultItems.push(EtherscanHeaderItem, { + type: SearchResultType.Etherscan, + address: validAddress, + }) + } + + return searchResultItems + }, [ + nftCollectionResults, + showNftCollectionsBeforeTokens, + showWalletSectionFirst, + tokenResults, + validAddress, + walletSearchResults, + ]) + + if (searchResultsLoading || walletsLoading) { + return + } + + if (error) { + return ( + + + + ) + } + + return ( + + + + }} + i18nKey="explore.search.empty.full" + values={{ searchQuery }} + /> + + + } + data={sortedSearchResults} + keyExtractor={getSearchResultId} + renderItem={(props): JSX.Element | null => { + // Find position of search result in list, but exclude header items + const position = + props.item.type === SEARCH_RESULT_HEADER_KEY + ? undefined + : props.index + + 1 - + sortedSearchResults + .slice(0, props.index + 1) + .filter((item) => item.type === SEARCH_RESULT_HEADER_KEY).length + return renderSearchItem({ + ...props, + searchContext: { + query: searchQuery, + suggestionCount: countTotalResults, + position, + }, + }) + }} + /> + + ) +} + +// Render function for FlatList of SearchResult items + +export const renderSearchItem = ({ + item: searchResult, + searchContext, + index, +}: ListRenderItemInfo & { + searchContext?: SearchContext +}): JSX.Element | null => { + switch (searchResult.type) { + case SEARCH_RESULT_HEADER_KEY: + return ( + + ) + case SearchResultType.Token: + return + case SearchResultType.ENSAddress: + return + case SearchResultType.Unitag: + return + case SearchResultType.WalletByAddress: + return + case SearchResultType.NFTCollection: + return + case SearchResultType.Etherscan: + return + default: + logger.warn( + 'SearchResultsSection', + 'renderSearchItem', + `Found invalid list item in search results: ${JSON.stringify(searchResult)}` + ) + return null + } +} + +function isPrefixTokenMatch(searchResult: TokenSearchResult, query: string): boolean { + return ( + searchResult.name?.toLowerCase().startsWith(query.toLowerCase()) || + searchResult.symbol.toLowerCase().startsWith(query.toLowerCase()) + ) +} diff --git a/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx b/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx new file mode 100644 index 0000000..e8eaf3c --- /dev/null +++ b/apps/mobile/src/components/explore/search/SearchSectionHeader.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { Flex, Text, TextProps } from 'ui/src' + +interface SectionHeaderTextProps { + title: string + icon?: JSX.Element +} + +export const SectionHeaderText = ({ + title, + icon, + ...rest +}: SectionHeaderTextProps & TextProps): JSX.Element => { + return ( + + {icon && icon} + + {title} + + + ) +} diff --git a/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap b/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap new file mode 100644 index 0000000..20afb9f --- /dev/null +++ b/apps/mobile/src/components/explore/search/__snapshots__/SearchPopularTokens.test.tsx.snap @@ -0,0 +1,831 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchPopularTokens renders without error 1`] = ` + + + + + + + + + + + + Token Full Name + + + + + + + + + + + 1,000 TFN + + + + + + + + + + + + + + + + + + Token Full Name + + + + + + + + + + + 1,000 TFN + + + + + + + + + + + + +`; + +exports[`SearchPopularTokens renders without error 2`] = ` + + + + + + + + + + + + + + Ethereum + + + + + + ETH + + + + + + + + + + + + + + + + + + + USD Coin + + + + + + USDC + + + + 0xa0b8...eb48 + + + + + + + + + + +`; diff --git a/apps/mobile/src/components/explore/search/constants.tsx b/apps/mobile/src/components/explore/search/constants.tsx new file mode 100644 index 0000000..32bebf8 --- /dev/null +++ b/apps/mobile/src/components/explore/search/constants.tsx @@ -0,0 +1 @@ +export const SEARCH_RESULT_HEADER_KEY = 'header' diff --git a/apps/mobile/src/components/explore/search/hooks.ts b/apps/mobile/src/components/explore/search/hooks.ts new file mode 100644 index 0000000..2c2e4f5 --- /dev/null +++ b/apps/mobile/src/components/explore/search/hooks.ts @@ -0,0 +1,123 @@ +import { useMemo } from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import { useENS } from 'wallet/src/features/ens/useENS' +import { SearchResultType, WalletSearchResult } from 'wallet/src/features/search/SearchResult' +import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' +import { useUnitagByAddress, useUnitagByName } from 'wallet/src/features/unitags/hooks' +import { getValidAddress } from 'wallet/src/utils/addresses' + +// eslint-disable-next-line complexity +export function useWalletSearchResults(query: string): { + wallets: WalletSearchResult[] + loading: boolean + exactENSMatch: boolean + exactUnitagMatch: boolean +} { + const validAddress: Address | undefined = useMemo( + () => getValidAddress(query, true, false) ?? undefined, + [query] + ) + + const querySkippedIfValidAddress = validAddress ? null : query + + // Search for matching .eth if not a valid address + const { + address: dotEthAddress, + name: dotEthName, + loading: dotEthLoading, + } = useENS(ChainId.Mainnet, querySkippedIfValidAddress, true) + + // Search for exact match for ENS if not a valid address + const { + address: ensAddress, + name: ensName, + loading: ensLoading, + } = useENS(ChainId.Mainnet, querySkippedIfValidAddress, false) + + // Search for matching Unitag by name + const { unitag: unitagByName, loading: unitagLoading } = useUnitagByName(query) + + // Search for matching Unitag by address + const { unitag: unitagByAddress, loading: unitagByAddressLoading } = + useUnitagByAddress(validAddress) + + // Search for matching EOA wallet address + const { isSmartContractAddress, loading: loadingIsSmartContractAddress } = + useIsSmartContractAddress(validAddress, ChainId.Mainnet) + + const hasENSResult = dotEthName && dotEthAddress + const hasEOAResult = validAddress && !isSmartContractAddress + + // Consider when to show sections + + // Only consider queries with the .eth suffix as an exact ENS match + const exactENSMatch = dotEthName?.toLowerCase() === query.toLowerCase() && query.includes('.eth') + + const results: WalletSearchResult[] = [] + + // Prioritize unitags + + if (unitagByName?.address?.address && unitagByName?.username) { + results.push({ + type: SearchResultType.Unitag, + address: unitagByName.address.address, + unitag: unitagByName.username, + }) + } + + // Add full address if relevant + if (unitagByAddress?.username && validAddress) { + results.push({ + type: SearchResultType.Unitag, + address: validAddress, + unitag: unitagByAddress.username, + }) + } + + // Add the raw ENS result if available and a unitag by address was not already added + if (!validAddress && ensAddress && ensName && !unitagByAddress?.username) { + results.push({ + type: SearchResultType.ENSAddress, + address: ensAddress, + ensName, + isRawName: !ensName.endsWith('.eth'), // Ensure raw name is used for subdomains only + }) + } + + // Add ENS result if it's different than the unitag result and raw ENS result + if ( + !validAddress && + hasENSResult && + dotEthAddress !== unitagByName?.address?.address && + dotEthAddress !== ensAddress + ) { + results.push({ + type: SearchResultType.ENSAddress, + address: dotEthAddress, + ensName: dotEthName, + }) + } + + // Do not show EOA address result if there is a Unitag result by address + if (hasEOAResult && !unitagByAddress?.username) { + results.push({ + type: SearchResultType.WalletByAddress, + address: validAddress, + }) + } + + // Ensure loading is returned + const walletsLoading = + dotEthLoading || + ensLoading || + loadingIsSmartContractAddress || + unitagLoading || + unitagByAddressLoading + + return { + loading: walletsLoading, + wallets: results, + exactENSMatch, + exactUnitagMatch: !!(unitagByName || unitagByAddress), + } +} diff --git a/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx new file mode 100644 index 0000000..8348dd9 --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchENSAddressItem.tsx @@ -0,0 +1,76 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' +import { Flex, Text } from 'ui/src' +import { imageSizes } from 'ui/src/theme' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' +import { getCompletedENSName } from 'wallet/src/features/ens/useENS' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { ENSAddressSearchResult } from 'wallet/src/features/search/SearchResult' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +type SearchENSAddressItemProps = { + searchResult: ENSAddressSearchResult + searchContext?: SearchContext +} + +export function SearchENSAddressItem({ + searchResult, + searchContext, +}: SearchENSAddressItemProps): JSX.Element { + const { t } = useTranslation() + + // Use `savedPrimaryEnsName` for WalletSearchResults that are stored in the search history + // so that we don't have to do an additional ENS fetch when loading search history + const { address, ensName, primaryENSName: savedPrimaryENSName, isRawName } = searchResult + const formattedAddress = sanitizeAddressText(shortenAddress(address)) + + // Get the completed name if it's not a raw name + const completedENSName = isRawName ? ensName : getCompletedENSName(ensName ?? null) + + /* + * Fetch primary ENS associated with `address` since it may resolve to an + * ENS different than the `ensName` searched + * ex. if searching `uni.eth` resolves to 0x123, and the primary ENS for 0x123 + * is `uniswap.eth`, then we should show "uni.eth | owned by uniswap.eth" + */ + const { data: fetchedPrimaryENSName, loading: isFetchingPrimaryENSName } = useENSName( + savedPrimaryENSName ? undefined : address + ) + + const primaryENSName = savedPrimaryENSName ?? fetchedPrimaryENSName + const isPrimaryENSName = completedENSName === primaryENSName + + const showAddress = searchResult.isRawName + const showOwnedBy = !isFetchingPrimaryENSName && !isPrimaryENSName && !showAddress + const showSecondLine = showAddress || showOwnedBy + + const { data: avatar } = useENSAvatar(address) + + return ( + + + + + + {completedENSName || formattedAddress} + + {showSecondLine ? ( + + {showOwnedBy && + t('explore.search.label.ownedBy', { + ownerAddress: primaryENSName || formattedAddress, + })} + {showAddress && formattedAddress} + + ) : null} + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx new file mode 100644 index 0000000..420e0b7 --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchEtherscanItem.tsx @@ -0,0 +1,58 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import { default as React } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { getBlockExplorerIcon } from 'src/components/icons/BlockExplorerIcon' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { Arrow } from 'wallet/src/components/icons/Arrow' +import { ChainId } from 'wallet/src/constants/chains' +import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { EtherscanSearchResult } from 'wallet/src/features/search/SearchResult' +import { ElementName } from 'wallet/src/telemetry/constants' +import { shortenAddress } from 'wallet/src/utils/addresses' +import { ExplorerDataType, getExplorerLink, openUri } from 'wallet/src/utils/linking' + +type SearchEtherscanItemProps = { + etherscanResult: EtherscanSearchResult +} + +export function SearchEtherscanItem({ etherscanResult }: SearchEtherscanItemProps): JSX.Element { + const colors = useSporeColors() + const dispatch = useAppDispatch() + + const { address } = etherscanResult + + const onPressViewEtherscan = async (): Promise => { + const explorerLink = getExplorerLink(ChainId.Mainnet, address, ExplorerDataType.ADDRESS) + await openUri(explorerLink) + dispatch( + addToSearchHistory({ + searchResult: etherscanResult, + }) + ) + } + + const EtherscanIcon = getBlockExplorerIcon(ChainId.Mainnet) + + return ( + + + + + {shortenAddress(address)} + + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx new file mode 100644 index 0000000..a4dc732 --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchNFTCollectionItem.tsx @@ -0,0 +1,103 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import { default as React } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { Screens } from 'src/screens/Screens' +import { Flex, Icons, Text, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { + NFTCollectionSearchResult, + SearchResultType, +} from 'wallet/src/features/search/SearchResult' +import { ElementName } from 'wallet/src/telemetry/constants' + +type NFTCollectionItemProps = { + collection: NFTCollectionSearchResult + searchContext?: SearchContext +} + +export function SearchNFTCollectionItem({ + collection, + searchContext, +}: NFTCollectionItemProps): JSX.Element { + const { name, address, chainId, isVerified, imageUrl } = collection + const dispatch = useAppDispatch() + const navigation = useAppStackNavigation() + + const onPress = (): void => { + navigation.navigate(Screens.NFTCollection, { + collectionAddress: address, + }) + + if (searchContext) { + sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { + query: searchContext.query, + name, + chain: chainId, + address, + type: 'collection', + suggestion_count: searchContext.suggestionCount, + position: searchContext.position, + isHistory: searchContext.isHistory, + }) + } + + dispatch( + addToSearchHistory({ + searchResult: { + type: SearchResultType.NFTCollection, + chainId, + address, + name, + imageUrl, + isVerified, + }, + }) + ) + } + + return ( + + + + {imageUrl ? ( + + ) : ( + + {name.slice(0, 1)} + + )} + + + + {name} + + + + {isVerified ? : null} + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx new file mode 100644 index 0000000..6b8c263 --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchTokenItem.tsx @@ -0,0 +1,118 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import { default as React } from 'react' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch } from 'src/app/hooks' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { useExploreTokenContextMenu } from 'src/components/explore/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { SafetyLevel } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import WarningIcon from 'wallet/src/components/icons/WarningIcon' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { SearchResultType, TokenSearchResult } from 'wallet/src/features/search/SearchResult' +import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { ElementName, SectionName } from 'wallet/src/telemetry/constants' +import { shortenAddress } from 'wallet/src/utils/addresses' +import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' + +type SearchTokenItemProps = { + token: TokenSearchResult + searchContext?: SearchContext +} + +export function SearchTokenItem({ token, searchContext }: SearchTokenItemProps): JSX.Element { + const isDarkMode = useIsDarkMode() + const dispatch = useAppDispatch() + const tokenDetailsNavigation = useTokenDetailsNavigation() + + const { chainId, address, name, symbol, logoUrl, safetyLevel } = token + const currencyId = address ? buildCurrencyId(chainId, address) : buildNativeCurrencyId(chainId) + + const onPress = (): void => { + tokenDetailsNavigation.preload(currencyId) + tokenDetailsNavigation.navigate(currencyId) + if (searchContext) { + sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { + query: searchContext.query, + name: name ?? '', + chain: token.chainId, + address: address ?? '', + type: 'token', + suggestion_count: searchContext.suggestionCount, + position: searchContext.position, + isHistory: searchContext.isHistory, + }) + } + dispatch( + addToSearchHistory({ + searchResult: { + type: SearchResultType.Token, + chainId, + address, + name, + symbol, + logoUrl, + safetyLevel, + }, + }) + ) + } + + const { menuActions, onContextMenuPress } = useExploreTokenContextMenu({ + chainId, + currencyId, + analyticsSection: SectionName.ExploreSearch, + }) + + return ( + + + + + + + + + {name} + + + {(safetyLevel === SafetyLevel.Blocked || + safetyLevel === SafetyLevel.StrongWarning) && ( + + )} + + + + {symbol} + + {address && ( + + + {shortenAddress(address)} + + + )} + + + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx b/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx new file mode 100644 index 0000000..bcfc2ba --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchUnitagItem.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' +import { Flex } from 'ui/src' +import { imageSizes } from 'ui/src/theme' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { UnitagSearchResult } from 'wallet/src/features/search/SearchResult' +import { useAvatar } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' + +type SearchUnitagItemProps = { + searchResult: UnitagSearchResult + searchContext?: SearchContext +} + +export function SearchUnitagItem({ + searchResult, + searchContext, +}: SearchUnitagItemProps): JSX.Element { + const { address, unitag } = searchResult + const { avatar } = useAvatar(address) + + const displayName = { name: unitag, type: DisplayNameType.Unitag } + + return ( + + + + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx new file mode 100644 index 0000000..5659c4b --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchWalletByAddressItem.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { SearchWalletItemBase } from 'src/components/explore/search/items/SearchWalletItemBase' +import { Flex, Text } from 'ui/src' +import { imageSizes } from 'ui/src/theme' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { useENSAvatar, useENSName } from 'wallet/src/features/ens/api' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { WalletByAddressSearchResult } from 'wallet/src/features/search/SearchResult' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +type SearchWalletByAddressItemProps = { + searchResult: WalletByAddressSearchResult + searchContext?: SearchContext +} + +export function SearchWalletByAddressItem({ + searchResult, + searchContext, +}: SearchWalletByAddressItemProps): JSX.Element { + const { address } = searchResult + const formattedAddress = sanitizeAddressText(shortenAddress(address)) + const { data: ensName } = useENSName(address) + const { data: avatar } = useENSAvatar(address) + + return ( + + + + + + {ensName || formattedAddress} + + {ensName ? ( + + {formattedAddress} + + ) : null} + + + + ) +} diff --git a/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx new file mode 100644 index 0000000..7f2dfea --- /dev/null +++ b/apps/mobile/src/components/explore/search/items/SearchWalletItemBase.tsx @@ -0,0 +1,99 @@ +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { PropsWithChildren, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useEagerExternalProfileNavigation } from 'src/app/navigation/hooks' +import { useToggleWatchedWalletCallback } from 'src/features/favorites/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { TouchableArea } from 'ui/src' +import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' +import { SearchContext } from 'wallet/src/features/search/SearchContext' +import { addToSearchHistory } from 'wallet/src/features/search/searchHistorySlice' +import { + extractDomain, + SearchResultType, + WalletSearchResult, +} from 'wallet/src/features/search/SearchResult' + +type SearchWalletItemBaseProps = { + searchResult: WalletSearchResult + searchContext?: SearchContext +} + +export function SearchWalletItemBase({ + children, + searchResult, + searchContext, +}: PropsWithChildren): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { preload, navigate } = useEagerExternalProfileNavigation() + const { address, type } = searchResult + const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) + + const onPress = (): void => { + navigate(address) + if (searchContext) { + const walletName = + type === SearchResultType.Unitag + ? searchResult.unitag + : type === SearchResultType.ENSAddress + ? searchResult.ensName + : undefined + sendMobileAnalyticsEvent(MobileEventName.ExploreSearchResultClicked, { + query: searchContext.query, + name: walletName, + address, + type: 'address', + domain: walletName ? extractDomain(walletName, type) : undefined, + suggestion_count: searchContext.suggestionCount, + position: searchContext.position, + isHistory: searchContext.isHistory, + }) + } + + if (type === SearchResultType.ENSAddress) { + dispatch( + addToSearchHistory({ + searchResult: { + ...searchResult, + primaryENSName: searchResult.primaryENSName, + }, + }) + ) + } else { + dispatch( + addToSearchHistory({ + searchResult, + }) + ) + } + } + + const toggleFavoriteWallet = useToggleWatchedWalletCallback(address) + + const menuActions = useMemo(() => { + return isFavorited + ? [{ title: t('explore.wallets.favorite.action.remove'), systemIcon: 'heart.fill' }] + : [{ title: t('explore.wallets.favorite.action.add'), systemIcon: 'heart' }] + }, [isFavorited, t]) + + return ( + + => { + await preload(address) + }}> + {children} + + + ) +} diff --git a/apps/mobile/src/components/explore/search/types.tsx b/apps/mobile/src/components/explore/search/types.tsx new file mode 100644 index 0000000..464e274 --- /dev/null +++ b/apps/mobile/src/components/explore/search/types.tsx @@ -0,0 +1,8 @@ +import { SearchResult } from 'wallet/src/features/search/SearchResult' +import { SEARCH_RESULT_HEADER_KEY } from './constants' + +// Header type used to render header text instead of SearchResult item + +export type SearchResultOrHeader = + | SearchResult + | { type: typeof SEARCH_RESULT_HEADER_KEY; title: string } diff --git a/apps/mobile/src/components/explore/search/utils.test.ts b/apps/mobile/src/components/explore/search/utils.test.ts new file mode 100644 index 0000000..533faac --- /dev/null +++ b/apps/mobile/src/components/explore/search/utils.test.ts @@ -0,0 +1,141 @@ +import { faker } from '@faker-js/faker' +import { + formatNFTCollectionSearchResults, + formatTokenSearchResults, + gqlNFTToNFTCollectionSearchResult, +} from 'src/components/explore/search/utils' +import { + Chain, + ExploreSearchQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { SearchResultType } from 'wallet/src/features/search/SearchResult' +import { + amount, + ethToken, + nftCollection, + nftContract, + token, + tokenMarket, + tokenProject, +} from 'wallet/src/test/fixtures' +import { createArray } from 'wallet/src/test/utils' + +type ExploreSearchResult = NonNullable + +describe(formatTokenSearchResults, () => { + it('returns undefined if there is no data', () => { + expect(formatTokenSearchResults(null, '')).toEqual(undefined) + }) + + it('filters out duplicate results', () => { + const searchToken = token() + const data = createArray(2, () => searchToken) + + const result = formatTokenSearchResults(data, '') + + expect(result).toHaveLength(1) + expect(result?.[0]?.address).toEqual(data[0].address) + }) + + it('uses tokens with highest volume for tokens with the same project id', () => { + const changedAddress = faker.finance.ethereumAddress() + + const data = [ + // Tokens with the same address and chain will have the same project id + ethToken({ + market: tokenMarket({ volume: amount({ value: 10 }) }), + }), + ethToken({ + address: changedAddress, + market: tokenMarket({ volume: amount({ value: 100 }) }), + }), + ethToken({ + market: tokenMarket({ volume: amount({ value: 20 }) }), + }), + ] + + const result = formatTokenSearchResults(data, '') + + // Filters out the first token (both tokens share the same project id) + expect(result).toHaveLength(1) + // Uses the token with highest volume + expect(result?.[0]?.address).toEqual(changedAddress) + }) + + it('sorts results by best search query match', () => { + const data: ExploreSearchResult['searchTokens'] = [ + ethToken({ project: tokenProject({ name: 'UniswapStartingName' }) }), + ethToken({ project: tokenProject({ name: 'Uniswap' }) }), + ] + + const result = formatTokenSearchResults(data, 'uniswap') + + expect(result).toHaveLength(2) + expect(result?.[0]?.name).toEqual('Uniswap') + expect(result?.[1]?.name).toEqual('UniswapStartingName') + }) + + it('properly formats token search result', () => { + const searchToken = token() + const data = [searchToken] + + const result = formatTokenSearchResults(data, '') + + expect(result).toHaveLength(1) + expect(result?.[0]?.type).toEqual(SearchResultType.Token) + expect(result?.[0]?.chainId).toEqual(fromGraphQLChain(searchToken.chain)) + expect(result?.[0]?.address).toEqual(searchToken.address) + expect(result?.[0]?.name).toEqual(searchToken.project?.name) + expect(result?.[0]?.symbol).toEqual(searchToken.symbol) + expect(result?.[0]?.logoUrl).toEqual(searchToken.project?.logoUrl) + expect(result?.[0]?.safetyLevel).toEqual(searchToken.project?.safetyLevel) + }) + + describe(gqlNFTToNFTCollectionSearchResult, () => { + const collection = nftCollection({ + nftContracts: [nftContract({ chain: Chain.Ethereum })], + }) + + it('returns null if required data is missing', () => { + expect(gqlNFTToNFTCollectionSearchResult({ ...collection, name: null })).toEqual(null) + expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: undefined })).toEqual( + null + ) + expect(gqlNFTToNFTCollectionSearchResult({ ...collection, nftContracts: [] })).toEqual(null) + }) + + it('properly formats NFT collection search result', () => { + const result = gqlNFTToNFTCollectionSearchResult(collection) + + expect(result?.type).toEqual(SearchResultType.NFTCollection) + expect(result?.chainId).toEqual(fromGraphQLChain(Chain.Ethereum)) + expect(result?.address).toEqual(collection.nftContracts[0]?.address) + expect(result?.name).toEqual(collection?.name) + expect(result?.imageUrl).toEqual(collection?.image?.url) + expect(result?.isVerified).toEqual(collection?.isVerified) + }) + }) + + describe(formatNFTCollectionSearchResults, () => { + it('returns undefined if there is no data', () => { + expect(formatNFTCollectionSearchResults(null)).toEqual(undefined) + }) + + it('filters out nfts that cannot be formatted', () => { + const topNFTCollections = createArray(2, nftCollection) + const nftSearchResult = { + edges: [ + ...topNFTCollections.map((nft) => ({ node: nft })), + { node: nftCollection({ name: null }) }, + ], + } + + const result = formatNFTCollectionSearchResults(nftSearchResult) + + expect(result).toHaveLength(2) + expect(result?.[0]?.address).toEqual(topNFTCollections[0].nftContracts[0]?.address) + expect(result?.[1]?.address).toEqual(topNFTCollections[1].nftContracts[0]?.address) + }) + }) +}) diff --git a/apps/mobile/src/components/explore/search/utils.ts b/apps/mobile/src/components/explore/search/utils.ts new file mode 100644 index 0000000..1e54e0c --- /dev/null +++ b/apps/mobile/src/components/explore/search/utils.ts @@ -0,0 +1,135 @@ +import { SEARCH_RESULT_HEADER_KEY } from 'src/components/explore/search/constants' +import { SearchResultOrHeader } from 'src/components/explore/search/types' +import { + Chain, + ExploreSearchQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { + NFTCollectionSearchResult, + SearchResultType, + TokenSearchResult, +} from 'wallet/src/features/search/SearchResult' +import { searchResultId } from 'wallet/src/features/search/searchHistorySlice' + +const MAX_TOKEN_RESULTS_COUNT = 4 + +type ExploreSearchResult = NonNullable + +// Formats the tokens portion of explore search results into sorted array of TokenSearchResult +export function formatTokenSearchResults( + data: ExploreSearchResult['searchTokens'], + searchQuery: string +): TokenSearchResult[] | undefined { + if (!data) { + return + } + + // Prevent showing "duplicate" token search results for tokens that are on multiple chains + // and share the same TokenProject id. Only show the token that has the highest 1Y Uniswap trading volume + // ex. UNI on Mainnet, Arbitrum, Optimism -> only show UNI on Mainnet b/c it has highest 1Y volume + const tokenResultsMap = data.reduce>( + (tokensMap, token) => { + if (!token) { + return tokensMap + } + + const { chain, address, symbol, project, market } = token + const chainId = fromGraphQLChain(chain) + + if (!chainId || !project) { + return tokensMap + } + + const { name, safetyLevel, logoUrl } = project + + const tokenResult: TokenSearchResult & { volume1D: number } = { + type: SearchResultType.Token, + chainId, + address: address ?? null, + name: name ?? null, + symbol: symbol ?? '', + safetyLevel: safetyLevel ?? null, + logoUrl: logoUrl ?? null, + volume1D: market?.volume?.value ?? 0, + } + + // For token results that share the same TokenProject id, use the token with highest volume + const currentTokenResult = tokensMap[project.id] + if (!currentTokenResult || tokenResult.volume1D > currentTokenResult.volume1D) { + tokensMap[project.id] = tokenResult + } + return tokensMap + }, + {} + ) + + return Object.values(tokenResultsMap) + .slice(0, MAX_TOKEN_RESULTS_COUNT) + .sort((res1: TokenSearchResult, res2: TokenSearchResult) => { + const res1Match = isExactTokenSearchResultMatch(res1, searchQuery) + const res2Match = isExactTokenSearchResultMatch(res2, searchQuery) + + if (res1Match && !res2Match) { + return -1 + } else if (!res1Match && res2Match) { + return 1 + } else { + return 0 + } + }) +} + +function isExactTokenSearchResultMatch(searchResult: TokenSearchResult, query: string): boolean { + return ( + searchResult.name?.toLowerCase() === query.toLowerCase() || + searchResult.symbol.toLowerCase() === query.toLowerCase() + ) +} + +export function formatNFTCollectionSearchResults( + data: ExploreSearchResult['nftCollections'] +): NFTCollectionSearchResult[] | undefined { + if (!data) { + return + } + + return data.edges.reduce((accum, { node }) => { + const formatted = gqlNFTToNFTCollectionSearchResult(node) + if (formatted) { + accum.push(formatted) + } + return accum + }, []) +} + +type NFTCollectionItemResult = NonNullable< + NonNullable>['edges']>[0] +>['node'] + +export const gqlNFTToNFTCollectionSearchResult = ( + node: NFTCollectionItemResult +): NFTCollectionSearchResult | null => { + const contract = node?.nftContracts?.[0] + // Only show NFT results that have fully populated results + const chainId = fromGraphQLChain(contract?.chain ?? Chain.Ethereum) + if (node.name && contract?.address && chainId) { + return { + type: SearchResultType.NFTCollection, + chainId, + address: contract.address, + name: node.name, + imageUrl: node?.image?.url ?? null, + isVerified: Boolean(node.isVerified), + } + } + return null +} + +export const getSearchResultId = (searchResult: SearchResultOrHeader): string => { + if (searchResult.type === SEARCH_RESULT_HEADER_KEY) { + return searchResult.title + } + // Unique ID for each search result + return searchResultId(searchResult) +} diff --git a/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx new file mode 100644 index 0000000..19cfd02 --- /dev/null +++ b/apps/mobile/src/components/fiatOnRamp/CtaButton.tsx @@ -0,0 +1,52 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import Trace from 'src/components/Trace/Trace' +import { MobileEventName } from 'src/features/telemetry/constants' +import { Button, Icons } from 'ui/src' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { ElementName } from 'wallet/src/telemetry/constants' + +interface FiatOnRampCtaButtonProps { + onPress: () => void + isLoading?: boolean + eligible: boolean + disabled: boolean + analyticsProperties?: Record + continueButtonText: string +} + +export function FiatOnRampCtaButton({ + continueButtonText, + isLoading, + eligible, + disabled, + analyticsProperties, + onPress, +}: FiatOnRampCtaButtonProps): JSX.Element { + const { t } = useTranslation() + const buttonAvailable = eligible || isLoading + const continueText = eligible ? continueButtonText : t('fiatOnRamp.error.unsupported') + return ( + + + + ) +} diff --git a/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx b/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx new file mode 100644 index 0000000..cbdda93 --- /dev/null +++ b/apps/mobile/src/components/fiatOnRamp/QuoteItem.tsx @@ -0,0 +1,154 @@ +import { Currency } from '@uniswap/sdk-core' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet } from 'react-native' +import { Loader } from 'src/components/loading' +import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks' +import { Flex, Icons, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { fonts, iconSizes } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { ImageUri } from 'wallet/src/features/images/ImageUri' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' + +function LogoLoader(): JSX.Element { + return ( + + ) +} + +export function FORQuoteItem({ + quote, + serviceProvider, + currency, + loading, + baseCurrency, + onPress, + showCarret, + active, +}: { + quote: FORQuote + serviceProvider: FORServiceProvider | undefined + currency: Maybe + loading: boolean + baseCurrency: FiatCurrencyInfo + onPress: () => void + showCarret?: boolean + active?: boolean +}): JSX.Element { + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + + const quoteAmount = useFormatExactCurrencyAmount( + (quote?.destinationAmount || 0).toString(), + currency + ) + + const quoteEquivalentInSourceCurrencyAmount = formatNumberOrString({ + value: quote.sourceAmount - quote.totalFee, + type: NumberType.FiatStandard, + currencyCode: baseCurrency.code.toLowerCase(), + }) + + const isDarkMode = useIsDarkMode() + const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode) + + return ( + + + {loading ? ( + + ) : ( + + + {logoUrl ? ( + } + imageStyle={ServiceProviderLogoStyles.icon} + uri={logoUrl} + /> + ) : ( + + )} + + + + {serviceProvider?.name} + + + + + {quoteAmount && ( + + {t('fiatOnRamp.quote.amount', { + tokenAmount: `${quoteAmount + getSymbolDisplayText(currency?.symbol)}`, + })} + + )} + + {t('fiatOnRamp.quote.amountAfterFees', { + tokenAmount: quoteEquivalentInSourceCurrencyAmount, + })} + + + {showCarret ? ( + + ) : ( + + )} + + + )} + + + ) +} + +function QuoteLoader({ showCarret }: { showCarret?: boolean }): JSX.Element { + return ( + + + + + + + + + + + {showCarret ? ( + + ) : ( + + )} + + + ) +} + +const ServiceProviderLogoStyles = StyleSheet.create({ + icon: { + height: iconSizes.icon40, + width: iconSizes.icon40, + }, +}) diff --git a/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx new file mode 100644 index 0000000..becc3ad --- /dev/null +++ b/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx @@ -0,0 +1,108 @@ +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { BackButtonView } from 'src/components/layout/BackButtonView' +import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay' +import { APP_STORE_LINK } from 'src/constants/urls' +import { UpgradeStatus } from 'src/features/forceUpgrade/types' +import { Statsig } from 'statsig-react-native' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { DYNAMIC_CONFIGS } from 'wallet/src/features/experiments/constants' +import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' +import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' + +export function ForceUpgradeModal(): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + + const [isVisible, setIsVisible] = useState(false) + const [upgradeStatus, setUpgradeStatus] = useState(UpgradeStatus.NotRequired) + + // signerAccounts could be empty if no seed phrase imported or in onboarding + const signerAccounts = useNonPendingSignerAccounts() + const mnemonicId = + signerAccounts.length > 0 + ? (signerAccounts?.[0] as SignerMnemonicAccount)?.mnemonicId + : undefined + + const [showSeedPhrase, setShowSeedPhrase] = useState(false) + + useEffect(() => { + const config = Statsig.getConfig(DYNAMIC_CONFIGS.ForceUpgrade) + const statusString = config.getValue('status')?.toString() + + let status = UpgradeStatus.NotRequired + if (statusString === 'recommended') { + status = UpgradeStatus.Recommended + } else if (statusString === 'required') { + status = UpgradeStatus.Required + } + setUpgradeStatus(status) + setIsVisible(status !== UpgradeStatus.NotRequired) + }, []) + + const onPressConfirm = async (): Promise => { + await openUri(APP_STORE_LINK, /*openExternalBrowser=*/ true, /*isSafeUri=*/ true) + } + + const onClose = (): void => { + setIsVisible(false) + } + + const onPressViewRecovery = (): void => { + setShowSeedPhrase(true) + } + + const onDismiss = (): void => { + setShowSeedPhrase(false) + } + + return ( + <> + {isVisible && ( + + + {t('forceUpgrade.description')} + + {mnemonicId && ( + + {t('forceUpgrade.action.recoveryPhrase')} + + )} + + )} + {mnemonicId && showSeedPhrase && ( + + + + + + + {t('forceUpgrade.label.recoveryPhrase')} + + + + + + )} + + ) +} + +const BACK_BUTTON_SIZE = 24 diff --git a/apps/mobile/src/components/gradients/GradientBackground.tsx b/apps/mobile/src/components/gradients/GradientBackground.tsx new file mode 100644 index 0000000..3f348c9 --- /dev/null +++ b/apps/mobile/src/components/gradients/GradientBackground.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { Flex, FlexProps } from 'ui/src' +import { zIndices } from 'ui/src/theme' + +// Fills up entire parent by default +export function GradientBackground({ children, ...rest }: FlexProps): JSX.Element { + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/components/gradients/LandingBackground.mock.tsx b/apps/mobile/src/components/gradients/LandingBackground.mock.tsx new file mode 100644 index 0000000..c066f43 --- /dev/null +++ b/apps/mobile/src/components/gradients/LandingBackground.mock.tsx @@ -0,0 +1,3 @@ +export const LandingBackground = (): JSX.Element | null => { + return null +} diff --git a/apps/mobile/src/components/gradients/LandingBackground.tsx b/apps/mobile/src/components/gradients/LandingBackground.tsx new file mode 100644 index 0000000..7acf7b3 --- /dev/null +++ b/apps/mobile/src/components/gradients/LandingBackground.tsx @@ -0,0 +1,109 @@ +import { useFocusEffect } from '@react-navigation/core' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { Image, Platform, ViewStyle } from 'react-native' +import Rive, { Alignment, Fit, RiveRef } from 'rive-react-native' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { useIsDarkMode, useMedia } from 'ui/src' +import { ONBOARDING_LANDING_DARK, ONBOARDING_LANDING_LIGHT } from 'ui/src/assets' +import { useDeviceDimensions } from 'ui/src/hooks/useDeviceDimensions' +import { isAndroid } from 'uniswap/src/utils/platform' +import { useTimeout } from 'utilities/src/time/timing' +import { Language } from 'wallet/src/features/language/constants' +import { useCurrentLanguage } from 'wallet/src/features/language/hooks' + +const stateMachineName = 'State Machine 1' + +const animationStyles: ViewStyle = { + width: '100%', + height: '100%', + position: 'absolute', + top: 0, +} + +const OnboardingAnimation = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + const animationRef = useRef(null) + const media = useMedia() + const fitValue = media.short ? Fit.Cover : Fit.FitHeight + const alignmentValue = media.short ? Alignment.BottomCenter : Alignment.Center + + return ( + + ) +} + +export const LandingBackground = (): JSX.Element | null => { + const navigation = useAppStackNavigation() + const [blurred, setBlurred] = useState(false) + const [hideAnimation, setHideAnimation] = useState(false) + const language = useCurrentLanguage() + + useEffect(() => { + return navigation.addListener('blur', () => { + // set this flag on blur (when navigating to another screen) + setBlurred(true) + }) + }, [navigation]) + + // callback to turn off the animation (so that we can turn it back + // on on focus) + const turnAnimationOff = useCallback(() => { + if (blurred) { + setHideAnimation(true) + } + }, [blurred]) + + // but make sure it's delayed a tiny bit, otherwise blur triggers + // immediately, so the animation would disappear before the screen + // transition animation happens + useTimeout(turnAnimationOff, 500) + + // reset animation when focusing on this screen again + useFocusEffect(() => { + setBlurred(false) + setHideAnimation(false) + }) + + if (hideAnimation) { + // this is an alternative way to "reset" the animation, because + // something about calling Rive's ref functions like .reset() and + // .play() seems to cause very hard-to-debug crashes in the + // underlying Swift code + return null + } + + // Android 9 and 10 have issues with Rive, so we fallback on image + if ( + // Android Platform.Version is always a number + (isAndroid && typeof Platform.Version === 'number' && Platform.Version < 30) || + language !== Language.English + ) { + return + } + + return +} + +const OnboardingStaticImage = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + const { fullHeight, fullWidth } = useDeviceDimensions() + return ( + + ) +} diff --git a/apps/mobile/src/components/gradients/UniconThemedGradient.tsx b/apps/mobile/src/components/gradients/UniconThemedGradient.tsx new file mode 100644 index 0000000..e5c1c0e --- /dev/null +++ b/apps/mobile/src/components/gradients/UniconThemedGradient.tsx @@ -0,0 +1,53 @@ +import React, { memo } from 'react' +import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg' +import { Tokens, getTokenValue } from 'ui/src' + +function _UniconThemedGradient({ + gradientStartColor, + gradientEndColor, + borderRadius, + middleOut = false, + opacity = 0.25, +}: { + gradientStartColor: string + gradientEndColor: string + // TODO(MOB-1518): use RadiusTokens after upgrading tamagui that exports it for us nicely + borderRadius: `$${keyof Tokens['radius']}` + middleOut?: boolean + opacity?: number +}): JSX.Element { + const id = `background${middleOut ? 'MiddleOut' : ''}}` + + return ( + + + {middleOut ? ( + // Creates a gradient with the start color in the middle, and the end color at the top and bottom + + + + + + + ) : ( + // Creates a gradient with the start color at the top, and the end color at the bottom + + + + + )} + + + + ) +} + +export const UniconThemedGradient = memo(_UniconThemedGradient) diff --git a/apps/mobile/src/components/home/ActivityTab.tsx b/apps/mobile/src/components/home/ActivityTab.tsx new file mode 100644 index 0000000..c99e0a8 --- /dev/null +++ b/apps/mobile/src/components/home/ActivityTab.tsx @@ -0,0 +1,118 @@ +import { ForwardedRef, forwardRef, memo, useMemo } from 'react' +import { FlatList, RefreshControl } from 'react-native' +import Animated from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { useAdaptiveFooter } from 'src/components/home/hooks' +import { + AnimatedBottomSheetFlatList, + AnimatedFlatList, +} from 'src/components/layout/AnimatedFlatList' +import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' +import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' +import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { isAndroid } from 'uniswap/src/utils/platform' +import { useActivityData } from 'wallet/src/features/activity/useActivityData' +import { ModalName } from 'wallet/src/telemetry/constants' + +export const ACTIVITY_TAB_DATA_DEPENDENCIES = [GQLQueries.TransactionList] + +const ESTIMATED_ITEM_SIZE = 92 + +export const ActivityTab = memo( + forwardRef, TabProps>(function _ActivityTab( + { + owner, + containerProps, + scrollHandler, + headerHeight, + isExternalProfile = false, + renderedInModal = false, + refreshing, + onRefresh, + }, + ref + ) { + const dispatch = useAppDispatch() + const colors = useSporeColors() + const insets = useDeviceInsets() + + const { trigger: biometricsTrigger } = useBiometricPrompt() + const { requiredForTransactions: requiresBiometrics } = useBiometricAppSettings() + + const { onContentSizeChange, adaptiveFooter } = useAdaptiveFooter( + containerProps?.contentContainerStyle + ) + + const onPressReceive = (): void => { + // in case we received a pending session from a previous scan after closing modal + dispatch(removePendingSession()) + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ) + } + + const { + maybeLoaderComponent, + maybeEmptyComponent, + renderActivityItem, + sectionData, + keyExtractor, + } = useActivityData({ + owner, + authTrigger: requiresBiometrics ? biometricsTrigger : undefined, + isExternalProfile, + emptyContainerStyle: containerProps?.emptyContainerStyle, + onPressEmptyState: onPressReceive, + }) + + const refreshControl = useMemo(() => { + return ( + + ) + }, [refreshing, headerHeight, onRefresh, colors.neutral3, insets.top]) + + const List = renderedInModal ? AnimatedBottomSheetFlatList : AnimatedFlatList + + return ( + + >} + ListEmptyComponent={maybeEmptyComponent} + // we add a footer to cover any possible space, so user can scroll the top menu all the way to the top + ListFooterComponent={ + <> + {maybeLoaderComponent} + {isExternalProfile ? null : adaptiveFooter} + + } + data={sectionData} + estimatedItemSize={ESTIMATED_ITEM_SIZE} + initialNumToRender={20} + keyExtractor={keyExtractor} + maxToRenderPerBatch={20} + refreshControl={refreshControl} + refreshing={refreshing} + renderItem={renderActivityItem} + showsVerticalScrollIndicator={false} + updateCellsBatchingPeriod={10} + onContentSizeChange={onContentSizeChange} + onRefresh={onRefresh} + onScroll={scrollHandler} + {...containerProps} + /> + + ) + }) +) diff --git a/apps/mobile/src/components/home/FeedTab.tsx b/apps/mobile/src/components/home/FeedTab.tsx new file mode 100644 index 0000000..33c7d03 --- /dev/null +++ b/apps/mobile/src/components/home/FeedTab.tsx @@ -0,0 +1,151 @@ +import { ForwardedRef, forwardRef, memo, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList, RefreshControl } from 'react-native' +import Animated from 'react-native-reanimated' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { useAdaptiveFooter } from 'src/components/home/hooks' +import { AnimatedFlatList } from 'src/components/layout/AnimatedFlatList' +import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { Loader } from 'src/components/loading' +import { openModal } from 'src/features/modals/modalSlice' +import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' +import { Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' +import { NoTransactions } from 'ui/src/components/icons/NoTransactions' +import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { isAndroid } from 'uniswap/src/utils/platform' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { useFormattedTransactionDataForFeed } from 'wallet/src/features/activity/hooks' +import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { generateActivityItemRenderer } from 'wallet/src/features/transactions/SummaryCards/utils' +import { useHideSpamTokensSetting } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' + +export const FEED_TAB_DATA_DEPENDENCIES = [GQLQueries.FeedTransactionList] + +const ESTIMATED_ITEM_SIZE = 92 + +const SectionTitle = ({ title }: { title: string }): JSX.Element => ( + + + {title} + + +) + +export const FeedTab = memo( + forwardRef, TabProps>(function _FeedTab( + { containerProps, scrollHandler, headerHeight, refreshing, onRefresh }, + ref + ) { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const colors = useSporeColors() + const insets = useDeviceInsets() + + const watchedWalletsSet = useAppSelector(selectWatchedAddressSet) + const watchedWalletsList = useMemo(() => Array.from(watchedWalletsSet), [watchedWalletsSet]) + + const { onContentSizeChange } = useAdaptiveFooter(containerProps?.contentContainerStyle) + + // Hide all spam transactions if active wallet has enabled setting. + const hideSpamTokens = useHideSpamTokensSetting() + + const renderActivityItem = useMemo(() => { + return generateActivityItemRenderer( + TransactionSummaryLayout, + , + SectionTitle, + undefined, + undefined + ) + }, []) + + const { onRetry, hasData, isLoading, isError, sectionData, keyExtractor } = + useFormattedTransactionDataForFeed(watchedWalletsList, hideSpamTokens) + + const onPressReceive = (): void => { + // in case we received a pending session from a previous scan after closing modal + dispatch(removePendingSession()) + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ) + } + + const errorCard = ( + + + + ) + + const emptyListView = ( + + } + title={t('home.feed.empty.title')} + onPress={onPressReceive} + /> + + ) + + let emptyComponent = null + if (!hasData && isError) { + emptyComponent = errorCard + } else if (!isLoading && emptyListView) { + emptyComponent = emptyListView + } + + const refreshControl = useMemo(() => { + return ( + + ) + }, [refreshing, headerHeight, onRefresh, colors.neutral3, insets.top]) + + if (!hasData && isError) { + return errorCard + } + + // We want to display the loading shimmer in the footer only when the data haven't been fetched yet + // (list items use their own loading shimmer so there is no need to display it in the footer) + const isLoadingInitially = isLoading && !sectionData + + return ( + + >} + ListEmptyComponent={emptyComponent} + // we add a footer to cover any possible space, so user can scroll the top menu all the way to the top + ListFooterComponent={<>{isLoadingInitially && }} + data={sectionData} + estimatedItemSize={ESTIMATED_ITEM_SIZE} + initialNumToRender={20} + keyExtractor={keyExtractor} + maxToRenderPerBatch={20} + refreshControl={refreshControl} + refreshing={refreshing} + renderItem={renderActivityItem} + showsVerticalScrollIndicator={false} + updateCellsBatchingPeriod={10} + onContentSizeChange={onContentSizeChange} + onRefresh={onRefresh} + onScroll={scrollHandler} + {...containerProps} + /> + + ) + }) +) diff --git a/apps/mobile/src/components/home/NftsTab.tsx b/apps/mobile/src/components/home/NftsTab.tsx new file mode 100644 index 0000000..e13462b --- /dev/null +++ b/apps/mobile/src/components/home/NftsTab.tsx @@ -0,0 +1,91 @@ +import { FlashList } from '@shopify/flash-list' +import React, { forwardRef, memo, useCallback, useMemo } from 'react' +import { RefreshControl } from 'react-native' +import { useAppStackNavigation } from 'src/app/navigation/types' +import { NftView } from 'src/components/NFT/NftView' +import { useAdaptiveFooter } from 'src/components/home/hooks' +import { TAB_BAR_HEIGHT, TabProps } from 'src/components/layout/TabHelpers' +import { Screens } from 'src/screens/Screens' +import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' +import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { isAndroid } from 'uniswap/src/utils/platform' +import { NftsList } from 'wallet/src/components/nfts/NftsList' +import { NFTItem } from 'wallet/src/features/nfts/types' + +export const NFTS_TAB_DATA_DEPENDENCIES = [GQLQueries.NftsTab] + +export const NftsTab = memo( + forwardRef, TabProps>(function _NftsTab( + { + owner, + containerProps, + scrollHandler, + isExternalProfile = false, + refreshing, + onRefresh, + headerHeight = 0, + renderedInModal = false, + }, + ref + ) { + const colors = useSporeColors() + const insets = useDeviceInsets() + const navigation = useAppStackNavigation() + + const { onContentSizeChange, footerHeight, adaptiveFooter } = useAdaptiveFooter( + containerProps?.contentContainerStyle + ) + + const renderNFTItem = useCallback( + (item: NFTItem) => { + const onPressNft = (): void => { + navigation.navigate(Screens.NFTItem, { + owner, + address: item.contractAddress ?? '', + tokenId: item.tokenId ?? '', + isSpam: item.isSpam, + fallbackData: item, + }) + } + + return + }, + [owner, navigation] + ) + + const refreshControl = useMemo(() => { + return ( + + ) + }, [refreshing, headerHeight, onRefresh, colors.neutral3, insets.top]) + + return ( + + + + ) + }) +) diff --git a/apps/mobile/src/components/home/TokensTab.tsx b/apps/mobile/src/components/home/TokensTab.tsx new file mode 100644 index 0000000..54d9fe9 --- /dev/null +++ b/apps/mobile/src/components/home/TokensTab.tsx @@ -0,0 +1,103 @@ +import { useStartProfiler } from '@shopify/react-native-performance' +import React, { forwardRef, memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { FlatList } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { TokenBalanceList } from 'src/components/TokenBalanceList/TokenBalanceList' +import { useTokenDetailsNavigation } from 'src/components/TokenDetails/hooks' +import { WalletEmptyState } from 'src/components/home/WalletEmptyState' +import { NoTokens } from 'src/components/icons/NoTokens' +import { TabContentProps, TabProps } from 'src/components/layout/TabHelpers' +import { openModal } from 'src/features/modals/modalSlice' +import { Screens } from 'src/screens/Screens' +import { Flex } from 'ui/src' +import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' +import { ModalName } from 'wallet/src/telemetry/constants' +import { CurrencyId } from 'wallet/src/utils/currencyId' + +export const TOKENS_TAB_DATA_DEPENDENCIES = [GQLQueries.PortfolioBalances] + +// ignore ref type + +export const TokensTab = memo( + forwardRef, TabProps & { isExternalProfile?: boolean }>( + function _TokensTab( + { + owner, + containerProps, + scrollHandler, + isExternalProfile = false, + renderedInModal = false, + onRefresh, + refreshing, + headerHeight, + }, + ref + ) { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const tokenDetailsNavigation = useTokenDetailsNavigation() + const startProfilerTimer = useStartProfiler() + + const onPressToken = useCallback( + (currencyId: CurrencyId): void => { + startProfilerTimer({ source: Screens.Home }) + tokenDetailsNavigation.navigate(currencyId) + }, + [startProfilerTimer, tokenDetailsNavigation] + ) + + // Update list empty styling based on which empty state is used + const formattedContainerProps: TabContentProps | undefined = useMemo(() => { + if (!containerProps) { + return undefined + } + if (!isExternalProfile) { + return { ...containerProps, emptyContainerStyle: {} } + } + return containerProps + }, [containerProps, isExternalProfile]) + + const onPressAction = useCallback((): void => { + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ) + }, [dispatch]) + + const renderEmpty = useMemo((): JSX.Element => { + // Show different empty state on external profile pages + return isExternalProfile ? ( + } + title={t('home.tokens.empty.title')} + onPress={onPressAction} + /> + ) : ( + + ) + }, [isExternalProfile, onPressAction, t]) + + return ( + + + + ) + } + ) +) diff --git a/apps/mobile/src/components/home/WalletEmptyState.tsx b/apps/mobile/src/components/home/WalletEmptyState.tsx new file mode 100644 index 0000000..c8c71b2 --- /dev/null +++ b/apps/mobile/src/components/home/WalletEmptyState.tsx @@ -0,0 +1,146 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import Trace from 'src/components/Trace/Trace' +import { openModal } from 'src/features/modals/modalSlice' +import { Flex, Icons, Text, TouchableArea } from 'ui/src' +import PaperStackIcon from 'ui/src/assets/icons/paper-stack.svg' +import { iconSizes, colors as rawColors } from 'ui/src/theme' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { ElementName, ElementNameType, ModalName } from 'wallet/src/telemetry/constants' +import { opacify } from 'wallet/src/utils/colors' + +interface ActionCardItem { + title: string + blurb: string + icon: JSX.Element + onPress: () => void + elementName: ElementNameType + badgeText?: string +} + +enum ActionOption { + Buy = 'Buy', + Import = 'Import', + Receive = 'Receive', +} + +export function WalletEmptyState(): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const activeAccount = useActiveAccount() + const isViewOnly = activeAccount?.type === AccountType.Readonly + + const options: { [key in ActionOption]: ActionCardItem } = useMemo( + () => ({ + [ActionOption.Buy]: { + title: t('home.tokens.empty.action.buy.title'), + blurb: t('home.tokens.empty.action.buy.description'), + elementName: ElementName.EmptyStateBuy, + icon: ( + } + /> + } + /> + ), + onPress: () => dispatch(openModal({ name: ModalName.FiatOnRamp })), + }, + [ActionOption.Receive]: { + title: t('home.tokens.empty.action.receive.title'), + blurb: t('home.tokens.empty.action.receive.description'), + elementName: ElementName.EmptyStateReceive, + icon: ( + } + /> + } + /> + ), + onPress: () => + dispatch( + openModal({ + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.WalletQr, + }) + ), + }, + [ActionOption.Import]: { + title: t('home.tokens.empty.action.import.title'), + blurb: t('home.tokens.empty.action.import.description'), + elementName: ElementName.EmptyStateImport, + icon: ( + + } + /> + ), + onPress: () => dispatch(openModal({ name: ModalName.AccountSwitcher })), + }, + }), + [dispatch, t] + ) + + // Order options based on view only status + const sortedOptions = isViewOnly ? [options.Import] : [options.Buy, options.Receive] + + return ( + + {sortedOptions.map((option) => ( + + ))} + + ) +} + +const ActionCard = ({ title, blurb, onPress, icon, elementName }: ActionCardItem): JSX.Element => ( + + + + {icon} + + + {title} + + + {blurb} + + + + + +) + +const IconContainer = ({ + backgroundColor, + icon, +}: { + backgroundColor: string + icon: JSX.Element +}): JSX.Element => ( + + {icon} + +) diff --git a/apps/mobile/src/components/home/hooks.tsx b/apps/mobile/src/components/home/hooks.tsx new file mode 100644 index 0000000..9672bf4 --- /dev/null +++ b/apps/mobile/src/components/home/hooks.tsx @@ -0,0 +1,68 @@ +import { useCallback, useEffect, useMemo } from 'react' +import { StyleProp, ViewStyle } from 'react-native' +import Animated, { SharedValue, useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { TAB_BAR_HEIGHT } from 'src/components/layout/TabHelpers' +import { useDeviceDimensions, useDeviceInsets } from 'ui/src' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' + +export function useAdaptiveFooter(contentContainerStyle?: StyleProp): { + onContentSizeChange?: (w: number, h: number) => void + footerHeight: SharedValue + adaptiveFooter: JSX.Element +} { + const { fullHeight } = useDeviceDimensions() + const insets = useDeviceInsets() + // Content is rendered under the navigation bar but not under the status bar + const maxContentHeight = fullHeight - insets.top + // Use maxContentHeight as the initial value to properly position the TabBar + // while changing tabs when data is loading (before the onContentSizeChange + // was called and appropriate footer height was calculated) + const footerHeight = useSharedValue(maxContentHeight) + const activeAccount = useActiveAccount() + + const onContentSizeChange = useCallback( + (_: number, contentHeight: number) => { + if (!contentContainerStyle) { + return + } + // The height of the footer added to the list can be calculated from + // the following equation (for collapsed tab bar): + // maxContentHeight = TAB_BAR_HEIGHT + + footerHeight + paddingBottom + // + // To get the we need to subtract padding already + // added to the content container style and the footer if it's already + // been rendered: + // = contentHeight - paddingTop - paddingBottom - footerHeight + // + // The resulting equation is: + // footerHeight = maxContentHeight - - TAB_BAR_HEIGHT - paddingBottom + // = maxContentHeight - (contentHeight - paddingTop - paddingBottom - footerHeight) - TAB_BAR_HEIGHT - paddingBottom + // = maxContentHeight + paddingTop + footerHeight - (contentHeight + TAB_BAR_HEIGHT) + const paddingTopProp = (contentContainerStyle as ViewStyle)?.paddingTop + const paddingTop = typeof paddingTopProp === 'number' ? paddingTopProp : 0 + const calculatedFooterHeight = + maxContentHeight + paddingTop + footerHeight.value - (contentHeight + TAB_BAR_HEIGHT) + + footerHeight.value = Math.max(0, calculatedFooterHeight) + }, + [footerHeight, contentContainerStyle, maxContentHeight] + ) + + useEffect(() => { + // Reset footer height to the initial value when the active account changes + // (the fullHeight value is used for the same reason as the initial value) + footerHeight.value = fullHeight + }, [activeAccount, footerHeight, fullHeight]) + + const footerStyle = useAnimatedStyle(() => ({ + height: footerHeight.value, + })) + + const adaptiveFooter = useMemo(() => , [footerStyle]) + + return { + onContentSizeChange: contentContainerStyle ? onContentSizeChange : undefined, + footerHeight, + adaptiveFooter, + } +} diff --git a/apps/mobile/src/components/icons/BiometricsIcon.tsx b/apps/mobile/src/components/icons/BiometricsIcon.tsx new file mode 100644 index 0000000..10f40fe --- /dev/null +++ b/apps/mobile/src/components/icons/BiometricsIcon.tsx @@ -0,0 +1,17 @@ +import { useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' +import { Icons } from 'ui/src' + +export function BiometricsIcon(): JSX.Element | null { + const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = + useDeviceSupportsBiometricAuth() + + if (isTouchIdSupported) { + return + } + + if (isFaceIdSupported) { + return + } + + return null +} diff --git a/apps/mobile/src/components/icons/BlockExplorerIcon.tsx b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx new file mode 100644 index 0000000..10db05e --- /dev/null +++ b/apps/mobile/src/components/icons/BlockExplorerIcon.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { SvgProps } from 'react-native-svg' +import { useIsDarkMode } from 'ui/src' +import { IconSizeTokens } from 'ui/src/theme' +import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' + +type IconComponentProps = SvgProps & { size?: IconSizeTokens | number } + +const iconsCache = new Map>() + +function buildIconComponent(chainId: ChainId): React.FC { + const explorer = CHAIN_INFO[chainId].explorer + const Component = ({ size }: IconComponentProps): JSX.Element => { + const isDarkMode = useIsDarkMode() + return isDarkMode ? : + } + Component.displayName = `BlockExplorerIcon_${explorer.name}` + iconsCache.set(chainId, Component) + return Component +} + +export function getBlockExplorerIcon(chainId: ChainId): React.FC { + return iconsCache.get(chainId) ?? buildIconComponent(chainId) +} diff --git a/apps/mobile/src/components/icons/Favorite.tsx b/apps/mobile/src/components/icons/Favorite.tsx new file mode 100644 index 0000000..81ac9f5 --- /dev/null +++ b/apps/mobile/src/components/icons/Favorite.tsx @@ -0,0 +1,49 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { + useAnimatedStyle, + useDerivedValue, + withSequence, + withTiming, +} from 'react-native-reanimated' +import { AnimatedFlex, useSporeColors } from 'ui/src' +import HeartIcon from 'ui/src/assets/icons/heart.svg' + +interface FavoriteButtonProps { + isFavorited: boolean + size: number +} + +const DELAY = 100 +const ANIMATION_CONFIG = { duration: DELAY } + +export const Favorite = ({ isFavorited, size }: FavoriteButtonProps): JSX.Element => { + const colors = useSporeColors() + const unfilledColor = colors.neutral2.val + + const getColor = useCallback( + () => (isFavorited ? colors.accent1.val : unfilledColor), + [isFavorited, colors.accent1, unfilledColor] + ) + + const [color, setColor] = useState(getColor()) + + useEffect(() => { + const timer = setTimeout(() => { + setColor(getColor()) + }, DELAY) + return () => clearTimeout(timer) + }, [getColor, isFavorited]) + + const scale = useDerivedValue(() => { + return withSequence(withTiming(0, ANIMATION_CONFIG), withTiming(1, ANIMATION_CONFIG)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFavorited]) + + const animatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: scale.value }] }), [scale]) + + return ( + + + + ) +} diff --git a/apps/mobile/src/components/icons/NoTokens.tsx b/apps/mobile/src/components/icons/NoTokens.tsx new file mode 100644 index 0000000..9783c16 --- /dev/null +++ b/apps/mobile/src/components/icons/NoTokens.tsx @@ -0,0 +1,19 @@ +import React, { memo } from 'react' +import OverlayIcon from 'src/components/icons/OverlayIcon' +import { Flex, useSporeColors } from 'ui/src' +import NoTokensFgIcon from 'ui/src/assets/icons/empty-state-coin.svg' +import NoTokensBgIcon from 'ui/src/assets/icons/empty-state-tokens.svg' + +export const NoTokens = memo(function _NoTokens() { + const colors = useSporeColors() + return ( + + } + overlay={} + right={0} + /> + + ) +}) diff --git a/apps/mobile/src/components/icons/OverlayIcon.tsx b/apps/mobile/src/components/icons/OverlayIcon.tsx new file mode 100644 index 0000000..d2c5181 --- /dev/null +++ b/apps/mobile/src/components/icons/OverlayIcon.tsx @@ -0,0 +1,19 @@ +import React, { ReactNode } from 'react' +import { Flex, FlexProps } from 'ui/src' + +type Props = { + icon: ReactNode + overlay: ReactNode +} & Pick + +// For overlaying icons in JSX +export default function OverlayIcon({ icon, overlay, ...props }: Props): JSX.Element { + return ( + <> + {icon} + + {overlay} + + + ) +} diff --git a/apps/mobile/src/components/icons/TripleDot.tsx b/apps/mobile/src/components/icons/TripleDot.tsx new file mode 100644 index 0000000..32488c1 --- /dev/null +++ b/apps/mobile/src/components/icons/TripleDot.tsx @@ -0,0 +1,17 @@ +import React, { memo } from 'react' +import { ColorTokens, Flex } from 'ui/src' + +type Props = { + size?: number + color?: ColorTokens +} + +export const TripleDot = memo(function _TripleDot({ size = 5, color = '$neutral2' }: Props) { + return ( + + + + + + ) +}) diff --git a/apps/mobile/src/components/input/PasswordInput.tsx b/apps/mobile/src/components/input/PasswordInput.tsx new file mode 100644 index 0000000..c204270 --- /dev/null +++ b/apps/mobile/src/components/input/PasswordInput.tsx @@ -0,0 +1,73 @@ +import React, { forwardRef, useState } from 'react' +import { TextInput as NativeTextInput } from 'react-native' +import { AnimatedFlex, Flex, TouchableArea, useSporeColors } from 'ui/src' +import EyeOffIcon from 'ui/src/assets/icons/eye-off.svg' +import EyeIcon from 'ui/src/assets/icons/eye.svg' +import { iconSizes } from 'ui/src/theme' +import { TextInput, TextInputProps } from 'wallet/src/components/input/TextInput' + +export const PasswordInput = forwardRef(function _PasswordInput( + props, + ref +) { + const colors = useSporeColors() + const [showPassword, setShowPassword] = useState(false) + + const { value, placeholder, onChangeText, returnKeyType, onSubmitEditing, ...rest } = props + + const onPressEyeIcon = (): void => { + setShowPassword(!showPassword) + } + + return ( + + + + + + {showPassword ? ( + + ) : ( + + )} + + + + + ) +}) diff --git a/apps/mobile/src/components/input/SelectionCircle.test.tsx b/apps/mobile/src/components/input/SelectionCircle.test.tsx new file mode 100644 index 0000000..f84b380 --- /dev/null +++ b/apps/mobile/src/components/input/SelectionCircle.test.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { SelectionCircle } from 'src/components/input/SelectionCircle' +import { render } from 'src/test/test-utils' + +it('renders selection circle', () => { + const tree = render() + expect(tree).toMatchSnapshot() +}) diff --git a/apps/mobile/src/components/input/SelectionCircle.tsx b/apps/mobile/src/components/input/SelectionCircle.tsx new file mode 100644 index 0000000..f4fefc4 --- /dev/null +++ b/apps/mobile/src/components/input/SelectionCircle.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { ColorTokens, Flex } from 'ui/src' +import { iconSizes } from 'ui/src/theme' + +interface SelectionCircleProps { + selected: boolean + size: keyof typeof iconSizes + unselectedColor?: ColorTokens + selectedColor?: ColorTokens +} + +export function SelectionCircle({ + selected, + size, + unselectedColor = '$neutral2', + selectedColor = '$accent1', +}: SelectionCircleProps): JSX.Element { + return ( + + + + ) +} diff --git a/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap new file mode 100644 index 0000000..3bc395c --- /dev/null +++ b/apps/mobile/src/components/input/__snapshots__/SelectionCircle.test.tsx.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders selection circle 1`] = ` + + + +`; diff --git a/apps/mobile/src/components/layout/AnimatedFlatList.tsx b/apps/mobile/src/components/layout/AnimatedFlatList.tsx new file mode 100644 index 0000000..ba714a0 --- /dev/null +++ b/apps/mobile/src/components/layout/AnimatedFlatList.tsx @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Adds ForwardRef to Animated.FlaList +// https://github.com/software-mansion/react-native-reanimated/issues/2976 + +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import React, { forwardRef, PropsWithChildren } from 'react' +import { FlatList, FlatListProps, LayoutChangeEvent, View } from 'react-native' +import Animated, { ILayoutAnimationBuilder } from 'react-native-reanimated' + +// difficult to properly type +const ReanimatedFlatList = Animated.createAnimatedComponent(FlatList as any) as any +const ReanimatedBottomSheetFlatList = Animated.createAnimatedComponent( + BottomSheetFlatList as any +) as any +const AnimatedView = Animated.createAnimatedComponent(View) + +const createCellRenderer = ( + itemLayoutAnimation?: ILayoutAnimationBuilder +): React.FC< + PropsWithChildren<{ + onLayout: (event: LayoutChangeEvent) => void + }> +> => { + const cellRenderer: React.FC< + PropsWithChildren<{ + onLayout: (event: LayoutChangeEvent) => void + }> + > = (props) => { + return ( + + {props.children} + + ) + } + + return cellRenderer +} + +interface ReanimatedFlatlistProps extends FlatListProps { + itemLayoutAnimation?: ILayoutAnimationBuilder + FlatListComponent?: FlatList +} + +/** + * re-create Reanimated FlatList but correctly pass on forwardRef in order to use scrollTo to scroll to the next page in our horizontal FlatList + * + * Source: https://github.com/software-mansion/react-native-reanimated/blob/main/src/reanimated2/component/FlatList.tsx + * + * TODO: [MOB-207] remove this and use Animated.FlatList directly when can use refs with it. Also type the generic T properly for FlatList and dont use `any` + */ +export const AnimatedFlatList = forwardRef, ReanimatedFlatlistProps>( + function _AnimatedFlatList( + { itemLayoutAnimation, FlatListComponent = ReanimatedFlatList, ...restProps }, + ref + ) { + // eslint-disable-next-line react-hooks/exhaustive-deps + const cellRenderer = React.useMemo(() => createCellRenderer(itemLayoutAnimation), []) + return + } +) + +/** + * In bottom sheet contexts, this will support pull to dismiss. + * See AnimatedFlatList for other props. + */ +export const AnimatedBottomSheetFlatList = forwardRef< + Animated.FlatList, + ReanimatedFlatlistProps +>(function _AnimatedBottomSheetFlatList(props, ref) { + return ( + + ) +}) diff --git a/apps/mobile/src/components/layout/BackButtonView.tsx b/apps/mobile/src/components/layout/BackButtonView.tsx new file mode 100644 index 0000000..d1ab3e0 --- /dev/null +++ b/apps/mobile/src/components/layout/BackButtonView.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { ColorTokens, Flex, Icons, Text } from 'ui/src' + +type Props = { + size?: number + color?: ColorTokens + showButtonLabel?: boolean +} + +export function BackButtonView({ size, color, showButtonLabel }: Props): JSX.Element { + const { t } = useTranslation() + + return ( + + + {showButtonLabel && ( + + {t('common.button.back')} + + )} + + ) +} diff --git a/apps/mobile/src/components/layout/BackHeader.tsx b/apps/mobile/src/components/layout/BackHeader.tsx new file mode 100644 index 0000000..1f76adf --- /dev/null +++ b/apps/mobile/src/components/layout/BackHeader.tsx @@ -0,0 +1,32 @@ +import React, { PropsWithChildren } from 'react' +import { BackButton } from 'src/components/buttons/BackButton' +import { Flex, FlexProps } from 'ui/src' + +const BACK_BUTTON_SIZE = 24 + +type BackButtonRowProps = { + alignment?: 'left' | 'center' + endAdornment?: JSX.Element + onPressBack?: () => void +} & FlexProps + +export function BackHeader({ + alignment = 'center', + children, + endAdornment = , + onPressBack, + ...spacingProps +}: PropsWithChildren): JSX.Element { + return ( + + + {children} + {endAdornment} + + ) +} diff --git a/apps/mobile/src/components/layout/Delayed.tsx b/apps/mobile/src/components/layout/Delayed.tsx new file mode 100644 index 0000000..dc5446a --- /dev/null +++ b/apps/mobile/src/components/layout/Delayed.tsx @@ -0,0 +1,25 @@ +import { PropsWithChildren, useReducer } from 'react' +import { useTimeout } from 'utilities/src/time/timing' + +export enum Delay { + Short = 500, + Normal = 2500, + Long = 5000, +} + +type Props = { + children: JSX.Element + waitBeforeShow?: Delay +} + +/** HOC to delay rendering a component by some time in ms. */ +export const Delayed = ({ + children, + waitBeforeShow = Delay.Short, +}: PropsWithChildren): JSX.Element | null => { + const [isShown, setIsShown] = useReducer(() => true, false) + + useTimeout(setIsShown, waitBeforeShow) + + return isShown ? children : null +} diff --git a/apps/mobile/src/components/layout/Screen.tsx b/apps/mobile/src/components/layout/Screen.tsx new file mode 100644 index 0000000..05971b0 --- /dev/null +++ b/apps/mobile/src/components/layout/Screen.tsx @@ -0,0 +1,71 @@ +import React, { useMemo } from 'react' +import { NativeSafeAreaViewProps } from 'react-native-safe-area-context' +import { Flex, FlexProps, useDeviceInsets } from 'ui/src' + +// Used to determine amount of top padding for short screens +export const SHORT_SCREEN_HEADER_HEIGHT_RATIO = 0.88 + +type ScreenProps = FlexProps & + // The SafeAreaView from react-native-safe-area-context also supports a `mode` prop which + // lets you choose if `edges` are added as margin or padding, but we don’t use that so + // our Screen component doesn't need to support it + Omit & { noInsets?: boolean } + +function SafeAreaWithInsets({ children, edges, noInsets, ...rest }: ScreenProps): JSX.Element { + // Safe area insets are wrong (0 when they shouldn't be) when using the + // component from react-native-safe-area-context, because when the initial screen is + // outside the viewport (as is the case with a screen slide-in animation on navigation) + // the safe area insets are calculated based on the initial screen, not the final screen. + // This is a known issue with react-native-safe-area-context, and the solution is to use + // the useSafeAreaInsets hook instead. See: + // https://github.com/th3rdwave/react-native-safe-area-context/issues/114 + const insets = useDeviceInsets() // useDeviceInsets uses useSafeAreaInsets internally + + const safeAreaStyles = useMemo(() => { + const style: { [key: string]: number } = {} + + if (noInsets) { + return style + } + + if (!edges) { + return { + paddingTop: insets.top, + paddingBottom: insets.bottom, + paddingLeft: insets.left, + paddingRight: insets.right, + } + } + if (edges?.includes('top')) { + style.paddingTop = insets.top + } + if (edges?.includes('bottom')) { + style.paddingBottom = insets.bottom + } + if (edges?.includes('left')) { + style.paddingLeft = insets.left + } + if (edges?.includes('right')) { + style.paddingRight = insets.right + } + return style + }, [edges, insets, noInsets]) + + return ( + + {children} + + ) +} + +export function Screen({ + backgroundColor = '$surface1', + children, + ...rest +}: ScreenProps): JSX.Element { + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/components/layout/TabHelpers.tsx b/apps/mobile/src/components/layout/TabHelpers.tsx new file mode 100644 index 0000000..89f1fae --- /dev/null +++ b/apps/mobile/src/components/layout/TabHelpers.tsx @@ -0,0 +1,160 @@ +/* eslint-disable react-native/no-unused-styles */ +import { FlashList, FlashListProps } from '@shopify/flash-list' +import React, { RefObject, useCallback, useMemo } from 'react' +import { + FlatList, + FlatListProps, + NativeScrollEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + ViewStyle, +} from 'react-native' +import Animated, { SharedValue } from 'react-native-reanimated' +import { Route } from 'react-native-tab-view' +import { PendingNotificationBadge } from 'src/features/notifications/PendingNotificationBadge' +import { Flex, Text } from 'ui/src' +import { colorsLight, spacing } from 'ui/src/theme' + +export const TAB_VIEW_SCROLL_THROTTLE = 16 +export const TAB_BAR_HEIGHT = 48 +export const SWIPE_THRESHOLD = 5 + +export const TAB_STYLES = StyleSheet.create({ + activeTabIndicator: { + backgroundColor: colorsLight.accent1, + bottom: 0, + height: 0, + position: 'absolute', + }, + container: { + flex: 1, + overflow: 'hidden', + }, + header: { + marginBottom: 0, + paddingBottom: 0, + position: 'absolute', + width: '100%', + zIndex: 1, + }, + headerContainer: { + left: 0, + position: 'absolute', + right: 0, + top: 0, + width: '100%', + zIndex: 1, + }, + tabBar: { + // add inactive border to bottom of tab bar + borderBottomWidth: 0, + margin: 0, + marginHorizontal: 0, + padding: 0, + // remove default shadow border under tab bar + shadowColor: colorsLight.none, + shadowOpacity: 0, + shadowRadius: 0, + top: 0, + }, + // For padding on the list components themselves within tabs. + tabListInner: { + paddingBottom: spacing.spacing12, + paddingTop: spacing.spacing8, + }, +}) + +export type HeaderConfig = { + heightExpanded: number + heightCollapsed: number +} + +export type ScrollPair = { + list: RefObject | RefObject> + position: Animated.SharedValue + index: number +} + +export type TabProps = { + owner: string + containerProps?: TabContentProps + scrollHandler?: (event: NativeSyntheticEvent) => void + isExternalProfile?: boolean + renderedInModal?: boolean + refreshing?: boolean + onRefresh?: () => void + headerHeight?: number +} + +export type TabContentProps = Partial> & { + loadingContainerStyle: StyleProp + emptyContainerStyle: StyleProp + contentContainerStyle?: StyleProp + estimatedItemSize?: number + onMomentumScrollEnd?: (event: NativeSyntheticEvent) => void + onScrollEndDrag?: (event: NativeSyntheticEvent) => void + scrollEventThrottle?: number +} + +export const renderTabLabel = ({ + route, + focused, + isExternalProfile, +}: { + route: Route + focused: boolean + isExternalProfile?: boolean +}): JSX.Element => { + return ( + + + {route.title} + + {/* Streamline UI by hiding the Activity tab spinner when focused + and showing it only on the specific pending transactions. */} + {route.title === 'Activity' && !isExternalProfile && !focused ? ( + + ) : null} + + ) +} + +/** + * Keeps tab content in sync, by scrolling content in case collapsing header height has changed between tabs + */ +export const useScrollSync = ( + currentTabIndex: SharedValue, + scrollPairs: ScrollPair[], + headerConfig: HeaderConfig +): { sync: (event: NativeSyntheticEvent) => void } => { + const sync: + | FlatListProps['onMomentumScrollEnd'] + | FlashListProps['onMomentumScrollEnd'] = useCallback( + (event: { nativeEvent: NativeScrollEvent }) => { + const { y } = event.nativeEvent.contentOffset + + const { heightCollapsed, heightExpanded } = headerConfig + + const headerDiff = heightExpanded - heightCollapsed + + for (const { list, position, index } of scrollPairs) { + const scrollPosition = position.value + + if (scrollPosition > headerDiff && y > headerDiff) { + continue + } + + if (index !== currentTabIndex.value) { + list.current?.scrollToOffset({ + offset: Math.min(y, headerDiff), + animated: false, + }) + } + } + }, + [currentTabIndex, scrollPairs, headerConfig] + ) + + return useMemo(() => ({ sync }), [sync]) +} diff --git a/apps/mobile/src/components/layout/VirtualizedList.tsx b/apps/mobile/src/components/layout/VirtualizedList.tsx new file mode 100644 index 0000000..326fb48 --- /dev/null +++ b/apps/mobile/src/components/layout/VirtualizedList.tsx @@ -0,0 +1,32 @@ +import React, { ComponentProps, PropsWithChildren } from 'react' +import { + AnimatedBottomSheetFlatList, + AnimatedFlatList, +} from 'src/components/layout/AnimatedFlatList' + +type VirtualizedListProps = PropsWithChildren>> & { + renderedInModal?: boolean +} + +/** Dummy component wrapping `FlatList` to behave like a ScrollView */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const VirtualizedList = React.forwardRef( + function _VirtualizedList({ children, renderedInModal, ...props }: VirtualizedListProps, ref) { + const List = renderedInModal ? AnimatedBottomSheetFlatList : AnimatedFlatList + return ( + {children}} + data={[]} + keyExtractor={(): string => 'key'} + keyboardShouldPersistTaps="always" + renderItem={null} + scrollEventThrottle={16} + showsHorizontalScrollIndicator={false} + showsVerticalScrollIndicator={false} + /> + ) + } +) diff --git a/apps/mobile/src/components/layout/screens/EdgeGestureTarget.tsx b/apps/mobile/src/components/layout/screens/EdgeGestureTarget.tsx new file mode 100644 index 0000000..d7b147e --- /dev/null +++ b/apps/mobile/src/components/layout/screens/EdgeGestureTarget.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { Flex, useDeviceDimensions } from 'ui/src' + +/** + * Adds a transparent box to the specific edge as a gesture target. + * Useful when rendering `BottomSheetFlatList`s inside a navigator. + */ +export function HorizontalEdgeGestureTarget({ + edge = 'left', + height, + top = 0, + width = 20, +}: { + edge?: 'left' | 'right' + height?: number + top?: number + width?: number +}): JSX.Element { + const dimensions = useDeviceDimensions() + + return ( + + ) +} diff --git a/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx new file mode 100644 index 0000000..59664ca --- /dev/null +++ b/apps/mobile/src/components/layout/screens/HeaderScrollScreen.tsx @@ -0,0 +1,91 @@ +import { useScrollToTop } from '@react-navigation/native' +import React, { PropsWithChildren, useRef } from 'react' +import { FlatList } from 'react-native-gesture-handler' +import { useAnimatedScrollHandler, useSharedValue, withTiming } from 'react-native-reanimated' +import { Screen } from 'src/components/layout/Screen' +import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' +import { ScrollHeader } from 'src/components/layout/screens/ScrollHeader' +import { VirtualizedList } from 'src/components/layout/VirtualizedList' +import { ColorTokens, Flex, flexStyles, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' + +// Distance to scroll to show scrolled state header elements +const SHOW_HEADER_SCROLL_Y_DISTANCE = 50 + +type HeaderScrollScreenProps = { + centerElement?: JSX.Element + rightElement?: JSX.Element + alwaysShowCenterElement?: boolean + fullScreen?: boolean // Expand to device edges + renderedInModal?: boolean // Apply styling to display within bottom sheet modal + showHandleBar?: boolean // add handlebar element to top of view + backgroundColor?: ColorTokens + backButtonColor?: ColorTokens +} + +export function HeaderScrollScreen({ + centerElement, + rightElement = , + alwaysShowCenterElement, + fullScreen = false, + renderedInModal = false, + showHandleBar = false, + backgroundColor = '$surface1', + backButtonColor, + children, +}: PropsWithChildren): JSX.Element { + const colors = useSporeColors() + + // difficult to properly type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const listRef = useRef>(null) + + // scrolls to top when tapping on the active tab + useScrollToTop(listRef) + + const scrollY = useSharedValue(0) + + // On scroll, centerElement and the bottom border fade in + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y + }, + onEndDrag: (event) => { + scrollY.value = withTiming(event.contentOffset.y > 0 ? SHOW_HEADER_SCROLL_Y_DISTANCE : 0) + }, + }) + + return ( + + {showHandleBar ? : null} + + + {children} + + + + + ) +} diff --git a/apps/mobile/src/components/layout/screens/ScrollHeader.tsx b/apps/mobile/src/components/layout/screens/ScrollHeader.tsx new file mode 100644 index 0000000..ddf9478 --- /dev/null +++ b/apps/mobile/src/components/layout/screens/ScrollHeader.tsx @@ -0,0 +1,134 @@ +import { useScrollToTop } from '@react-navigation/native' +import React, { ReactElement, useMemo } from 'react' +import { StyleProp, ViewStyle } from 'react-native' +import Animated, { + Extrapolate, + SharedValue, + interpolate, + useAnimatedStyle, +} from 'react-native-reanimated' +import { BackButton } from 'src/components/buttons/BackButton' +import { WithScrollToTop } from 'src/components/layout/screens/WithScrollToTop' +import { AnimatedFlex, ColorTokens, Flex, useDeviceInsets } from 'ui/src' +import { iconSizes, zIndices } from 'ui/src/theme' + +type ScrollHeaderProps = { + scrollY: SharedValue + showHeaderScrollYDistance: number + // hard to type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + listRef: React.MutableRefObject + centerElement?: JSX.Element + rightElement?: JSX.Element + alwaysShowCenterElement?: boolean + fullScreen?: boolean // Expand to device edges + backgroundColor?: ColorTokens + backButtonColor?: ColorTokens +} + +/** + * Fixed header that will fade in on scroll. Define values in parent, to be used by some + * relevant list component. + * + * Used to achieve functionality of HeaderScrollScreen, but can be used in any context. One + * example is using a scrolled above a full screen view like NFTCollectionScreen. + */ +export function ScrollHeader({ + listRef, + scrollY, + showHeaderScrollYDistance, + centerElement, + rightElement = , + alwaysShowCenterElement, + fullScreen = false, + backgroundColor, + backButtonColor, +}: ScrollHeaderProps): JSX.Element { + // scrolls to top when tapping on the active tab + useScrollToTop(listRef) + + const visibleOnScrollStyle = useAnimatedStyle(() => { + return { + opacity: interpolate( + scrollY.value, + [0, showHeaderScrollYDistance], + [0, 1], + Extrapolate.CLAMP + ), + } + }) + + const insets = useDeviceInsets() + const headerRowStyles = useMemo(() => { + return fullScreen + ? { + paddingTop: insets.top, + } + : { paddingTop: 0 } + }, [fullScreen, insets.top]) + + const headerWrapperStyles = fullScreen ? [visibleOnScrollStyle, { zIndex: zIndices.popover }] : [] + + return ( + + + + + + {alwaysShowCenterElement ? ( + centerElement + ) : ( + {centerElement} + )} + + {rightElement} + + + + + ) +} + +// If full screen, extend content to edge of device screen with an absolute position. +function HeaderWrapper({ + fullScreen, + children, + style, + backgroundColor = '$surface1', +}: { + fullScreen: boolean + children: ReactElement + style?: StyleProp>> + backgroundColor?: ColorTokens +}): JSX.Element { + if (!fullScreen) { + return {children} + } + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx b/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx new file mode 100644 index 0000000..7b1982b --- /dev/null +++ b/apps/mobile/src/components/layout/screens/WithScrollToTop.tsx @@ -0,0 +1,17 @@ +import React, { forwardRef, PropsWithChildren } from 'react' +import { Pressable } from 'react-native' + +// accept any ref +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const WithScrollToTop = forwardRef>( + function _WithScrollToTop({ children }: PropsWithChildren, ref) { + const onPress = (): void => { + if (!ref || typeof ref === 'function') { + return + } + ref.current.scrollToOffset({ animated: true, offset: 0 }) + } + + return {children} + } +) diff --git a/apps/mobile/src/components/loading/TransactionLoader.tsx b/apps/mobile/src/components/loading/TransactionLoader.tsx new file mode 100644 index 0000000..8faac97 --- /dev/null +++ b/apps/mobile/src/components/loading/TransactionLoader.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { Flex, Text } from 'ui/src' +import { TXN_HISTORY_ICON_SIZE } from 'wallet/src/features/transactions/SummaryCards/utils' + +interface TransactionLoaderProps { + opacity: number +} + +export function TransactionLoader({ opacity }: TransactionLoaderProps): JSX.Element { + return ( + + + + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/loading/WaveLoader.tsx b/apps/mobile/src/components/loading/WaveLoader.tsx new file mode 100644 index 0000000..8da3355 --- /dev/null +++ b/apps/mobile/src/components/loading/WaveLoader.tsx @@ -0,0 +1,54 @@ +import React, { useEffect } from 'react' +import { StyleSheet } from 'react-native' +import Animated, { + interpolate, + useAnimatedStyle, + useSharedValue, + withRepeat, + withTiming, +} from 'react-native-reanimated' +import { useChartDimensions } from 'src/components/PriceExplorer/useChartDimensions' +import { Flex, useSporeColors } from 'ui/src' +import Wave from 'ui/src/assets/backgrounds/wave.svg' + +const WAVE_WIDTH = 416 +const WAVE_DURATION = 2000 + +export function WaveLoader(): JSX.Element { + const colors = useSporeColors() + const yPosition = useSharedValue(0) + const { chartHeight } = useChartDimensions() + + useEffect(() => { + yPosition.value = withRepeat(withTiming(1, { duration: WAVE_DURATION }), Infinity, false) + + // only want to do this once on mount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + translateX: interpolate(yPosition.value, [0, 1], [0, -WAVE_WIDTH]), + }, + ], + })) + + return ( + + + + + + + + + + ) +} diff --git a/apps/mobile/src/components/loading/index.tsx b/apps/mobile/src/components/loading/index.tsx new file mode 100644 index 0000000..e0eafff --- /dev/null +++ b/apps/mobile/src/components/loading/index.tsx @@ -0,0 +1,63 @@ +import React, { memo } from 'react' +import { TransactionLoader } from 'src/components/loading/TransactionLoader' +import { WaveLoader } from 'src/components/loading/WaveLoader' +import { Flex, FlexLoader, FlexLoaderProps, getToken, Skeleton } from 'ui/src' + +function Graph(): JSX.Element { + return ( + + + + ) +} + +export const Transaction = memo(function _Transaction({ + repeat = 1, +}: { + repeat?: number +}): JSX.Element { + return ( + + + {new Array(repeat).fill(null).map((_, i, { length }) => ( + + + + ))} + + + ) +}) + +function Box(props: FlexLoaderProps): JSX.Element { + return ( + + + + ) +} + +function Image(): JSX.Element { + return ( + + + + ) +} + +function Favorite({ height, contrast }: { height?: number; contrast?: boolean }): JSX.Element { + return ( + + {/* surface3 because these only show up on explore modal which has a blurred bg that makes neutral3 look weird */} + + + ) +} + +export const Loader = { + Box, + Transaction, + Graph, + Image, + Favorite, +} diff --git a/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx b/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx new file mode 100644 index 0000000..52cbef1 --- /dev/null +++ b/apps/mobile/src/components/mnemonic/HiddenMnemonicWordView.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { Flex, Text } from 'ui/src' + +const LEFT_COLUMN_INDEXES = [1, 2, 3, 4, 5, 6] +const RIGHT_COLUMN_INDEXES = [7, 8, 9, 10, 11, 12] +export function HiddenMnemonicWordView(): JSX.Element { + return ( + + + + + + + + + ) +} + +function HiddenWordViewColumn({ indexes }: { indexes: number[] }): JSX.Element { + return ( + <> + {indexes.map((value) => ( + + {value} + + + ))} + + ) +} diff --git a/apps/mobile/src/components/mnemonic/MnemonicConfirmation.mock.tsx b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.mock.tsx new file mode 100644 index 0000000..e4729d4 --- /dev/null +++ b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.mock.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react' +import { ViewProps } from 'react-native' +import { Flex, flexStyles, HiddenFromScreenReaders, Text } from 'ui/src' + +type MnemonicConfirmationProps = ViewProps & { + mnemonicId: Address + onConfirmComplete: () => void +} + +/** + * Replaces MnemonicConfirmation native screen during e2e testing because detox do not support + * native components + */ +export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element { + useEffect(() => { + props.onConfirmComplete() + }, [props]) + + return ( + + + Mocked confirmation screen + + + ) +} diff --git a/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx new file mode 100644 index 0000000..1f50f6d --- /dev/null +++ b/apps/mobile/src/components/mnemonic/MnemonicConfirmation.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import { requireNativeComponent, StyleProp, ViewProps } from 'react-native' +import { FlexProps, flexStyles, HiddenFromScreenReaders, useDeviceDimensions } from 'ui/src' + +interface NativeMnemonicConfirmationProps { + mnemonicId: Address + shouldShowSmallText: boolean + onConfirmComplete: () => void +} + +const NativeMnemonicConfirmation = + requireNativeComponent('MnemonicConfirmation') + +type MnemonicConfirmationProps = ViewProps & { + mnemonicId: Address + onConfirmComplete: () => void +} + +const mnemonicConfirmationStyle: StyleProp = { + flex: 1, + flexGrow: 1, +} + +export function MnemonicConfirmation(props: MnemonicConfirmationProps): JSX.Element { + const { fullHeight } = useDeviceDimensions() + const shouldShowSmallText = fullHeight < 700 + + return ( + + + + ) +} diff --git a/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx b/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx new file mode 100644 index 0000000..8498093 --- /dev/null +++ b/apps/mobile/src/components/mnemonic/MnemonicDisplay.tsx @@ -0,0 +1,26 @@ +import React from 'react' +import { requireNativeComponent, StyleSheet, ViewProps } from 'react-native' +import { flexStyles, HiddenFromScreenReaders } from 'ui/src' + +interface NativeMnemonicDisplayProps { + mnemonicId: Address +} + +const NativeMnemonicDisplay = requireNativeComponent('MnemonicDisplay') + +type MnemonicDisplayProps = ViewProps & NativeMnemonicDisplayProps + +const styles = StyleSheet.create({ + mnemonicDisplay: { + flex: 1, + flexGrow: 1, + }, +}) + +export function MnemonicDisplay(props: MnemonicDisplayProps): JSX.Element { + return ( + + + + ) +} diff --git a/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx new file mode 100644 index 0000000..e04ac72 --- /dev/null +++ b/apps/mobile/src/components/mnemonic/SeedPhraseDisplay.tsx @@ -0,0 +1,117 @@ +import { addScreenshotListener } from 'expo-screen-capture' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { usePrevious } from 'react-native-wagmi-charts' +import { HiddenMnemonicWordView } from 'src/components/mnemonic/HiddenMnemonicWordView' +import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { Button, Flex } from 'ui/src' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = { + mnemonicId: string + onDismiss?: () => void + walletNeedsRestore?: boolean +} + +export function SeedPhraseDisplay({ + mnemonicId, + onDismiss, + walletNeedsRestore, +}: Props): JSX.Element { + const { t } = useTranslation() + const { isModalOpen: isWalletRestoreModalOpen } = useWalletRestore({ openModalImmediately: true }) + const [showScreenShotWarningModal, setShowScreenShotWarningModal] = useState(false) + const [showSeedPhrase, setShowSeedPhrase] = useState(false) + const [showSeedPhraseViewWarningModal, setShowSeedPhraseViewWarningModal] = useState( + !walletNeedsRestore + ) + + const prevIsWalletRestoreModalOpen = usePrevious(isWalletRestoreModalOpen) + + useEffect(() => { + if (prevIsWalletRestoreModalOpen && !isWalletRestoreModalOpen) { + onDismiss?.() + } + }) + + const onShowSeedPhraseConfirmed = (): void => { + setShowSeedPhrase(true) + setShowSeedPhraseViewWarningModal(false) + } + + const onConfirmWarning = async (): Promise => { + if (biometricAuthRequiredForAppAccess || biometricAuthRequiredForTransactions) { + await biometricTrigger() + } else { + onShowSeedPhraseConfirmed() + } + } + + const { + requiredForAppAccess: biometricAuthRequiredForAppAccess, + requiredForTransactions: biometricAuthRequiredForTransactions, + } = useBiometricAppSettings() + const { trigger: biometricTrigger } = useBiometricPrompt(onShowSeedPhraseConfirmed) + + useEffect(() => { + const listener = addScreenshotListener(() => setShowScreenShotWarningModal(showSeedPhrase)) + return () => listener?.remove() + }, [showSeedPhrase]) + + return ( + <> + + {showSeedPhrase ? ( + + + + ) : ( + + )} + + + + + + {showSeedPhraseViewWarningModal && ( + { + setShowSeedPhraseViewWarningModal(false) + if (!showSeedPhrase) { + onDismiss?.() + } + }} + onConfirm={onConfirmWarning} + /> + )} + {showScreenShotWarningModal && ( + setShowScreenShotWarningModal(false)} + /> + )} + + ) +} diff --git a/apps/mobile/src/components/mnemonic/constants.ts b/apps/mobile/src/components/mnemonic/constants.ts new file mode 100644 index 0000000..f9f7b20 --- /dev/null +++ b/apps/mobile/src/components/mnemonic/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_MNEMONIC_DISPLAY_HEIGHT = 300 +export const FULL_MNEMONIC_DISPLAY_HEIGHT = 348 diff --git a/apps/mobile/src/components/modals/Modal.tsx b/apps/mobile/src/components/modals/Modal.tsx new file mode 100644 index 0000000..613cdae --- /dev/null +++ b/apps/mobile/src/components/modals/Modal.tsx @@ -0,0 +1,107 @@ +import { Modal as BaseModal, ModalProps, StyleSheet, View } from 'react-native' +import { CloseButton } from 'src/components/buttons/CloseButton' +import { Flex, FlexProps, Text, TouchableArea } from 'ui/src' + +interface Props extends ModalProps { + position?: 'top' | 'center' | 'bottom' + title?: string + hide?: () => void + dismissable?: boolean + showCloseButton?: boolean + width?: number | '100%' + dimBackground?: boolean +} + +// TODO: [MOB-197] excluding some props here due to bug with React Native's Modal and reanimated +// May be resolved after upgrading reanimated to latest but uncertain +// https://github.com/facebook/react-native/issues/32329 +export function Modal({ + animationType = 'none', + visible, + hide, + showCloseButton, + title, + position, + width, + dimBackground, + children, + dismissable = true, + transparent = true, + presentationStyle = 'overFullScreen', +}: // ...rest (TODO above) +Props): JSX.Element { + let justifyContent: FlexProps['justifyContent'] = 'center' + if (position === 'top') { + justifyContent = 'flex-start' + } + if (position === 'bottom') { + justifyContent = 'flex-end' + } + + return ( + + + + {title && ( + + {title} + + )} + {hide && showCloseButton && ( + + + + )} + {children} + + + + ) +} + +const modalBoxBaseStyle = StyleSheet.create({ + base: { + alignItems: 'center', + borderRadius: 15, + elevation: 5, + margin: 20, + padding: 20, + position: 'relative', + shadowColor: '#000', + shadowOffset: { + height: 2, + width: 0, + }, + shadowOpacity: 0.25, + shadowRadius: 4, + }, +}) + +const style = StyleSheet.create({ + bgDimmed: { + backgroundColor: 'rgba(0,0,0,0.3)', + }, + closeButtonContainer: { + position: 'absolute', + right: 20, + top: 20, + }, + modalBox: modalBoxBaseStyle.base, + modalBoxFullWidth: { + ...modalBoxBaseStyle.base, + margin: 0, + padding: 0, + }, +}) diff --git a/apps/mobile/src/components/notifications/Badge.tsx b/apps/mobile/src/components/notifications/Badge.tsx new file mode 100644 index 0000000..ddfeab6 --- /dev/null +++ b/apps/mobile/src/components/notifications/Badge.tsx @@ -0,0 +1,34 @@ +import React, { memo, PropsWithChildren } from 'react' +import { Flex } from 'ui/src' +import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' + +type Props = PropsWithChildren<{ + address: Address +}> + +const NOTIFICATION_DOT_SIZE = 12 + +function _NotificationBadge({ children, address }: Props): JSX.Element { + const hasNotifications = useSelectAddressHasNotifications(address) + return ( + + {hasNotifications ? ( + + ) : null} + {children} + + ) +} + +export const NotificationBadge = memo(_NotificationBadge) diff --git a/apps/mobile/src/components/sortableGrid/ActiveItemDecoration.tsx b/apps/mobile/src/components/sortableGrid/ActiveItemDecoration.tsx new file mode 100644 index 0000000..533c43d --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/ActiveItemDecoration.tsx @@ -0,0 +1,82 @@ +import { PropsWithChildren } from 'react' +import { StyleSheet } from 'react-native' +import Animated, { + interpolate, + interpolateColor, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import { colors, opacify } from 'ui/src/theme' +import { useSortableGridContext } from './SortableGridProvider' + +type ActiveItemDecorationProps = PropsWithChildren<{ + renderIndex: number +}> + +export default function ActiveItemDecoration({ + renderIndex, + children, +}: ActiveItemDecorationProps): JSX.Element { + const { + touchedIndex, + activeItemScale, + previousActiveIndex, + activeItemOpacity, + activeItemShadowOpacity, + dragActivationProgress, + } = useSortableGridContext() + + const pressProgress = useSharedValue(0) + + useAnimatedReaction( + () => ({ + isTouched: touchedIndex.value === renderIndex, + wasTouched: previousActiveIndex.value === renderIndex, + progress: dragActivationProgress.value, + }), + ({ isTouched, wasTouched, progress }) => { + if (isTouched) { + // If the item is currently touched, we want to animate the press progress + // (change the decoration) based on the drag activation progress + pressProgress.value = Math.max(pressProgress.value, progress) + } else if (wasTouched) { + // If the item was touched (the user released the finger) and the item + // was previously touched, we want to animate it based on the decreasing + // press progress + pressProgress.value = Math.min(pressProgress.value, progress) + } else { + // For all other cases, we want to ensure that the press progress is reset + // and all non-touched items are not decorated + pressProgress.value = withTiming(0) + } + } + ) + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [ + { + scale: interpolate(pressProgress.value, [0, 1], [1, activeItemScale.value]), + }, + ], + opacity: interpolate(pressProgress.value, [0, 1], [1, activeItemOpacity.value]), + shadowColor: interpolateColor( + pressProgress.value, + [0, 1], + ['transparent', opacify(100 * activeItemShadowOpacity.value, colors.black)] + ), + })) + + return {children} +} + +const styles = StyleSheet.create({ + shadow: { + borderRadius: 0, + elevation: 40, + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.25, + shadowRadius: 5, + }, +}) diff --git a/apps/mobile/src/components/sortableGrid/SortableGrid.tsx b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx new file mode 100644 index 0000000..5224ada --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/SortableGrid.tsx @@ -0,0 +1,136 @@ +import { memo, useRef } from 'react' +import { LayoutChangeEvent, MeasureLayoutOnSuccessCallback, View } from 'react-native' +import { Flex, FlexProps } from 'ui/src' +import SortableGridItem from './SortableGridItem' +import SortableGridProvider, { useSortableGridContext } from './SortableGridProvider' +import { useStableCallback } from './hooks' +import { AutoScrollProps, SortableGridChangeEvent, SortableGridRenderItem } from './types' +import { defaultKeyExtractor } from './utils' + +type SortableGridProps = Omit, 'keyExtractor'> & + AutoScrollProps & { + onChange: (e: SortableGridChangeEvent) => void + onDragStart?: () => void + onDragEnd?: () => void + keyExtractor?: (item: I, index: number) => string + editable?: boolean + activeItemScale?: number + activeItemOpacity?: number + activeItemShadowOpacity?: number + } + +function SortableGrid({ + data, + onDragStart, + onDragEnd, + keyExtractor: keyExtractorProp, + activeItemScale, + activeItemOpacity, + activeItemShadowOpacity, + onChange: onChangeProp, + scrollableRef, + scrollY, + visibleHeight, + editable, + numColumns = 1, + ...rest +}: SortableGridProps): JSX.Element { + const keyExtractor = useStableCallback(keyExtractorProp ?? defaultKeyExtractor) + const onChange = useStableCallback(onChangeProp) + + const providerProps = { + activeItemScale, + activeItemOpacity, + activeItemShadowOpacity, + data, + editable, + onChange, + scrollY, + scrollableRef, + visibleHeight, + onDragStart, + onDragEnd, + } + + const gridProps = { + data, + keyExtractor, + numColumns, + scrollableRef, + ...rest, + } + + return ( + + + + ) +} + +type SortableGridInnerProps = FlexProps & { + keyExtractor: (item: I, index: number) => string + numColumns?: number + data: I[] + renderItem: SortableGridRenderItem + containerRef?: React.RefObject +} + +function SortableGridInner({ + data, + renderItem, + numColumns = 1, + keyExtractor, + containerRef, + ...flexProps +}: SortableGridInnerProps): JSX.Element { + const { gridContainerRef, containerStartOffset, containerEndOffset, touchedIndex } = + useSortableGridContext() + const internalDataRef = useRef(data) + + const measureContainer = useStableCallback((e: LayoutChangeEvent) => { + // If there is no parent element, assume the grid is the first child + // in the scrollable container + if (!containerRef?.current) { + containerEndOffset.value = e.nativeEvent.layout.height + return + } + + // Otherwise, measure its offset relative to the scrollable container + const onSuccess: MeasureLayoutOnSuccessCallback = (x, y, w, h) => { + containerStartOffset.value = y + containerEndOffset.value = y + h + } + + const parentNode = containerRef.current + const gridNode = gridContainerRef.current + + if (gridNode) { + gridNode.measureLayout(parentNode, onSuccess) + } + }) + + // Update only if the user doesn't interact with the grid + // (we don't want to reorder items based on the input data + // while the user is dragging an item) + if (touchedIndex.value === null) { + internalDataRef.current = data + } + + return ( + + {internalDataRef.current.map((item, index) => ( + + ))} + + ) +} + +const MemoSortableGridInner = memo(SortableGridInner) as typeof SortableGridInner + +export default SortableGrid diff --git a/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx new file mode 100644 index 0000000..e631e9f --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/SortableGridItem.tsx @@ -0,0 +1,286 @@ +import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import { MeasureLayoutOnSuccessCallback, StyleSheet } from 'react-native' +import { Gesture, GestureDetector } from 'react-native-gesture-handler' +import Animated, { + runOnJS, + runOnUI, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + useWorkletCallback, + withTiming, +} from 'react-native-reanimated' +import ActiveItemDecoration from './ActiveItemDecoration' +import { useSortableGridContext } from './SortableGridProvider' +import { TIME_TO_ACTIVATE_PAN } from './constants' +import { useAnimatedZIndex, useItemOrderUpdater } from './hooks' +import { SortableGridRenderItem } from './types' + +type SortableGridItemProps = { + item: I + index: number + renderItem: SortableGridRenderItem + numColumns: number +} + +function SortableGridItem({ + item, + index: renderIndex, + renderItem, + numColumns, +}: SortableGridItemProps): JSX.Element { + const viewRef = useRef(null) + + // Current state + const { + gridContainerRef, + activeIndex, + activeTranslation: activeTranslationValue, + itemAtIndexMeasurements: itemAtIndexMeasurementsValue, + renderIndexToDisplayIndex, + touchedIndex, + editable, + dragActivationProgress, + setActiveIndex, + previousActiveIndex, + scrollOffsetDiff, + } = useSortableGridContext() + + const isActive = activeIndex === renderIndex + const isActiveValue = useSharedValue(isActive) + const isTouched = useDerivedValue(() => touchedIndex.value === renderIndex) + + useEffect(() => { + isActiveValue.value = isActive + }, [isActive, isActiveValue]) + + // Cell animations + const displayIndexValue = useDerivedValue( + () => renderIndexToDisplayIndex.value[renderIndex] ?? renderIndex + ) + const contentHeight = useSharedValue(0) + // Translation based on cells reordering + // (e.g when the item is swapped with the active item) + const orderTranslateX = useSharedValue(0) + const orderTranslateY = useSharedValue(0) + // Reset order translation on re-render + orderTranslateX.value = 0 + orderTranslateY.value = 0 + // Translation based on the user dragging the item + // (we keep it separate to animate the dropped item to the target + // position without flickering when items are re-rendered in + // the new order and the drop animation has not finished yet) + const dragTranslateX = useSharedValue(0) + const dragTranslateY = useSharedValue(0) + + const zIndex = useAnimatedZIndex(renderIndex) + useItemOrderUpdater(renderIndex, activeIndex, displayIndexValue, numColumns) + + const updateCellMeasurements = useCallback(() => { + const onSuccess: MeasureLayoutOnSuccessCallback = (x, y, w, h) => { + runOnUI(() => { + const currentMeasurements = itemAtIndexMeasurementsValue.value + currentMeasurements[renderIndex] = { x, y, width: w, height: h } + itemAtIndexMeasurementsValue.value = [...currentMeasurements] + contentHeight.value = h + })() + } + + const listContainerNode = gridContainerRef.current + const listItemNode = viewRef.current + + if (listItemNode && listContainerNode) { + listItemNode.measureLayout(listContainerNode, onSuccess) + } + }, [gridContainerRef, itemAtIndexMeasurementsValue, renderIndex, contentHeight]) + + const getItemOrderTranslation = useWorkletCallback(() => { + const itemAtIndexMeasurements = itemAtIndexMeasurementsValue.value + const displayIndex = displayIndexValue.value + const renderMeasurements = itemAtIndexMeasurements[renderIndex] + const displayMeasurements = itemAtIndexMeasurements[displayIndex] + + if (!renderMeasurements || !displayMeasurements) { + return { x: 0, y: 0 } + } + + return { + x: displayMeasurements.x - renderMeasurements.x, + y: displayMeasurements.y - renderMeasurements.y, + } + }, [renderIndex]) + + const handleDragEnd = useWorkletCallback(() => { + dragActivationProgress.value = withTiming(0, { duration: TIME_TO_ACTIVATE_PAN }) + touchedIndex.value = null + if (!isActiveValue.value) { + return + } + // Reset the active item + previousActiveIndex.value = renderIndex + // Reset this before state is updated to disable animated reactions + // earlier (the state is always updated with a delay) + isActiveValue.value = false + + // Translate the previously active item to its target position + const orderTranslation = getItemOrderTranslation() + // Update the current order translation and modify the drag translation + // at the same time (this prevents flickering when items are re-rendered) + dragTranslateX.value = dragTranslateX.value - orderTranslation.x + dragTranslateY.value = dragTranslateY.value + scrollOffsetDiff.value - orderTranslation.y + orderTranslateX.value = orderTranslation.x + orderTranslateY.value = orderTranslation.y + // Animate the remaining translation + dragTranslateX.value = withTiming(0) + dragTranslateY.value = withTiming(0) + + // Reset the active item index + runOnJS(setActiveIndex)(null) + }, [renderIndex, getItemOrderTranslation]) + + // Translates the currently active (dragged) item + useAnimatedReaction( + () => ({ + activeTranslation: activeTranslationValue.value, + active: isActiveValue.value, + }), + ({ active, activeTranslation }) => { + if (!active || touchedIndex.value === null) { + return + } + dragTranslateX.value = activeTranslation.x + dragTranslateY.value = activeTranslation.y + } + ) + + // Translates the item when it's not active and is swapped with the active item + useAnimatedReaction( + () => ({ + displayIndex: displayIndexValue.value, + itemAtIndexMeasurements: itemAtIndexMeasurementsValue.value, + active: isActiveValue.value, + }), + ({ displayIndex, active, itemAtIndexMeasurements }) => { + if (active) { + return + } + + const renderMeasurements = itemAtIndexMeasurements[renderIndex] + const displayMeasurements = itemAtIndexMeasurements[displayIndex] + if (!renderMeasurements || !displayMeasurements) { + return + } + + if (activeIndex !== null && touchedIndex.value !== null) { + // If the order changes as a result of the user dragging an item, + // translate the item to its new position with animation + orderTranslateX.value = withTiming(displayMeasurements.x - renderMeasurements.x) + orderTranslateY.value = withTiming(displayMeasurements.y - renderMeasurements.y) + } else if (renderIndex !== previousActiveIndex.value) { + // If the order changes as a result of the data change, reset + // the item position without animation (it re-renders in the new position, + // so the previously applied translation is no longer valid) + orderTranslateX.value = 0 + orderTranslateY.value = 0 + } + }, + [renderIndex, activeIndex] + ) + + const panGesture = useMemo( + () => + Gesture.Pan() + .activateAfterLongPress(TIME_TO_ACTIVATE_PAN) + .onTouchesDown(() => { + touchedIndex.value = renderIndex + previousActiveIndex.value = null + dragActivationProgress.value = withTiming(1, { duration: TIME_TO_ACTIVATE_PAN }) + }) + .onStart(() => { + if (touchedIndex.value !== renderIndex) { + return + } + activeTranslationValue.value = { x: 0, y: 0 } + dragActivationProgress.value = withTiming(1, { duration: TIME_TO_ACTIVATE_PAN }) + runOnJS(setActiveIndex)(renderIndex) + }) + .onUpdate((e) => { + if (!isActiveValue.value) { + return + } + activeTranslationValue.value = { x: e.translationX, y: e.translationY } + }) + .onTouchesCancelled(handleDragEnd) + .onEnd(handleDragEnd) + .onTouchesUp(handleDragEnd) + .enabled(editable), + [ + activeTranslationValue, + dragActivationProgress, + isActiveValue, + handleDragEnd, + previousActiveIndex, + touchedIndex, + renderIndex, + setActiveIndex, + editable, + ] + ) + + const animatedCellStyle = useAnimatedStyle(() => ({ + zIndex: zIndex.value, + height: contentHeight.value > 0 ? contentHeight.value : undefined, + })) + + const animatedOrderStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: orderTranslateX.value }, { translateY: orderTranslateY.value }], + })) + + const animatedDragStyle = useAnimatedStyle(() => ({ + transform: [ + { translateX: dragTranslateX.value }, + { translateY: dragTranslateY.value + (isActiveValue.value ? scrollOffsetDiff.value : 0) }, + ], + })) + + const content = useMemo( + () => + renderItem({ + index: renderIndex, + item, + dragActivationProgress, + isTouched, + }), + [renderIndex, dragActivationProgress, item, renderItem, isTouched] + ) + + const cellStyle = { + width: `${100 / numColumns}%`, + } + + return ( + // The outer view is used to resize the cell to the size of the new item + // in case the new item height is different than the height of the previous one + + + {/* The inner view will be translated during grid items reordering */} + + + {content} + + + + + ) +} + +const styles = StyleSheet.create({ + noTranslation: { + transform: [{ translateX: 0 }, { translateY: 0 }], + }, +}) + +export default memo(SortableGridItem) as (props: SortableGridItemProps) => JSX.Element diff --git a/apps/mobile/src/components/sortableGrid/SortableGridProvider.tsx b/apps/mobile/src/components/sortableGrid/SortableGridProvider.tsx new file mode 100644 index 0000000..a2e8f42 --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/SortableGridProvider.tsx @@ -0,0 +1,242 @@ +import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' +import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react' +import { View } from 'react-native' +import { + useAnimatedReaction, + useDerivedValue, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import { TIME_TO_ACTIVATE_PAN } from './constants' +import { useAutoScroll, useStableCallback } from './hooks' +import { + AutoScrollProps, + ItemMeasurements, + SortableGridChangeEvent, + SortableGridContextType, +} from './types' + +const SortableGridContext = createContext(null) + +export function useSortableGridContext(): SortableGridContextType { + const context = useContext(SortableGridContext) + + if (!context) { + throw new Error('useSortableGridContext must be used within a SortableGridProvider') + } + + return context +} + +type SortableGridProviderProps = AutoScrollProps & { + data: I[] + children: React.ReactNode + activeItemScale?: number + activeItemOpacity?: number + activeItemShadowOpacity?: number + editable?: boolean + onDragStart?: () => void + onDragEnd?: () => void + onChange: (e: SortableGridChangeEvent) => void +} + +export default function SortableGridProvider({ + children, + onChange, + activeItemScale: activeItemScaleProp = 1.1, + activeItemOpacity: activeItemOpacityProp = 0.7, + activeItemShadowOpacity: activeItemShadowOpacityProp = 0.5, + editable = true, + visibleHeight, + onDragStart, + onDragEnd, + scrollableRef, + scrollY, + data, +}: SortableGridProviderProps): JSX.Element { + const isInitialRenderRef = useRef(true) + const prevDataRef = useRef([]) + + // Active cell settings + const activeItemScale = useDerivedValue(() => activeItemScaleProp) + const activeItemOpacity = useDerivedValue(() => activeItemOpacityProp) + const activeItemShadowOpacity = useDerivedValue(() => activeItemShadowOpacityProp) + + // We have to use a state here because the activeIndex must be + // immediately set to null when the data changes (reanimated shared value + // updates are always delayed and can result in animation flickering) + const [activeIndexState, setActiveIndex] = useState(null) + const previousActiveIndex = useSharedValue(null) + const gridContainerRef = useRef(null) + const touchedIndex = useSharedValue(null) + const activeTranslation = useSharedValue({ x: 0, y: 0 }) + const dragActivationProgress = useSharedValue(0) + const itemAtIndexMeasurements = useSharedValue([]) + + // Tells which item is currently displayed at each index + // (e.g. the item at index 0 in the data array was moved to the index 2 + // in the displayed grid, so the render index of the item at index 2 is 0 + // (the item displayed at index 2 is the item at index 0 in the data array)) + const displayToRenderIndex = useSharedValue(data.map((_, index) => index)) + + // Tells where the item rendered at each index was moved in the displayed grid + // (e.g. the item at index 0 in the data array was moved to the index 2 + // in the displayed grid, so the display index of the item at index 0 is 2) + // (the reverse mapping of displayToRenderIndex) + const renderIndexToDisplayIndex = useDerivedValue(() => { + const result: number[] = [] + const displayToRender = displayToRenderIndex.value + for (let i = 0; i < displayToRender.length; i++) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + result[displayToRender[i]!] = i + } + return result + }) + + // Auto scroll settings + // Values used to scroll the container to the proper offset + // (updated from the SortableGridInner component) + const containerStartOffset = useSharedValue(0) + const containerEndOffset = useSharedValue(0) + const startScrollOffset = useSharedValue(0) + const scrollOffsetDiff = useDerivedValue(() => scrollY.value - startScrollOffset.value) + + let activeIndex = activeIndexState + const dataChanged = + (!isInitialRenderRef.current && prevDataRef.current.length !== data.length) || + prevDataRef.current.some((item, index) => item !== data[index]) + if (dataChanged) { + prevDataRef.current = data + displayToRenderIndex.value = data.map((_, index) => index) + itemAtIndexMeasurements.value = itemAtIndexMeasurements.value.slice(0, data.length) + activeIndex = null + } + + const isDragging = useDerivedValue(() => activeIndex !== null && touchedIndex.value !== null) + + // Automatically scrolls the container when the active item is dragged + // out of the container bounds + useAutoScroll( + activeIndex, + touchedIndex, + itemAtIndexMeasurements, + activeTranslation, + scrollOffsetDiff, + containerStartOffset, + containerEndOffset, + visibleHeight, + scrollY, + scrollableRef + ) + + const handleOrderChange = useStableCallback((fromIndex: number) => { + const toIndex = renderIndexToDisplayIndex.value[fromIndex] + if (toIndex === undefined || toIndex === fromIndex) { + return + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const newData = displayToRenderIndex.value.map((displayIndex) => data[displayIndex]!) + onChange({ data: newData, fromIndex, toIndex }) + }) + + const handleSetActiveIndex = useStableCallback((index: number | null) => { + // Because this function is run from worklet functions with runOnJS, + // it might be executed after the delay, when the item was released + // so we check if the item is still being dragged before setting the + // active index + if ((index === null || touchedIndex.value !== null) && index !== activeIndex) { + impactAsync(index === null ? ImpactFeedbackStyle.Light : ImpactFeedbackStyle.Medium).catch( + () => undefined + ) + if (index !== null) { + onDragStart?.() + } else { + onDragEnd?.() + } + startScrollOffset.value = scrollY.value + setActiveIndex(index) + } + }) + + useEffect(() => { + const prevActiveIndex = previousActiveIndex.value + if (prevActiveIndex !== null) { + handleOrderChange(prevActiveIndex) + activeTranslation.value = { x: 0, y: 0 } + } + }, [ + activeIndex, + previousActiveIndex, + handleOrderChange, + activeTranslation, + startScrollOffset, + scrollY, + ]) + + useEffect(() => { + isInitialRenderRef.current = false + }, []) + + useAnimatedReaction( + () => ({ + isActive: activeIndex !== null, + offsetDiff: scrollOffsetDiff.value, + }), + ({ isActive, offsetDiff }) => { + if (!isActive && Math.abs(offsetDiff) > 0) { + dragActivationProgress.value = withTiming(0, { duration: TIME_TO_ACTIVATE_PAN }) + } + }, + [activeIndex] + ) + + const contextValue = useMemo( + () => ({ + activeTranslation, + gridContainerRef, + activeIndex, + editable, + scrollY, + itemAtIndexMeasurements, + renderIndexToDisplayIndex, + displayToRenderIndex, + setActiveIndex: handleSetActiveIndex, + previousActiveIndex, + touchedIndex, + activeItemScale, + isDragging, + dragActivationProgress, + scrollOffsetDiff, + activeItemOpacity, + visibleHeight, + activeItemShadowOpacity, + containerStartOffset, + containerEndOffset, + }), + [ + activeIndex, + visibleHeight, + activeTranslation, + itemAtIndexMeasurements, + dragActivationProgress, + renderIndexToDisplayIndex, + handleSetActiveIndex, + displayToRenderIndex, + editable, + scrollY, + isDragging, + previousActiveIndex, + scrollOffsetDiff, + touchedIndex, + activeItemScale, + activeItemOpacity, + activeItemShadowOpacity, + containerStartOffset, + containerEndOffset, + ] + ) + + return ( + {children} + ) +} diff --git a/apps/mobile/src/components/sortableGrid/constants.ts b/apps/mobile/src/components/sortableGrid/constants.ts new file mode 100644 index 0000000..1a67cf5 --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/constants.ts @@ -0,0 +1,3 @@ +export const TIME_TO_ACTIVATE_PAN = 300 +export const TOUCH_SLOP = 10 +export const AUTO_SCROLL_THRESHOLD = 50 diff --git a/apps/mobile/src/components/sortableGrid/hooks.ts b/apps/mobile/src/components/sortableGrid/hooks.ts new file mode 100644 index 0000000..2923ec8 --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/hooks.ts @@ -0,0 +1,300 @@ +import { useCallback, useRef } from 'react' +import { FlatList, ScrollView } from 'react-native' +import { SharedValue, runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated' +import { useSortableGridContext } from './SortableGridProvider' +import { AUTO_SCROLL_THRESHOLD } from './constants' +import { ItemMeasurements } from './types' + +export function useStableCallback< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + C extends (...args: Array) => any +>(callback?: C): C { + const callbackRef = useRef(callback) + callbackRef.current = callback + + return useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return + (...args: Array) => callbackRef.current?.(...args), + [] + ) as C +} + +export function useAnimatedZIndex(renderIndex: number): SharedValue { + const { touchedIndex: touchedIndexValue, previousActiveIndex: previousActiveIndexValue } = + useSortableGridContext() + const zIndexValue = useSharedValue(0) + + useAnimatedReaction( + () => ({ + touchedIndex: touchedIndexValue.value, + previousActiveIndex: previousActiveIndexValue.value, + }), + ({ touchedIndex, previousActiveIndex }) => { + if (touchedIndex === null) { + return null + } + if (renderIndex === touchedIndex) { + // Display the currently touched item on top of all other items + zIndexValue.value = 10000 + } else if (renderIndex === previousActiveIndex) { + // Display the previously active item on top of other items + // except the currently touched item (used to properly position + // items before their drop animations finishes) + zIndexValue.value = 9999 + } else { + zIndexValue.value = 0 + } + } + ) + + return zIndexValue +} + +export function useItemOrderUpdater( + renderIndex: number, + activeRenderIndex: number | null, + displayIndexValue: SharedValue, + numColumns: number +): void { + const { + touchedIndex: touchedIndexValue, + activeTranslation: activeTranslationValue, + itemAtIndexMeasurements: itemAtIndexMeasurementsValue, + displayToRenderIndex: displayToRenderIndexValue, + renderIndexToDisplayIndex: renderIndexToDisplayIndexValue, + scrollOffsetDiff: scrollOffsetDiffValue, + } = useSortableGridContext() + + useAnimatedReaction( + () => ({ + activeTranslation: activeTranslationValue.value, + displayIndex: displayIndexValue.value, + scrollOffsetDiff: scrollOffsetDiffValue.value, + touchedIndex: touchedIndexValue.value, + }), + ({ displayIndex, activeTranslation, scrollOffsetDiff, touchedIndex }) => { + // Return if there is no active item or the current item is the active item + // (only active item neighbors decide if the order should be updated) + if ( + activeRenderIndex === null || + touchedIndex === null || + activeRenderIndex === renderIndex + ) { + return + } + + const itemAtIndexMeasurements = itemAtIndexMeasurementsValue.value + // The current item might have moved so we get its measurements based on + // the display index (e.g. if the item that is at index 0 in the data array + // was moved to index 1, its current offset is determined by the cell that + // is at index 1 in the grid) + const itemMeasurements = itemAtIndexMeasurements[displayIndex] + // For the active item, we always use the render index to get the measurements + // as the activeTranslation is calculated in relation to the render position + const activeItemMeasurements = itemAtIndexMeasurements[activeRenderIndex] + if (!itemMeasurements || !activeItemMeasurements) { + return + } + + // Active item data + const activeAbsoluteX = activeItemMeasurements.x + activeTranslation.x + const activeAbsoluteY = activeItemMeasurements.y + activeTranslation.y + const activeWidth = activeItemMeasurements.width + const activeHeight = activeItemMeasurements.height + + // Current item data + const itemAbsoluteX = itemMeasurements.x + const itemAbsoluteY = itemMeasurements.y - scrollOffsetDiff + const itemWidth = itemMeasurements.width + const itemHeight = itemMeasurements.height + + // + // Check if the current element is on the boundary of the container + // + const renderIndexToDisplayIndex = renderIndexToDisplayIndexValue.value + + const itemsCount = renderIndexToDisplayIndex.length + const columnIndex = displayIndex % numColumns + const rowIndex = Math.floor(displayIndex / numColumns) + const itemsInColumnCount = + Math.floor(itemsCount / numColumns) + (columnIndex < itemsCount % numColumns ? 1 : 0) + + const isInFirstRow = displayIndex < numColumns + const isLastInColumn = rowIndex === itemsInColumnCount - 1 + const isInLastColumn = columnIndex === numColumns - 1 + const isInFirstColumn = columnIndex === 0 + + // Return if the active item is not overlapping the current item + // by at least 50% in any direction (with the exception of the boundary items) + if ( + // Right neighbor of the active item + (activeAbsoluteX + activeWidth < itemAbsoluteX + itemWidth / 2 && !isInFirstColumn) || + // Left neighbor of the active item + (activeAbsoluteX > itemAbsoluteX + itemWidth / 2 && !isInLastColumn) || + // Bottom neighbor of the active item + (activeAbsoluteY + activeHeight < itemAbsoluteY + itemHeight / 2 && !isInFirstRow) || + // Top neighbor of the active item + (activeAbsoluteY > itemAbsoluteY + itemHeight / 2 && !isLastInColumn) + ) { + return + } + + const displayToRenderIndex = displayToRenderIndexValue.value + const activeDisplayIndex = renderIndexToDisplayIndex[activeRenderIndex] + if (activeDisplayIndex === undefined) { + return + } + + // + // Swap the order of the current item and the active item + // + if (displayIndex < activeDisplayIndex) { + // Insert the current item before the active item + displayToRenderIndexValue.value = [ + ...displayToRenderIndex.slice(0, displayIndex), + activeRenderIndex, + ...displayToRenderIndex.slice(displayIndex, activeDisplayIndex), + ...displayToRenderIndex.slice(activeDisplayIndex + 1), + ] + } else { + // Insert the current item after the active item + displayToRenderIndexValue.value = [ + ...displayToRenderIndex.slice(0, activeDisplayIndex), + ...displayToRenderIndex.slice(activeDisplayIndex + 1, displayIndex + 1), + activeRenderIndex, + ...displayToRenderIndex.slice(displayIndex + 1), + ] + } + }, + [renderIndex, activeRenderIndex] + ) +} + +export function useAutoScroll( + activeIndex: number | null, + touchedIndex: SharedValue, + itemAtIndexMeasurements: SharedValue>, + activeTranslation: SharedValue<{ x: number; y: number }>, + scrollOffsetDiff: SharedValue, + containerStartOffset: SharedValue, + containerEndOffset: SharedValue, + visibleHeightValue: SharedValue, + scrollYValue: SharedValue, + scrollableRef: React.RefObject +): void { + const scrollTarget = useSharedValue(0) + const scrollDirection = useSharedValue(0) // 1 = down, -1 = up + + const scrollToOffset = useStableCallback((offset: number) => { + const scrollable = scrollableRef.current + if (!scrollable || touchedIndex.value === null) { + return + } + + if ('scrollTo' in scrollable) { + scrollable.scrollTo({ y: offset, animated: true }) + } else { + scrollable.scrollToOffset({ offset, animated: true }) + } + }) + + useAnimatedReaction( + () => touchedIndex.value, + () => { + // Reset when the active index changes + scrollDirection.value = 0 + } + ) + + useAnimatedReaction( + () => { + if (activeIndex === null) { + return null + } + const activeMeasurements = itemAtIndexMeasurements.value[activeIndex] + if (!activeMeasurements) { + return null + } + + return { + itemAbsoluteY: + activeMeasurements.y + + activeTranslation.value.y + + containerStartOffset.value + + scrollOffsetDiff.value, + activeHeight: activeMeasurements.height, + minOffset: containerStartOffset.value, + maxOffset: containerEndOffset.value - visibleHeightValue.value, + visibleHeight: visibleHeightValue.value, + scrollY: scrollYValue.value, + } + }, + (props) => { + if (!props) { + return + } + const { itemAbsoluteY, scrollY, minOffset, maxOffset, activeHeight, visibleHeight } = props + + let currentScrollTarget = scrollTarget.value + let currentScrollDirection = scrollDirection.value + + /** + * |----------------------| + * | content above grid | + * |----------------------| <- minOffset (- threshold to scroll a bit above the grid) + * | invisible grid above | + * | (optional) | - if the scrollable container was scrolled down + * |----------------------| + * | visible grid part | + * |----------------------| + * | invisible grid below | - if the scrollable container was scrolled up enough + * | (optional) | + * |----------------------| <- maxOffset (+ threshold to scroll a bit below the grid) + * | content below grid | + * |----------------------| + */ + // If the active item is above the current scroll position (with small threshold + // to start scrolling earlier) and the scroll position is not at the top of the + // grid, scroll up + if ( + itemAbsoluteY < scrollY + AUTO_SCROLL_THRESHOLD && + scrollY > minOffset - AUTO_SCROLL_THRESHOLD + ) { + currentScrollTarget = Math.max(minOffset - AUTO_SCROLL_THRESHOLD, scrollY - activeHeight) + currentScrollDirection = -1 + } + // If the active item is below the current scroll position (with small threshold + // to start scrolling earlier) and the scroll position is not at the bottom of the + // grid, scroll down + else if ( + itemAbsoluteY + activeHeight > scrollY + visibleHeight - AUTO_SCROLL_THRESHOLD && + scrollY < maxOffset + AUTO_SCROLL_THRESHOLD + ) { + currentScrollTarget = Math.min(maxOffset + AUTO_SCROLL_THRESHOLD, scrollY + activeHeight) + currentScrollDirection = 1 + } + + const scrollDiff = Math.abs(currentScrollTarget - scrollTarget.value) + + if ( + // Don't scroll if the difference is too small (limit JS thread updates that + // become laggy when too many are triggered) and the scroll direction is the same + // as before and the scroll target is still far enough from the min/max offset + (scrollDiff < 0.75 * activeHeight && + currentScrollDirection === scrollDirection.value && + currentScrollTarget > minOffset - AUTO_SCROLL_THRESHOLD && + currentScrollTarget < maxOffset + AUTO_SCROLL_THRESHOLD) || + // Don't scroll if the difference is too small and the target can be considered + // reached + Math.abs(scrollDiff) < 2 + ) { + return + } + + scrollDirection.value = currentScrollDirection + scrollTarget.value = currentScrollTarget + runOnJS(scrollToOffset)(currentScrollTarget) + }, + [activeIndex] + ) +} diff --git a/apps/mobile/src/components/sortableGrid/index.ts b/apps/mobile/src/components/sortableGrid/index.ts new file mode 100644 index 0000000..4c53e56 --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/index.ts @@ -0,0 +1,2 @@ +export { default as SortableGrid } from './SortableGrid' +export * from './types' diff --git a/apps/mobile/src/components/sortableGrid/types.ts b/apps/mobile/src/components/sortableGrid/types.ts new file mode 100644 index 0000000..2b90f5d --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/types.ts @@ -0,0 +1,64 @@ +import { FlatList, ScrollView, View } from 'react-native' +import { SharedValue } from 'react-native-reanimated' + +export type Require = Required> & Omit + +export type ItemMeasurements = { + height: number + width: number + x: number + y: number +} + +export type AutoScrollProps = { + scrollableRef: React.RefObject + visibleHeight: SharedValue + scrollY: SharedValue + // The parent container inside the scrollable that wraps the grid + // (e.g. when the grid is rendered inside the FlatList header) + // if not provided, we assume that the grid is the first child in + // the scrollable container + containerRef?: React.RefObject +} + +export type SortableGridContextType = { + gridContainerRef: React.RefObject + itemAtIndexMeasurements: SharedValue + dragActivationProgress: SharedValue + activeIndex: number | null + previousActiveIndex: SharedValue + activeTranslation: SharedValue<{ x: number; y: number }> + scrollOffsetDiff: SharedValue + renderIndexToDisplayIndex: SharedValue + setActiveIndex: (index: number | null) => void + onDragStart?: () => void + displayToRenderIndex: SharedValue + activeItemScale: SharedValue + visibleHeight: SharedValue + activeItemOpacity: SharedValue + activeItemShadowOpacity: SharedValue + touchedIndex: SharedValue + editable: boolean + containerStartOffset: SharedValue + containerEndOffset: SharedValue +} + +export type SortableGridRenderItemInfo = { + item: I + index: number + dragActivationProgress: SharedValue + isTouched: SharedValue +} + +export type SortableGridRenderItem = (info: SortableGridRenderItemInfo) => JSX.Element + +export type Vector = { + x: number + y: number +} + +export type SortableGridChangeEvent = { + data: I[] + fromIndex: number + toIndex: number +} diff --git a/apps/mobile/src/components/sortableGrid/utils.ts b/apps/mobile/src/components/sortableGrid/utils.ts new file mode 100644 index 0000000..f2a7bdc --- /dev/null +++ b/apps/mobile/src/components/sortableGrid/utils.ts @@ -0,0 +1,29 @@ +import { FlatList, ScrollView } from 'react-native' + +const hasProp = ( + object: O, + prop: P +): object is O & Record => { + return prop in object +} + +export const defaultKeyExtractor = (item: I, index: number): string => { + if (typeof item === 'string') { + return item + } + + if (typeof item === 'object' && item !== null) { + if (hasProp(item, 'id')) { + return String(item.id) + } + if (hasProp(item, 'key')) { + return String(item.key) + } + } + + return String(index) +} + +export const isScrollView = (scrollable: ScrollView | FlatList): scrollable is ScrollView => { + return 'scrollTo' in scrollable +} diff --git a/apps/mobile/src/components/text/AnimatedText.test.tsx b/apps/mobile/src/components/text/AnimatedText.test.tsx new file mode 100644 index 0000000..3a51ebc --- /dev/null +++ b/apps/mobile/src/components/text/AnimatedText.test.tsx @@ -0,0 +1,93 @@ +import { fireEvent, render } from '@testing-library/react-native' +import React from 'react' +import { makeMutable } from 'react-native-reanimated' +import { act } from 'react-test-renderer' +import { AnimatedText } from 'src/components/text/AnimatedText' +import { renderWithProviders } from 'src/test/render' + +describe(AnimatedText, () => { + it('renders without error', () => { + const tree = render() + + expect(tree).toMatchInlineSnapshot(` + + `) + }) + + describe('when text is in the loading state', () => { + it('displays text placeholder with loading shimmer when the loading property is true', async () => { + const tree = renderWithProviders() + + const shimmerPlaceholder = tree.getByTestId('shimmer-placeholder') + + fireEvent(shimmerPlaceholder, 'layout', { + nativeEvent: { + layout: { + width: 100, + height: 100, + }, + }, + }) + + const textPlaceholder = tree.queryByTestId('text-placeholder') + const shimmer = await tree.findByTestId('shimmer') + + expect(textPlaceholder).toBeTruthy() + expect(shimmer).toBeTruthy() + }) + + it('displays the loading placeholder without shimmer when the loading property has "no-shimmer" value', () => { + const tree = renderWithProviders() + + const shimmerPlaceholder = tree.queryByTestId('shimmer-placeholder') + expect(shimmerPlaceholder).toBeFalsy() + + const textPlaceholder = tree.queryByTestId('text-placeholder') + const shimmer = tree.queryByTestId('shimmer') + + expect(textPlaceholder).toBeTruthy() + expect(shimmer).toBeFalsy() + }) + }) + + describe('when text is not in the loading state', () => { + it('updates text when text value is modified', async () => { + const textValue = makeMutable('Initial') + const tree = renderWithProviders() + + expect(tree.queryByDisplayValue('Initial')).toBeTruthy() + + textValue.value = 'Updated' + + await act(() => { + // We must re-render the component to see the updated text + // (updating the animated value does not trigger a re-render and + // doesn't modify props returned in jest's tree) + tree.rerender() + }) + + expect(tree.queryByDisplayValue('Updated')).toBeTruthy() + }) + }) +}) diff --git a/apps/mobile/src/components/text/AnimatedText.tsx b/apps/mobile/src/components/text/AnimatedText.tsx new file mode 100644 index 0000000..4cb62ca --- /dev/null +++ b/apps/mobile/src/components/text/AnimatedText.tsx @@ -0,0 +1,129 @@ +import React from 'react' +import { + TextProps as RNTextProps, + StyleSheet, + TextInput, + TextInputProps, + useWindowDimensions, +} from 'react-native' +import Animated, { useAnimatedProps } from 'react-native-reanimated' +import { Flex, TextProps as TamaTextProps, TextFrame, usePropsAndStyle } from 'ui/src' +import { TextLoaderWrapper } from 'ui/src/components/text/Text' +import { fonts } from 'ui/src/theme' + +// base animated text component using a TextInput +// forked from https://github.com/wcandillon/react-native-redash/blob/master/src/ReText.tsx +// and modified to support the loading state +Animated.addWhitelistedNativeProps({ text: true }) + +type TextPropsBase = TamaTextProps & Omit + +type TextProps = TextPropsBase & { + text?: Animated.SharedValue + style?: Animated.AnimateProps['style'] + loading?: boolean | 'no-shimmer' + loadingPlaceholderText?: string +} + +const AnimatedTextInput = Animated.createAnimatedComponent(TextInput) + +export const BaseAnimatedText = ({ + style, + text, + loading, + loadingPlaceholderText = '000.00', + ...rest +}: TextProps): JSX.Element => { + const animatedProps = useAnimatedProps(() => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return { + text: text?.value, + // Here we use any because the text prop is not available in the type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any + }) + + if (loading) { + return ( + + + {/* Use empty input for loading shimmer height calculation (it is different + than the text component height) */} + + {/* Use the text component to properly calculate the width of the loading shimmer. + An input component with a width dependent on the length of the content was sometimes + rendered with a very small width regardless of the text passed as a value */} + + {loadingPlaceholderText} + + + + ) + } + + return ( + + ) +} +// end of forked from https://github.com/wcandillon/react-native-redash/blob/master/src/ReText.tsx + +// gives you tamagui props with reanimated support +/** + * @deprecated Prefer + * + * See: https://tamagui.dev/docs/core/animations + * + * TODO(MOB-1948): Remove this + * */ +export const AnimatedText = ({ style, ...propsIn }: TextProps): JSX.Element => { + const variant = propsIn.variant ?? 'body2' + const [props, textStyles] = usePropsAndStyle( + { + variant, + ...propsIn, + }, + { + forComponent: TextFrame, + } + ) + + const { fontScale } = useWindowDimensions() + const enableFontScaling = fontScale > 1 + const multiplier = fonts[variant].maxFontSizeMultiplier + + return ( + + ) +} + +const styles = StyleSheet.create({ + input: { + padding: 0, // inputs have default padding on Android + }, + loadingInput: { + marginHorizontal: 0, + opacity: 0, + paddingHorizontal: 0, + width: 0, + }, + loadingPlaceholder: { + opacity: 0, + }, +}) diff --git a/apps/mobile/src/components/text/DecimalNumber.test.tsx b/apps/mobile/src/components/text/DecimalNumber.test.tsx new file mode 100644 index 0000000..626917c --- /dev/null +++ b/apps/mobile/src/components/text/DecimalNumber.test.tsx @@ -0,0 +1,25 @@ +import React from 'react' +import { DecimalNumber } from 'src/components/text/DecimalNumber' +import { render } from 'src/test/test-utils' + +it('renders a DecimalNumber', () => { + const tree = render( + + ) + + expect(tree).toMatchSnapshot() +}) + +it('renders a DecimalNumber without a comma separator', () => { + const tree = render( + + ) + + expect(tree).toMatchSnapshot() +}) + +it('renders a DecimalNumber without a decimal part', () => { + const tree = render() + + expect(tree).toMatchSnapshot() +}) diff --git a/apps/mobile/src/components/text/DecimalNumber.tsx b/apps/mobile/src/components/text/DecimalNumber.tsx new file mode 100644 index 0000000..c8d669f --- /dev/null +++ b/apps/mobile/src/components/text/DecimalNumber.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { Text, TextProps } from 'ui/src' +import { TextVariantTokens } from 'ui/src/theme' + +type DecimalNumberProps = TextProps & { + number?: number + formattedNumber: string + separator?: string + variant: TextVariantTokens + loading?: boolean + decimalThreshold?: number // below this value (not including) decimal part would have wholePartColor too +} + +// Utility component to display decimal numbers where the decimal part +// is dimmed +export function DecimalNumber({ + loading = false, + number, + formattedNumber, + separator = '.', + variant, + decimalThreshold = 1, + ...rest +}: DecimalNumberProps): JSX.Element { + const [pre, post] = formattedNumber.split(separator) + + const decimalPartColor = + number === undefined || number >= decimalThreshold ? '$neutral3' : '$neutral1' + + return ( + + {pre} + {post && ( + + {separator} + {post} + + )} + + ) +} diff --git a/apps/mobile/src/components/text/LongMarkdownText.test.tsx b/apps/mobile/src/components/text/LongMarkdownText.test.tsx new file mode 100644 index 0000000..c6adee1 --- /dev/null +++ b/apps/mobile/src/components/text/LongMarkdownText.test.tsx @@ -0,0 +1,155 @@ +import React from 'react' +import { MarkdownProps } from 'react-native-markdown-display' +import { ReactTestInstance } from 'react-test-renderer' +import { LongMarkdownText } from 'src/components/text/LongMarkdownText' +import { fireEvent, render, within } from 'src/test/test-utils' +import { fonts } from 'ui/src/theme' + +const TEXT_VARIANT = 'body2' +const LINE_HEIGHT = fonts[TEXT_VARIANT].lineHeight + +const SHORT_TEXT = 'Short text' +const LONG_TEXT = 'Some very long text' + +jest.mock('react-native-markdown-display', () => { + const Markdown = jest.requireActual('react-native-markdown-display').default + + return { + __esModule: true, // this property makes Markdown renderering work in the es module + default: jest.fn().mockImplementation((props: MarkdownProps) => ), + } +}) + +const fireLayoutEvent = (instance: ReactTestInstance, lines: number): void => { + const height = lines * LINE_HEIGHT + + fireEvent(instance, 'layout', { + nativeEvent: { + layout: { + height, + width: 100, + }, + }, + }) +} + +const renderMarkdown = (text: string): ReturnType => + render() + +const measureMarkdown = (tree: ReturnType, numberOfLines: number): void => { + const markdownWrapperInstance = tree.getByTestId('markdown-wrapper') + fireLayoutEvent(markdownWrapperInstance, numberOfLines) +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const getMarkdownPropsWithHeight = (height: number | 'auto'): any => + expect.objectContaining({ + style: expect.objectContaining({ + body: expect.objectContaining({ + height, // height auto means the text doesn't exceed the limit + }), + }), + }) + +describe(LongMarkdownText, () => { + const MockedMarkdown = require('react-native-markdown-display').default as jest.Mock + + it('renders without error', () => { + const tree = renderMarkdown(LONG_TEXT) + + expect(tree).toMatchSnapshot() + }) + + describe('short text not exceeding the limit', () => { + it('shows the entire text', () => { + const tree = renderMarkdown(SHORT_TEXT) + measureMarkdown(tree, 1) // Assume Short text is one line + + // props are at index 0, ref is at index 1 + expect(MockedMarkdown.mock.lastCall[0]).toEqual( + getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + ) + }) + + it('does not display the "read more" button', () => { + const tree = renderMarkdown(SHORT_TEXT) + measureMarkdown(tree, 1) // Assume Short text is one line + + const readMoreButton = tree.queryByTestId('read-more-button') + + expect(readMoreButton).toBeNull() + }) + }) + + describe('long text exceeding the limit', () => { + describe('when the text is not expanded', () => { + it('limits the number of visible lines', () => { + const tree = renderMarkdown(LONG_TEXT) + + measureMarkdown(tree, 5) // Assume Some very long text is five lines + + expect(MockedMarkdown.mock.lastCall[0]).toEqual( + getMarkdownPropsWithHeight(LINE_HEIGHT * 3) // Height is limited to 3 lines + ) + }) + + it('displays the "read more" button', () => { + const tree = renderMarkdown(LONG_TEXT) + + measureMarkdown(tree, 5) // Assume Some very long text is five lines + + const readMoreButton = tree.queryByTestId('read-more-button') + + expect(readMoreButton).toBeTruthy() + expect(within(readMoreButton!).getByText('Read more')).toBeTruthy() + }) + }) + + describe('when the text is expanded', () => { + it('shows the entire text', () => { + const tree = renderMarkdown(LONG_TEXT) + + measureMarkdown(tree, 5) // Assume Some very long text is five lines + + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) + + expect(MockedMarkdown.mock.lastCall[0]).toEqual( + getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + ) + }) + + it('displays the "read less" button', () => { + const tree = renderMarkdown(LONG_TEXT) + + measureMarkdown(tree, 5) // Assume Some very long text is five lines + + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) + + expect(readMoreButton).toBeTruthy() + + expect(within(readMoreButton!).getByText('Read less')).toBeTruthy() + }) + }) + + it('toggles the text when the "read more/less" button is pressed', () => { + const tree = renderMarkdown(LONG_TEXT) + + measureMarkdown(tree, 5) // Assume Some very long text is five lines + + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) // expand + + expect(MockedMarkdown.mock.lastCall[0]).toEqual( + getMarkdownPropsWithHeight('auto') // height auto means the text doesn't exceed the limit + ) + + fireEvent.press(readMoreButton) // collapse + + expect(MockedMarkdown.mock.lastCall[0]).toEqual( + getMarkdownPropsWithHeight(LINE_HEIGHT * 3) // Height is limited to 3 lines + ) + }) + }) +}) diff --git a/apps/mobile/src/components/text/LongMarkdownText.tsx b/apps/mobile/src/components/text/LongMarkdownText.tsx new file mode 100644 index 0000000..491fda5 --- /dev/null +++ b/apps/mobile/src/components/text/LongMarkdownText.tsx @@ -0,0 +1,126 @@ +import React, { useCallback, useReducer, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { LayoutChangeEvent } from 'react-native' +import Markdown, { MarkdownProps } from 'react-native-markdown-display' +import { Flex, SpaceTokens, Text, useSporeColors } from 'ui/src' +import { fonts } from 'ui/src/theme' +import { openUri } from 'wallet/src/utils/linking' + +type LongMarkdownTextProps = { + initialDisplayedLines?: number + text: string + gap?: SpaceTokens + color?: string + linkColor?: string + codeBackgroundColor?: string + readMoreOrLessColor?: string + variant?: keyof typeof fonts +} + +export function LongMarkdownText(props: LongMarkdownTextProps): JSX.Element { + const colors = useSporeColors() + const { t } = useTranslation() + const { + initialDisplayedLines = 3, + text, + gap = '$spacing8', + color = colors.neutral1.val, + linkColor = colors.neutral2.val, + readMoreOrLessColor = colors.neutral2.val, + codeBackgroundColor = colors.surface3.val, + variant = 'body2', + } = props + + const [expanded, toggleExpanded] = useReducer((isExpanded) => !isExpanded, true) + const [textLengthExceedsLimit, setTextLengthExceedsLimit] = useState(false) + const [textLineHeight, setTextLineHeight] = useState(fonts[variant].lineHeight) + const initialContentHeightRef = useRef() + const maxVisibleHeight = textLineHeight * initialDisplayedLines + + const onMarkdownLayout = useCallback( + (event: LayoutChangeEvent) => { + if (initialContentHeightRef.current !== undefined) { + return + } + const textContentHeight = event.nativeEvent.layout.height + const currentLines = Math.floor(textContentHeight / textLineHeight) + setTextLengthExceedsLimit(currentLines > initialDisplayedLines) + toggleExpanded() + initialContentHeightRef.current = textContentHeight + }, + [initialDisplayedLines, textLineHeight] + ) + + const codeStyle = { backgroundColor: codeBackgroundColor, borderColor: 'transparent' } + + const markdownStyle: MarkdownProps['style'] = { + body: { + color, + fontFamily: fonts[variant].family, + overflow: 'hidden', + height: !textLengthExceedsLimit || expanded ? 'auto' : maxVisibleHeight, + }, + code_inline: codeStyle, + fence: codeStyle, + code_block: codeStyle, + link: { color: linkColor }, + paragraph: { + marginBottom: 0, + marginTop: 0, + fontSize: fonts.body2.fontSize, + lineHeight: textLineHeight, + }, + } + + return ( + + + {/* Render fake one-line markdown to properly measure the height of a single text line */} + { + setTextLineHeight(height) + }}> + + + { + // add our own custom link handler since it has security checks that only open http/https links + openUri(url).catch(() => undefined) + return false + }} + // HACK: children prop no in TS definition + {...{ children: text }} + /> + + + {/* Text is removed vs hidden using opacity to ensure spacing after the element is consistent in all cases. + This will cause mild thrash as data loads into a page but will ensure consistent spacing */} + {textLengthExceedsLimit ? ( + + {expanded ? t('common.longText.button.less') : t('common.longText.button.more')} + + ) : null} + + ) +} diff --git a/apps/mobile/src/components/text/LongText.test.tsx b/apps/mobile/src/components/text/LongText.test.tsx new file mode 100644 index 0000000..4ace94e --- /dev/null +++ b/apps/mobile/src/components/text/LongText.test.tsx @@ -0,0 +1,117 @@ +import React from 'react' +import { ReactTestInstance } from 'react-test-renderer' +import { LongText } from 'src/components/text/LongText' +import { fireEvent, render, within } from 'src/test/test-utils' + +const SHORT_TEXT = 'Short text' +const LONG_TEXT = 'Some very long text' + +const fireTextLayoutEvent = (instance: ReactTestInstance, lines: number): void => { + fireEvent(instance, 'textLayout', { + nativeEvent: { + lines: Array.from({ length: lines }).map(() => ({ + width: 100, + height: 20, + ascender: 20, + })), + }, + }) +} + +describe(LongText, () => { + it('renders without error', () => { + const tree = render() + + expect(tree).toMatchSnapshot() + }) + + describe('short text not exceeding the limit', () => { + it('shows the entire text', () => { + const tree = render() + + const textInstance = tree.getByText(SHORT_TEXT) + expect(textInstance.props.numberOfLines).toBeUndefined() + + fireTextLayoutEvent(textInstance, 1) // Assume Short text is one line + + // the number of lines will be the same as the initialDisplayedLines + expect(textInstance.props.numberOfLines).toBe(3) + }) + + it('does not display the "read more" button', () => { + const tree = render() + + fireTextLayoutEvent(tree.getByText(SHORT_TEXT), 1) // Assume Short text is one line + const readMoreButton = tree.queryByTestId('read-more-button') + + expect(readMoreButton).toBeNull() + }) + }) + + describe('long text exceeding the limit', () => { + describe('when the text is not expanded', () => { + it('limits the number of visible lines', () => { + const tree = render() + + const textInstance = tree.getByText(LONG_TEXT) + fireTextLayoutEvent(textInstance, 5) // Assume Some very long text is five lines + + expect(textInstance.props.numberOfLines).toBe(3) + }) + + it('displays the "read more" button', () => { + const tree = render() + + fireTextLayoutEvent(tree.getByText(LONG_TEXT), 5) // Assume Some very long text is five lines + const readMoreButton = tree.queryByTestId('read-more-button') + + expect(readMoreButton).toBeTruthy() + expect(within(readMoreButton!).getByText('Read more')).toBeTruthy() + }) + }) + + describe('when the text is expanded', () => { + it('shows the entire text', () => { + const tree = render() + + const textInstance = tree.getByText(LONG_TEXT) + fireTextLayoutEvent(textInstance, 5) // Assume Some very long text is five lines + + expect(textInstance.props.numberOfLines).toBe(3) + + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) + + expect(textInstance.props.numberOfLines).toBeUndefined() + }) + + it('displays the "read less" button', () => { + const tree = render() + + fireTextLayoutEvent(tree.getByText(LONG_TEXT), 5) // Assume Some very long text is five lines + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) + + expect(within(readMoreButton).getByText('Read less')).toBeTruthy() + }) + }) + + it('toggles the text when the "read more/less" button is pressed', () => { + const tree = render() + + fireTextLayoutEvent(tree.getByText(LONG_TEXT), 5) // Assume Some very long text is five lines + const readMoreButton = tree.getByTestId('read-more-button') + fireEvent.press(readMoreButton) // expand + + expect(tree.getByText(LONG_TEXT).props.numberOfLines).toBeUndefined() + + fireEvent.press(readMoreButton) // collapse + + expect(tree.getByText(LONG_TEXT).props.numberOfLines).toBe(3) + + fireEvent.press(readMoreButton) // expand + + expect(tree.getByText(LONG_TEXT).props.numberOfLines).toBeUndefined() + }) + }) +}) diff --git a/apps/mobile/src/components/text/LongText.tsx b/apps/mobile/src/components/text/LongText.tsx new file mode 100644 index 0000000..b9f727d --- /dev/null +++ b/apps/mobile/src/components/text/LongText.tsx @@ -0,0 +1,77 @@ +import React, { ComponentProps, useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, TextLayoutEventData } from 'react-native' +import { Flex, SpaceTokens, Text, useSporeColors } from 'ui/src' +import { fonts } from 'ui/src/theme' + +type LongTextProps = { + initialDisplayedLines?: number + text: string + gap?: SpaceTokens + color?: string + linkColor?: string + codeBackgroundColor?: string + readMoreOrLessColor?: string + variant?: keyof typeof fonts +} & Omit< + ComponentProps, + 'children' | 'numberOfLines' | 'onTextLayout' | 'color' | 'variant' +> + +export function LongText(props: LongTextProps): JSX.Element { + const colors = useSporeColors() + const { t } = useTranslation() + const { + initialDisplayedLines = 3, + text, + gap = '$spacing8', + color = colors.neutral1.val, + readMoreOrLessColor = colors.neutral2.val, + variant = 'body2', + ...rest + } = props + + const [expanded, setExpanded] = useState(true) + const [textLengthExceedsLimit, setTextLengthExceedsLimit] = useState(false) + const isInitializedRef = useRef(false) + + const onTextLayout = useCallback( + (e: NativeSyntheticEvent) => { + // Only needs to measure full number of lines once + if (isInitializedRef.current) { + return + } + setTextLengthExceedsLimit(e.nativeEvent.lines.length > initialDisplayedLines) + setExpanded(false) + isInitializedRef.current = true + }, + [initialDisplayedLines] + ) + + return ( + + + {text} + + + {/* Text is removed vs hidden using opacity to ensure spacing after the element is consistent in all cases. + This will cause mild thrash as data loads into a page but will ensure consistent spacing */} + {textLengthExceedsLimit ? ( + setExpanded(!expanded)}> + {expanded ? t('common.longText.button.less') : t('common.longText.button.more')} + + ) : null} + + ) +} diff --git a/apps/mobile/src/components/text/Pill.stories.mdx b/apps/mobile/src/components/text/Pill.stories.mdx new file mode 100644 index 0000000..0ebac54 --- /dev/null +++ b/apps/mobile/src/components/text/Pill.stories.mdx @@ -0,0 +1,22 @@ +import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs' + +import { Pill } from './Pill' + + + +export const Template = (args) => void 0} /> + +# `Pill` + + + + {Template.bind({})} + + + + diff --git a/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx b/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx new file mode 100644 index 0000000..d7c5a8a --- /dev/null +++ b/apps/mobile/src/components/text/TextWithFuseMatches.test.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { TextWithFuseMatches } from 'src/components/text/TextWithFuseMatches' +import { render } from 'src/test/test-utils' + +it('renders text without matches', () => { + const tree = render() + expect(tree).toMatchSnapshot() +}) + +it('renders text with few matches', () => { + const tree = render( + + ) + expect(tree).toMatchSnapshot() +}) diff --git a/apps/mobile/src/components/text/TextWithFuseMatches.tsx b/apps/mobile/src/components/text/TextWithFuseMatches.tsx new file mode 100644 index 0000000..d561b11 --- /dev/null +++ b/apps/mobile/src/components/text/TextWithFuseMatches.tsx @@ -0,0 +1,71 @@ +import Fuse from 'fuse.js' +import React from 'react' +import { Flex, Text, TextProps } from 'ui/src' +import { TextVariantTokens } from 'ui/src/theme' + +interface TextWithFuseMatchesProps { + text: string + matches?: readonly Fuse.FuseResultMatch[] + variant?: TextVariantTokens + numberOfLines?: Pick +} + +export function TextWithFuseMatches({ + matches, + text, + variant = 'body1', + numberOfLines = 1, +}: TextWithFuseMatchesProps & TextProps): JSX.Element { + if (!matches || matches.length === 0) { + return ( + + {text} + + ) + } + + const charIsMatch = new Set() + for (const match of matches) { + for (const index of match.indices) { + for (let i = index[0]; i < index[1] + 1; i++) { + charIsMatch.add(i) + } + } + } + + // PERF: batch pieces? + const pieces = [] + for (let i = 0; i < text.length; i++) { + if (charIsMatch.has(i)) { + pieces.push([text[i], true]) + } else { + pieces.push([text[i], false]) + } + } + + const elements = ( + <> + {pieces.map((p, i) => { + if (p[1]) { + return ( + + {p[0]} + + ) + } else { + return ( + + {p[0]} + + ) + } + })} + + ) + + return ( + + {elements} + + ) +} diff --git a/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap new file mode 100644 index 0000000..474d115 --- /dev/null +++ b/apps/mobile/src/components/text/__snapshots__/DecimalNumber.test.tsx.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders a DecimalNumber 1`] = ` + + 14,123 + + . + 78 + + +`; + +exports[`renders a DecimalNumber without a comma separator 1`] = ` + + 14 + + , + 23 + + +`; + +exports[`renders a DecimalNumber without a decimal part 1`] = ` + + 14,123 + +`; diff --git a/apps/mobile/src/components/text/__snapshots__/LongMarkdownText.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/LongMarkdownText.test.tsx.snap new file mode 100644 index 0000000..8536d0b --- /dev/null +++ b/apps/mobile/src/components/text/__snapshots__/LongMarkdownText.test.tsx.snap @@ -0,0 +1,119 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LongMarkdownText renders without error 1`] = ` + + + + + + + + . + + + + + + + + + + Some very long text + + + + + + +`; diff --git a/apps/mobile/src/components/text/__snapshots__/LongText.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/LongText.test.tsx.snap new file mode 100644 index 0000000..7f43af2 --- /dev/null +++ b/apps/mobile/src/components/text/__snapshots__/LongText.test.tsx.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LongText renders without error 1`] = ` + + + Some very long text + + +`; diff --git a/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap b/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap new file mode 100644 index 0000000..f6a3234 --- /dev/null +++ b/apps/mobile/src/components/text/__snapshots__/TextWithFuseMatches.test.tsx.snap @@ -0,0 +1,362 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders text with few matches 1`] = ` + + + A + + + + + + t + + + e + + + x + + + t + + + + + + w + + + i + + + t + + + h + + + o + + + u + + + t + + + + + + m + + + a + + + t + + + c + + + h + + + e + + + s + + +`; + +exports[`renders text without matches 1`] = ` + + A text without matches + +`; diff --git a/apps/mobile/src/components/tokens/TokenMetadata.tsx b/apps/mobile/src/components/tokens/TokenMetadata.tsx new file mode 100644 index 0000000..9a65ae2 --- /dev/null +++ b/apps/mobile/src/components/tokens/TokenMetadata.tsx @@ -0,0 +1,21 @@ +import React, { PropsWithChildren } from 'react' +import { FlexAlignType } from 'react-native' +import { Flex } from 'ui/src' + +type TokenMetadataProps = PropsWithChildren<{ + align?: FlexAlignType +}> + +/** Helper component to format rhs metadata for a given token. */ +export const TokenMetadata = ({ + children, + align = 'flex-end', +}: TokenMetadataProps): JSX.Element => { + return ( + + + {children} + + + ) +} diff --git a/apps/mobile/src/components/tooltip/TooltipButton.tsx b/apps/mobile/src/components/tooltip/TooltipButton.tsx new file mode 100644 index 0000000..e7b891d --- /dev/null +++ b/apps/mobile/src/components/tooltip/TooltipButton.tsx @@ -0,0 +1,62 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ColorValue, Keyboard } from 'react-native' +import { TouchableArea, TouchableAreaProps, useSporeColors } from 'ui/src' +import InfoCircle from 'ui/src/assets/icons/info-circle.svg' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +const DEFAULT_ICON_SIZE = 20 + +type InfoButtonProps = { + modalText: string + modalTitle: string + modalIcon?: JSX.Element + modalContent?: JSX.Element + backgroundIconColor?: ColorValue + size?: number + closeText?: string +} & TouchableAreaProps + +export function TooltipInfoButton({ + size, + backgroundIconColor, + closeText, + modalText, + modalTitle, + modalIcon, + modalContent, + ...rest +}: InfoButtonProps): JSX.Element { + const colors = useSporeColors() + const [showModal, setShowModal] = useState(false) + const { t } = useTranslation() + return ( + <> + { + Keyboard.dismiss() + setShowModal(true) + }} + {...rest}> + + + {showModal && ( + setShowModal(false)}> + {modalContent ?? null} + + )} + + ) +} diff --git a/apps/mobile/src/components/unicons/Unicon.test.tsx b/apps/mobile/src/components/unicons/Unicon.test.tsx new file mode 100644 index 0000000..dd9f378 --- /dev/null +++ b/apps/mobile/src/components/unicons/Unicon.test.tsx @@ -0,0 +1,40 @@ +import React from 'react' +import { render } from 'src/test/test-utils' +import { deriveUniconAttributeIndices, isEthAddress, Unicon, UniconAttributes } from 'ui/src' + +it('renders a Unicon', () => { + const tree = render( + + ).toJSON() + expect(tree).toMatchSnapshot() +}) + +it('fails to render a Unicon if given an invalid eth address', () => { + const tree = render() + expect(tree).toMatchSnapshot() +}) + +it('identifies valid and invalid eth addresses', () => { + const normal = '0x0c7213bac2B9e7b99ABa344243C9de84227911Be' + const no0X = '0c7213bac2B9e7b99ABa344243C9de84227911Be' + const tooShort = '0x0c713bac2B9e7b99ABa344243C9de84227911Be' + const tooLong = '0x0c7213bac2B9e7b99ABa344243C9de84227911Beaaa' + const definitelyAnAddress = 'mymoneydontjigglejiggle' + + expect(isEthAddress(normal)).toBe(true) + expect(isEthAddress(no0X)).toBe(false) + expect(isEthAddress(tooShort)).toBe(false) + expect(isEthAddress(tooLong)).toBe(false) + expect(isEthAddress(definitelyAnAddress)).toBe(false) +}) + +it('derives attribute indices from eth addresses', () => { + const specialAddress = '0x01010101c2B9e7b99ABa344243C9de84227911Be' + const derivedIndices = deriveUniconAttributeIndices(specialAddress) + expect(derivedIndices).toEqual({ + [UniconAttributes.GradientStart]: 1, + [UniconAttributes.GradientEnd]: 1, + [UniconAttributes.Container]: 1, + [UniconAttributes.Shape]: 1, + }) +}) diff --git a/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap b/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap new file mode 100644 index 0000000..3b520e6 --- /dev/null +++ b/apps/mobile/src/components/unicons/__snapshots__/Unicon.test.tsx.snap @@ -0,0 +1,158 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fails to render a Unicon if given an invalid eth address 1`] = `null`; + +exports[`renders a Unicon 1`] = ` + + + + + } + > + + + + + + + + + + + } + > + + + + + +`; diff --git a/apps/mobile/src/components/unitags/AvatarSelection.ts b/apps/mobile/src/components/unitags/AvatarSelection.ts new file mode 100644 index 0000000..1ade25e --- /dev/null +++ b/apps/mobile/src/components/unitags/AvatarSelection.ts @@ -0,0 +1,56 @@ +import { ImageLibraryOptions, launchImageLibrary } from 'react-native-image-picker' +import { useNftsTabQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NUM_FIRST_NFTS } from 'wallet/src/components/nfts/NftsList' +import { formatNftItems } from 'wallet/src/features/nfts/utils' + +// Selected image will be shrunk to max width/height +// URI will then be for an image of those dimensions +const IMAGE_OPTIONS: ImageLibraryOptions = { + mediaType: 'photo', + maxWidth: 500, + maxHeight: 500, + quality: 1, // best quality + includeBase64: false, + selectionLimit: 1, +} + +export async function selectPhotoFromLibrary(): Promise { + const response = await launchImageLibrary(IMAGE_OPTIONS) + if (!response.didCancel && !response.errorCode && response.assets) { + return response.assets[0]?.uri + } +} + +export function useAvatarSelectionHandler({ + address, + avatarImageUri, + setAvatarImageUri, + showModal, +}: { + address: string + avatarImageUri: string | undefined + setAvatarImageUri: (uri: string) => void + showModal: () => void +}): { avatarSelectionHandler: () => Promise; hasNFTs: boolean } { + const { data: nftsData } = useNftsTabQuery({ + variables: { ownerAddress: address, first: NUM_FIRST_NFTS, filter: { filterSpam: false } }, + }) + const nftItems = formatNftItems(nftsData) + + const hasNFTs = nftItems !== undefined && nftItems?.length > 0 + const hasAvatarImage = avatarImageUri && avatarImageUri !== '' + + if (hasNFTs || hasAvatarImage) { + return { avatarSelectionHandler: async () => showModal(), hasNFTs } + } else { + return { + avatarSelectionHandler: async (): Promise => { + const selectedPhoto = await selectPhotoFromLibrary() + if (selectedPhoto) { + setAvatarImageUri(selectedPhoto) + } + }, + hasNFTs, + } + } +} diff --git a/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx b/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx new file mode 100644 index 0000000..6a92d30 --- /dev/null +++ b/apps/mobile/src/components/unitags/ChangeUnitagModal.tsx @@ -0,0 +1,333 @@ +import { useNavigation } from '@react-navigation/native' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator, EmitterSubscription, Keyboard } from 'react-native' +import { getUniqueId } from 'react-native-device-info' +import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' +import { fonts, spacing } from 'ui/src/theme' +import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' +import { UnitagErrorCodes } from 'uniswap/src/features/unitags/types' +import { isIOS } from 'uniswap/src/utils/platform' +import { logger } from 'utilities/src/logger/logger' +import { useAsyncData } from 'utilities/src/react/hooks' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { changeUnitag } from 'wallet/src/features/unitags/api' +import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' +import { useCanAddressClaimUnitag, useCanClaimUnitagName } from 'wallet/src/features/unitags/hooks' +import { parseUnitagErrorCode } from 'wallet/src/features/unitags/utils' +import { useWalletSigners } from 'wallet/src/features/wallet/context' +import { useAccount } from 'wallet/src/features/wallet/hooks' +import { useAppDispatch } from 'wallet/src/state' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants' + +export function ChangeUnitagModal({ + unitag, + address, + onClose, +}: { + unitag: string + address: Address + onClose: () => void +}): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const navigation = useNavigation() + const dispatch = useAppDispatch() + const { data: deviceId } = useAsyncData(getUniqueId) + const account = useAccount(address) + const signerManager = useWalletSigners() + + const [newUnitag, setNewUnitag] = useState(unitag) + const [keyboardHeight, setKeyboardHeight] = useState(0) + const [showConfirmModal, setShowConfirmModal] = useState(false) + const [isCheckingUnitag, setIsCheckingUnitag] = useState(false) + const [isChangeResponseLoading, setIsChangeResponseLoading] = useState(false) + const [unitagToCheck, setUnitagToCheck] = useState(unitag) + + const { error: canClaimUnitagNameError, loading: loadingUnitagErrorCheck } = + useCanClaimUnitagName(address, unitagToCheck) + const { errorCode } = useCanAddressClaimUnitag(address, true) + const { triggerRefetchUnitags } = useUnitagUpdater() + + const isUnitagEdited = unitag !== newUnitag + const isUnitagInvalid = + newUnitag === unitagToCheck && !!canClaimUnitagNameError && !loadingUnitagErrorCheck + const isUnitagValid = + isUnitagEdited && !canClaimUnitagNameError && !loadingUnitagErrorCheck && !!newUnitag + const hasReachedAddressLimit = errorCode === UnitagErrorCodes.AddressLimitReached + const isSubmitButtonDisabled = + isCheckingUnitag || + isChangeResponseLoading || + !deviceId || + hasReachedAddressLimit || + !isUnitagEdited || + !newUnitag || + isUnitagInvalid + + const onFinishEditing = (): void => { + Keyboard.dismiss() + } + + const onCloseConfirmModal = (): void => { + setShowConfirmModal(false) + } + + const onPressSaveChanges = (): void => { + if (newUnitag !== unitagToCheck) { + // Unitag needs to be checked for errors and availability + setIsCheckingUnitag(true) + setUnitagToCheck(newUnitag) + } else if (isUnitagValid) { + // If unitag is unchanged and is available, continue to speedbump + onFinishEditing() + setShowConfirmModal(true) + } + } + + const onChangeSubmit = async (): Promise => { + if (!deviceId) { + logger.error(new Error('DeviceId is undefined'), { + tags: { file: 'ChangeUnitagModal', function: 'onChangeSubmit' }, + }) + return // Should never hit this condition. Button is disabled if deviceId is undefined + } + + onFinishEditing() + setShowConfirmModal(false) + setIsChangeResponseLoading(true) + try { + // Change unitag backend call + const { data: changeResponse } = await changeUnitag({ + username: unitagToCheck, + deviceId, + account, + signerManager, + }) + setIsChangeResponseLoading(false) + + // If change failed and returns an error code, display the error message + if (!changeResponse.success && !!changeResponse.errorCode) { + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: parseUnitagErrorCode(t, unitagToCheck, changeResponse.errorCode), + }) + ) + return + } + + // If change succeeded, exit the modal and display a success message + if (changeResponse.success) { + sendWalletAnalyticsEvent(UnitagEventName.UnitagChanged) + triggerRefetchUnitags() + dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: t('unitags.notification.username.title'), + }) + ) + navigation.goBack() + onClose() + } + } catch (e) { + // If some other error occurs, log it and display a generic error message + logger.error(e, { + tags: { file: 'ChangeUnitagModal', function: 'onChangeSubmit' }, + }) + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: t('unitags.notification.username.error'), + }) + ) + onClose() + setIsChangeResponseLoading(false) + } + } + + // This useEffect makes KeyboardAvoidingView work when inside a BottomSheetModal + // Dynamically add bottom padding equal to keyboard height so that elements have room to shift up + useEffect(() => { + let showSubscription: EmitterSubscription + let hideSubscription: EmitterSubscription + + if (isIOS) { + // Using keyboardWillShow makes it feel more responsive, but only available on iOS + showSubscription = Keyboard.addListener('keyboardWillShow', (e) => { + setKeyboardHeight(e.endCoordinates.height) + }) + hideSubscription = Keyboard.addListener('keyboardWillHide', () => { + setKeyboardHeight(0) + }) + } else { + // keyboardDidShow only emits after the keyboard has fully appeared + showSubscription = Keyboard.addListener('keyboardDidShow', (e) => { + setKeyboardHeight(e.endCoordinates.height) + }) + hideSubscription = Keyboard.addListener('keyboardDidHide', () => { + setKeyboardHeight(0) + }) + } + + return () => { + showSubscription.remove() + hideSubscription.remove() + } + }, []) + + // When useUnitagError completes loading, if unitag is valid then continue to speedbump + useEffect(() => { + if (isCheckingUnitag && !!unitagToCheck && !loadingUnitagErrorCheck) { + setIsCheckingUnitag(false) + // If unitagError is defined, it's rendered in UI. If no error, continue to speedbump + if (unitagToCheck === newUnitag && isUnitagValid) { + onFinishEditing() + setShowConfirmModal(true) + } + } + }, [isCheckingUnitag, isUnitagValid, loadingUnitagErrorCheck, newUnitag, unitagToCheck]) + + return ( + <> + {showConfirmModal && ( + + )} + + 0 ? keyboardHeight - spacing.spacing20 : '$spacing12'} + pt="$spacing12" + px="$spacing24"> + + {t('unitags.editUsername.title')} + + + setNewUnitag(text.trim().toLowerCase())} + onSubmitEditing={onFinishEditing} + /> + + + {UNITAG_SUFFIX} + + + + {hasReachedAddressLimit ? ( + + + {t('unitags.editUsername.warning.max')} + + + ) : ( + + + {t('unitags.editUsername.warning.default')} + + + )} + {isUnitagEdited && unitagToCheck === newUnitag && canClaimUnitagNameError && ( + + + {canClaimUnitagNameError} + + + )} + + + + + + + ) +} + +function ChangeUnitagConfirmModal({ + onClose, + onChangeSubmit, +}: { + onClose: () => void + onChangeSubmit: () => Promise +}): JSX.Element { + const { t } = useTranslation() + return ( + + + + + + + {t('unitags.editUsername.confirm.title')} + + + {t('unitags.editUsername.confirm.subtitle')} + + + + + + + + ) +} diff --git a/apps/mobile/src/components/unitags/ChooseNftModal.tsx b/apps/mobile/src/components/unitags/ChooseNftModal.tsx new file mode 100644 index 0000000..5fc616a --- /dev/null +++ b/apps/mobile/src/components/unitags/ChooseNftModal.tsx @@ -0,0 +1,47 @@ +import { NftView } from 'src/components/NFT/NftView' +import { useDeviceInsets, useSporeColors } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { NftsList } from 'wallet/src/components/nfts/NftsList' +import { NFTItem } from 'wallet/src/features/nfts/types' +import { ModalName } from 'wallet/src/telemetry/constants' + +type ChooseNftProps = { + address: string + setPhotoUri: (uri?: string) => void + onClose: () => void +} + +export const ChooseNftModal = ({ address, setPhotoUri, onClose }: ChooseNftProps): JSX.Element => { + const colors = useSporeColors() + const insets = useDeviceInsets() + + const renderNFT = (item: NFTItem): JSX.Element => { + const onPressNft = (): void => { + setPhotoUri(item.imageUrl) + onClose() + } + return + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx new file mode 100644 index 0000000..3a9e7c4 --- /dev/null +++ b/apps/mobile/src/components/unitags/ChoosePhotoOptionsModal.tsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { selectPhotoFromLibrary } from 'src/components/unitags/AvatarSelection' +import { ChooseNftModal } from 'src/components/unitags/ChooseNftModal' +import { Flex, Icons, Text, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type ChoosePhotoOptionsProps = { + address: Maybe
+ hasNFTs: boolean + setPhotoUri: (uri?: string) => void + onClose: () => void + showRemoveOption: boolean +} + +export const ChoosePhotoOptionsModal = ({ + address, + hasNFTs, + setPhotoUri, + onClose, + showRemoveOption, +}: ChoosePhotoOptionsProps): JSX.Element => { + const colors = useSporeColors() + const [showNftsList, setShowNftsList] = useState(false) + + const onPressNftsList = async (): Promise => { + setShowNftsList(true) + } + + const onCloseNftsList = (): void => { + setShowNftsList(false) + onClose() + } + + const onRemovePhoto = async (): Promise => { + setPhotoUri(undefined) + onClose() + } + + const onPressCameraRoll = async (): Promise => { + const selectedPhoto = await selectPhotoFromLibrary() + // Close needs to happen before setting the photo, otherwise the handler can get cut short + onClose() + if (selectedPhoto) { + setPhotoUri(selectedPhoto) + } + } + + const options = [ + { + key: `${ElementName.OpenCameraRoll}`, + onPress: onPressCameraRoll, + item: , + }, + ] + + if (hasNFTs) { + options.push({ + key: `${ElementName.OpenNftsList}`, + onPress: onPressNftsList, + item: , + }) + } + + if (showRemoveOption) { + options.push({ + key: `${ElementName.Remove}`, + onPress: onRemovePhoto, + item: , + }) + } + + return ( + <> + + + + {options.map((option) => ( + + {option.item} + + ))} + + + + {showNftsList && address && ( + + )} + + ) +} + +enum PhotoAction { + BrowseCameraRoll = 'camera-roll', + BrowseNftsList = 'nfts-list', + RemovePhoto = 'remove-photo', +} + +const ChoosePhotoOption = ({ type }: { type: PhotoAction }): JSX.Element => { + const { t } = useTranslation() + return ( + + {type === PhotoAction.BrowseCameraRoll && ( + + )} + {type === PhotoAction.BrowseNftsList && ( + + )} + {type === PhotoAction.RemovePhoto && ( + + )} + + + {type === PhotoAction.BrowseCameraRoll && t('unitags.choosePhoto.option.cameraRoll')} + {type === PhotoAction.BrowseNftsList && t('unitags.choosePhoto.option.nft')} + {type === PhotoAction.RemovePhoto && t('unitags.choosePhoto.option.remove')} + + + + ) +} diff --git a/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx new file mode 100644 index 0000000..079b351 --- /dev/null +++ b/apps/mobile/src/components/unitags/DeleteUnitagModal.tsx @@ -0,0 +1,120 @@ +import { useNavigation } from '@react-navigation/native' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator } from 'react-native' +import { Button, Flex, Icons, Text, useSporeColors } from 'ui/src' +import { fonts } from 'ui/src/theme' +import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' +import { logger } from 'utilities/src/logger/logger' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { deleteUnitag } from 'wallet/src/features/unitags/api' +import { useWalletSigners } from 'wallet/src/features/wallet/context' +import { useAccount } from 'wallet/src/features/wallet/hooks' +import { useAppDispatch } from 'wallet/src/state' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants' + +export function DeleteUnitagModal({ + unitag, + address, + onClose, +}: { + unitag: string + address: Address + onClose: () => void +}): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const navigation = useNavigation() + const dispatch = useAppDispatch() + const { triggerRefetchUnitags } = useUnitagUpdater() + const account = useAccount(address) + const signerManager = useWalletSigners() + const [isDeleting, setIsDeleting] = useState(false) + + const handleDeleteError = (): void => { + setIsDeleting(false) + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: t('unitags.notification.delete.error'), + }) + ) + onClose() + } + + const onDelete = async (): Promise => { + try { + setIsDeleting(true) + const { data: deleteResponse } = await deleteUnitag({ + username: unitag, + account, + signerManager, + }) + setIsDeleting(false) + + if (!deleteResponse?.success) { + handleDeleteError() + return + } + + if (deleteResponse?.success) { + sendWalletAnalyticsEvent(UnitagEventName.UnitagRemoved) + triggerRefetchUnitags() + dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: t('unitags.notification.delete.title'), + }) + ) + navigation.goBack() + onClose() + } + } catch (e) { + logger.error(e, { + tags: { file: 'DeleteUnitagModal', function: 'onDelete' }, + }) + handleDeleteError() + } + } + + return ( + + + + + + + {t('unitags.delete.confirm.title')} + + + {t('unitags.delete.confirm.subtitle')} + + + + + + + ) +} diff --git a/apps/mobile/src/components/unitags/HeaderRow.tsx b/apps/mobile/src/components/unitags/HeaderRow.tsx new file mode 100644 index 0000000..0cd097f --- /dev/null +++ b/apps/mobile/src/components/unitags/HeaderRow.tsx @@ -0,0 +1,28 @@ +import React from 'react' +import { BackButton } from 'src/components/buttons/BackButton' +import { Flex, Text, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' + +export function HeaderRow({ + headingText, + tooltipButton, +}: { + headingText?: string + tooltipButton?: JSX.Element +}): JSX.Element { + return ( + + + + + + + {headingText && ( + + {headingText} + + )} + {tooltipButton} + + ) +} diff --git a/apps/mobile/src/components/unitags/UnitagBanner.tsx b/apps/mobile/src/components/unitags/UnitagBanner.tsx new file mode 100644 index 0000000..5f4083b --- /dev/null +++ b/apps/mobile/src/components/unitags/UnitagBanner.tsx @@ -0,0 +1,174 @@ +import React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { Keyboard, StyleProp, ViewStyle } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { openModal } from 'src/features/modals/modalSlice' +import { Screens, UnitagScreens } from 'src/screens/Screens' +import { + Flex, + Image, + Text, + TouchableArea, + useDeviceDimensions, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import { UNITAGS_BANNER_VERTICAL_DARK, UNITAGS_BANNER_VERTICAL_LIGHT } from 'ui/src/assets' +import { borderRadii, iconSizes, spacing } from 'ui/src/theme' +import { selectHasCompletedUnitagsIntroModal } from 'wallet/src/features/behaviorHistory/selectors' +import { setHasSkippedUnitagPrompt } from 'wallet/src/features/behaviorHistory/slice' +import { UNITAG_SUFFIX_NO_LEADING_DOT } from 'wallet/src/features/unitags/constants' +import { useAppSelector } from 'wallet/src/state' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants' + +const IMAGE_ASPECT_RATIO = 0.42 +const IMAGE_SCREEN_WIDTH_PROPORTION = 0.18 +const COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION = 0.15 + +export function UnitagBanner({ + address, + compact, + entryPoint, +}: { + address: Address + compact?: boolean + entryPoint: Screens.Home | Screens.Settings +}): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + const { fullWidth } = useDeviceDimensions() + const isDarkMode = useIsDarkMode() + const colors = useSporeColors() + const hasCompletedUnitagsIntroModal = useAppSelector(selectHasCompletedUnitagsIntroModal) + + const imageWidth = compact + ? COMPACT_IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth + : IMAGE_SCREEN_WIDTH_PROPORTION * fullWidth + const imageHeight = imageWidth / IMAGE_ASPECT_RATIO + const analyticsEntryPoint = entryPoint === Screens.Home ? 'home' : 'settings' + + const onPressClaimNow = (): void => { + Keyboard.dismiss() + sendWalletAnalyticsEvent(UnitagEventName.UnitagBannerActionTaken, { + action: 'claim', + entryPoint: analyticsEntryPoint, + }) + + if (hasCompletedUnitagsIntroModal) { + navigate(Screens.UnitagStack, { + screen: UnitagScreens.ClaimUnitag, + params: { + entryPoint, + address, + }, + }) + } else { + dispatch(openModal({ name: ModalName.UnitagsIntro, initialState: { address, entryPoint } })) + } + } + + const onPressMaybeLater = (): void => { + sendWalletAnalyticsEvent(UnitagEventName.UnitagBannerActionTaken, { + action: 'dismiss', + entryPoint: analyticsEntryPoint, + }) + dispatch(setHasSkippedUnitagPrompt(true)) + } + + const baseButtonStyle: StyleProp = { + backgroundColor: colors.accent1.get(), + borderRadius: borderRadii.rounded20, + justifyContent: 'center', + height: iconSizes.icon36, + paddingVertical: spacing.spacing8, + paddingHorizontal: spacing.spacing12, + } + + return ( + + {compact ? ( + + + }} + i18nKey="unitags.banner.title.compact" + values={{ unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT }} + /> + + + ) : ( + + + + {t('unitags.banner.title.full', { + unitagDomain: UNITAG_SUFFIX_NO_LEADING_DOT, + })} + + + {t('unitags.banner.subtitle')} + + + + {/* TODO: replace with Button when it's extensible enough to accommodate designs */} + + + {t('unitags.banner.button.claim')} + + + + + {t('common.button.later')} + + + + + )} + + + + + ) +} diff --git a/apps/mobile/src/components/unitags/UnitagProfilePicture.tsx b/apps/mobile/src/components/unitags/UnitagProfilePicture.tsx new file mode 100644 index 0000000..0e28039 --- /dev/null +++ b/apps/mobile/src/components/unitags/UnitagProfilePicture.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { Flex, useSporeColors } from 'ui/src' +import { isSVGUri } from 'utilities/src/format/urls' +import { AccountIcon } from 'wallet/src/components/accounts/AccountIcon' +import { useENSAvatar } from 'wallet/src/features/ens/api' +import { ImageUri } from 'wallet/src/features/images/ImageUri' +import { RemoteImage } from 'wallet/src/features/images/RemoteImage' + +export function UnitagProfilePicture({ + address, + unitagAvatarUri, + size, +}: { + address: Address + size: number + unitagAvatarUri?: string +}): JSX.Element { + const colors = useSporeColors() + const { data: ensAvatar } = useENSAvatar(address) + + return unitagAvatarUri ? ( + + {isSVGUri(unitagAvatarUri) ? ( + + ) : ( + + )} + + ) : ( + + ) +} diff --git a/apps/mobile/src/components/unitags/UnitagWithProfilePicture.tsx b/apps/mobile/src/components/unitags/UnitagWithProfilePicture.tsx new file mode 100644 index 0000000..3515778 --- /dev/null +++ b/apps/mobile/src/components/unitags/UnitagWithProfilePicture.tsx @@ -0,0 +1,42 @@ +import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' +import { Flex, Text } from 'ui/src' +import { imageSizes, spacing } from 'ui/src/theme' +import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' + +export const UnitagWithProfilePicture = ({ + unitag, + address, + profilePictureUri, +}: { + unitag: string + address: Address + profilePictureUri?: string +}): JSX.Element => { + return ( + + + + + {unitag} + + {UNITAG_SUFFIX} + + + + + ) +} diff --git a/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx b/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx new file mode 100644 index 0000000..bd866d4 --- /dev/null +++ b/apps/mobile/src/components/unitags/UnitagsIntroModal.tsx @@ -0,0 +1,93 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import React from 'react' +import { useTranslation } from 'react-i18next' +import 'react-native-reanimated' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { TermsOfService } from 'src/screens/Onboarding/TermsOfService' +import { Screens, UnitagScreens } from 'src/screens/Screens' +import { Button, Flex, GeneratedIcon, Icons, Image, Text, useIsDarkMode } from 'ui/src' +import { UNITAGS_INTRO_BANNER_DARK, UNITAGS_INTRO_BANNER_LIGHT } from 'ui/src/assets' +import { iconSizes } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { setHasCompletedUnitagsIntroModal } from 'wallet/src/features/behaviorHistory/slice' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function UnitagsIntroModal(): JSX.Element { + const { t } = useTranslation() + const isDarkMode = useIsDarkMode() + const appDispatch = useAppDispatch() + const modalState = useAppSelector(selectModalState(ModalName.UnitagsIntro)).initialState + const address = modalState?.address + const entryPoint = modalState?.entryPoint + + const onClose = (): void => { + appDispatch(closeModal({ name: ModalName.UnitagsIntro })) + } + + const onPressClaimOneNow = (): void => { + if (!entryPoint) { + throw new Error('Missing entry point in UnitagsIntroModal') + } + + appDispatch(setHasCompletedUnitagsIntroModal(true)) + navigate(Screens.UnitagStack, { + screen: UnitagScreens.ClaimUnitag, + params: { + entryPoint, + address, + }, + }) + if (address) { + sendWalletAnalyticsEvent(SharedEventName.TERMS_OF_SERVICE_ACCEPTED, { address }) + } + onClose() + } + + return ( + + + + {t('unitags.intro.title')} + + {t('unitags.intro.subtitle')} + + + + + + + + + + + + + + + + + + + ) +} + +function BodyItem({ Icon, title }: { Icon: GeneratedIcon; title: string }): JSX.Element { + return ( + + + + {title} + + + ) +} diff --git a/apps/mobile/src/constants/urls.ts b/apps/mobile/src/constants/urls.ts new file mode 100644 index 0000000..993fbc0 --- /dev/null +++ b/apps/mobile/src/constants/urls.ts @@ -0,0 +1,5 @@ +// AppsFlyer automatically routes to the correct store based on the device +export const APP_STORE_LINK = 'https://uniswapwallet.onelink.me/8q3y/97upfib8' + +export const APP_FEEDBACK_LINK = + 'https://docs.google.com/forms/d/e/1FAIpQLSepzL5aMuSfRhSgw0zDw_gVmc2aeVevfrb1UbOwn6WGJ--46w/viewform' diff --git a/apps/mobile/src/data/README.md b/apps/mobile/src/data/README.md new file mode 100644 index 0000000..cc26e41 --- /dev/null +++ b/apps/mobile/src/data/README.md @@ -0,0 +1,165 @@ +Note. We migrated to Apollo client in November. This needs to be updated with information for Apollo. + +# Relay + +[Relay](https://relay.dev/docs/) is a GraphQL client built for scale with unique benefits: +* collocating data dependencies in components with GraphQL fragments +* query compiler that aggregates and optimizes data requirements +* data consistency across components + +## Principles + +Recommended readings: +* [Why React at Uniswap Labs?](https://www.notion.so/uniswaplabs/GraphQL-Client-949780e7d105405c87cdd0147bd2f84b) +* [Thinking in GraphQL](https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/) +* [Thinking in Relay](https://relay.dev/docs/principles-and-architecture/thinking-in-relay/) + +| Commands | | +|---|---| +| `yarn relay:schema` | Fetch latest GraphQL schema | +| `yarn relay:compile` | Run relay compiler (validates queries, generates typings) | +| `yarn relay:compile -w` | " in watch mode | + +## Schema Overview + +GraphQL schema can be fetched directly from our Uniswap API via `yarn relay:schema` (written to `src/data/schema.graphql`). + +`Query` type defines query entrypoints: + +```ts +type Query { + tokens(contracts: [ContractInput!]!): [Token] + tokenProjects(contracts: [ContractInput!]!): [TokenProject] + ... +} +``` + +## Architecture Overview + +| | | | +|--|--|--| +| Relay runtime | [relay.ts](./relay.tsx) | Defines `fetchQuery`, `Network` and `store` | +| Navigation with preloaded data | [useEagerNavigation](../app/navigation/useEagerNavigation.ts) | Utility hook + +### Persisted cache + +Persisting cache data across sessions improves app start up time (or at least perceived time). There's no use case for true offline support at the moment. + +[relay.ts](./relay.tsx) defines a `RelayPersistedGate` and handles a simple load/dump cache strategy. + +**Typical flow (top to bottom)** + +1. screens define top level queries +2. using navigation utils, preload queries on navigation intent, and route query ref through route params +3. screens can reference this data using `usePreloadedQuery` and the ref +4. screens pass query/fragment refs down to sub-components +5. those sub-components can define fragments for the data they need, and grab it using `useFragment` and the key they accept + +### Navigation with preloaded data + +This follows [A Guided Tour](https://relay.dev/docs/guided-tour/) with a specific example from our codebase. + +#### Step 1. Define data requirements for each component with a **GraphQL fragment** + +```tsx +const tokenDetailsStatsFragment = graphql` + fragment TokenDetailsStats_tokenProject on TokenProject { + description + name + marketCap { + value + } + volume24h: volume(duration: DAY) { + value + } + ... + } +` +``` + +#### Step 2. Render fragment with `useFragment` hook + +```tsx +type Props = { + tokenProject: TokenDetailsStats_TokenProject$key +} + +function TokenDetailsStats({ tokenProject }: Props) { + const data = useFragment( + // fragment from step 1 + tokenDetailsStatsFragment, + // fragment reference from props + tokenProject + ) + + return {data} +} +``` + +Notes: +* Fragment only defines data requirements on `TokenProject`, but not *which* token project to read +* Fragment reference `tokenProject: TokenDetailsStats_TokenProject$key` is passed down from a preloaded query and defines which `TokenProject` to read +* `TokenDetailsStats` is automatically subscribed to data updates + +#### Step 3. Compose fragment into a query using `usePreloadedQuery` + +Fragments **cannot be fetched by themselves**, they must be included in a query. + +```tsx +type Props = { + queryRef: OfflineQuery +} + +function TokenDetailsScreen() { + const data = usePreloadedQuery( + graphql` + query TokenDetailsScreenQuery($contract: ContractInput!) { + tokenProjects(contracts: [$contract]) { + # Fragment from step 1 + ...TokenDetailsStats_tokenProject + } + } + `, + queryRef + ) + + return +} +``` + +#### Step 4. Preload and navigate + +The token details screen is now ready to receive a preloaded query, and components have defined the data they need through fragments. + +```tsx +function TokenRow({ currency }) { + + // Get preload utils for tokenDetailsQuery + const { registerNavigationIntent, preloadedNavigate } = useEagerNavigation(tokenDetailsScreenQuery) + + // Eagerly fetch token details data + const onPressIn = () => { + registerNavigationIntent({ + chain: currency.chain, + address: currency.address + }) + } + + // Navigate to TokenDetails with preloaded query ref + const onPress = () => { + preloadedNavigate(Screens.TokenDetails, { currency }) + } + + return + + ) +} + +function PasswordStrengthText({ strength }: { strength: PasswordStrength }): JSX.Element { + const { t } = useTranslation() + const { color } = getPasswordStrengthTextAndColor(strength) + + const hasPassword = strength !== PasswordStrength.NONE + let strengthText: string = '' + switch (strength) { + case PasswordStrength.STRONG: + strengthText = t('settings.setting.backup.password.strong') + break + case PasswordStrength.MEDIUM: + strengthText = t('settings.setting.backup.password.medium') + break + case PasswordStrength.WEAK: + strengthText = t('settings.setting.backup.password.weak') + break + default: + break + } + + return ( + + + {strengthText} + + + ) +} diff --git a/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx new file mode 100644 index 0000000..8765cb9 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/CloudBackupProcessingAnimation.tsx @@ -0,0 +1,138 @@ +import { useFocusEffect } from '@react-navigation/core' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import React, { useCallback, useEffect, useReducer } from 'react' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator, Alert } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList, SettingsStackParamList } from 'src/app/navigation/types' +import { backupMnemonicToCloudStorage } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { Flex, Text, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { promiseMinDelay } from 'utilities/src/time/timing' +import { CheckmarkCircle } from 'wallet/src/components/icons/CheckmarkCircle' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType, BackupType } from 'wallet/src/features/wallet/accounts/types' +import { useAccount } from 'wallet/src/features/wallet/hooks' + +type Props = { + accountAddress: Address + password: string + onBackupComplete: () => void + onErrorPress: () => void + navigation: + | NativeStackNavigationProp + | NativeStackNavigationProp +} + +/** Screen to perform secure recovery phrase backup to Cloud */ +export function CloudBackupProcessingAnimation({ + accountAddress, + onBackupComplete, + onErrorPress, + password, + navigation, +}: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const colors = useSporeColors() + + const account = useAccount(accountAddress) + if (account.type !== AccountType.SignerMnemonic) { + throw new Error('Account is not mnemonic account') + } + const mnemonicId = account?.mnemonicId + + const [processing, doneProcessing] = useReducer(() => false, true) + + // Handle finished backing up to Cloud + useEffect(() => { + if (account?.backups?.includes(BackupType.Cloud)) { + doneProcessing() + // Show success state for 1s before navigating + const timer = setTimeout(onBackupComplete, ONE_SECOND_MS) + return () => clearTimeout(timer) + } + }, [account?.backups, onBackupComplete]) + + // Handle backup to Cloud when screen appears + const backup = useCallback(async () => { + try { + // Ensure processing state is shown for at least 1s + await promiseMinDelay(backupMnemonicToCloudStorage(mnemonicId, password), ONE_SECOND_MS) + + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.AddBackupMethod, + address: accountAddress, + backupMethod: BackupType.Cloud, + }) + ) + } catch (error) { + logger.error(error, { + tags: { file: 'CloudBackupProcessingScreen', function: 'onPressNext' }, + }) + + Alert.alert( + t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }), + t('settings.setting.backup.error.message.full', { + cloudProviderName: getCloudProviderName(), + }), + [ + { + text: t('common.button.ok'), + style: 'default', + onPress: onErrorPress, + }, + ] + ) + } + }, [accountAddress, dispatch, mnemonicId, onErrorPress, password, t]) + + /** + * Delays cloud backup to avoid android oauth consent screen blocking navigation transition + */ + useFocusEffect( + useCallback(() => { + return navigation.addListener('transitionEnd', async () => { + await backup() + }) + }, [backup, navigation]) + ) + + const iconSize = iconSizes.icon40 + + return processing ? ( + + + + + + {t('settings.setting.backup.status.inProgress', { + cloudProviderName: getCloudProviderName(), + })} + + + ) : ( + + + + {t('settings.setting.backup.status.complete', { + cloudProviderName: getCloudProviderName(), + })} + + + ) +} diff --git a/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts b/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts new file mode 100644 index 0000000..f13f239 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/RNCloudStorageBackupsManager.ts @@ -0,0 +1,47 @@ +interface RNCloudStorageBackupsManager { + isCloudStorageAvailable: () => Promise + deleteCloudStorageMnemonicBackup: (mnemonicId: string) => Promise + startFetchingCloudStorageBackups: () => Promise + stopFetchingCloudStorageBackups: () => Promise + backupMnemonicToCloudStorage: (mnemonicId: string, password: string) => Promise + restoreMnemonicFromCloudStorage: (mnemonicId: string, password: string) => Promise +} + +declare module 'react-native' { + interface NativeModulesStatic { + RNCloudStorageBackupsManager: RNCloudStorageBackupsManager + } +} +import { NativeModules } from 'react-native' + +const { RNCloudStorageBackupsManager } = NativeModules + +export function isCloudStorageAvailable(): Promise { + return RNCloudStorageBackupsManager.isCloudStorageAvailable() +} + +export function deleteCloudStorageMnemonicBackup(mnemonicId: string): Promise { + return RNCloudStorageBackupsManager.deleteCloudStorageMnemonicBackup(mnemonicId) +} + +export function startFetchingCloudStorageBackups(): Promise { + return RNCloudStorageBackupsManager.startFetchingCloudStorageBackups() +} + +export function stopFetchingCloudStorageBackups(): Promise { + return RNCloudStorageBackupsManager.stopFetchingCloudStorageBackups() +} + +export function backupMnemonicToCloudStorage( + mnemonicId: string, + password: string +): Promise { + return RNCloudStorageBackupsManager.backupMnemonicToCloudStorage(mnemonicId, password) +} + +export function restoreMnemonicFromCloudStorage( + mnemonicId: string, + password: string +): Promise { + return RNCloudStorageBackupsManager.restoreMnemonicFromCloudStorage(mnemonicId, password) +} diff --git a/apps/mobile/src/features/CloudBackup/cloudBackupSlice.ts b/apps/mobile/src/features/CloudBackup/cloudBackupSlice.ts new file mode 100644 index 0000000..57c2e74 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/cloudBackupSlice.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' + +export interface CloudBackupState { + backupsFound: CloudStorageMnemonicBackup[] +} + +export const initialCloudBackupState: Readonly = { + backupsFound: [], +} + +const slice = createSlice({ + name: 'cloudBackup', + initialState: initialCloudBackupState, + reducers: { + foundCloudBackup: (state, action: PayloadAction<{ backup: CloudStorageMnemonicBackup }>) => { + const { backup } = action.payload + const duplicateBackup = state.backupsFound.some((b) => b.mnemonicId === backup.mnemonicId) + if (!duplicateBackup) { + state.backupsFound.push(backup) + } + }, + clearCloudBackups: (state) => { + state.backupsFound = [] + }, + }, +}) + +export const { foundCloudBackup, clearCloudBackups } = slice.actions +export const { reducer: cloudBackupReducer } = slice diff --git a/apps/mobile/src/features/CloudBackup/hooks.ts b/apps/mobile/src/features/CloudBackup/hooks.ts new file mode 100644 index 0000000..1a73c9a --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/hooks.ts @@ -0,0 +1,11 @@ +import { useAppSelector } from 'src/app/hooks' +import { selectCloudBackups } from 'src/features/CloudBackup/selectors' +import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' + +export function useCloudBackups(mnemonicId?: string): CloudStorageMnemonicBackup[] { + const backups = useAppSelector(selectCloudBackups) + if (mnemonicId) { + return backups.filter((b) => b.mnemonicId === mnemonicId) + } + return backups +} diff --git a/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts b/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts new file mode 100644 index 0000000..1732e64 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/passwordLockoutSlice.ts @@ -0,0 +1,37 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' + +export interface PasswordLockoutState { + passwordAttempts: number + endTime?: number +} + +export const initialPasswordLockoutState: Readonly = { + passwordAttempts: 0, +} + +const slice = createSlice({ + name: 'passwordLockout', + initialState: initialPasswordLockoutState, + reducers: { + incrementPasswordAttempts: (state) => { + state.passwordAttempts++ + }, + resetPasswordAttempts: (state) => { + state.passwordAttempts = 0 + }, + setLockoutEndTime: (state, action: PayloadAction<{ lockoutEndTime: number }>) => { + state.endTime = action.payload.lockoutEndTime + }, + resetLockoutEndTime: (state) => { + state.endTime = undefined + }, + }, +}) + +export const { + incrementPasswordAttempts, + resetPasswordAttempts, + setLockoutEndTime, + resetLockoutEndTime, +} = slice.actions +export const { reducer: passwordLockoutReducer } = slice diff --git a/apps/mobile/src/features/CloudBackup/saga.ts b/apps/mobile/src/features/CloudBackup/saga.ts new file mode 100644 index 0000000..79fca29 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/saga.ts @@ -0,0 +1,60 @@ +import { Action } from '@reduxjs/toolkit' +import { NativeEventEmitter, NativeModule, NativeModules } from 'react-native' +import { eventChannel } from 'redux-saga' +import { foundCloudBackup } from 'src/features/CloudBackup/cloudBackupSlice' +import { + CloudStorageBackupsManagerEventType, + CloudStorageMnemonicBackup, +} from 'src/features/CloudBackup/types' +import { call, fork, put, take } from 'typed-redux-saga' +import { logger } from 'utilities/src/logger/logger' + +function createCloudStorageBackupManagerChannel(eventEmitter: NativeEventEmitter) { + return eventChannel((emit) => { + const foundCloudBackupHandler = (backup: CloudStorageMnemonicBackup): void => { + logger.debug('CloudBackupSaga', 'foundCloudBackupHandler', 'Found account backup', backup) + emit(foundCloudBackup({ backup })) + } + + const eventEmitters = [ + { + type: CloudStorageBackupsManagerEventType.FoundCloudBackup, + handler: foundCloudBackupHandler, + }, + ] + + for (const { type, handler } of eventEmitters) { + eventEmitter.addListener(type, handler) + } + + const unsubscribe = (): void => { + for (const { type } of eventEmitters) { + eventEmitter.removeAllListeners(type) + } + } + + return unsubscribe + }) +} + +export function* cloudBackupsManagerSaga() { + yield* fork(watchCloudStorageBackupEvents) +} + +export function* watchCloudStorageBackupEvents() { + const CloudManagerEvents = new NativeEventEmitter( + NativeModules.RNCloudStorageBackupsManager as unknown as NativeModule + ) + const channel = yield* call(createCloudStorageBackupManagerChannel, CloudManagerEvents) + + while (true) { + try { + const payload = yield* take(channel) + yield* put(payload) + } catch (error) { + logger.error(error, { + tags: { file: 'CloudBackup/saga', function: 'watchCloudStorageBackupEvents' }, + }) + } + } +} diff --git a/apps/mobile/src/features/CloudBackup/selectors.ts b/apps/mobile/src/features/CloudBackup/selectors.ts new file mode 100644 index 0000000..c4c97c3 --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/selectors.ts @@ -0,0 +1,14 @@ +import { MobileState } from 'src/app/reducer' +import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' + +export const selectCloudBackups = (state: MobileState): CloudStorageMnemonicBackup[] => { + return state.cloudBackup.backupsFound +} + +export const selectPasswordAttempts = (state: MobileState): number => { + return state.passwordLockout.passwordAttempts +} + +export const selectLockoutEndTime = (state: MobileState): number | undefined => { + return state.passwordLockout.endTime +} diff --git a/apps/mobile/src/features/CloudBackup/types.ts b/apps/mobile/src/features/CloudBackup/types.ts new file mode 100644 index 0000000..8a5559a --- /dev/null +++ b/apps/mobile/src/features/CloudBackup/types.ts @@ -0,0 +1,9 @@ +export enum CloudStorageBackupsManagerEventType { + FoundCloudBackup = 'FoundCloudBackup', +} + +export interface CloudStorageMnemonicBackup { + mnemonicId: string + createdAt: number + googleDriveEmail?: string +} diff --git a/apps/mobile/src/features/analytics/appsflyer.tsx b/apps/mobile/src/features/analytics/appsflyer.tsx new file mode 100644 index 0000000..b3dc318 --- /dev/null +++ b/apps/mobile/src/features/analytics/appsflyer.tsx @@ -0,0 +1,25 @@ +import appsFlyer from 'react-native-appsflyer' +import { isBetaBuild, isDevBuild } from 'src/utils/version' +import { config } from 'uniswap/src/config' +import { logger } from 'utilities/src/logger/logger' + +export function initAppsFlyer(): void { + appsFlyer.initSdk( + { + devKey: config.appsflyerApiKey, + isDebug: isDevBuild() || isBetaBuild(), + appId: config.appsflyerAppId, + onInstallConversionDataListener: false, + onDeepLinkListener: false, + timeToWaitForATTUserAuthorization: 10, + // Ensures we have to manually start the SDK to respect any opting out + manualStart: true, + }, + (result) => { + logger.debug('appsflyer', 'initAppsFlyer', 'Result:', result) + }, + (error) => { + logger.error(error, { tags: { file: 'appsflyer', function: 'initAppsFlyer' } }) + } + ) +} diff --git a/apps/mobile/src/features/appRating/saga.ts b/apps/mobile/src/features/appRating/saga.ts new file mode 100644 index 0000000..ca393cf --- /dev/null +++ b/apps/mobile/src/features/appRating/saga.ts @@ -0,0 +1,198 @@ +import * as StoreReview from 'expo-store-review' +import { Alert } from 'react-native' +import { APP_FEEDBACK_LINK } from 'src/constants/urls' +import { hasConsecutiveRecentSwapsSelector } from 'src/features/appRating/selectors' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { call, delay, put, select, takeLatest } from 'typed-redux-saga' +import { isAndroid } from 'uniswap/src/utils/platform' +import { logger } from 'utilities/src/logger/logger' +import { ONE_DAY_MS, ONE_SECOND_MS } from 'utilities/src/time/time' +import { finalizeTransaction } from 'wallet/src/features/transactions/slice' +import { TransactionStatus, TransactionType } from 'wallet/src/features/transactions/types' +import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' +import { setAppRating } from 'wallet/src/features/wallet/slice' +import { appSelect } from 'wallet/src/state' +import { openUri } from 'wallet/src/utils/linking' + +// at most once per reminder period (120 days) +const MIN_PROMPT_REMINDER_MS = 120 * ONE_DAY_MS +// remind after a longer delay when user filled the feedback form (180 days) +const MIN_FEEDBACK_REMINDER_MS = 180 * ONE_DAY_MS +// small delay to help ux +const SWAP_FINALIZED_PROMPT_DELAY_MS = 3 * ONE_SECOND_MS + +export function* appRatingWatcherSaga() { + function* processFinalizedTx(action: ReturnType) { + // count successful swaps + + // TODO(MOB-1814): Remove once Android goes live + if (isAndroid) { + return + } + + if ( + action.payload.typeInfo.type === TransactionType.Swap && + action.payload.status === TransactionStatus.Success + ) { + yield* delay(SWAP_FINALIZED_PROMPT_DELAY_MS) + yield* call(maybeRequestAppRating) + } + } + + yield* takeLatest(finalizeTransaction.type, processFinalizedTx) +} + +function* maybeRequestAppRating() { + try { + const canRequestReview = yield* call(StoreReview.hasAction) + if (!canRequestReview) { + return + } + + const activeAddress = yield* select(selectActiveAccountAddress) + if (!activeAddress) { + return + } + + // Conditions + const appRatingProvidedMs = yield* appSelect((state) => state.wallet.appRatingProvidedMs) + if (appRatingProvidedMs) { + return + } // avoids prompting again + + const appRatingPromptedMs = yield* appSelect((state) => state.wallet.appRatingPromptedMs) + const appRatingFeedbackProvidedMs = yield* appSelect( + (state) => state.wallet.appRatingFeedbackProvidedMs + ) + + const consecutiveSwapsCondition = yield* appSelect(hasConsecutiveRecentSwapsSelector) + + // prompt if enough time has passed since last prompt or last feedback provided + const reminderCondition = + (appRatingPromptedMs !== undefined && + Date.now() - appRatingPromptedMs > MIN_PROMPT_REMINDER_MS) || + (appRatingFeedbackProvidedMs !== undefined && + Date.now() - appRatingFeedbackProvidedMs > MIN_FEEDBACK_REMINDER_MS) + + const hasNeverPrompted = appRatingPromptedMs === undefined + const shouldPrompt = consecutiveSwapsCondition && (hasNeverPrompted || reminderCondition) + + if (!shouldPrompt) { + logger.debug( + 'appRating', + 'maybeRequestAppRating', + `Skipping app rating (lastPrompt: ${appRatingPromptedMs}, lastProvided: ${appRatingProvidedMs}, consecutiveSwapsCondition: ${consecutiveSwapsCondition})` + ) + return + } + + logger.info( + 'appRating', + 'maybeRequestAppRating', + `Requesting app rating (lastPrompt: ${appRatingPromptedMs}, lastProvided: ${appRatingProvidedMs}, consecutiveSwapsCondition: ${consecutiveSwapsCondition})` + ) + + // Alerts + const shouldShowNativeReviewModal = yield* call(openRatingOptionsAlert) + + if (shouldShowNativeReviewModal) { + yield* call(openNativeReviewModal) + + // expo-review does not return whether a rating was actually provided. + // assume it was and mark rating as provided. + yield* put(setAppRating({ ratingProvided: true })) + + sendMobileAnalyticsEvent(MobileEventName.AppRating, { + type: 'store-review', + appRatingPromptedMs, + appRatingProvidedMs, + }) + } else { + // show feedback form + const feedbackSent = yield* call(openFeedbackRequestAlert) + + if (feedbackSent) { + yield* put(setAppRating({ feedbackProvided: true })) + + sendMobileAnalyticsEvent(MobileEventName.AppRating, { + type: 'feedback-form', + appRatingPromptedMs, + appRatingProvidedMs, + }) + } else { + yield* put(setAppRating({ feedbackProvided: false })) + + sendMobileAnalyticsEvent(MobileEventName.AppRating, { + type: 'remind', + appRatingPromptedMs, + appRatingProvidedMs, + }) + } + } + } catch (e) { + logger.error(e, { tags: { file: 'appRating', function: 'maybeRequestAppRating' } }) + } +} + +/** + * Opens the app rating request alert. Either opens the native review modal + * or the feedback form if user wishes to provide feedback. + */ +async function openRatingOptionsAlert() { + return new Promise((resolve) => { + Alert.alert( + 'Enjoying Uniswap Wallet?', + "Let us know if you're having a good experience with this app", + [ + { + text: 'Not really', + onPress: () => resolve(false), + style: 'cancel', + }, + { + text: 'Yes', + onPress: () => { + openNativeReviewModal().catch((e) => + logger.error(e, { + tags: { file: 'appRating/saga', function: 'openRatingOptionsAlert' }, + }) + ) + resolve(true) + }, + isPreferred: true, + }, + ] + ) + }) +} + +/** Opens feedback request modal which will redirect to our feedback form. */ +async function openFeedbackRequestAlert() { + return new Promise((resolve) => { + Alert.alert("We're sorry to hear that.", 'Let us know how we can improve your experience', [ + { + text: 'Send feedback', + onPress: () => { + openUri(APP_FEEDBACK_LINK).catch((e) => + logger.error(e, { tags: { file: 'appRating/saga', function: 'openFeedbackAlert' } }) + ) + resolve(true) + }, + isPreferred: true, + }, + { text: 'Maybe later', onPress: () => resolve(false), style: 'cancel' }, + ]) + }) +} + +/** Opens the native store review modal that will send the rating to the store. */ +async function openNativeReviewModal() { + try { + if (await StoreReview.hasAction()) { + await StoreReview.requestReview() + } + } catch (e) { + logger.error(e, { tags: { file: 'appRating/saga', function: 'useAppRating' } }) + } +} diff --git a/apps/mobile/src/features/appRating/selectors.test.ts b/apps/mobile/src/features/appRating/selectors.test.ts new file mode 100644 index 0000000..655f712 --- /dev/null +++ b/apps/mobile/src/features/appRating/selectors.test.ts @@ -0,0 +1,146 @@ +import { hasConsecutiveRecentSwapsSelector } from 'src/features/appRating/selectors' +import { ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' +import { ChainId } from 'wallet/src/constants/chains' +import { + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { RootState } from 'wallet/src/state' +import { signerMnemonicAccount } from 'wallet/src/test/fixtures' +import { preloadedWalletState } from 'wallet/src/test/fixtures/wallet/redux' + +const account = signerMnemonicAccount() + +const MOCK_DATE_PROMPTED = Date.now() + +const state = { + ...preloadedWalletState(), + wallet: { + appRatingProvidedMs: MOCK_DATE_PROMPTED, + }, + transactions: { + [account.address]: { + [ChainId.Mainnet]: { + '0x123': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x456': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x890': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Approve }, + status: TransactionStatus.Success, + } as TransactionDetails, + }, + }, + }, +} as unknown as RootState + +describe('consecutiveSwapsSelector', () => { + it('returns false for empty state', () => { + const isConsecutiveSwaps = hasConsecutiveRecentSwapsSelector({ + ...state, + transactions: {}, + }) + + expect(isConsecutiveSwaps).toBeFalsy() + }) + + it('returns false when no new swaps since prompt', () => { + const condition = hasConsecutiveRecentSwapsSelector({ + ...state, + wallet: { appRatingPromptedMs: MOCK_DATE_PROMPTED + 2000 }, + } as RootState) + + expect(condition).toBeFalsy() + }) + + it('returns false when last swaps contain failure', () => { + const isConsecutiveSwaps = hasConsecutiveRecentSwapsSelector({ + ...state, + transactions: { + [account.address]: { + [ChainId.Mainnet]: { + '0x123': { + addedTime: MOCK_DATE_PROMPTED, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x456': { + addedTime: MOCK_DATE_PROMPTED + 500, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x890': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Failed, + } as TransactionDetails, + }, + }, + }, + }) + + expect(isConsecutiveSwaps).toBeFalsy() + }) + + it('returns false for consecutive success, but not swap type', () => { + const isConsecutiveSwaps = hasConsecutiveRecentSwapsSelector({ + ...state, + transactions: { + [account.address]: { + [ChainId.Mainnet]: { + '0x123': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x890': { + addedTime: MOCK_DATE_PROMPTED + 1000, + typeInfo: { type: TransactionType.Approve }, + status: TransactionStatus.Success, + } as TransactionDetails, + }, + }, + }, + }) + + expect(isConsecutiveSwaps).toBeFalsy() + }) + + it('returns false when last swap was done over a min ago', () => { + const isConsecutiveSwaps = hasConsecutiveRecentSwapsSelector({ + ...state, + transactions: { + [account.address]: { + [ChainId.Mainnet]: { + '0x123': { + addedTime: MOCK_DATE_PROMPTED - ONE_HOUR_MS, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + '0x456': { + addedTime: MOCK_DATE_PROMPTED - 2 * ONE_MINUTE_MS, + typeInfo: { type: TransactionType.Swap }, + status: TransactionStatus.Success, + } as TransactionDetails, + }, + }, + }, + }) + + expect(isConsecutiveSwaps).toBeFalsy() + }) + + it('returns true for consecutive swaps', () => { + const isConsecutiveSwaps = hasConsecutiveRecentSwapsSelector(state) + + expect(isConsecutiveSwaps).toBeTruthy() + }) +}) diff --git a/apps/mobile/src/features/appRating/selectors.ts b/apps/mobile/src/features/appRating/selectors.ts new file mode 100644 index 0000000..dc3bf54 --- /dev/null +++ b/apps/mobile/src/features/appRating/selectors.ts @@ -0,0 +1,46 @@ +import { createSelector, Selector } from '@reduxjs/toolkit' +import { flattenObjectOfObjects } from 'utilities/src/primitives/objects' +import { ONE_MINUTE_MS } from 'utilities/src/time/time' +import { selectTransactions } from 'wallet/src/features/transactions/selectors' +import { TransactionStateMap } from 'wallet/src/features/transactions/slice' +import { + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { RootState } from 'wallet/src/state' + +const NUM_CONSECUTIVE_SWAPS = 2 + +export const hasConsecutiveRecentSwapsSelector: Selector = createSelector( + [selectTransactions, (state: RootState): number => state.wallet.appRatingPromptedMs ?? 0], + (transactions: TransactionStateMap, appRatingPromptedMs): boolean => { + const swapTxs: Array = [] + + const txs = flattenObjectOfObjects(transactions) + for (const tx of txs) { + for (const transaction of Object.values(tx)) { + // ignore transactions completed before last prompt + if (transaction.addedTime < appRatingPromptedMs) { + continue + } + + if (transaction.typeInfo.type === TransactionType.Swap) { + swapTxs.push(transaction) + } + } + } + + const recentSwaps = swapTxs.slice(-NUM_CONSECUTIVE_SWAPS) + const mostRecentSwapTime = recentSwaps[recentSwaps.length - 1]?.addedTime + const mostRecentSwapLessThanMinAgo = Boolean( + mostRecentSwapTime && Date.now() - mostRecentSwapTime < ONE_MINUTE_MS + ) + + return ( + swapTxs.length >= NUM_CONSECUTIVE_SWAPS && + recentSwaps.every((tx) => tx.status === TransactionStatus.Success) && + mostRecentSwapLessThanMinAgo + ) + } +) diff --git a/apps/mobile/src/features/authentication/LockScreenModal.tsx b/apps/mobile/src/features/authentication/LockScreenModal.tsx new file mode 100644 index 0000000..f68b6bc --- /dev/null +++ b/apps/mobile/src/features/authentication/LockScreenModal.tsx @@ -0,0 +1,70 @@ +import React from 'react' +import { Image, StyleSheet } from 'react-native' +import { Modal } from 'src/components/modals/Modal' +import { useLockScreenContext } from 'src/features/authentication/lockScreenContext' +import { useBiometricPrompt } from 'src/features/biometrics/hooks' +import { Flex, TouchableArea, useDeviceDimensions, useDeviceInsets, useIsDarkMode } from 'ui/src' +import { UNISWAP_LOGO_LARGE } from 'ui/src/assets' +import { isAndroid } from 'uniswap/src/utils/platform' + +export const SPLASH_SCREEN = { uri: 'SplashScreen' } + +export function LockScreenModal(): JSX.Element | null { + const { isLockScreenVisible, animationType, setIsLockScreenVisible } = useLockScreenContext() + const { trigger } = useBiometricPrompt(() => setIsLockScreenVisible(false)) + const insets = useDeviceInsets() + const dimensions = useDeviceDimensions() + const isDarkMode = useIsDarkMode() + + if (!isLockScreenVisible) { + return null + } + + return ( + + => trigger()}> + + {/* Android has a different implementation, which is not set in stone yet, so skipping it for now */} + {isAndroid ? ( + + ) : ( + + )} + + + + ) +} + +const style = StyleSheet.create({ + logoStyle: { + height: 180, + width: 165, + }, +}) diff --git a/apps/mobile/src/features/authentication/lockScreenContext.tsx b/apps/mobile/src/features/authentication/lockScreenContext.tsx new file mode 100644 index 0000000..78336f6 --- /dev/null +++ b/apps/mobile/src/features/authentication/lockScreenContext.tsx @@ -0,0 +1,82 @@ +import { useIsFocused } from '@react-navigation/core' +import React, { createContext, PropsWithChildren, useContext, useState } from 'react' +import { useBiometricAppSettings } from 'src/features/biometrics/hooks' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' + +export interface LockScreenContextValue { + isLockScreenVisible: boolean + animationType: AnimationType + setIsLockScreenVisible: (value: boolean) => void + setAnimationType: (value: AnimationType) => void +} + +type AnimationType = 'none' | 'slide' | 'fade' | undefined + +const lockScreenContextValue: LockScreenContextValue = { + isLockScreenVisible: true, + animationType: 'none', + setIsLockScreenVisible: () => null, + setAnimationType: () => null, +} + +const LockScreenContext = createContext(lockScreenContextValue) + +export function LockScreenContextProvider({ children }: PropsWithChildren): JSX.Element { + const { requiredForAppAccess } = useBiometricAppSettings() + const [isVisible, setIsVisible] = useState(requiredForAppAccess) + const [animation, setAnimation] = useState('none') + + const setIsLockScreenVisible = (value: boolean): void => { + setIsVisible(value) + } + + const setAnimationType = (value: AnimationType): void => { + setAnimation(value) + } + + return ( + + {children} + + ) +} + +export function useLockScreenContext(): LockScreenContextValue { + return useContext(LockScreenContext) +} + +export function useLockScreenOnBlur(isDisabled?: boolean): void { + // Show splash screen if app switcher is opened + const { setIsLockScreenVisible } = useLockScreenContext() + const isFocused = useIsFocused() + useAppStateTrigger('inactive', 'active', () => { + if (!isFocused || isDisabled) { + return + } + setIsLockScreenVisible(false) + }) + useAppStateTrigger('active', 'inactive', () => { + if (!isFocused || isDisabled) { + return + } + setIsLockScreenVisible(true) + }) + useAppStateTrigger('background', 'active', () => { + if (!isFocused || isDisabled) { + return + } + setIsLockScreenVisible(false) + }) + useAppStateTrigger('active', 'background', () => { + if (!isFocused || isDisabled) { + return + } + setIsLockScreenVisible(true) + }) +} diff --git a/apps/mobile/src/features/balances/PortfolioBalance.tsx b/apps/mobile/src/features/balances/PortfolioBalance.tsx new file mode 100644 index 0000000..97a6e35 --- /dev/null +++ b/apps/mobile/src/features/balances/PortfolioBalance.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import AnimatedNumber from 'src/components/AnimatedNumber' +import { Flex, Shine } from 'ui/src' +import { NumberType } from 'utilities/src/format/types' +import { RelativeChange } from 'wallet/src/components/text/RelativeChange' +import { PollingInterval } from 'wallet/src/constants/misc' +import { isWarmLoadingStatus } from 'wallet/src/data/utils' +import { usePortfolioTotalValue } from 'wallet/src/features/dataApi/balances' +import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' +import { useAppFiatCurrency, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' + +interface PortfolioBalanceProps { + owner: Address +} + +export function PortfolioBalance({ owner }: PortfolioBalanceProps): JSX.Element { + const { data, loading, networkStatus } = usePortfolioTotalValue({ + address: owner, + // TransactionHistoryUpdater will refetch this query on new transaction. + // No need to be super aggressive with polling here. + pollInterval: PollingInterval.Normal, + }) + const currency = useAppFiatCurrency() + const currencyComponents = useAppFiatCurrencyInfo() + const { convertFiatAmount, convertFiatAmountFormatted } = useLocalizationContext() + + const isLoading = loading && !data + const isWarmLoading = !!data && isWarmLoadingStatus(networkStatus) + + const { percentChange, absoluteChangeUSD, balanceUSD } = data || {} + + const totalBalance = convertFiatAmountFormatted(balanceUSD, NumberType.PortfolioBalance) + const { amount: absoluteChange } = convertFiatAmount(absoluteChangeUSD) + // TODO gary re-enabling this for USD/Euros only, replace with more scalable approach + const shouldFadePortfolioDecimals = + (currency === FiatCurrency.UnitedStatesDollar || currency === FiatCurrency.Euro) && + currencyComponents.symbolAtFront + + return ( + + + + + + + + ) +} diff --git a/apps/mobile/src/features/balances/hooks.ts b/apps/mobile/src/features/balances/hooks.ts new file mode 100644 index 0000000..d5ecf1a --- /dev/null +++ b/apps/mobile/src/features/balances/hooks.ts @@ -0,0 +1,195 @@ +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, Share } from 'react-native' +import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { useAppDispatch } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { openModal } from 'src/features/modals/modalSlice' +import { useNavigateToSend } from 'src/features/send/hooks' +import { useNavigateToSwap } from 'src/features/swap/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { ChainId } from 'wallet/src/constants/chains' +import { usePortfolioCacheUpdater } from 'wallet/src/features/dataApi/balances' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { toggleTokenVisibility } from 'wallet/src/features/favorites/slice' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { + CurrencyId, + areCurrencyIdsEqual, + currencyIdToAddress, + currencyIdToChain, +} from 'wallet/src/utils/currencyId' +import { getTokenUrl } from 'wallet/src/utils/linking' + +interface TokenMenuParams { + currencyId: CurrencyId + tokenSymbolForNotification?: Nullable + portfolioBalance?: Nullable +} +type MenuAction = ContextMenuAction & { onPress: () => void } + +// Provide context menu related data for token +export function useTokenContextMenu({ + currencyId, + tokenSymbolForNotification, + portfolioBalance, +}: TokenMenuParams): { + menuActions: Array + onContextMenuPress: (e: NativeSyntheticEvent) => void +} { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const activeAccountAddress = useActiveAccountAddressWithThrow() + const navigateToSwap = useNavigateToSwap() + const navigateToSend = useNavigateToSend() + + const activeAccountHoldsToken = + portfolioBalance && areCurrencyIdsEqual(currencyId, portfolioBalance?.currencyInfo.currencyId) + const isHidden = !!portfolioBalance?.isHidden + + const currencyAddress = currencyIdToAddress(currencyId) + const currencyChainId = currencyIdToChain(currencyId) ?? ChainId.Mainnet + + const onPressSend = useCallback(() => { + // Do not show warning modal speed-bump if user is trying to send tokens they own + navigateToSend(currencyAddress, currencyChainId) + }, [currencyAddress, currencyChainId, navigateToSend]) + + const onPressReceive = useCallback( + () => + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ), + [dispatch] + ) + + const onPressSwap = useCallback( + (currencyField: CurrencyField) => { + // Do not show warning modal speed-bump if user is trying to swap tokens they own + navigateToSwap(currencyField, currencyAddress, currencyChainId) + }, + [currencyAddress, currencyChainId, navigateToSwap] + ) + + const onPressShare = useCallback(async () => { + const tokenUrl = getTokenUrl(currencyId) + if (!tokenUrl) { + return + } + try { + await Share.share({ + message: tokenUrl, + }) + sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, { + entity: ShareableEntity.Token, + url: tokenUrl, + }) + } catch (error) { + logger.error(error, { tags: { file: 'balances/hooks.ts', function: 'onPressShare' } }) + } + }, [currencyId]) + + const updateCache = usePortfolioCacheUpdater(activeAccountAddress) + + const onPressHiddenStatus = useCallback(() => { + /** + * This update changes the parameters sent in the call to `portfolios`, + * resulting in a full reload of the portfolio from the server. + * To avoid the empty state while fetching the new portfolio, we manually + * modify the current one in the cache. + */ + updateCache(!isHidden, portfolioBalance ?? undefined) + + dispatch( + toggleTokenVisibility({ + accountAddress: activeAccountAddress, + currencyId: currencyId.toLowerCase(), + currentlyVisible: !isHidden, + }) + ) + if (tokenSymbolForNotification) { + dispatch( + pushNotification({ + type: AppNotificationType.AssetVisibility, + visible: !isHidden, + hideDelay: 2 * ONE_SECOND_MS, + assetName: tokenSymbolForNotification, + }) + ) + } + }, [ + activeAccountAddress, + currencyId, + dispatch, + isHidden, + tokenSymbolForNotification, + updateCache, + portfolioBalance, + ]) + + const menuActions = useMemo( + (): MenuAction[] => [ + { + title: t('common.button.buy'), + systemIcon: 'arrow.down', + onPress: () => onPressSwap(CurrencyField.OUTPUT), + }, + { + title: t('common.button.sell'), + systemIcon: 'arrow.up', + onPress: () => onPressSwap(CurrencyField.INPUT), + }, + { + title: t('common.button.send'), + systemIcon: 'paperplane', + onPress: onPressSend, + }, + { + title: t('common.button.receive'), + systemIcon: 'qrcode', + onPress: onPressReceive, + }, + { + title: t('common.button.share'), + systemIcon: 'square.and.arrow.up', + onPress: onPressShare, + }, + ...(activeAccountHoldsToken + ? [ + { + title: isHidden ? t('tokens.action.unhide') : t('tokens.action.hide'), + systemIcon: isHidden ? 'eye' : 'eye.slash', + destructive: !isHidden, + onPress: onPressHiddenStatus, + }, + ] + : []), + ], + [ + t, + onPressSend, + onPressReceive, + onPressShare, + activeAccountHoldsToken, + isHidden, + onPressHiddenStatus, + onPressSwap, + ] + ) + + const onContextMenuPress = useCallback( + (e: NativeSyntheticEvent): void => { + menuActions[e.nativeEvent.index]?.onPress?.() + }, + [menuActions] + ) + + return { menuActions, onContextMenuPress } +} diff --git a/apps/mobile/src/features/biometrics/context.tsx b/apps/mobile/src/features/biometrics/context.tsx new file mode 100644 index 0000000..69e6627 --- /dev/null +++ b/apps/mobile/src/features/biometrics/context.tsx @@ -0,0 +1,54 @@ +import { hasHardwareAsync } from 'expo-local-authentication' +import React, { createContext, PropsWithChildren, useContext, useState } from 'react' +import { BiometricAuthenticationStatus } from 'src/features/biometrics' +import { useAsyncData } from 'utilities/src/react/hooks' +import { debounceCallback } from 'utilities/src/time/timing' + +export interface BiometricContextValue { + authenticationStatus: BiometricAuthenticationStatus + setAuthenticationStatus: (value: BiometricAuthenticationStatus) => void + deviceSupportsBiometrics: boolean | undefined +} + +const biometricContextValue: BiometricContextValue = { + authenticationStatus: BiometricAuthenticationStatus.Invalid, + setAuthenticationStatus: () => undefined, + deviceSupportsBiometrics: undefined, +} + +const BiometricContext = createContext(biometricContextValue) + +export function BiometricContextProvider({ children }: PropsWithChildren): JSX.Element { + // global authenticationStatus + const [status, setStatus] = useState( + BiometricAuthenticationStatus.Invalid + ) + const { triggerDebounce, cancelDebounce } = debounceCallback( + () => setStatus(BiometricAuthenticationStatus.Invalid), + 10000 + ) + const setAuthenticationStatus = (value: BiometricAuthenticationStatus): void => { + setStatus(value) + if (value === BiometricAuthenticationStatus.Authenticated) { + triggerDebounce() + } else { + cancelDebounce() + } + } + const { data: deviceSupportsBiometrics } = useAsyncData(hasHardwareAsync) + + return ( + + {children} + + ) +} + +export function useBiometricContext(): BiometricContextValue { + return useContext(BiometricContext) +} diff --git a/apps/mobile/src/features/biometrics/hooks.tsx b/apps/mobile/src/features/biometrics/hooks.tsx new file mode 100644 index 0000000..46d173a --- /dev/null +++ b/apps/mobile/src/features/biometrics/hooks.tsx @@ -0,0 +1,135 @@ +import { + AuthenticationType, + hasHardwareAsync, + isEnrolledAsync, + supportedAuthenticationTypesAsync, +} from 'expo-local-authentication' +import { useAppSelector } from 'src/app/hooks' +import { BiometricAuthenticationStatus, tryLocalAuthenticate } from 'src/features/biometrics' +import { useBiometricContext } from 'src/features/biometrics/context' +import { BiometricSettingsState } from 'src/features/biometrics/slice' +import { isAndroid } from 'uniswap/src/utils/platform' +import { useAsyncData } from 'utilities/src/react/hooks' + +type TriggerArgs = { + params?: T + successCallback?: (params?: T) => void + failureCallback?: () => void +} + +/** + * Hook shortcut to use the biometric prompt. + * + * It can be used by either declaring the success/failure callbacks at the time you call the hook, + * or by declaring them when you call the trigger function: + * + * Example 1: + * + * ```ts + * const { trigger } = useBiometricPrompt(() => { success() }, () => { failure() }) + * triger({ + * params: { ... }, + * }) + * ``` + * + * Example 2: + * + * ```ts + * const { trigger } = useBiometricPrompt() + * triger({ + * successCallback: () => { success() }, + * failureCallback: () => { success() }, + * params: { ... }, + * }) + * ``` + * + * TODO(MOB-2523): standardize usage of this hook and remove the style of Example 1. + * + * @returns trigger Trigger the OS biometric flow and invokes successCallback on success. + */ +export function useBiometricPrompt( + successCallback?: (params?: T) => void, + failureCallback?: () => void +): { + trigger: (args?: TriggerArgs) => Promise +} { + const { setAuthenticationStatus } = useBiometricContext() + + const trigger = async (args?: TriggerArgs): Promise => { + setAuthenticationStatus(BiometricAuthenticationStatus.Authenticating) + const authStatus = await tryLocalAuthenticate() + + setAuthenticationStatus(authStatus) + + const _successCallback = args?.successCallback ?? successCallback + const _failureCallback = args?.failureCallback ?? failureCallback + + if ( + biometricAuthenticationSuccessful(authStatus) || + biometricAuthenticationDisabledByOS(authStatus) + ) { + _successCallback?.(args?.params) + } else { + _failureCallback?.() + } + } + + return { trigger } +} + +export function biometricAuthenticationSuccessful(status: BiometricAuthenticationStatus): boolean { + return status === BiometricAuthenticationStatus.Authenticated +} + +export function biometricAuthenticationRejected(status: BiometricAuthenticationStatus): boolean { + return status === BiometricAuthenticationStatus.Rejected +} + +export function biometricAuthenticationDisabledByOS( + status: BiometricAuthenticationStatus +): boolean { + return ( + status === BiometricAuthenticationStatus.Unsupported || + status === BiometricAuthenticationStatus.MissingEnrollment + ) +} + +/** + * Check function of biometric device support + * @returns object representing biometric auth support by type + */ +export function useDeviceSupportsBiometricAuth(): { touchId: boolean; faceId: boolean } { + // check if device supports biometric authentication + const authenticationTypes = useAsyncData(supportedAuthenticationTypesAsync).data + return { + touchId: authenticationTypes?.includes(AuthenticationType.FINGERPRINT) ?? false, + faceId: authenticationTypes?.includes(AuthenticationType.FACIAL_RECOGNITION) ?? false, + } +} + +export const checkOsBiometricAuthEnabled = async (): Promise => { + const [compatible, enrolled] = await Promise.all([hasHardwareAsync(), isEnrolledAsync()]) + return compatible && enrolled +} + +/** + * Hook to determine whether biometric auth is enabled in OS settings + * @returns if Face ID or Touch ID is enabled + */ +export function useOsBiometricAuthEnabled(): boolean | undefined { + return useAsyncData(checkOsBiometricAuthEnabled).data +} + +export function useBiometricAppSettings(): BiometricSettingsState { + const biometricSettings = useAppSelector((state) => state.biometricSettings) + return biometricSettings +} + +export function useBiometricName(isTouchIdSupported: boolean, shouldCapitalize?: boolean): string { + if (isAndroid) { + return shouldCapitalize ? 'Biometrics' : 'biometrics' + } + + // iOS is always capitalized + return isTouchIdSupported ? 'Touch ID' : 'Face ID' +} diff --git a/apps/mobile/src/features/biometrics/index.test.ts b/apps/mobile/src/features/biometrics/index.test.ts new file mode 100644 index 0000000..2d774b5 --- /dev/null +++ b/apps/mobile/src/features/biometrics/index.test.ts @@ -0,0 +1,54 @@ +import { authenticateAsync, hasHardwareAsync, isEnrolledAsync } from 'expo-local-authentication' +import { BiometricAuthenticationStatus, tryLocalAuthenticate } from 'src/features/biometrics' + +jest.mock('expo-local-authentication') + +const mockedHasHardwareAsync = >hasHardwareAsync +const mockedIsEnrolledAsync = >isEnrolledAsync +const mockedAuthenticateAsync = >authenticateAsync + +describe(tryLocalAuthenticate, () => { + it('checks hardware compatibility', async () => { + mockedHasHardwareAsync.mockResolvedValue(false) + + const status = await tryLocalAuthenticate() + + expect(status).toEqual(BiometricAuthenticationStatus.Unsupported) + }) + + it('checks enrollement', async () => { + mockedHasHardwareAsync.mockResolvedValue(true) + mockedIsEnrolledAsync.mockResolvedValue(false) + mockedAuthenticateAsync.mockResolvedValue({ success: false, error: '' }) + + const status = await tryLocalAuthenticate() + + expect(status).toEqual(BiometricAuthenticationStatus.MissingEnrollment) + }) + + it('fails to authenticate when user rejects', async () => { + mockedHasHardwareAsync.mockResolvedValue(true) + mockedIsEnrolledAsync.mockResolvedValue(true) + mockedAuthenticateAsync.mockResolvedValue({ success: false, error: '' }) + + const status = await tryLocalAuthenticate() + + expect(status).toEqual(BiometricAuthenticationStatus.Rejected) + }) + + it('authenticates when user accepts', async () => { + mockedHasHardwareAsync.mockResolvedValue(true) + mockedIsEnrolledAsync.mockResolvedValue(true) + mockedAuthenticateAsync.mockResolvedValue({ success: true }) + + const status = await tryLocalAuthenticate() + + expect(status).toEqual(BiometricAuthenticationStatus.Authenticated) + }) + + it('always return authenticated when disabled', async () => { + const status = await tryLocalAuthenticate() + + expect(status).toEqual(BiometricAuthenticationStatus.Authenticated) + }) +}) diff --git a/apps/mobile/src/features/biometrics/index.ts b/apps/mobile/src/features/biometrics/index.ts new file mode 100644 index 0000000..66132c6 --- /dev/null +++ b/apps/mobile/src/features/biometrics/index.ts @@ -0,0 +1,90 @@ +import { + authenticateAsync, + hasHardwareAsync, + isEnrolledAsync, + LocalAuthenticationResult, +} from 'expo-local-authentication' +import { NativeModulesProxy } from 'expo-modules-core' +import { logger } from 'utilities/src/logger/logger' +import i18n from 'wallet/src/i18n/i18n' + +const ELA = NativeModulesProxy.ExpoLocalAuthentication + +/** + * Biometric authentication statuses + * Note. Sorted by authentication level + */ +export enum BiometricAuthenticationStatus { + Unsupported = 'UNSUPPORTED', + MissingEnrollment = 'MISSING_ENROLLMENT', + Rejected = 'REJECTED', + Authenticated = 'AUTHENTICATED', + Canceled = 'CANCELED', + Authenticating = 'AUTHENTICATING', + Lockout = 'LOCKOUT', + UserCancel = 'USER_CANCEL', + SystemCancel = 'SYSTEM_CANCEL', + Invalid = 'INVALID', +} + +export async function enroll(): Promise { + ELA?.enrollForAuthentication() +} + +// TODO: [MOB-220] Move into a saga +export async function tryLocalAuthenticate(): Promise { + try { + const compatible = await hasHardwareAsync() + + if (!compatible) { + return BiometricAuthenticationStatus.Unsupported + } + + /** + * Important: ExpoLocalAuthentication.isEnrolledAsync() method nested in isEnrolledAsync() returns false when + when users exceeds the amount of retries. Exactly the same when user has no biometric setup on the device + and thats why we have to call authenticateAsync to be able to distinguish between different errors. + */ + const enrolled = await isEnrolledAsync() + const result = await authenticateAsync({ + cancelLabel: i18n.t('common.button.cancel'), + promptMessage: i18n.t('settings.setting.biometrics.auth'), + requireConfirmation: false, + }) + + if (result.success === true) { + return BiometricAuthenticationStatus.Authenticated + } + + if (isInLockout(result)) { + return BiometricAuthenticationStatus.Lockout + } + + if (isCanceledByUser(result)) { + return BiometricAuthenticationStatus.UserCancel + } + + if (isCanceledBySystem(result)) { + return BiometricAuthenticationStatus.SystemCancel + } + + if (!enrolled) { + return BiometricAuthenticationStatus.MissingEnrollment + } + + return BiometricAuthenticationStatus.Rejected + } catch (error) { + logger.error(error, { tags: { file: 'biometrics/index', function: 'tryLocalAuthenticate' } }) + + return BiometricAuthenticationStatus.Rejected + } +} + +const isInLockout = (result: LocalAuthenticationResult): boolean => + result.success === false && result.error === 'lockout' + +const isCanceledByUser = (result: LocalAuthenticationResult): boolean => + result.success === false && result.error === 'user_cancel' + +const isCanceledBySystem = (result: LocalAuthenticationResult): boolean => + result.success === false && result.error === 'system_cancel' diff --git a/apps/mobile/src/features/biometrics/slice.ts b/apps/mobile/src/features/biometrics/slice.ts new file mode 100644 index 0000000..f0645e1 --- /dev/null +++ b/apps/mobile/src/features/biometrics/slice.ts @@ -0,0 +1,44 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' + +export enum BiometricSettingType { + RequiredForAppAccess, + RequiredForTransactions, +} + +export interface BiometricSettingsState { + requiredForAppAccess: boolean + requiredForTransactions: boolean +} + +export const initialBiometricsSettingsState: BiometricSettingsState = { + requiredForAppAccess: false, + requiredForTransactions: false, +} + +const slice = createSlice({ + name: 'biometricSettings', + initialState: initialBiometricsSettingsState, + reducers: { + setRequiredForAppAccess: (state, action: PayloadAction) => { + state.requiredForAppAccess = action.payload + }, + setRequiredForTransactions: (state, action: PayloadAction) => { + state.requiredForTransactions = action.payload + }, + resetSettings: () => initialBiometricsSettingsState, + }, + extraReducers: (builder) => { + builder.addCase(setFinishedOnboarding, (state, action) => { + // disable biometrics if user has no wallets + if (!action.payload.finishedOnboarding) { + state.requiredForAppAccess = false + state.requiredForTransactions = false + } + }) + }, +}) + +export const { setRequiredForAppAccess, setRequiredForTransactions, resetSettings } = slice.actions + +export const biometricSettingsReducer = slice.reducer diff --git a/apps/mobile/src/features/biometrics/useBiometricCheck.ts b/apps/mobile/src/features/biometrics/useBiometricCheck.ts new file mode 100644 index 0000000..f8a7524 --- /dev/null +++ b/apps/mobile/src/features/biometrics/useBiometricCheck.ts @@ -0,0 +1,74 @@ +import { useCallback } from 'react' +import { useLockScreenContext } from 'src/features/authentication/lockScreenContext' +import { BiometricAuthenticationStatus } from 'src/features/biometrics' +import { useBiometricContext } from 'src/features/biometrics/context' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { hideSplashScreen } from 'src/utils/splashScreen' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' +import { useAsyncData } from 'utilities/src/react/hooks' + +// TODO: [MOB-221] handle scenario where user has biometrics enabled as in-app security but disables it at the OS level +export function useBiometricCheck(): void { + const { requiredForAppAccess } = useBiometricAppSettings() + const { setIsLockScreenVisible } = useLockScreenContext() + const { authenticationStatus } = useBiometricContext() + const successCallback = (): void => { + setIsLockScreenVisible(false) + } + + const { trigger } = useBiometricPrompt(successCallback) + + const triggerBiometricCheck = useCallback(async () => { + if (requiredForAppAccess) { + await trigger() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) // runs only on mount so it doesn't run on setting change + + useAsyncData(triggerBiometricCheck) + + useAppStateTrigger('background', 'active', async () => { + if (requiredForAppAccess) { + await trigger() + } + }) + + useAppStateTrigger('inactive', 'background', () => { + if (requiredForAppAccess) { + setIsLockScreenVisible(true) + } + }) + + useAppStateTrigger('active', 'background', () => { + if (requiredForAppAccess) { + setIsLockScreenVisible(true) + } + }) + + useAppStateTrigger('inactive', 'active', async () => { + hideSplashScreen() // In case of a race condition where splash screen is not hidden, we want to hide when FaceID forces an app state change + // Requires negative check because we don't want to authenticate when switching between active and inactive state + // It is just required for the case when authentication was requested but user went to app switcher and back to the app + // to avoid authentication + if ( + requiredForAppAccess && + authenticationStatus !== BiometricAuthenticationStatus.Authenticating && + authenticationStatus !== BiometricAuthenticationStatus.SystemCancel && + authenticationStatus !== BiometricAuthenticationStatus.UserCancel && + authenticationStatus !== BiometricAuthenticationStatus.Rejected && + authenticationStatus !== BiometricAuthenticationStatus.Lockout + ) { + setIsLockScreenVisible(false) + } + }) + + useAppStateTrigger('active', 'inactive', () => { + hideSplashScreen() // In case of a race condition where splash screen is not hidden, we want to hide when FaceID forces an app state change + if ( + requiredForAppAccess && + authenticationStatus !== BiometricAuthenticationStatus.Authenticating + ) { + setIsLockScreenVisible(true) + } + }) +} diff --git a/apps/mobile/src/features/contracts/useContract.ts b/apps/mobile/src/features/contracts/useContract.ts new file mode 100644 index 0000000..d9018d1 --- /dev/null +++ b/apps/mobile/src/features/contracts/useContract.ts @@ -0,0 +1,43 @@ +// Based partly on https://github.com/Uniswap/interface/blob/main/src/hooks/useContract.ts + +import { Contract, ContractInterface } from 'ethers' +import { useMemo } from 'react' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { Erc20 } from 'uniswap/src/abis/types' +import { logger } from 'utilities/src/logger/logger' +import { ChainId } from 'wallet/src/constants/chains' +import { useContractManager, useProvider } from 'wallet/src/features/wallet/context' + +export function useContract( + chainId: ChainId, + addressOrAddressMap: string | { [chainId: number]: string } | undefined, + ABI: ContractInterface +): T | null { + const provider = useProvider(chainId) + const contractsManager = useContractManager() + + return useMemo(() => { + if (!addressOrAddressMap || !ABI || !provider || !chainId) { + return null + } + let address: Address | undefined + if (typeof addressOrAddressMap === 'string') { + address = addressOrAddressMap + } else { + address = addressOrAddressMap[chainId] + } + if (!address) { + return null + } + try { + return contractsManager.getOrCreateContract(chainId, address, provider, ABI) + } catch (error) { + logger.error(error, { tags: { file: 'useContract', function: 'useContract' } }) + return null + } + }, [chainId, addressOrAddressMap, ABI, provider, contractsManager]) as T +} + +export function useTokenContract(chainId: ChainId, tokenAddress?: Address): Erc20 | null { + return useContract(chainId, tokenAddress, ERC20_ABI) +} diff --git a/apps/mobile/src/features/dataApi/balances.test.ts b/apps/mobile/src/features/dataApi/balances.test.ts new file mode 100644 index 0000000..1965491 --- /dev/null +++ b/apps/mobile/src/features/dataApi/balances.test.ts @@ -0,0 +1,48 @@ +import { preloadedMobileState } from 'src/test/fixtures' +import { act, renderHook, waitFor } from 'src/test/test-utils' +import { SAMPLE_CURRENCY_ID_1, portfolio, portfolioBalances } from 'wallet/src/test/fixtures' +import { queryResolvers } from 'wallet/src/test/utils' +import { useBalances } from './balances' + +const preloadedState = preloadedMobileState() + +describe(useBalances, () => { + it('returns null if no currency was specified', async () => { + const { result } = renderHook(() => useBalances(undefined), { + preloadedState, + }) + + await act(() => undefined) + + expect(result.current).toEqual(null) + }) + + it('returns empty array if no balances are available', async () => { + const { result } = renderHook(() => useBalances([SAMPLE_CURRENCY_ID_1]), { + preloadedState, + }) + + expect(result.current).toEqual(null) // null while data is loading + + await act(() => undefined) + + expect(result.current).toEqual([]) // empty array when data is loaded + }) + + it('returns balances for specified currencies if they exist in the portfolio', async () => { + const Portfolio = portfolio() + const balances = portfolioBalances({ portfolio: Portfolio }) + const { resolvers } = queryResolvers({ + portfolios: () => [Portfolio], + }) + const { result } = renderHook( + () => useBalances(balances.map(({ currencyInfo: { currencyId } }) => currencyId)), + { preloadedState, resolvers } + ) + + await waitFor(() => { + // The response contains only the first currency as the second one is not in the portfolio + expect(result.current).toEqual(balances) + }) + }) +}) diff --git a/apps/mobile/src/features/dataApi/balances.ts b/apps/mobile/src/features/dataApi/balances.ts new file mode 100644 index 0000000..e864e82 --- /dev/null +++ b/apps/mobile/src/features/dataApi/balances.ts @@ -0,0 +1,24 @@ +import { useMemo } from 'react' +import { usePortfolioBalances } from 'wallet/src/features/dataApi/balances' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { CurrencyId } from 'wallet/src/utils/currencyId' + +/** Helper hook to retrieve balances for a set of currencies for the active account. */ +export function useBalances(currencies: CurrencyId[] | undefined): PortfolioBalance[] | null { + const address = useActiveAccountAddressWithThrow() + const { data: balances } = usePortfolioBalances({ + address, + fetchPolicy: 'cache-and-network', + }) + + return useMemo(() => { + if (!currencies || !currencies.length || !balances) { + return null + } + + return currencies + .map((id: CurrencyId) => balances[id] ?? null) + .filter((x): x is PortfolioBalance => Boolean(x)) + }, [balances, currencies]) +} diff --git a/apps/mobile/src/features/deepLinking/README.md b/apps/mobile/src/features/deepLinking/README.md new file mode 100644 index 0000000..c6948b8 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/README.md @@ -0,0 +1,42 @@ +# Universal Links + +Universal links allow 3rd parties to prompt the app to open to specific screens when it is installed on their device. If the app isn't installed it will open that page in Safari (a 404 on uniswap.org in this case). All universal links must use the the prefix `https://uniswap.org/app`. + +## Supported Screens + +Currently, there are two screens that have deep link support: `transaction` and `swap` screens. These screens are specified by setting the query parameter `screen`. Given the wallet supports multiple imported addresses, all routes must also specify the `userAddress` the deep link is referring to. + +Failing to include a valid `screen` or `userAddress` will result in the app opening up to the Home screen. + +### Activity Screen + +Routes to activity screen for given `userAddress`. + +Example: + +``` +https://uniswap.org/app?screen=transaction&userAddress=0x123...789 +``` + +### Swap Screen + +When routing to the swap screen, including the various swap data as query parameters will open the swap screen with the swap details populated. Failing to include the required swap parameters or providing invalid parameters will result in the swap screen opening without any details populated. + +Parameters: + +- `inputCurrencyId`: the currency the user wishes to swap. Must be of format - +- `ouputCurrencyId`: the currency the user wishes to receive. Must be of format - +- `currencyField`: used to specify whether `amount` refers to how much the user wishes to expend or receive. Value can either be `input` or `output` +- `amount`: the currency amount the user either wishes to expend or receive + +Example (swap 100 Ethereum mainnet DAI for Ethereum mainnet UNI): + +``` +https://uniswap.org/app?screen=swap&userAddress=0x123...789&inputCurrencyId=1-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=1-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=input&amount=100 +``` + +Example (swap Polygon DAI for 100 Polygon UNI): + +``` +https://uniswap.org/app?screen=swap&userAddress=0x123...789&inputCurrencyId=137-0x6B175474E89094C44Da98b954EedeAC495271d0F&outputCurrencyId=137-0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984¤cyField=output&amount=100 +``` diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts new file mode 100644 index 0000000..64905e2 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.test.ts @@ -0,0 +1,371 @@ +import { expectSaga } from 'redux-saga-test-plan' +import { call } from 'redux-saga/effects' +import { navigationRef } from 'src/app/navigation/NavigationContainer' +import { + handleDeepLink, + handleUniswapAppDeepLink, + handleWalletConnectDeepLink, + LinkSource, + parseAndValidateUserAddress, +} from 'src/features/deepLinking/handleDeepLinkSaga' + +import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' +import { openModal, OpenModalParams } from 'src/features/modals/modalSlice' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/saga' +import { Screens } from 'src/screens/Screens' +import { UNISWAP_APP_HOSTNAME } from 'uniswap/src/constants/urls' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import { ModalName } from 'wallet/src/telemetry/constants' +import { + SAMPLE_CURRENCY_ID_1, + SAMPLE_CURRENCY_ID_2, + SAMPLE_SEED_ADDRESS_1, + SAMPLE_SEED_ADDRESS_2, + signerMnemonicAccount, +} from 'wallet/src/test/fixtures' + +const account = signerMnemonicAccount() + +const swapUrl = `https://uniswap.org/app?screen=swap&userAddress=${account.address}&inputCurrencyId=${SAMPLE_CURRENCY_ID_1}&outputCurrencyId=${SAMPLE_CURRENCY_ID_2}¤cyField=INPUT` +const transactionUrl = `https://uniswap.org/app?screen=transaction&userAddress=${account.address}` +const swapDeepLinkPayload = { url: swapUrl, coldStart: false } +const transactionDeepLinkPayload = { url: transactionUrl, coldStart: false } +const unsupportedScreenDeepLinkPayload = { + url: `https://uniswap.org/app?screen=send&userAddress=${account.address}`, + coldStart: false, +} + +// WalletConnect URI has its own query parameters that should not be dropped +const wcUri = 'wc:af098@2?relay-protocol=irn&symKey=51e' +export const wcUniversalLinkUrl = `https://uniswap.org/app/wc?uri=${wcUri}` +export const wcAsParamInUniwapScheme = `uniswap://wc?uri=${wcUri}` +export const wcInUniwapScheme = `uniswap://${wcUri}` +const invalidUrlSchemeUrl = `uniswap://invalid?param=pepe` + +const stateWithActiveAccountAddress = { + wallet: { + accounts: { + [account.address]: account, + }, + activeAccountAddress: account.address, + }, +} + +describe(handleDeepLink, () => { + beforeAll(() => { + jest.spyOn(navigationRef, 'isReady').mockReturnValue(true) + jest.spyOn(navigationRef, 'navigate').mockReturnValue(undefined) + }) + + it('Routes to the swap deep link handler if screen=swap and userAddress is valid', () => { + return expectSaga(handleDeepLink, { payload: swapDeepLinkPayload, type: '' }) + .withState(stateWithActiveAccountAddress) + .call(parseAndValidateUserAddress, account.address) + .put(setAccountAsActive(account.address)) + .call(sendMobileAnalyticsEvent, MobileEventName.DeepLinkOpened, { + url: swapDeepLinkPayload.url, + screen: 'swap', + is_cold_start: swapDeepLinkPayload.coldStart, + }) + .silentRun() + }) + + it('Routes to the transaction deep link handler if screen=transaction and userAddress is valid', () => { + return expectSaga(handleDeepLink, { payload: transactionDeepLinkPayload, type: '' }) + .withState(stateWithActiveAccountAddress) + .call(handleTransactionLink) + .call(sendMobileAnalyticsEvent, MobileEventName.DeepLinkOpened, { + url: transactionDeepLinkPayload.url, + screen: 'transaction', + is_cold_start: transactionDeepLinkPayload.coldStart, + }) + .silentRun() + }) + + it('Fails if the screen param is not supported', () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined) + + return expectSaga(handleDeepLink, { payload: unsupportedScreenDeepLinkPayload, type: '' }) + .withState(stateWithActiveAccountAddress) + .silentRun() + }) + + it('Fails if the userAddress does not exist in the wallet', () => { + return expectSaga(handleDeepLink, { payload: swapDeepLinkPayload, type: '' }) + .withState({ + wallet: { + accounts: {}, + activeAccountAddress: null, + }, + }) + .returns(undefined) + .silentRun() + }) + + it('Handles WalletConnect connection using Universal Link URL', () => { + return expectSaga(handleDeepLink, { + payload: { url: wcUniversalLinkUrl, coldStart: false }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .provide([[call(waitForWcWeb3WalletIsReady), undefined]]) + .call(handleWalletConnectDeepLink, wcUri) + .returns(undefined) + .silentRun() + }) + + it('Handles WalletConnect connection using Uniswap URL scheme with WalletConnect URI as query param', () => { + return expectSaga(handleDeepLink, { + payload: { url: wcAsParamInUniwapScheme, coldStart: false }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .provide([[call(waitForWcWeb3WalletIsReady), undefined]]) + .call(handleWalletConnectDeepLink, wcUri) + .returns(undefined) + .silentRun() + }) + + it('Handles WalletConnect connection using Uniswap URL scheme with WalletConnect URI', () => { + return expectSaga(handleDeepLink, { + payload: { url: wcInUniwapScheme, coldStart: false }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .provide([[call(waitForWcWeb3WalletIsReady), undefined]]) + .call(handleWalletConnectDeepLink, wcUri) + .returns(undefined) + .silentRun() + }) + + it('Handles WalletConnect connection using WalletConnect URI', () => { + return expectSaga(handleDeepLink, { + payload: { url: wcUri, coldStart: false }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .provide([[call(waitForWcWeb3WalletIsReady), undefined]]) + .call(handleWalletConnectDeepLink, wcUri) + .returns(undefined) + .silentRun() + }) + + it('Fails arbitrary URL scheme deep link', () => { + return expectSaga(handleDeepLink, { + payload: { url: invalidUrlSchemeUrl, coldStart: false }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .returns(undefined) + .silentRun() + }) + + it('Handles Share NFT Item Universal Link', async () => { + const path = `nfts/asset/${SAMPLE_SEED_ADDRESS_1}/123` + const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` + const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const expectedModal: OpenModalParams = { + name: ModalName.Explore, + initialState: { + screen: Screens.NFTItem, + params: { + address: SAMPLE_SEED_ADDRESS_1, + tokenId: '123', + isSpam: false, + }, + }, + } + + await expectSaga(handleDeepLink, { + payload: { + url: hashedUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, `#/${path}`, hashedUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + + await expectSaga(handleDeepLink, { + payload: { + url: pathUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, path, pathUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + }) + + it('Handles Share NFT Collection Universal Link', async () => { + const path = `nfts/collection/${SAMPLE_SEED_ADDRESS_1}` + const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` + const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const expectedModal: OpenModalParams = { + name: ModalName.Explore, + initialState: { + screen: Screens.NFTCollection, + params: { + collectionAddress: SAMPLE_SEED_ADDRESS_1, + }, + }, + } + + await expectSaga(handleDeepLink, { + payload: { + url: hashedUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, `#/${path}`, hashedUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + + await expectSaga(handleDeepLink, { + payload: { + url: pathUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, path, pathUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + }) + + it('Handles Share Token Item Universal Link', async () => { + const path = `tokens/ethereum/${SAMPLE_SEED_ADDRESS_1}` + const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` + const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const expectedModal: OpenModalParams = { + name: ModalName.Explore, + initialState: { + screen: Screens.TokenDetails, + params: { + currencyId: `1-${SAMPLE_SEED_ADDRESS_1}`, + }, + }, + } + + await expectSaga(handleDeepLink, { + payload: { + url: hashedUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, `#/${path}`, hashedUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + + await expectSaga(handleDeepLink, { + payload: { + url: pathUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, path, pathUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + }) + + it('Handles Share currently active Account Address Universal Link', () => { + const hash = `#/address/${account.address}` + const url = `https://${UNISWAP_APP_HOSTNAME}/${hash}` + return expectSaga(handleDeepLink, { + payload: { + url, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, hash, url, LinkSource.Share) + .returns(undefined) + .silentRun() + }) + + it('Handles Share already added Account Address Universal Link', () => { + const hash = `#/address/${SAMPLE_SEED_ADDRESS_2}` + const url = `https://${UNISWAP_APP_HOSTNAME}/${hash}` + return expectSaga(handleDeepLink, { + payload: { + url, + coldStart: false, + }, + type: '', + }) + .withState({ + wallet: { + accounts: { + [account.address]: account, + [SAMPLE_SEED_ADDRESS_2]: account, + }, + activeAccountAddress: account.address, + }, + }) + .call(handleUniswapAppDeepLink, hash, url, LinkSource.Share) + .put(setAccountAsActive(SAMPLE_SEED_ADDRESS_2)) + .returns(undefined) + .silentRun() + }) + + it('Handles Share external Account Address Universal Link', async () => { + const path = `address/${SAMPLE_SEED_ADDRESS_2}` + const pathUrl = `https://${UNISWAP_APP_HOSTNAME}/${path}` + const hashedUrl = `https://${UNISWAP_APP_HOSTNAME}/#/${path}` + const expectedModal: OpenModalParams = { + name: ModalName.Explore, + initialState: { + screen: Screens.ExternalProfile, + params: { + address: SAMPLE_SEED_ADDRESS_2, + }, + }, + } + + await expectSaga(handleDeepLink, { + payload: { + url: hashedUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, `#/${path}`, hashedUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + + await expectSaga(handleDeepLink, { + payload: { + url: pathUrl, + coldStart: false, + }, + type: '', + }) + .withState(stateWithActiveAccountAddress) + .call(handleUniswapAppDeepLink, path, pathUrl, LinkSource.Share) + .put(openModal(expectedModal)) + .returns(undefined) + .silentRun() + }) +}) diff --git a/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts new file mode 100644 index 0000000..0ca094a --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleDeepLinkSaga.ts @@ -0,0 +1,361 @@ +import { createAction } from '@reduxjs/toolkit' +import { parseUri } from '@walletconnect/utils' +import { Alert } from 'react-native' +import { URL } from 'react-native-url-polyfill' +import { appSelect } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga' +import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' +import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' +import { openModal } from 'src/features/modals/modalSlice' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { waitForWcWeb3WalletIsReady } from 'src/features/walletConnect/saga' +import { pairWithWalletConnectURI } from 'src/features/walletConnect/utils' +import { setDidOpenFromDeepLink } from 'src/features/walletConnect/walletConnectSlice' +import { WidgetType } from 'src/features/widgets/widgets' +import { Screens } from 'src/screens/Screens' +import { call, put, takeLatest } from 'typed-redux-saga' +import { UNISWAP_APP_HOSTNAME, uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { fromUniswapWebAppLink } from 'wallet/src/features/chains/utils' +import { + selectAccounts, + selectActiveAccount, + selectActiveAccountAddress, + selectNonPendingAccounts, +} from 'wallet/src/features/wallet/selectors' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import i18n from 'wallet/src/i18n/i18n' +import { ModalName } from 'wallet/src/telemetry/constants' +import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' +import { UNISWAP_APP_NATIVE_TOKEN, openUri } from 'wallet/src/utils/linking' + +export interface DeepLink { + url: string + coldStart: boolean +} + +export enum LinkSource { + Widget = 'Widget', + Share = 'Share', +} + +export const UNISWAP_URL_SCHEME = 'uniswap://' +export const UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM = 'uniswap://wc?uri=' +const UNISWAP_URL_SCHEME_WIDGET = 'uniswap://widget/' +export const UNISWAP_WALLETCONNECT_URL = uniswapUrls.appBaseUrl + '/wc?uri=' +const WALLETCONNECT_URI_SCHEME = 'wc:' // https://eips.ethereum.org/EIPS/eip-1328 + +const NFT_ITEM_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/asset\/(0x[a-fA-F0-9]{40})\/(\d+)$/ +const NFT_COLLECTION_SHARE_LINK_HASH_REGEX = /^(#\/)?nfts\/collection\/(0x[a-fA-F0-9]{40})$/ +const TOKEN_SHARE_LINK_HASH_REGEX = RegExp( + // eslint-disable-next-line no-useless-escape + `^(#\/)?tokens\/([\\w\\d]*)\/(0x[a-fA-F0-9]{40}|${UNISWAP_APP_NATIVE_TOKEN})$` +) +const ADDRESS_SHARE_LINK_HASH_REGEX = /^(#\/)?address\/(0x[a-fA-F0-9]{40})$/ + +export const openDeepLink = createAction('deeplink/open') + +export function* deepLinkWatcher() { + yield* takeLatest(openDeepLink.type, handleDeepLink) +} + +export function* handleUniswapAppDeepLink(path: string, url: string, linkSource: LinkSource) { + // Navigate to the home page to ensure that a page isn't already open as a screen, + // which causes the bottom sheet to break + navigate(Screens.Home) + + // Handle NFT Item share (ex. https://app.uniswap.org/#/nfts/asset/0x.../123) + if (NFT_ITEM_SHARE_LINK_HASH_REGEX.test(path)) { + const [, , contractAddress, tokenId] = path.match(NFT_ITEM_SHARE_LINK_HASH_REGEX) || [] + if (!contractAddress || !tokenId) { + return + } + yield* put( + openModal({ + name: ModalName.Explore, + initialState: { + screen: Screens.NFTItem, + params: { + address: contractAddress, + tokenId, + isSpam: false, + }, + }, + }) + ) + yield* call(sendMobileAnalyticsEvent, MobileEventName.ShareLinkOpened, { + entity: ShareableEntity.NftItem, + url, + }) + return + } + + // Handle NFT collection share (ex. https://app.uniswap.org/#/nfts/collection/0x...) + if (NFT_COLLECTION_SHARE_LINK_HASH_REGEX.test(path)) { + const [, , contractAddress] = path.match(NFT_COLLECTION_SHARE_LINK_HASH_REGEX) || [] + if (!contractAddress) { + return + } + yield* put( + openModal({ + name: ModalName.Explore, + initialState: { + screen: Screens.NFTCollection, + params: { + collectionAddress: contractAddress, + }, + }, + }) + ) + yield* call(sendMobileAnalyticsEvent, MobileEventName.ShareLinkOpened, { + entity: ShareableEntity.NftCollection, + url, + }) + return + } + + // Handle Token share (ex. https://app.uniswap.org/#/tokens/ethereum/0x...) + if (TOKEN_SHARE_LINK_HASH_REGEX.test(path)) { + const [, , network, contractAddress] = path.match(TOKEN_SHARE_LINK_HASH_REGEX) || [] + const chainId = network && fromUniswapWebAppLink(network) + if (!chainId || !contractAddress) { + return + } + const currencyId = + contractAddress === UNISWAP_APP_NATIVE_TOKEN + ? buildNativeCurrencyId(chainId) + : buildCurrencyId(chainId, contractAddress) + yield* put( + openModal({ + name: ModalName.Explore, + initialState: { + screen: Screens.TokenDetails, + params: { + currencyId, + }, + }, + }) + ) + if (linkSource === LinkSource.Share) { + yield* call(sendMobileAnalyticsEvent, MobileEventName.ShareLinkOpened, { + entity: ShareableEntity.Token, + url, + }) + } else { + yield* call(sendMobileAnalyticsEvent, MobileEventName.WidgetClicked, { + widget_type: WidgetType.TokenPrice, + url, + }) + } + return + } + + // Handle Address share (ex. https://app.uniswap.org/#/address/0x...) + if (ADDRESS_SHARE_LINK_HASH_REGEX.test(path)) { + const [, , accountAddress] = path.match(ADDRESS_SHARE_LINK_HASH_REGEX) || [] + if (!accountAddress) { + return + } + const accounts = yield* appSelect(selectNonPendingAccounts) + const activeAccountAddress = yield* appSelect(selectActiveAccountAddress) + if (accountAddress === activeAccountAddress) { + return + } + + const isInternal = Boolean(accounts?.[accountAddress]) + if (isInternal) { + yield* put(setAccountAsActive(accountAddress)) + } else { + yield* put( + openModal({ + name: ModalName.Explore, + initialState: { + screen: Screens.ExternalProfile, + params: { + address: accountAddress, + }, + }, + }) + ) + } + yield* call(sendMobileAnalyticsEvent, MobileEventName.ShareLinkOpened, { + entity: ShareableEntity.Wallet, + url, + }) + return + } +} + +// eslint-disable-next-line complexity +export function* handleDeepLink(action: ReturnType) { + const { coldStart } = action.payload + try { + const url = new URL(action.payload.url) + const screen = url.searchParams.get('screen') + const userAddress = url.searchParams.get('userAddress') + const fiatOnRamp = url.searchParams.get('fiatOnRamp') === 'true' + + const activeAccount = yield* appSelect(selectActiveAccount) + if (!activeAccount) { + // For app.uniswap.org links it should open a browser with the link + // instead of handling it inside the app + if (url.hostname === UNISWAP_APP_HOSTNAME) { + yield* call(openUri, action.payload.url, /* openExternalBrowser */ true) + } + // Skip handling any other deep links + return + } + + // Handle WC deep link via connections in the format uniswap://wc?uri=${WC_URI} + // Ex: uniswap://wc?uri=wc:123@2?relay-protocol=irn&symKey=51e + if (action.payload.url.startsWith(UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM)) { + let wcUri = action.payload.url.split(UNISWAP_URL_SCHEME_WALLETCONNECT_AS_PARAM)[1] + if (!wcUri) { + return + } + // Decode URI to handle special characters like %3A => : + wcUri = decodeURIComponent(wcUri) + yield* call(handleWalletConnectDeepLink, wcUri) + return + } + + // Handle WC deep link via connections in the format uniswap://${WC_URI} + // Ex: uniswap://wc:123@2?relay-protocol=irn&symKey=51e + if (action.payload.url.startsWith(UNISWAP_URL_SCHEME + WALLETCONNECT_URI_SCHEME)) { + let wcUri = action.payload.url.split(UNISWAP_URL_SCHEME)[1] + if (!wcUri) { + return + } + // Decode URI to handle special characters like %3A => : + wcUri = decodeURIComponent(wcUri) + yield* call(handleWalletConnectDeepLink, wcUri) + return + } + + // Handles deep links from Uniswap Widgets (ex. uniswap://widget/#/tokens/ethereum/0x...) + if (action.payload.url.startsWith(UNISWAP_URL_SCHEME_WIDGET)) { + yield* call(handleUniswapAppDeepLink, url.hash, action.payload.url, LinkSource.Widget) + return + } + + // Skip handling any non-WalletConnect uniswap:// URL scheme deep links for now for security reasons + // Currently only used on WalletConnect Universal Link web page fallback button (https://uniswap.org/app/wc) + if (action.payload.url.startsWith(UNISWAP_URL_SCHEME)) { + // Set didOpenFromDeepLink so that `returnToPreviousApp()` is enabled during WalletConnect flows + yield* put(setDidOpenFromDeepLink(true)) + return + } + + /* + Handle WC universal links connections in the format https://uniswap.org/app/wc?uri=wc:123 + Notice that we assume the URL has only one parameter, named uri, which is the WallectConnect URI. + Any other parameter present in the URI is considered to be part of the WallectConnect URI. + For example, in the URL below, symKey is a parameter of the WallectConnect URI. + https://uniswap.org/app/wc?uri=wc:111f1ff289d1cc5a70ec5354779c6a82b3bde5ac72476f7f67326c38a4ce99f2@2?relay-protocol=irn&symKey=75e152d915a717da9f7bca3df23a0c65fcc4725d769f877ccfaa1f65270cded2 + */ + if (action.payload.url.startsWith(UNISWAP_WALLETCONNECT_URL)) { + // Only initial session connections include `uri` param, signing requests only link to /wc and should be ignored + const wcUri = action.payload.url.split(UNISWAP_WALLETCONNECT_URL).pop() + if (!wcUri) { + return + } + yield* call(handleWalletConnectDeepLink, decodeURIComponent(wcUri)) + return + } + + // Handle plain WalletConnect URIs + if (action.payload.url.startsWith(WALLETCONNECT_URI_SCHEME)) { + const wcUri = decodeURIComponent(action.payload.url) + yield* call(handleWalletConnectDeepLink, wcUri) + return + } + + if (screen && userAddress) { + const validUserAddress = yield* call(parseAndValidateUserAddress, userAddress) + yield* put(setAccountAsActive(validUserAddress)) + + switch (screen) { + case 'transaction': + if (fiatOnRamp) { + yield* call(handleMoonpayReturnLink) + } else { + yield* call(handleTransactionLink) + } + break + case 'swap': + yield* call(handleSwapLink, url) + break + default: + throw new Error('Invalid or unsupported screen') + } + } + + if (url.hostname === UNISWAP_APP_HOSTNAME) { + const urlParts = url.href.split(`${UNISWAP_APP_HOSTNAME}/`) + const urlPath = urlParts.length >= 1 ? (urlParts[1] as string) : '' + yield* call(handleUniswapAppDeepLink, urlPath, action.payload.url, LinkSource.Share) + return + } + + yield* call(sendMobileAnalyticsEvent, MobileEventName.DeepLinkOpened, { + url: url.toString(), + screen: screen ?? 'other', + is_cold_start: coldStart, + }) + } catch (error) { + yield* call(logger.error, error, { + tags: { file: 'handleDeepLinkSaga', function: 'handleDeepLink' }, + }) + } +} + +export function* handleWalletConnectDeepLink(wcUri: string) { + yield* call(waitForWcWeb3WalletIsReady) + + const wcUriVersion = parseUri(wcUri).version + + if (wcUriVersion === 1) { + Alert.alert( + i18n.t('walletConnect.error.unsupportedV1.title'), + i18n.t('walletConnect.error.unsupportedV1.message'), + [{ text: i18n.t('common.button.ok') }] + ) + return + } + + if (wcUriVersion === 2) { + try { + yield* call(pairWithWalletConnectURI, wcUri) + } catch (error) { + logger.error(error, { + tags: { file: 'handleDeepLinkSaga', function: 'handleWalletConnectDeepLink' }, + }) + Alert.alert( + i18n.t('walletConnect.error.general.title'), + i18n.t('walletConnect.error.general.message') + ) + } + } + + // Set didOpenFromDeepLink so that `returnToPreviousApp()` is enabled during WalletConnect flows + yield* put(setDidOpenFromDeepLink(true)) +} + +export function* parseAndValidateUserAddress(userAddress: string | null) { + if (!userAddress) { + throw new Error('No `userAddress` provided') + } + + const userAccounts = yield* appSelect(selectAccounts) + const matchingAccount = Object.values(userAccounts).find( + (account) => account.address.toLowerCase() === userAddress.toLowerCase() + ) + + if (!matchingAccount) { + throw new Error('User address supplied in path does not exist in wallet') + } + + return matchingAccount.address +} diff --git a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts new file mode 100644 index 0000000..a8cdeed --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.test.ts @@ -0,0 +1,20 @@ +import { call, put } from '@redux-saga/core/effects' +import { expectSaga } from 'redux-saga-test-plan' +import { navigate } from 'src/app/navigation/rootNavigation' +import { handleMoonpayReturnLink } from 'src/features/deepLinking/handleMoonpayReturnLinkSaga' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' +import { forceFetchFiatOnRampTransactions } from 'wallet/src/features/transactions/slice' +import { dismissInAppBrowser } from 'wallet/src/utils/linking' + +describe(handleMoonpayReturnLink, () => { + it('Navigates to the home screen activity tab when coming back from moonpay', () => { + return expectSaga(handleMoonpayReturnLink) + .provide([ + [put(forceFetchFiatOnRampTransactions), undefined], + [call(navigate, Screens.Home, { tab: HomeScreenTabIndex.Activity }), undefined], + [call(dismissInAppBrowser), undefined], + ]) + .silentRun() + }) +}) diff --git a/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts new file mode 100644 index 0000000..a609152 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleMoonpayReturnLinkSaga.ts @@ -0,0 +1,12 @@ +import { navigate } from 'src/app/navigation/rootNavigation' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' +import { call, put } from 'typed-redux-saga' +import { forceFetchFiatOnRampTransactions } from 'wallet/src/features/transactions/slice' +import { dismissInAppBrowser } from 'wallet/src/utils/linking' + +export function* handleMoonpayReturnLink() { + yield* put(forceFetchFiatOnRampTransactions()) + yield* call(navigate, Screens.Home, { tab: HomeScreenTabIndex.Activity }) + yield* call(dismissInAppBrowser) +} diff --git a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts new file mode 100644 index 0000000..1da7794 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.test.ts @@ -0,0 +1,178 @@ +import { URL } from 'react-native-url-polyfill' +import { expectSaga } from 'redux-saga-test-plan' +import { handleSwapLink } from 'src/features/deepLinking/handleSwapLinkSaga' +import { openModal } from 'src/features/modals/modalSlice' +import { ChainId } from 'wallet/src/constants/chains' +import { DAI, UNI } from 'wallet/src/constants/tokens' +import { AssetType } from 'wallet/src/entities/assets' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { ModalName } from 'wallet/src/telemetry/constants' +import { signerMnemonicAccount } from 'wallet/src/test/fixtures' + +const account = signerMnemonicAccount() + +const formSwapUrl = ( + userAddress?: Address, + chain?: ChainId | number, + inputAddress?: string, + outputAddress?: string, + currencyField?: string, + amount?: string +): URL => + new URL( + `https://uniswap.org/app?screen=swap +&userAddress=${userAddress} +&inputCurrencyId=${chain}-${inputAddress} +&outputCurrencyId=${chain}-${outputAddress} +¤cyField=${currencyField} +&amount=${amount}`.trim() + ) + +const formTransactionState = ( + chain?: ChainId, + inputAddress?: string, + outputAddress?: string, + currencyField?: string, + amount?: string +): { + input: { + address: string | undefined + chainId: ChainId | undefined + type: AssetType + } + output: { + address: string | undefined + chainId: ChainId | undefined + type: AssetType + } + exactCurrencyField: string | undefined + exactAmountToken: string | undefined +} => ({ + [CurrencyField.INPUT]: { + address: inputAddress, + chainId: chain, + type: AssetType.Currency, + }, + [CurrencyField.OUTPUT]: { + address: outputAddress, + chainId: chain, + type: AssetType.Currency, + }, + exactCurrencyField: !currencyField + ? currencyField + : currencyField.toLowerCase() === 'output' + ? CurrencyField.OUTPUT + : CurrencyField.INPUT, + exactAmountToken: amount, +}) + +const swapUrl = formSwapUrl( + account.address, + ChainId.Mainnet, + DAI.address, + UNI[ChainId.Mainnet].address, + 'input', + '100' +) + +const invalidOutputCurrencySwapUrl = formSwapUrl( + account.address, + ChainId.Mainnet, + DAI.address, + undefined, + 'input', + '100' +) + +const invalidInputTokenSwapURl = formSwapUrl( + account.address, + ChainId.Mainnet, + '0x00', + UNI[ChainId.Mainnet].address, + 'input', + '100' +) + +const invalidChainSwapUrl = formSwapUrl( + account.address, + 23, + DAI.address, + UNI[ChainId.Mainnet].address, + 'input', + '100' +) + +const invalidAmountSwapUrl = formSwapUrl( + account.address, + ChainId.Mainnet, + DAI.address, + UNI[ChainId.Mainnet].address, + 'input', + 'not a number' +) + +const invalidCurrencyFieldSwapUrl = formSwapUrl( + account.address, + ChainId.Mainnet, + DAI.address, + UNI[ChainId.Mainnet].address, + 'token1', + '100' +) + +const swapFormState = formTransactionState( + ChainId.Mainnet, + DAI.address, + UNI[ChainId.Mainnet].address, + 'input', + '100' +) as TransactionState + +describe(handleSwapLink, () => { + describe('valid inputs', () => { + it('Navigates to the swap screen with all params if all inputs are valid', () => { + return expectSaga(handleSwapLink, swapUrl) + .put(openModal({ name: ModalName.Swap, initialState: swapFormState })) + .silentRun() + }) + }) + + describe('invalid inputs', () => { + beforeAll(() => { + jest.spyOn(console, 'error').mockImplementation(() => undefined) + }) + + it('Navigates to an empty swap screen if outputCurrency is invalid', () => { + return expectSaga(handleSwapLink, invalidOutputCurrencySwapUrl) + .put(openModal({ name: ModalName.Swap })) + .silentRun() + }) + + it('Navigates to an empty swap screen if inputToken is invalid', () => { + return expectSaga(handleSwapLink, invalidInputTokenSwapURl) + .put(openModal({ name: ModalName.Swap })) + .silentRun() + }) + + it('Navigates to an empty swap screen if the chain is not supported', () => { + return expectSaga(handleSwapLink, invalidChainSwapUrl) + .put(openModal({ name: ModalName.Swap })) + .silentRun() + }) + + it('Navigates to an empty swap screen if the swap amount is invalid', () => { + return expectSaga(handleSwapLink, invalidAmountSwapUrl) + .put(openModal({ name: ModalName.Swap })) + .silentRun() + }) + + it('Navigates to an empty swap screen if currency field is invalid', () => { + return expectSaga(handleSwapLink, invalidCurrencyFieldSwapUrl) + .put(openModal({ name: ModalName.Swap })) + .silentRun() + }) + }) +}) diff --git a/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts new file mode 100644 index 0000000..7c7a279 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleSwapLinkSaga.ts @@ -0,0 +1,120 @@ +import { BigNumber } from 'ethers' +import { openModal } from 'src/features/modals/modalSlice' +import { put } from 'typed-redux-saga' +import { logger } from 'utilities/src/logger/logger' +import { ALL_SUPPORTED_CHAIN_IDS } from 'wallet/src/constants/chains' +import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { ModalName } from 'wallet/src/telemetry/constants' +import { getValidAddress } from 'wallet/src/utils/addresses' +import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' + +export function* handleSwapLink(url: URL) { + try { + const { + inputChain, + inputAddress, + outputChain, + outputAddress, + exactCurrencyField, + exactAmountToken, + } = parseAndValidateSwapParams(url) + + const inputAsset: CurrencyAsset = { + address: inputAddress, + chainId: inputChain, + type: AssetType.Currency, + } + + const outputAsset: CurrencyAsset = { + address: outputAddress, + chainId: outputChain, + type: AssetType.Currency, + } + + const swapFormState: TransactionState = { + [CurrencyField.INPUT]: inputAsset, + [CurrencyField.OUTPUT]: outputAsset, + exactCurrencyField, + exactAmountToken, + } + + yield* put(openModal({ name: ModalName.Swap, initialState: swapFormState })) + } catch (error) { + logger.error(error, { tags: { file: 'handleSwapLinkSaga', function: 'handleSwapLink' } }) + yield* put(openModal({ name: ModalName.Swap })) + } +} + +const parseAndValidateSwapParams = (url: URL) => { + const inputCurrencyId = url.searchParams.get('inputCurrencyId') + const outputCurrencyId = url.searchParams.get('outputCurrencyId') + const currencyField = url.searchParams.get('currencyField') + const exactAmountToken = url.searchParams.get('amount') ?? '0' + + if (!inputCurrencyId) { + throw new Error('No inputCurrencyId') + } + + if (!outputCurrencyId) { + throw new Error('No outputCurrencyId') + } + + const inputChain = currencyIdToChain(inputCurrencyId) + const inputAddress = currencyIdToAddress(inputCurrencyId) + + const outputChain = currencyIdToChain(outputCurrencyId) + const outputAddress = currencyIdToAddress(outputCurrencyId) + + if (!inputChain || !inputAddress) { + throw new Error('Invalid inputCurrencyId. Must be of format `-`') + } + + if (!outputChain || !outputAddress) { + throw new Error('Invalid outputCurrencyId. Must be of format `-`') + } + + if (!getValidAddress(inputAddress, true)) { + throw new Error('Invalid tokenAddress provided within inputCurrencyId') + } + + if (!getValidAddress(outputAddress, true)) { + throw new Error('Invalid tokenAddress provided within outputCurrencyId') + } + + if (!ALL_SUPPORTED_CHAIN_IDS.includes(inputChain)) { + throw new Error('Invalid inputCurrencyId. Chain ID is not supported') + } + + if (!ALL_SUPPORTED_CHAIN_IDS.includes(outputChain)) { + throw new Error('Invalid outputCurrencyId. Chain ID is not supported') + } + + try { + BigNumber.from(exactAmountToken).toNumber() // throws if exactAmount string is not a valid number + } catch (error) { + throw new Error('Invalid swap amount') + } + + if ( + !currencyField || + (currencyField.toLowerCase() !== 'input' && currencyField.toLowerCase() !== 'output') + ) { + throw new Error('Invalid currencyField. Must be either `input` or `output`') + } + + const exactCurrencyField = + currencyField.toLowerCase() === 'output' ? CurrencyField.OUTPUT : CurrencyField.INPUT + + return { + inputChain, + inputAddress, + outputChain, + outputAddress, + exactCurrencyField, + exactAmountToken, + } +} diff --git a/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.test.ts b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.test.ts new file mode 100644 index 0000000..55548be --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.test.ts @@ -0,0 +1,18 @@ +import { call, put } from '@redux-saga/core/effects' +import { expectSaga } from 'redux-saga-test-plan' +import { navigate } from 'src/app/navigation/rootNavigation' +import { handleTransactionLink } from 'src/features/deepLinking/handleTransactionLinkSaga' +import { closeAllModals } from 'src/features/modals/modalSlice' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' + +describe(handleTransactionLink, () => { + it('Navigates to the home screen when opening a transaction notification', () => { + return expectSaga(handleTransactionLink) + .provide([ + [put(closeAllModals()), undefined], + [call(navigate, Screens.Home, { tab: HomeScreenTabIndex.Activity }), undefined], + ]) + .silentRun() + }) +}) diff --git a/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts new file mode 100644 index 0000000..f74e4e9 --- /dev/null +++ b/apps/mobile/src/features/deepLinking/handleTransactionLinkSaga.ts @@ -0,0 +1,10 @@ +import { navigate } from 'src/app/navigation/rootNavigation' +import { closeAllModals } from 'src/features/modals/modalSlice' +import { HomeScreenTabIndex } from 'src/screens/HomeScreenTabIndex' +import { Screens } from 'src/screens/Screens' +import { call, put } from 'typed-redux-saga' + +export function* handleTransactionLink() { + yield* call(navigate, Screens.Home, { tab: HomeScreenTabIndex.Activity }) + yield* put(closeAllModals()) +} diff --git a/apps/mobile/src/features/explore/utils.ts b/apps/mobile/src/features/explore/utils.ts new file mode 100644 index 0000000..d829bbf --- /dev/null +++ b/apps/mobile/src/features/explore/utils.ts @@ -0,0 +1,129 @@ +import { TokenItemData } from 'src/components/explore/TokenItem' +import { AppTFunction } from 'ui/src/i18n/types' +import { TokenSortableField } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { + ClientTokensOrderBy, + TokenMetadataDisplayType, + TokensOrderBy, +} from 'wallet/src/features/wallet/types' + +/** + * Returns server and client orderBy values to use for topTokens query and client side sorting + * + * Uses server side sort by Volume if applying a client side sort after + * ex. % change sorting use the top 100 tokens by Uniswap Volume, then sorts by % change + * + * Note that server side sort by Volume (TokenSortableField.Volume) requires an + * additional client side sort because there may be a discrepancy in the server's + * sort by Volume list which is calculated once per 24h and each token's + * token.market.volume which is updated more frequently + * + * @param orderBy currently selected TokensOrderBy value to sort tokens by + * @returns serverOrderBy to be used in topTokens query, clientOrderBy to be used to determine if client side sort is necessary + */ +export function getTokensOrderByValues(orderBy: TokensOrderBy): { + serverOrderBy: TokenSortableField + clientOrderBy: ClientTokensOrderBy | undefined +} { + const requiresClientOrderBy = Object.values(ClientTokensOrderBy).includes(orderBy) + + return { + serverOrderBy: requiresClientOrderBy + ? TokenSortableField.Volume + : (orderBy as TokenSortableField), + clientOrderBy: requiresClientOrderBy + ? (orderBy as ClientTokensOrderBy) + : orderBy === TokenSortableField.Volume + ? ClientTokensOrderBy.Volume24hDesc + : undefined, + } +} + +/** + * Returns a compare function to sort tokens client side. + */ +export function getClientTokensOrderByCompareFn( + orderBy: ClientTokensOrderBy +): (a: TokenItemData, b: TokenItemData) => number { + let compareField: keyof TokenItemData + let direction = 0 + + switch (orderBy) { + case ClientTokensOrderBy.PriceChangePercentage24hAsc: + compareField = 'pricePercentChange24h' + direction = 1 + break + case ClientTokensOrderBy.PriceChangePercentage24hDesc: + compareField = 'pricePercentChange24h' + direction = -1 + break + case ClientTokensOrderBy.Volume24hDesc: + compareField = 'volume24h' + direction = -1 + break + } + + return (a: TokenItemData, b: TokenItemData) => { + // undefined values sort to bottom + if (a[compareField] === undefined) { + return 1 + } + if (b[compareField] === undefined) { + return -1 + } + return Number(a[compareField]) - Number(b[compareField]) > 0 ? direction : -1 * direction + } +} + +export function getTokenMetadataDisplayType(orderBy: TokensOrderBy): TokenMetadataDisplayType { + switch (orderBy) { + case TokenSortableField.MarketCap: + return TokenMetadataDisplayType.MarketCap + case TokenSortableField.Volume: + return TokenMetadataDisplayType.Volume + case TokenSortableField.TotalValueLocked: + return TokenMetadataDisplayType.TVL + case ClientTokensOrderBy.PriceChangePercentage24hDesc: + return TokenMetadataDisplayType.Symbol + case ClientTokensOrderBy.PriceChangePercentage24hAsc: + return TokenMetadataDisplayType.Symbol + default: + throw new Error('Unexpected order by value ' + orderBy) + } +} + +// Label shown in the popover context menu. +export function getTokensOrderByMenuLabel(orderBy: TokensOrderBy, t: AppTFunction): string { + switch (orderBy) { + case TokenSortableField.MarketCap: + return t('explore.tokens.sort.option.marketCap') + case TokenSortableField.Volume: + return t('explore.tokens.sort.option.volume') + case TokenSortableField.TotalValueLocked: + return t('explore.tokens.sort.option.totalValueLocked') + case ClientTokensOrderBy.PriceChangePercentage24hDesc: + return t('explore.tokens.sort.option.priceIncrease') + case ClientTokensOrderBy.PriceChangePercentage24hAsc: + return t('explore.tokens.sort.option.priceDecrease') + default: + throw new Error('Unexpected order by value ' + orderBy) + } +} + +// Label shown when option is selected in dropdown. +export function getTokensOrderBySelectedLabel(orderBy: TokensOrderBy, t: AppTFunction): string { + switch (orderBy) { + case TokenSortableField.MarketCap: + return t('explore.tokens.sort.label.marketCap') + case TokenSortableField.Volume: + return t('explore.tokens.sort.label.volume') + case TokenSortableField.TotalValueLocked: + return t('explore.tokens.sort.label.totalValueLocked') + case ClientTokensOrderBy.PriceChangePercentage24hDesc: + return t('explore.tokens.sort.label.priceIncrease') + case ClientTokensOrderBy.PriceChangePercentage24hAsc: + return t('explore.tokens.sort.label.priceDecrease') + default: + throw new Error('Unexpected order by value in option text ' + orderBy) + } +} diff --git a/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx new file mode 100644 index 0000000..d8a86d1 --- /dev/null +++ b/apps/mobile/src/features/externalProfile/ProfileContextMenu.tsx @@ -0,0 +1,125 @@ +import { impactAsync } from 'expo-haptics' +import React, { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, Share } from 'react-native' +import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { useAppDispatch } from 'src/app/hooks' +import { TripleDot } from 'src/components/icons/TripleDot' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { Flex, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { useUnitagByAddress } from 'wallet/src/features/unitags/hooks' +import { setClipboard } from 'wallet/src/utils/clipboard' +import { ExplorerDataType, getExplorerLink, getProfileUrl, openUri } from 'wallet/src/utils/linking' + +type MenuAction = { + title: string + action: () => void + systemIcon: string +} + +export function ProfileContextMenu({ address }: { address: Address }): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { unitag } = useUnitagByAddress(address) + + const onPressCopyAddress = useCallback(async () => { + if (!address) { + return + } + await impactAsync() + await setClipboard(address) + dispatch( + pushNotification({ type: AppNotificationType.Copied, copyType: CopyNotificationType.Address }) + ) + }, [address, dispatch]) + + const openExplorerLink = useCallback(async () => { + await openUri(getExplorerLink(ChainId.Mainnet, address, ExplorerDataType.ADDRESS)) + }, [address]) + + const onReportProfile = useCallback(async () => { + const params = new URLSearchParams() + params.append('tf_11041337007757', address) // Wallet Address + params.append('tf_7005922218125', 'report_unitag') // Report Type Dropdown + const prefilledRequestUrl = uniswapUrls.helpRequestUrl + '?' + params.toString() + openUri(prefilledRequestUrl).catch((e) => + logger.error(e, { tags: { file: 'ProfileContextMenu', function: 'reportProfileLink' } }) + ) + }, [address]) + + const onPressShare = useCallback(async () => { + if (!address) { + return + } + try { + const url = getProfileUrl(address) + await Share.share({ + message: url, + }) + sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, { + entity: ShareableEntity.Wallet, + url, + }) + } catch (error) { + logger.error(error, { tags: { file: 'ProfileContextMenu', function: 'onPressShare' } }) + } + }, [address]) + + const menuActions = useMemo(() => { + const options: MenuAction[] = [ + { + title: t('account.wallet.action.viewExplorer', { + blockExplorerName: CHAIN_INFO[ChainId.Mainnet].explorer.name, + }), + action: openExplorerLink, + systemIcon: 'link', + }, + { + title: t('account.wallet.action.copy'), + action: onPressCopyAddress, + systemIcon: 'square.on.square', + }, + { + title: t('common.button.share'), + action: onPressShare, + systemIcon: 'square.and.arrow.up', + }, + ] + if (unitag) { + options.push({ + title: t('account.wallet.action.report'), + action: onReportProfile, + systemIcon: 'flag', + }) + } + return options + }, [onPressCopyAddress, onPressShare, onReportProfile, openExplorerLink, t, unitag]) + + return ( + ): Promise => { + await menuActions[e.nativeEvent.index]?.action() + }}> + + + + + + + ) +} diff --git a/apps/mobile/src/features/externalProfile/ProfileHeader.tsx b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx new file mode 100644 index 0000000..6f53003 --- /dev/null +++ b/apps/mobile/src/features/externalProfile/ProfileHeader.tsx @@ -0,0 +1,341 @@ +import React, { memo, useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { StatusBar, StyleSheet } from 'react-native' +import { FadeIn } from 'react-native-reanimated' +import Svg, { ClipPath, Defs, RadialGradient, Rect, Stop } from 'react-native-svg' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { BackButton } from 'src/components/buttons/BackButton' +import { Favorite } from 'src/components/icons/Favorite' +import { LongText } from 'src/components/text/LongText' +import { ProfileContextMenu } from 'src/features/externalProfile/ProfileContextMenu' +import { useToggleWatchedWalletCallback } from 'src/features/favorites/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { + AnimatedFlex, + Flex, + Icons, + Image, + LinearGradient, + ScrollView, + Text, + TouchableArea, + getUniconV2Colors, + useIsDarkMode, + useSporeColors, + useUniconColors, +} from 'ui/src' +import { ENS_LOGO } from 'ui/src/assets' +import { iconSizes, imageSizes } from 'ui/src/theme' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { useENSDescription, useENSName, useENSTwitterUsername } from 'wallet/src/features/ens/api' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { selectWatchedAddressSet } from 'wallet/src/features/favorites/selectors' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { useAvatar, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' +import { useExtractedColors } from 'wallet/src/utils/colors' +import { openUri } from 'wallet/src/utils/linking' + +const HEADER_GRADIENT_HEIGHT = 144 +const HEADER_ICON_SIZE = 72 + +interface ProfileHeaderProps { + address: Address +} + +const HEADER_SOLID_COLOR_OPACITY = 0.1 + +export const solidHeaderProps = { + minOpacity: HEADER_SOLID_COLOR_OPACITY, + maxOpacity: HEADER_SOLID_COLOR_OPACITY, +} + +export const ProfileHeader = memo(function ProfileHeader({ + address, +}: ProfileHeaderProps): JSX.Element { + const colors = useSporeColors() + const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() + const isFavorited = useAppSelector(selectWatchedAddressSet).has(address) + + const displayName = useDisplayName(address, { includeUnitagSuffix: true }) + + // Note that if a user has a Unitag AND ENS, this prioritizes the Unitag's metadata over the ENS metadata + const nameToFetchENSMetadata = + (displayName?.type === DisplayNameType.ENS || displayName?.type === DisplayNameType.Unitag) && + displayName?.name + ? displayName.name + : undefined + + // ENS avatar and avatar colors + const { avatar, loading: avatarLoading } = useAvatar(address) + const { data: primaryENSName } = useENSName(address) + const { data: twitter } = useENSTwitterUsername(nameToFetchENSMetadata) + const { data: bio } = useENSDescription(nameToFetchENSMetadata) + const showENSName = primaryENSName && primaryENSName !== displayName?.name + + const { colors: avatarColors } = useExtractedColors(avatar) + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + + const hasAvatar = !!avatar && !avatarLoading + + // Unicon colors + const { gradientStart: uniconGradientStart, gradientEnd: uniconGradientEnd } = + useUniconColors(address) + + // UniconV2 colors + const { color } = getUniconV2Colors(address) + + // Wait for avatar, then render avatar extracted colors or unicon colors if no avatar + const fixedGradientColors: [string, string] = useMemo(() => { + if (avatarLoading || (hasAvatar && !avatarColors)) { + return [colors.surface1.val, colors.surface1.val] + } + if (hasAvatar && avatarColors && avatarColors.base) { + return [avatarColors.base, avatarColors.base] + } + return [ + isUniconsV2Enabled ? color : uniconGradientStart, + isUniconsV2Enabled ? color : uniconGradientEnd, + ] + }, [ + avatarColors, + hasAvatar, + avatarLoading, + colors.surface1, + uniconGradientEnd, + uniconGradientStart, + color, + isUniconsV2Enabled, + ]) + + const onPressFavorite = useToggleWatchedWalletCallback(address) + + const initialSendState = useMemo(() => { + return { + recipient: address, + exactAmountToken: '', + exactAmountFiat: '', + exactCurrencyField: CurrencyField.INPUT, + [CurrencyField.INPUT]: null, + [CurrencyField.OUTPUT]: null, + } + }, [address]) + + const onPressSend = useCallback(() => { + dispatch( + openModal({ + name: ModalName.Send, + ...{ initialState: initialSendState }, + }) + ) + }, [dispatch, initialSendState]) + + const onPressTwitter = useCallback(async () => { + if (twitter) { + await openUri(`https://twitter.com/${twitter}`) + } + }, [twitter]) + + const { t } = useTranslation() + + return ( + + + {/* fixed gradient at 0.2 opacity overlaid on surface1 */} + + + + + + {hasAvatar && avatarColors?.primary ? ( + + ) : ( + + )} + + + {/* header row */} + + + + + + + + {/* button content */} + + + + + {bio ? ( + + ) : null} + + {(twitter || showENSName) && ( + + + {twitter ? ( + + + + + {twitter} + + + + ) : null} + {showENSName ? ( + + + + {primaryENSName} + + + ) : null} + + + )} + + + + + + + + + + + {t('common.button.send')} + + + + + + + + ) +}) + +export const HeaderRadial = memo(function HeaderRadial({ + color, + borderRadius, + minOpacity, + maxOpacity, +}: { + color: string + borderRadius?: number + minOpacity?: number + maxOpacity?: number +}): JSX.Element { + return ( + + + + + + + + + + + + + ) +}) + +const styles = StyleSheet.create({ + buttonShadow: { + elevation: 2, + shadowOffset: { + height: 2, + width: 0, + }, + shadowOpacity: 0.04, + shadowRadius: 4, + }, +}) diff --git a/apps/mobile/src/features/favorites/hooks.ts b/apps/mobile/src/features/favorites/hooks.ts new file mode 100644 index 0000000..6f0bd1f --- /dev/null +++ b/apps/mobile/src/features/favorites/hooks.ts @@ -0,0 +1,60 @@ +import { useCallback, useMemo } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { + makeSelectHasTokenFavorited, + selectWatchedAddressSet, +} from 'wallet/src/features/favorites/selectors' +import { + addFavoriteToken, + addWatchedAddress, + removeFavoriteToken, + removeWatchedAddress, +} from 'wallet/src/features/favorites/slice' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' +import { CurrencyId, currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' + +export function useToggleFavoriteCallback(id: CurrencyId, isFavoriteToken: boolean): () => void { + const dispatch = useAppDispatch() + const token = useCurrencyInfo(id) + + return useCallback(() => { + if (isFavoriteToken) { + dispatch(removeFavoriteToken({ currencyId: id })) + } else { + sendMobileAnalyticsEvent(MobileEventName.FavoriteItem, { + address: currencyIdToAddress(id), + chain: currencyIdToChain(id) as number, + type: 'token', + name: token?.currency.name, + }) + dispatch(addFavoriteToken({ currencyId: id })) + } + }, [dispatch, id, isFavoriteToken, token]) +} + +export function useToggleWatchedWalletCallback(address: Address): () => void { + const dispatch = useAppDispatch() + const isFavoriteWallet = useAppSelector(selectWatchedAddressSet).has(address) + const displayName = useDisplayName(address) + + return useCallback(() => { + if (isFavoriteWallet) { + dispatch(removeWatchedAddress({ address })) + } else { + sendMobileAnalyticsEvent(MobileEventName.FavoriteItem, { + address, + type: 'wallet', + name: displayName?.name, + }) + dispatch(addWatchedAddress({ address })) + } + }, [address, dispatch, isFavoriteWallet, displayName]) +} + +export function useSelectHasTokenFavorited(currencyId: string): boolean { + const selectHasTokenFavorited = useMemo(makeSelectHasTokenFavorited, []) + return useAppSelector((state) => selectHasTokenFavorited(state, currencyId)) +} diff --git a/apps/mobile/src/features/favorites/slice.test.ts b/apps/mobile/src/features/favorites/slice.test.ts new file mode 100644 index 0000000..fc5d5a8 --- /dev/null +++ b/apps/mobile/src/features/favorites/slice.test.ts @@ -0,0 +1,51 @@ +import { createStore, Store } from '@reduxjs/toolkit' +import { + addFavoriteToken, + favoritesReducer, + FavoritesState, + removeFavoriteToken, +} from 'wallet/src/features/favorites/slice' + +describe(favoritesReducer, () => { + let store: Store + + beforeAll(() => { + jest.spyOn(console, 'warn').mockImplementation(() => undefined) + }) + + beforeEach(() => { + store = createStore(favoritesReducer, { + tokens: [], + watchedAddresses: [], + nftsData: {}, + tokensVisibility: {}, + }) + }) + + it('adds favorites', () => { + expect(store.getState().tokens.length).toEqual(0) + + store.dispatch(addFavoriteToken({ currencyId: '0xdeadbeef' })) + expect(store.getState().tokens).toEqual(['0xdeadbeef']) + + // handles dupes + store.dispatch(addFavoriteToken({ currencyId: '0xdeadbeef' })) + expect(store.getState().tokens).toEqual(['0xdeadbeef']) + }) + + it('removes favorites', () => { + store.dispatch(addFavoriteToken({ currencyId: '0xdeadbeef' })) + store.dispatch(addFavoriteToken({ currencyId: '0xdefaced' })) + + expect(store.getState().tokens).toEqual(['0xdeadbeef', '0xdefaced']) + + store.dispatch(removeFavoriteToken({ currencyId: '0xdefaced' })) + expect(store.getState().tokens).toEqual(['0xdeadbeef']) + + // handles missing tokens + store.dispatch(removeFavoriteToken({ currencyId: '0xdefaced' })) + + store.dispatch(removeFavoriteToken({ currencyId: '0xdeadbeef' })) + expect(store.getState().tokens).toEqual([]) + }) +}) diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx new file mode 100644 index 0000000..dc29fbf --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModal.tsx @@ -0,0 +1,28 @@ +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { ExchangeTransferConnecting } from 'src/screens/ExchangeTransferConnecting' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function ExchangeTransferModal(): JSX.Element | null { + const dispatch = useAppDispatch() + const onClose = (): void => { + dispatch(closeModal({ name: ModalName.ExchangeTransferModal })) + } + + const { initialState } = useAppSelector(selectModalState(ModalName.ExchangeTransferModal)) + const serviceProvider = initialState?.serviceProvider + + return serviceProvider ? ( + + + + ) : null +} diff --git a/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts new file mode 100644 index 0000000..4f2c949 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/ExchangeTransferModalState.ts @@ -0,0 +1,5 @@ +import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' + +export interface ExchangeTransferModalState { + serviceProvider: FORTransferInstitution +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx new file mode 100644 index 0000000..a5f7008 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAggregatorModal.tsx @@ -0,0 +1,32 @@ +import React, { useCallback } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { FiatOnRampStackNavigator } from 'src/app/navigation/navigation' +import { closeModal } from 'src/features/modals/modalSlice' +import { useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function FiatOnRampAggregatorModal(): JSX.Element { + const colors = useSporeColors() + + const dispatch = useAppDispatch() + const onClose = useCallback((): void => { + dispatch(closeModal({ name: ModalName.FiatOnRampAggregator })) + }, [dispatch]) + + return ( + + + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx new file mode 100644 index 0000000..e1b2cdf --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampAmountSection.tsx @@ -0,0 +1,299 @@ +import { impactAsync } from 'expo-haptics' +import React, { useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { + LayoutChangeEvent, + NativeSyntheticEvent, + TextInput, + TextInputProps, + TextInputSelectionChangeEventData, +} from 'react-native' +import { TouchableOpacity } from 'react-native-gesture-handler' +import { useAnimatedStyle, useSharedValue } from 'react-native-reanimated' +import { useFormatExactCurrencyAmount } from 'src/features/fiatOnRamp/hooks' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { AnimatedFlex, ColorTokens, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { fonts, iconSizes, spacing } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { usePrevious } from 'utilities/src/react/hooks' +import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' +import { CurrencyLogo } from 'wallet/src/components/CurrencyLogo/CurrencyLogo' +import { AmountInput } from 'wallet/src/components/input/AmountInput' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { Pill } from 'wallet/src/components/text/Pill' +import { CurrencyInfo } from 'wallet/src/features/dataApi/types' +import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { ElementName } from 'wallet/src/telemetry/constants' +import { errorShakeAnimation } from 'wallet/src/utils/animations' +import { getSymbolDisplayText } from 'wallet/src/utils/currency' +import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' + +const MAX_INPUT_FONT_SIZE = 56 +const MIN_INPUT_FONT_SIZE = 32 + +// if font changes from `fontFamily.sansSerif.regular` or `MAX_INPUT_FONT_SIZE` +// changes from 36 then width value must be adjusted +const MAX_CHAR_PIXEL_WIDTH = 40 + +const PREDEFINED_AMOUNTS = [100, 300, 1000] + +type OnChangeAmount = (amount: string) => void + +interface Props { + showNativeKeyboard: boolean + onInputPanelLayout: (event: LayoutChangeEvent) => void + inputRef: React.RefObject + disabled?: boolean + showSoftInputOnFocus: boolean + value: string + setSelection: (selection: TextInputProps['selection']) => void + errorColor: ColorTokens | undefined + errorText: string | undefined + currency: FiatOnRampCurrency + onEnterAmount: OnChangeAmount + onChoosePredifendAmount: OnChangeAmount + quoteAmount: number + quoteCurrencyAmountReady: boolean + selectTokenLoading: boolean + onTokenSelectorPress: () => void + predefinedAmountsSupported: boolean + appFiatCurrencySupported: boolean + fiatCurrencyInfo: FiatCurrencyInfo +} + +export function FiatOnRampAmountSection({ + showNativeKeyboard, + onInputPanelLayout, + inputRef, + disabled, + showSoftInputOnFocus, + value, + setSelection, + errorColor, + errorText, + currency, + onEnterAmount, + onChoosePredifendAmount, + quoteAmount, + quoteCurrencyAmountReady, + selectTokenLoading, + onTokenSelectorPress, + predefinedAmountsSupported, + appFiatCurrencySupported, + fiatCurrencyInfo, +}: Props): JSX.Element { + const { t } = useTranslation() + const { + onLayout: onInputLayout, + fontSize, + onSetFontSize, + } = useDynamicFontSizing(MAX_CHAR_PIXEL_WIDTH, MAX_INPUT_FONT_SIZE, MIN_INPUT_FONT_SIZE) + const prevErrorText = usePrevious(errorText) + + const onChangeValue = + (next: OnChangeAmount) => + (newAmount: string): void => { + onSetFontSize(newAmount) + next(newAmount) + } + + const onSelectionChange = ({ + nativeEvent: { + selection: { start, end }, + }, + }: NativeSyntheticEvent): void => { + setSelection({ start, end }) + } + + const { formatNumberOrString } = useLocalizationContext() + + const inputShakeX = useSharedValue(0) + const inputAnimatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: inputShakeX.value }], + })) + + useEffect(() => { + async function shake(): Promise { + inputShakeX.value = errorShakeAnimation(inputShakeX) + await impactAsync() + } + if (errorText && prevErrorText !== errorText) { + shake().catch(() => undefined) + } + }, [errorText, inputShakeX, prevErrorText]) + + // Design has asked to make it around 100ms and DEFAULT_DELAY is 200ms + const debouncedErrorText = useDebounce(errorText, DEFAULT_DELAY / 2) + + return ( + + + + {debouncedErrorText && errorColor && ( + + {debouncedErrorText} + + )} + + + + + {currency.currencyInfo && ( + + )} + {predefinedAmountsSupported ? ( + + {PREDEFINED_AMOUNTS.map((amount) => ( + + ))} + + ) : null} + {!appFiatCurrencySupported ? ( + + + {t('fiatOnRamp.error.usd')} + + + ) : null} + + + ) +} + +interface SelectTokenButtonProps { + onPress: () => void + selectedCurrencyInfo: CurrencyInfo + amount: number + disabled?: boolean + loading?: boolean +} + +function SelectTokenButton({ + selectedCurrencyInfo, + onPress, + amount, + disabled, + loading, +}: SelectTokenButtonProps): JSX.Element { + const formattedAmount = useFormatExactCurrencyAmount( + amount.toString(), + selectedCurrencyInfo.currency + ) + const textColor = disabled || loading ? '$neutral3' : '$neutral2' + + return ( + + + {loading ? ( + + ) : ( + + )} + + {formattedAmount} + + + {getSymbolDisplayText(selectedCurrencyInfo.currency.symbol)} + + + + + ) +} + +// Predefined amount is only supported for certain currencies +function PredefinedAmount({ + amount, + onPress, + currentAmount, + fiatCurrencyInfo, +}: { + amount: number + currentAmount: string + onPress: (amount: string) => void + fiatCurrencyInfo: FiatCurrencyInfo +}): JSX.Element { + const colors = useSporeColors() + const { addFiatSymbolToNumber } = useLocalizationContext() + const formattedAmount = addFiatSymbolToNumber({ + value: amount, + currencyCode: fiatCurrencyInfo.code, + currencySymbol: fiatCurrencyInfo.symbol, + }) + + const highlighted = currentAmount === amount.toString() + + return ( + => { + await impactAsync() + onPress(amount.toString()) + }}> + + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx new file mode 100644 index 0000000..d47b636 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampConnecting.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Image, ImageBackground, StyleSheet } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { AnimatedFlex, Flex, Text, useDeviceInsets, useIsDarkMode } from 'ui/src' +import { + FOR_CONNECTING_BACKGROUND_DARK, + FOR_CONNECTING_BACKGROUND_LIGHT, + UNISWAP_LOGO_LARGE, +} from 'ui/src/assets' +import { iconSizes } from 'ui/src/theme' + +export const SERVICE_PROVIDER_ICON_SIZE = 90 +export const SERVICE_PROVIDER_ICON_BORDER_RADIUS = 20 + +export function FiatOnRampConnectingView({ + amount, + quoteCurrencyCode, + serviceProviderName, + serviceProviderLogo, +}: { + amount?: string + quoteCurrencyCode?: string + serviceProviderName: string + serviceProviderLogo?: JSX.Element +}): JSX.Element { + const insets = useDeviceInsets() + const { t } = useTranslation() + + const isDarkMode = useIsDarkMode() + return ( + + + + + + + {serviceProviderLogo} + + + + {t('fiatOnRamp.connection.message', { serviceProvider: serviceProviderName })} + + {quoteCurrencyCode && amount && ( + + {t('fiatOnRamp.connection.quote', { + amount, + currencySymbol: quoteCurrencyCode, + })} + + )} + + + + ) +} + +const styles = StyleSheet.create({ + background: { + flex: 1, + justifyContent: 'center', + }, + uniswapLogo: { + height: iconSizes.icon64, + width: iconSizes.icon64, + }, + uniswapLogoWrapper: { + backgroundColor: '#FFEFF8', // #FFD8EF with 40% opacity on a white background + borderRadius: SERVICE_PROVIDER_ICON_BORDER_RADIUS, + height: SERVICE_PROVIDER_ICON_SIZE, + width: SERVICE_PROVIDER_ICON_SIZE, + }, +}) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx new file mode 100644 index 0000000..b728ee1 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampContext.tsx @@ -0,0 +1,87 @@ +/** + * This context is used to persist Fiat On Ramp related data between Fiat On Ramp screens. + */ +import React, { createContext, useContext, useState } from 'react' +import { SectionListData } from 'react-native' +import { getCountry } from 'react-native-localize' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { getNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { FiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' + +interface FiatOnRampContextType { + quotesSections?: SectionListData[] | undefined + setQuotesSections: (quotesSections: SectionListData[] | undefined) => void + selectedQuote?: FORQuote + setSelectedQuote: (quote: FORQuote | undefined) => void + countryCode: string + setCountryCode: (countryCode: string) => void + baseCurrencyInfo?: FiatCurrencyInfo + setBaseCurrencyInfo: (baseCurrency: FiatCurrencyInfo | undefined) => void + quoteCurrency: FiatOnRampCurrency + setQuoteCurrency: (quoteCurrency: FiatOnRampCurrency) => void + amount?: number + setAmount: (amount: number | undefined) => void + serviceProviders?: FORServiceProvider[] + setServiceProviders: (serviceProviders: FORServiceProvider[] | undefined) => void +} + +const initialState: FiatOnRampContextType = { + setQuotesSections: () => undefined, + setSelectedQuote: () => undefined, + setCountryCode: () => undefined, + setBaseCurrencyInfo: () => undefined, + setQuoteCurrency: () => undefined, + setAmount: () => undefined, + setServiceProviders: () => undefined, + countryCode: '', + quoteCurrency: { currencyInfo: undefined }, +} + +const FiatOnRampContext = createContext(initialState) + +export function useFiatOnRampContext(): FiatOnRampContextType { + return useContext(FiatOnRampContext) +} + +export function FiatOnRampProvider({ children }: { children: React.ReactNode }): JSX.Element { + const [quotesSections, setQuotesSections] = useState() + const [selectedQuote, setSelectedQuote] = useState() + const [countryCode, setCountryCode] = useState(getCountry()) + const [baseCurrencyInfo, setBaseCurrencyInfo] = useState() + const [amount, setAmount] = useState() + const [serviceProviders, setServiceProviders] = useState() + + // We hardcode ETH as the starting currency + const ethCurrencyInfo = useCurrencyInfo( + buildCurrencyId(ChainId.Mainnet, getNativeAddress(ChainId.Mainnet)) + ) + const [quoteCurrency, setQuoteCurrency] = useState({ + currencyInfo: ethCurrencyInfo, + }) + + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx new file mode 100644 index 0000000..64b6293 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryListModal.tsx @@ -0,0 +1,175 @@ +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import React, { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItemInfo } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { SvgUri } from 'react-native-svg' +import { Loader } from 'src/components/loading' +import { FOR_MODAL_SNAP_POINTS } from 'src/features/fiatOnRamp/constants' +import { + AnimatedFlex, + Flex, + Text, + TouchableArea, + useDeviceDimensions, + useDeviceInsets, + useSporeColors, +} from 'ui/src' +import Check from 'ui/src/assets/icons/check.svg' +import { fonts, iconSizes, spacing } from 'ui/src/theme' +import { bubbleToTop } from 'utilities/src/primitives/array' +import { useDebounce } from 'utilities/src/time/timing' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' +import { useFiatOnRampAggregatorCountryListQuery } from 'wallet/src/features/fiatOnRamp/api' +import { FORCountry } from 'wallet/src/features/fiatOnRamp/types' +import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils' +import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' +import { ModalName } from 'wallet/src/telemetry/constants' + +const ICON_SIZE = 32 // design prefers a custom value here + +interface CountrySelectorProps { + onSelectCountry: (country: FORCountry) => void + countryCode: string +} + +function key(item: FORCountry): string { + return item.countryCode +} + +function CountrySelectorContent({ + onSelectCountry, + countryCode, +}: CountrySelectorProps): JSX.Element { + const { t } = useTranslation() + const insets = useDeviceInsets() + const colors = useSporeColors() + + const { data, isLoading } = useFiatOnRampAggregatorCountryListQuery() + + const [searchText, setSearchText] = useState('') + + const debouncedSearchText = useDebounce(searchText) + + const filteredData: FORCountry[] = useMemo(() => { + if (!data) { + return [] + } + return bubbleToTop(data.supportedCountries, (c) => c.countryCode === countryCode).filter( + (item) => + !debouncedSearchText || + item.displayName.toLowerCase().startsWith(debouncedSearchText.toLowerCase()) + ) + }, [countryCode, data, debouncedSearchText]) + + const renderItem = useCallback( + ({ item }: ListRenderItemInfo): JSX.Element => { + const countryFlagUrl = getCountryFlagSvgUrl(item.countryCode) + + return ( + onSelectCountry(item)}> + + + + + {item.displayName} + {item.countryCode === countryCode && ( + + + + )} + + + ) + }, + [colors.accent1, countryCode, onSelectCountry] + ) + + return ( + + + {t('fiatOnRamp.region.title')} + + + + + {isLoading ? ( + + ) : ( + } + bounces={true} + contentContainerStyle={{ paddingBottom: insets.bottom + spacing.spacing12 }} + data={filteredData} + focusHook={useBottomSheetFocusHook} + keyExtractor={key} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="always" + renderItem={renderItem} + showsVerticalScrollIndicator={false} + windowSize={5} + /> + )} + + + + ) +} + +const CountryListPlaceholder = React.memo(function CountryListPlaceholder({ + itemsCount, +}: { + itemsCount: number +}): JSX.Element { + const { fullWidth } = useDeviceDimensions() + return ( + + {new Array(itemsCount).fill(null).map((_, i) => ( + + + + + ))} + + ) +}) + +export function FiatOnRampCountryListModal({ + onClose, + onSelectCountry, + countryCode, +}: { + onClose: () => void +} & CountrySelectorProps): JSX.Element { + const colors = useSporeColors() + + return ( + + + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryPicker.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryPicker.tsx new file mode 100644 index 0000000..44ee569 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampCountryPicker.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import { SvgUri } from 'react-native-svg' +import Trace from 'src/components/Trace/Trace' +import { Flex, Icons, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { getCountryFlagSvgUrl } from 'wallet/src/features/fiatOnRamp/utils' +import { ElementName } from 'wallet/src/telemetry/constants' + +const ICON_SIZE = iconSizes.icon16 + +export function FiatOnRampCountryPicker({ + onPress, + countryCode, +}: { + onPress: () => void + countryCode: Maybe +}): JSX.Element | null { + if (!countryCode) { + return null + } + + const countryFlagUrl = getCountryFlagSvgUrl(countryCode) + + return ( + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx new file mode 100644 index 0000000..ab68d36 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampModal.tsx @@ -0,0 +1,306 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet, TextInput } from 'react-native' +import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' +import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' +import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' +import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection' +import { + FiatOnRampConnectingView, + SERVICE_PROVIDER_ICON_SIZE, +} from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { useMoonpayFiatOnRamp, useMoonpaySupportedTokens } from 'src/features/fiatOnRamp/hooks' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { closeModal } from 'src/features/modals/modalSlice' +import { AnimatedFlex, Flex, Text, useDeviceInsets, useSporeColors } from 'ui/src' +import MoonpayLogo from 'ui/src/assets/logos/svg/moonpay.svg' +import { NumberType } from 'utilities/src/format/types' +import { useTimeout } from 'utilities/src/time/timing' +import { TextInputProps } from 'wallet/src/components/input/TextInput' +import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' +import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { getNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useCurrencyInfo } from 'wallet/src/features/tokens/useCurrencyInfo' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { FiatOnRampEventName, ModalName } from 'wallet/src/telemetry/constants' +import { WalletEventProperties } from 'wallet/src/telemetry/types' +import { buildCurrencyId } from 'wallet/src/utils/currencyId' +import { openUri } from 'wallet/src/utils/linking' +import { FiatOnRampTokenSelectorModal } from './FiatOnRampTokenSelector' + +const MOONPAY_UNSUPPORTED_REGION_HELP_URL = + 'https://support.uniswap.org/hc/en-us/articles/11306664890381-Why-isn-t-MoonPay-available-in-my-region-' + +const PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES = ['USD', 'EUR', 'GBP', 'AUD', 'CAD', 'SGD'] + +const CONNECTING_TIMEOUT = 2000 + +export function FiatOnRampModal(): JSX.Element { + const colors = useSporeColors() + + const dispatch = useAppDispatch() + const onClose = useCallback((): void => { + dispatch(closeModal({ name: ModalName.FiatOnRamp })) + }, [dispatch]) + + return ( + + + + ) +} + +function FiatOnRampContent({ onClose }: { onClose: () => void }): JSX.Element { + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + const inputRef = useRef(null) + + const { isSheetReady } = useBottomSheetContext() + + const [showConnectingToMoonpayScreen, setShowConnectingToMoonpayScreen] = useState(false) + + const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = + useShouldShowNativeKeyboard() + + const [selection, setSelection] = useState() + + const resetSelection = (start: number, end?: number): void => { + setSelection({ start, end: end ?? start }) + } + + const [value, setValue] = useState('') + + // We hardcode ETH as the starting currency + const ethCurrencyInfo = useCurrencyInfo( + buildCurrencyId(ChainId.Mainnet, getNativeAddress(ChainId.Mainnet)) + ) + + const [currency, setCurrency] = useState({ + currencyInfo: ethCurrencyInfo, + moonpayCurrencyCode: 'eth', + }) + + const { appFiatCurrencySupportedInMoonpay, moonpaySupportedFiatCurrency } = + useMoonpayFiatCurrencySupportInfo() + + // We only support predefined amounts for certain currencies. + // If the user's app fiat currency is not supported in Moonpay, + // we fallback to USD (which does allow for predefined amounts) + const predefinedAmountsSupported = + PREDEFINED_AMOUNTS_SUPPORTED_CURRENCIES.includes(moonpaySupportedFiatCurrency.code) || + !appFiatCurrencySupportedInMoonpay + + // We might not have ethCurrencyInfo when this component is initially rendered. + // If `ethCurrencyInfo` becomes available later while currency.currencyInfo is still unset, we update the currency state accordingly. + useEffect(() => { + if (ethCurrencyInfo && !currency.currencyInfo) { + setCurrency({ ...currency, currencyInfo: ethCurrencyInfo }) + } + }, [currency, currency.currencyInfo, ethCurrencyInfo]) + + const { + eligible, + quoteAmount, + isLoading, + isError, + externalTransactionId, + dispatchAddTransaction, + fiatOnRampHostUrl, + quoteCurrencyAmountReady, + quoteCurrencyAmountLoading, + errorText, + errorColor, + } = useMoonpayFiatOnRamp({ + baseCurrencyAmount: value, + quoteCurrencyCode: currency.moonpayCurrencyCode, + quoteChainId: currency.currencyInfo?.currency.chainId ?? ChainId.Mainnet, + }) + + useTimeout( + async () => { + if (fiatOnRampHostUrl) { + await openUri(fiatOnRampHostUrl) + dispatchAddTransaction() + onClose() + } + }, + // setTimeout would be called inside this hook, only when delay >= 0 + showConnectingToMoonpayScreen ? CONNECTING_TIMEOUT : -1 + ) + + const buttonEnabled = + !isLoading && (!eligible || (!isError && fiatOnRampHostUrl && quoteCurrencyAmountReady)) + + const onChangeValue = + (source: WalletEventProperties[FiatOnRampEventName.FiatOnRampAmountEntered]['source']) => + (newAmount: string): void => { + sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampAmountEntered, { + source, + }) + setValue(newAmount) + } + + const [showTokenSelector, setShowTokenSelector] = useState(false) + + useEffect(() => { + if (showTokenSelector) { + // hide keyboard when user goes to token selector screen + inputRef.current?.blur() + } else if (showNativeKeyboard && eligible) { + // autofocus + inputRef.current?.focus() + } + }, [showNativeKeyboard, eligible, showTokenSelector]) + + const selectTokenLoading = quoteCurrencyAmountLoading && !errorText && !!value + + const { + list: supportedTokensList, + loading: supportedTokensLoading, + error: supportedTokensError, + refetch: supportedTokensRefetch, + } = useMoonpaySupportedTokens() + + const insets = useDeviceInsets() + + const onSelectCurrency = (newCurrency: FiatOnRampCurrency): void => { + setCurrency(newCurrency) + setShowTokenSelector(false) + if (newCurrency.currencyInfo?.currency.symbol) { + sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampTokenSelected, { + token: newCurrency.currencyInfo.currency.symbol.toLowerCase(), + }) + } + } + + return ( + + {!showConnectingToMoonpayScreen && ( + + {isSheetReady && ( + + + {t('common.button.buy')} + { + setShowTokenSelector(true) + }} + /> + + {!showNativeKeyboard && ( + + )} + => { + if (eligible) { + setShowConnectingToMoonpayScreen(true) + } else { + await openUri(MOONPAY_UNSUPPORTED_REGION_HELP_URL) + } + }} + /> + + + )} + {showTokenSelector && ( + setShowTokenSelector(false)} + onRetry={supportedTokensRefetch} + onSelectCurrency={onSelectCurrency} + /> + )} + + )} + {showConnectingToMoonpayScreen && ( + + + + } + serviceProviderName="MoonPay" + /> + )} + + ) +} + +const styles = StyleSheet.create({ + moonpayLogoWrapper: { + backgroundColor: '#7D00FF', + }, +}) diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx new file mode 100644 index 0000000..29ad763 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTokenSelector.tsx @@ -0,0 +1,64 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { TokenFiatOnRampList } from 'src/components/TokenSelector/TokenFiatOnRampList' +import Trace from 'src/components/Trace/Trace' +import { FOR_MODAL_SNAP_POINTS } from 'src/features/fiatOnRamp/constants' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { AnimatedFlex, Flex, Text, useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ElementName, ModalName, SectionName } from 'wallet/src/telemetry/constants' + +interface Props { + onSelectCurrency: (currency: FiatOnRampCurrency) => void + onRetry: () => void + onClose: () => void + error: boolean + loading: boolean + list: FiatOnRampCurrency[] | undefined +} + +export function FiatOnRampTokenSelectorModal({ + error, + list, + loading, + onClose, + onRetry, + onSelectCurrency, +}: { onClose: () => void } & Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + + return ( + + + + + {t('fiatOnRamp.button.chooseToken')} + + + + + + + + ) +} diff --git a/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx new file mode 100644 index 0000000..d3fe53e --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/FiatOnRampTransferInstitutionSelector.tsx @@ -0,0 +1,111 @@ +import { BottomSheetFlatList } from '@gorhom/bottom-sheet' +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { useCallback } from 'react' +import { ListRenderItemInfo } from 'react-native' +import { getCountry } from 'react-native-localize' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppDispatch } from 'src/app/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { AnimatedFlex, Flex, Loader, Text, TouchableArea } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { useFiatOnRampAggregatorTransferInstitutionsQuery } from 'wallet/src/features/fiatOnRamp/api' +import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' +import { RemoteImage } from 'wallet/src/features/images/RemoteImage' +import { ModalName } from 'wallet/src/telemetry/constants' + +function key(item: FORTransferInstitution): string { + return item.id as string +} + +const CEX_ICON_SIZE = iconSizes.icon36 +const CEX_ICON_BORDER_RADIUS = 12 + +function CEXItemWrapper({ + institution, + onSelectTransferInstitution, +}: { + institution: FORTransferInstitution + onSelectTransferInstitution: (transferInstitution: FORTransferInstitution) => void +}): JSX.Element | null { + const onPress = (): void => onSelectTransferInstitution(institution) + + return ( + + + + + + {institution.name} + + + + + ) +} + +export function TransferInstitutionSelector({ onClose }: { onClose: () => void }): JSX.Element { + const dispatch = useAppDispatch() + const { data, isLoading } = useFiatOnRampAggregatorTransferInstitutionsQuery({ + countryCode: getCountry(), + }) + + const onSelectTransferInstitution = useCallback( + (transferInstitution: FORTransferInstitution) => { + dispatch( + openModal({ + name: ModalName.ExchangeTransferModal, + initialState: { serviceProvider: transferInstitution }, + }) + ) + onClose() + }, + [dispatch, onClose] + ) + + const renderItem = useCallback( + ({ item: institution }: ListRenderItemInfo) => ( + + ), + [onSelectTransferInstitution] + ) + + return ( + + + {isLoading ? ( + + ) : ( + + )} + + + ) +} + +const renderItemSeparator = (): JSX.Element => diff --git a/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts b/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts new file mode 100644 index 0000000..b8cdce1 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/aggregatorHooks.ts @@ -0,0 +1,146 @@ +import { SerializedError } from '@reduxjs/toolkit' +import { FetchBaseQueryError, skipToken } from '@reduxjs/toolkit/query/react' +import { useTranslation } from 'react-i18next' +import { Delay } from 'src/components/layout/Delayed' +import { ColorTokens } from 'ui/src' +import { NumberType } from 'utilities/src/format/types' +import { useDebounce } from 'utilities/src/time/timing' +import { FiatCurrency } from 'wallet/src/features/fiatCurrency/constants' +import { + FiatCurrencyInfo, + useAppFiatCurrencyInfo, + useFiatCurrencyInfo, +} from 'wallet/src/features/fiatCurrency/hooks' +import { + useFiatOnRampAggregatorCryptoQuoteQuery, + useFiatOnRampAggregatorSupportedFiatCurrenciesQuery, +} from 'wallet/src/features/fiatOnRamp/api' +import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' +import { + isFiatOnRampApiError, + isInvalidRequestAmountTooHigh, + isInvalidRequestAmountTooLow, +} from 'wallet/src/features/fiatOnRamp/utils' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' + +export function useMeldFiatCurrencySupportInfo(countryCode: string): { + appFiatCurrencySupportedInMeld: boolean + meldSupportedFiatCurrency: FiatCurrencyInfo +} { + // Not all the currencies are supported by Meld, so we need to fallback to USD if the currency is not supported + const appFiatCurrencyInfo = useAppFiatCurrencyInfo() + const fallbackCurrencyInfo = useFiatCurrencyInfo(FiatCurrency.UnitedStatesDollar) + const appFiatCurrencyCode = appFiatCurrencyInfo.code.toLowerCase() + + const { data: supportedFiatCurrencies } = useFiatOnRampAggregatorSupportedFiatCurrenciesQuery({ + countryCode, + }) + + const appFiatCurrencySupported = + !supportedFiatCurrencies || + supportedFiatCurrencies.fiatCurrencies.some( + (currency): boolean => appFiatCurrencyCode === currency.fiatCurrencyCode.toLowerCase() + ) + const meldSupportedFiatCurrency = appFiatCurrencySupported + ? appFiatCurrencyInfo + : fallbackCurrencyInfo + + return { + appFiatCurrencySupportedInMeld: appFiatCurrencySupported, + meldSupportedFiatCurrency, + } +} + +/** + * Hook to load quotes + */ +export function useFiatOnRampQuotes({ + baseCurrencyAmount, + baseCurrencyCode, + quoteCurrencyCode, + countryCode, +}: { + baseCurrencyAmount?: number + baseCurrencyCode: string | undefined + quoteCurrencyCode: string | undefined + countryCode: string | undefined +}): { + loading: boolean + error?: FetchBaseQueryError | SerializedError + quotes: FORQuote[] | undefined +} { + const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) + const walletAddress = useActiveAccountAddress() + + const { + currentData: quotesResponse, + isFetching: quotesFetching, + error: quotesError, + } = useFiatOnRampAggregatorCryptoQuoteQuery( + baseCurrencyAmount && countryCode && quoteCurrencyCode && baseCurrencyCode + ? { + sourceAmount: baseCurrencyAmount, + sourceCurrencyCode: baseCurrencyCode, + destinationCurrencyCode: quoteCurrencyCode, + countryCode, + walletAddress: walletAddress ?? '', + } + : skipToken, + { + refetchOnMountOrArgChange: true, + } + ) + + const loading = quotesFetching || debouncedBaseCurrencyAmount !== baseCurrencyAmount + + // if user is entering base amount -> ignore previous errors + const error = debouncedBaseCurrencyAmount !== baseCurrencyAmount ? undefined : quotesError + + return { + loading, + error, + quotes: quotesResponse?.quotes ?? undefined, + } +} + +export function useParseFiatOnRampError( + error: unknown, + currencyCode: string +): { + errorText: string | undefined + errorColor: ColorTokens | undefined +} { + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + + let errorText, errorColor: ColorTokens | undefined + if (!error) { + return { errorText, errorColor } + } + + errorText = t('fiatOnRamp.error.default') + errorColor = '$DEP_accentWarning' + + if (isFiatOnRampApiError(error)) { + if (isInvalidRequestAmountTooLow(error)) { + const formattedAmount = formatNumberOrString({ + value: error.data.context.minimumAllowed, + type: NumberType.FiatStandard, + currencyCode, + }) + errorText = t('fiatOnRamp.error.min', { amount: formattedAmount }) + errorColor = '$statusCritical' + } else if (isInvalidRequestAmountTooHigh(error)) { + const formattedAmount = formatNumberOrString({ + value: error.data.context.maximumAllowed, + type: NumberType.FiatStandard, + currencyCode, + }) + errorText = t('fiatOnRamp.error.max', { amount: formattedAmount }) + errorColor = '$statusCritical' + } + } + + return { errorText, errorColor } +} diff --git a/apps/mobile/src/features/fiatOnRamp/constants.ts b/apps/mobile/src/features/fiatOnRamp/constants.ts new file mode 100644 index 0000000..5345144 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/constants.ts @@ -0,0 +1 @@ +export const FOR_MODAL_SNAP_POINTS = ['70%', '100%'] diff --git a/apps/mobile/src/features/fiatOnRamp/hooks.ts b/apps/mobile/src/features/fiatOnRamp/hooks.ts new file mode 100644 index 0000000..7c9ce23 --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/hooks.ts @@ -0,0 +1,466 @@ +import { skipToken } from '@reduxjs/toolkit/query/react' +import { Currency } from '@uniswap/sdk-core' +import { useCallback, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { Delay } from 'src/components/layout/Delayed' +import { FiatOnRampCurrency } from 'src/features/fiatOnRamp/types' +import { ColorTokens, useSporeColors } from 'ui/src' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { isAndroid } from 'uniswap/src/utils/platform' +import { logger } from 'utilities/src/logger/logger' +import { useDebounce } from 'utilities/src/time/timing' +import { useAllCommonBaseCurrencies } from 'wallet/src/components/TokenSelector/hooks' +import { BRIDGED_BASE_ADDRESSES } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { fromMoonpayNetwork } from 'wallet/src/features/chains/utils' +import { CurrencyInfo } from 'wallet/src/features/dataApi/types' +import { + useFiatOnRampAggregatorSupportedTokensQuery, + useFiatOnRampBuyQuoteQuery, + useFiatOnRampIpAddressQuery, + useFiatOnRampLimitsQuery, + useFiatOnRampSupportedTokensQuery, + useFiatOnRampWidgetUrlQuery, +} from 'wallet/src/features/fiatOnRamp/api' +import { useMoonpayFiatCurrencySupportInfo } from 'wallet/src/features/fiatOnRamp/hooks' +import { FORSupportedToken, MoonpayCurrency } from 'wallet/src/features/fiatOnRamp/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { addTransaction } from 'wallet/src/features/transactions/slice' +import { + FiatPurchaseTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { createTransactionId } from 'wallet/src/features/transactions/utils' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { getFormattedCurrencyAmount } from 'wallet/src/utils/currency' +import { ValueType } from 'wallet/src/utils/getCurrencyAmount' + +const ETH_POLYGON_MOONPAY_CODE = 'eth_polygon' +const WETH_POLYGON_MOONPAY_CODE = 'weth_polygon' +const BNB_MAINNET_MOONPAY_CODE = 'bnb' + +export function useFormatExactCurrencyAmount( + currencyAmount: string, + currency: Maybe +): string | undefined { + const formatter = useLocalizationContext() + + if (!currencyAmount || !currency) { + return + } + + const formattedAmount = getFormattedCurrencyAmount( + currency, + currencyAmount, + formatter, + true, + ValueType.Exact + ) + + // when formattedAmount is not empty it has an empty space in the end + return formattedAmount === '' ? '0 ' : formattedAmount +} + +/** Returns a new externalTransactionId and a callback to store the transaction. */ +export function useFiatOnRampTransactionCreator( + ownerAddress: string, + chainId: ChainId, + initialTypeInfo?: Partial +): { + externalTransactionId: string + dispatchAddTransaction: () => void +} { + const dispatch = useAppDispatch() + + const externalTransactionId = useRef(createTransactionId()) + + const dispatchAddTransaction = useCallback(() => { + // adds a dummy transaction detail for now + // later, we will attempt to look up information for that id + const transactionDetail: TransactionDetails = { + chainId, + id: externalTransactionId.current, + from: ownerAddress, + typeInfo: { + ...initialTypeInfo, + type: TransactionType.FiatPurchase, + syncedWithBackend: false, + }, + status: TransactionStatus.Pending, + addedTime: Date.now(), + hash: '', + options: { request: {} }, + } + // use addTransaction action so transactionWatcher picks it up + dispatch(addTransaction(transactionDetail)) + }, [initialTypeInfo, chainId, ownerAddress, dispatch]) + + return { externalTransactionId: externalTransactionId.current, dispatchAddTransaction } +} + +const MOONPAY_FEES_INCLUDED = true + +/** + * Hook to provide data from Moonpay for Fiat On Ramp Input Amount screen. + */ +export function useMoonpayFiatOnRamp({ + baseCurrencyAmount, + quoteCurrencyCode, + quoteChainId, +}: { + baseCurrencyAmount: string + quoteCurrencyCode: string | undefined + quoteChainId: ChainId +}): { + eligible: boolean + quoteAmount: number + quoteCurrencyAmountReady: boolean + quoteCurrencyAmountLoading: boolean + isLoading: boolean + externalTransactionId: string + dispatchAddTransaction: () => void + fiatOnRampHostUrl?: string + isError: boolean + errorText?: string + errorColor?: ColorTokens +} { + const colors = useSporeColors() + + const debouncedBaseCurrencyAmount = useDebounce(baseCurrencyAmount, Delay.Short) + + // we can consider adding `ownerAddress` as a prop to this modal in the future + // for now, always assume the user wants to fund the current account + const activeAccountAddress = useActiveAccountAddressWithThrow() + + const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( + activeAccountAddress, + quoteChainId + ) + + const { moonpaySupportedFiatCurrency: baseCurrency } = useMoonpayFiatCurrencySupportInfo() + const baseCurrencyCode = baseCurrency.code.toLowerCase() + const baseCurrencySymbol = baseCurrency.symbol + + const { + data: limitsData, + isLoading: limitsLoading, + isError: limitsLoadingQueryError, + } = useFiatOnRampLimitsQuery( + quoteCurrencyCode + ? { + baseCurrencyCode, + quoteCurrencyCode, + areFeesIncluded: MOONPAY_FEES_INCLUDED, + } + : skipToken + ) + + const { maxBuyAmount } = limitsData?.baseCurrency ?? { + maxBuyAmount: Infinity, + } + + // we're adding +1 here because MoonPay API is not precise with limits + // and an actual lower limit is a bit above the number, they provide in limits api + const minBuyAmount = limitsData?.baseCurrency?.minBuyAmount + ? limitsData.baseCurrency.minBuyAmount + 1 + : 0 + + const parsedBaseCurrencyAmount = parseFloat(baseCurrencyAmount) + const amountIsTooSmall = parsedBaseCurrencyAmount < minBuyAmount + const amountIsTooLarge = parsedBaseCurrencyAmount > maxBuyAmount + const isBaseCurrencyAmountValid = + !!parsedBaseCurrencyAmount && !amountIsTooSmall && !amountIsTooLarge + + const { + data: fiatOnRampHostUrl, + isError: isWidgetUrlQueryError, + isLoading: isWidgetUrlLoading, + } = useFiatOnRampWidgetUrlQuery( + // PERF: could consider skipping this call until eligibility in determined (ux tradeoffs) + // as-is, avoids waterfalling requests => better ux + quoteCurrencyCode + ? { + ownerAddress: activeAccountAddress, + colorCode: colors.accent1.val, + externalTransactionId, + amount: baseCurrencyAmount, + currencyCode: quoteCurrencyCode, + baseCurrencyCode, + redirectUrl: `${ + isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl + }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + } + : skipToken + ) + const { + data: buyQuote, + isFetching: buyQuoteLoading, + isError: buyQuoteLoadingQueryError, + } = useFiatOnRampBuyQuoteQuery( + // When isBaseCurrencyAmountValid is false and the user enters any digit, + // isBaseCurrencyAmountValid becomes true. Since there were no prior calls to the API, + // it takes the debouncedBaseCurrencyAmount and immediately calls an API. + // This only truly matters in the beginning and in cases where the debouncedBaseCurrencyAmount + // is changed while isBaseCurrencyAmountValid is false." + quoteCurrencyCode && + isBaseCurrencyAmountValid && + debouncedBaseCurrencyAmount === baseCurrencyAmount + ? { + baseCurrencyCode, + baseCurrencyAmount: debouncedBaseCurrencyAmount, + quoteCurrencyCode, + areFeesIncluded: MOONPAY_FEES_INCLUDED, + } + : skipToken + ) + + const quoteAmount = buyQuote?.quoteCurrencyAmount ?? 0 + + const { + data: ipAddressData, + isLoading: isEligibleLoading, + isError: isFiatBuyAllowedQueryError, + } = useFiatOnRampIpAddressQuery() + + const eligible = Boolean(ipAddressData?.isBuyAllowed) + + const isLoading = isEligibleLoading || isWidgetUrlLoading + const isError = + isFiatBuyAllowedQueryError || + isWidgetUrlQueryError || + buyQuoteLoadingQueryError || + limitsLoadingQueryError + + const quoteCurrencyAmountLoading = + buyQuoteLoading || limitsLoading || debouncedBaseCurrencyAmount !== baseCurrencyAmount + + const quoteCurrencyAmountReady = isBaseCurrencyAmountValid && !quoteCurrencyAmountLoading + + const { addFiatSymbolToNumber } = useLocalizationContext() + const minBuyAmountWithFiatSymbol = addFiatSymbolToNumber({ + value: minBuyAmount, + currencyCode: baseCurrencyCode, + currencySymbol: baseCurrencySymbol, + }) + const maxBuyAmountWithFiatSymbol = addFiatSymbolToNumber({ + value: maxBuyAmount, + currencyCode: baseCurrencyCode, + currencySymbol: baseCurrencySymbol, + }) + + const { errorText, errorColor } = useMoonpayError( + isError, + amountIsTooSmall, + amountIsTooLarge, + minBuyAmountWithFiatSymbol, + maxBuyAmountWithFiatSymbol + ) + + return { + eligible, + quoteAmount, + quoteCurrencyAmountReady, + quoteCurrencyAmountLoading, + isLoading, + externalTransactionId, + dispatchAddTransaction, + fiatOnRampHostUrl, + isError, + errorText, + errorColor, + } +} + +function useMoonpayError( + hasError: boolean, + amountIsTooSmall: boolean, + amountIsTooLarge: boolean, + minBuyAmountWithFiatSymbol: string, + maxBuyAmountWithFiatSymbol: string +): { + errorText: string | undefined + errorColor: ColorTokens | undefined +} { + const { t } = useTranslation() + + let errorText, errorColor: ColorTokens | undefined + + if (hasError) { + errorText = t('fiatOnRamp.error.default') + errorColor = '$DEP_accentWarning' + } else if (amountIsTooSmall) { + errorText = t('fiatOnRamp.error.min', { amount: minBuyAmountWithFiatSymbol }) + errorColor = '$statusCritical' + } else if (amountIsTooLarge) { + errorText = t('fiatOnRamp.error.max', { amount: maxBuyAmountWithFiatSymbol }) + errorColor = '$statusCritical' + } + + return { errorText, errorColor } +} + +function findTokenOptionForFiatOnRampToken( + commonBaseCurrencies: CurrencyInfo[] | undefined = [], + fiatOnRampToken: FORSupportedToken +): Maybe { + return commonBaseCurrencies.find( + (item) => + item && + fiatOnRampToken.cryptoCurrencyCode.toLowerCase() === item.currency.symbol?.toLowerCase() && + fiatOnRampToken.chainId === item.currency.chainId.toString() + ) +} + +function findTokenOptionForMoonpayCurrency( + commonBaseCurrencies: CurrencyInfo[] | undefined = [], + moonpayCurrency: MoonpayCurrency +): Maybe { + const currencyInfo = commonBaseCurrencies.find((item) => { + // Moonpay uses WETH on Polygon to represent ETH on Polygon + const moonpayCurrencyCode = + moonpayCurrency.code === ETH_POLYGON_MOONPAY_CODE + ? WETH_POLYGON_MOONPAY_CODE + : moonpayCurrency.code + const [tokenSymbol, network] = moonpayCurrencyCode.split('_') + const chainId = fromMoonpayNetwork(network) + return ( + item && + tokenSymbol && + tokenSymbol.toLowerCase() === item.currency.symbol?.toLowerCase() && + chainId === item.currency.chainId + ) + }) + if ( + !currencyInfo && + !BRIDGED_BASE_ADDRESSES.find((bridgedAddress) => + areAddressesEqual(bridgedAddress, moonpayCurrency.metadata?.contractAddress) + ) && + // We do not support BNB onboarding and Moonpay does not return an address for it so map it manually + moonpayCurrency.code !== BNB_MAINNET_MOONPAY_CODE + ) { + logger.error(`Moonpay currency ${moonpayCurrency.code} cannot be mapped`, { + tags: { file: 'fiatOnRamp/hooks', function: 'useMoonpaySupportedTokens' }, + extra: { + chainId: moonpayCurrency.metadata?.chainId, + address: moonpayCurrency.metadata?.contractAddress, + }, + }) + } + return currencyInfo +} + +export function useFiatOnRampSupportedTokens({ + sourceCurrencyCode, + countryCode, +}: { + sourceCurrencyCode: string + countryCode: string +}): { + error: boolean + list: FiatOnRampCurrency[] | undefined + loading: boolean + refetch: () => void +} { + const { + data: supportedTokensResponse, + isLoading: supportedTokensLoading, + error: supportedTokensError, + refetch: refetchSupportedTokens, + } = useFiatOnRampAggregatorSupportedTokensQuery({ fiatCurrency: sourceCurrencyCode, countryCode }) + + const { + data: commonBaseCurrencies, + error: commonBaseCurrenciesError, + loading: commonBaseCurrenciesLoading, + refetch: refetchCommonBaseCurrencies, + } = useAllCommonBaseCurrencies() + + const list = useMemo( + () => + (supportedTokensResponse?.supportedTokens || []) + .map((fiatOnRampToken) => ({ + currencyInfo: findTokenOptionForFiatOnRampToken(commonBaseCurrencies, fiatOnRampToken), + })) + .filter((item) => !!item.currencyInfo), + [commonBaseCurrencies, supportedTokensResponse?.supportedTokens] + ) + + const loading = supportedTokensLoading || commonBaseCurrenciesLoading + const error = Boolean(supportedTokensError || commonBaseCurrenciesError) + const refetch = async (): Promise => { + if (supportedTokensError) { + await refetchSupportedTokens?.() + } + if (commonBaseCurrenciesError) { + refetchCommonBaseCurrencies?.() + } + } + + return { list, loading, error, refetch } +} + +export function useMoonpaySupportedTokens(): { + error: boolean + list: FiatOnRampCurrency[] | undefined + loading: boolean + refetch: () => void +} { + // this should be already cached by the time we need it + const { + data: ipAddressData, + isLoading: ipAddressLoading, + isError: ipAddressError, + refetch: refetchIpAddress, + } = useFiatOnRampIpAddressQuery() + + const { + data: supportedTokens, + isLoading: supportedTokensLoading, + isError: supportedTokensError, + refetch: refetchSupportedTokens, + } = useFiatOnRampSupportedTokensQuery( + { + isUserInUS: ipAddressData?.alpha3 === 'USA' ?? false, + stateInUS: ipAddressData?.state, + }, + { skip: !ipAddressData } + ) + + const { + data: commonBaseCurrencies, + error: commonBaseCurrenciesError, + loading: commonBaseCurrenciesLoading, + refetch: refetchCommonBaseCurrencies, + } = useAllCommonBaseCurrencies() + + const list = useMemo( + () => + (supportedTokens || []) + .map((fiatOnRampToken) => ({ + currencyInfo: findTokenOptionForMoonpayCurrency(commonBaseCurrencies, fiatOnRampToken), + moonpayCurrencyCode: fiatOnRampToken.code, + })) + .filter((item) => !!item.currencyInfo), + [commonBaseCurrencies, supportedTokens] + ) + + const loading = ipAddressLoading || supportedTokensLoading || commonBaseCurrenciesLoading + const error = Boolean(ipAddressError || supportedTokensError || commonBaseCurrenciesError) + const refetch = async (): Promise => { + if (ipAddressError) { + await refetchIpAddress() + } + if (supportedTokensError) { + await refetchSupportedTokens() + } + if (commonBaseCurrenciesError) { + refetchCommonBaseCurrencies?.() + } + } + + return { list, loading, error, refetch } +} diff --git a/apps/mobile/src/features/fiatOnRamp/types.ts b/apps/mobile/src/features/fiatOnRamp/types.ts new file mode 100644 index 0000000..d8d154f --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/types.ts @@ -0,0 +1,11 @@ +import { CurrencyInfo } from 'wallet/src/features/dataApi/types' + +export type FiatOnRampCurrency = { + currencyInfo: Maybe + moonpayCurrencyCode?: string +} + +export enum InitialQuoteSelection { + MostRecent, + Best, +} diff --git a/apps/mobile/src/features/fiatOnRamp/utils.ts b/apps/mobile/src/features/fiatOnRamp/utils.ts new file mode 100644 index 0000000..8b7d39c --- /dev/null +++ b/apps/mobile/src/features/fiatOnRamp/utils.ts @@ -0,0 +1,8 @@ +import { FORQuote, FORServiceProvider } from 'wallet/src/features/fiatOnRamp/types' + +export function getServiceProviderForQuote( + quote: FORQuote | undefined, + serviceProviders: FORServiceProvider[] | undefined +): FORServiceProvider | undefined { + return serviceProviders?.find((sp) => sp.serviceProvider === quote?.serviceProvider) +} diff --git a/apps/mobile/src/features/firebase/firebaseDataSaga.ts b/apps/mobile/src/features/firebase/firebaseDataSaga.ts new file mode 100644 index 0000000..d8a748c --- /dev/null +++ b/apps/mobile/src/features/firebase/firebaseDataSaga.ts @@ -0,0 +1,249 @@ +import firebase from '@react-native-firebase/app' +import auth from '@react-native-firebase/auth' +import firestore from '@react-native-firebase/firestore' +import { appSelect } from 'src/app/hooks' +import { + getFirebaseUidOrError, + getFirestoreMetadataRef, + getFirestoreUidRef, +} from 'src/features/firebase/utils' +import { getOneSignalUserIdOrError } from 'src/features/notifications/Onesignal' +import { call, put, select, takeEvery, takeLatest } from 'typed-redux-saga' +import { logger } from 'utilities/src/logger/logger' +import { getKeys } from 'utilities/src/primitives/objects' +import { Language } from 'wallet/src/features/language/constants' +import { getLocale } from 'wallet/src/features/language/hooks' +import { selectCurrentLanguage, setCurrentLanguage } from 'wallet/src/features/language/slice' +import { + EditAccountAction, + TogglePushNotificationParams, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' +import { + makeSelectAccountNotificationSetting, + selectAccounts, + selectNonPendingAccounts, +} from 'wallet/src/features/wallet/selectors' +import { editAccount, setAccountsNonPending } from 'wallet/src/features/wallet/slice' + +interface AccountMetadata { + name?: string + type?: AccountType + avatar?: string + testnetsEnabled?: boolean + locale?: string +} + +function* initFirebase() { + try { + const firebaseAuth = auth() + yield* call([firebaseAuth, 'signInAnonymously']) + logger.debug('initFirebaseSaga', 'initFirebase', 'Firebase initialization successful') + } catch (error) { + logger.error(error, { + tags: { file: 'firebaseDataSaga', function: 'initFirebase' }, + }) + } +} + +export function* firebaseDataWatcher() { + yield* call(initFirebase) + + // Can't merge with `editAccountSaga` because it can't handle simultaneous actions + yield* takeEvery(editAccountActions.trigger, editAccountDataInFirebase) + yield* takeLatest(setCurrentLanguage, syncLanguageWithFirebase) + yield* takeEvery(setAccountsNonPending, syncAccountWithFirebase) +} + +function* syncLanguageWithFirebase(actionData: ReturnType) { + const accounts = yield* select(selectNonPendingAccounts) + const addresses = Object.keys(accounts) + + yield* call(updateFirebaseLanguage, addresses, actionData.payload) +} + +function* syncAccountWithFirebase(actionData: ReturnType) { + const currentLanguage = yield* select(selectCurrentLanguage) + yield* call(updateFirebaseLanguage, actionData.payload, currentLanguage) +} + +function* updateFirebaseLanguage(addresses: Address[], language: Language) { + const locale = getLocale(language) + + for (const address of addresses) { + yield* put( + editAccountActions.trigger({ + type: EditAccountAction.UpdateLanguage, + address, + locale, + }) + ) + } +} + +function* editAccountDataInFirebase(actionData: ReturnType) { + const { payload } = actionData + const { type, address } = payload + + switch (type) { + case EditAccountAction.Remove: + yield* call(removeAccountFromFirebase, address, payload.notificationsEnabled) + break + case EditAccountAction.Rename: + yield* call(renameAccountInFirebase, address, payload.newName) + break + case EditAccountAction.TogglePushNotification: + yield* call(toggleFirebaseNotificationSettings, payload) + break + case EditAccountAction.ToggleTestnetSettings: + yield* call(maybeUpdateFirebaseMetadata, address, { testnetsEnabled: payload.enabled }) + break + case EditAccountAction.UpdateLanguage: + yield* call(maybeUpdateFirebaseMetadata, address, { locale: payload.locale }) + break + default: + break + } +} + +function* addAccountToFirebase(account: Account) { + const { name, type, address } = account + const testnetsEnabled = false + const currentLanguage = yield* select(selectCurrentLanguage) + const currentLocale = getLocale(currentLanguage) + + try { + yield* call(mapFirebaseUidToAddresses, [address]) + yield* call(updateFirebaseMetadata, address, { + type, + name, + testnetsEnabled, + locale: currentLocale, + }) + } catch (error) { + logger.error(error, { tags: { file: 'firebaseDataSaga', function: 'addAccountToFirebase' } }) + } +} + +export function* removeAccountFromFirebase(address: Address, notificationsEnabled: boolean) { + try { + if (!notificationsEnabled) { + return + } + yield* call(deleteFirebaseMetadata, address) + yield* call(disassociateFirebaseUidFromAddresses, [address]) + } catch (error) { + logger.error(error, { + tags: { file: 'firebaseDataSaga', function: 'removeAccountFromFirebase' }, + }) + } +} + +const selectAccountNotificationSetting = makeSelectAccountNotificationSetting() + +export function* renameAccountInFirebase(address: Address, newName: string) { + try { + yield* call(maybeUpdateFirebaseMetadata, address, { name: newName }) + } catch (error) { + logger.error(error, { tags: { file: 'firebaseDataSaga', function: 'renameAccountInFirebase' } }) + } +} + +export function* toggleFirebaseNotificationSettings({ + address, + enabled, +}: TogglePushNotificationParams) { + try { + const accounts = yield* appSelect(selectAccounts) + const account = accounts[address] + if (!account) { + throw new Error(`Account not found for address ${address}`) + } + + if (enabled) { + yield* call(addAccountToFirebase, account) + } else { + yield* call(removeAccountFromFirebase, address, true) + } + + yield* put( + editAccount({ + address, + updatedAccount: { + ...account, + pushNotificationsEnabled: enabled, + }, + }) + ) + } catch (error) { + logger.error(error, { + tags: { file: 'firebaseDataSaga', function: 'toggleFirebaseNotificationSettings' }, + }) + } +} + +async function mapFirebaseUidToAddresses(addresses: Address[]): Promise { + const firebaseApp = firebase.app() + const uid = getFirebaseUidOrError(firebaseApp) + const batch = firestore(firebaseApp).batch() + addresses.forEach((address: string) => { + const uidRef = getFirestoreUidRef(firebaseApp, address) + batch.set(uidRef, { [uid]: true }, { merge: true }) + }) + + await batch.commit() +} + +async function disassociateFirebaseUidFromAddresses(addresses: Address[]): Promise { + const firebaseApp = firebase.app() + const uid = getFirebaseUidOrError(firebaseApp) + const batch = firestore(firebaseApp).batch() + addresses.forEach((address: string) => { + const uidRef = getFirestoreUidRef(firebaseApp, address) + batch.update(uidRef, { [uid]: firebase.firestore.FieldValue.delete() }) + }) + + await batch.commit() +} + +function* maybeUpdateFirebaseMetadata(address: Address, metadata: AccountMetadata) { + const notificationsEnabled = yield* select(selectAccountNotificationSetting, address) + + if (!notificationsEnabled) { + return + } + + yield* call(updateFirebaseMetadata, address, metadata) +} + +async function updateFirebaseMetadata(address: Address, metadata: AccountMetadata): Promise { + try { + const firebaseApp = firebase.app() + const pushId = await getOneSignalUserIdOrError() + const metadataRef = getFirestoreMetadataRef(firebaseApp, address, pushId) + + // Firestore does not support updating properties with an `undefined` value so must strip them out + const metadataWithDefinedPropsOnly = getKeys(metadata).reduce( + (obj: Record, prop) => { + const value = metadata[prop] + if (value !== undefined) { + obj[prop] = value + } + return obj + }, + {} + ) + + await metadataRef.set(metadataWithDefinedPropsOnly, { merge: true }) + } catch (error) { + logger.error(error, { tags: { file: 'firebaseDataSaga', function: 'updateFirebaseMetadata' } }) + } +} + +async function deleteFirebaseMetadata(address: Address): Promise { + const firebaseApp = firebase.app() + const pushId = await getOneSignalUserIdOrError() + const metadataRef = getFirestoreMetadataRef(firebaseApp, address, pushId) + await metadataRef.delete() +} diff --git a/apps/mobile/src/features/firebase/utils.ts b/apps/mobile/src/features/firebase/utils.ts new file mode 100644 index 0000000..e23ec4d --- /dev/null +++ b/apps/mobile/src/features/firebase/utils.ts @@ -0,0 +1,49 @@ +import type { ReactNativeFirebase } from '@react-native-firebase/app' +import '@react-native-firebase/auth' +import firestore, { FirebaseFirestoreTypes } from '@react-native-firebase/firestore' +import { isBetaBuild, isDevBuild } from 'src/utils/version' + +const ADDRESS_DATA_COLLECTION = 'address_data' +const DEV_ADDRESS_DATA_COLLECTION = 'dev_address_data' +const BETA_ADDRESS_DATA_COLLECTION = 'beta_address_data' + +export const getFirebaseUidOrError = (firebaseApp: ReactNativeFirebase.FirebaseApp): string => { + const uid = firebaseApp.auth().currentUser?.uid + if (!uid) { + throw new Error('User must be signed in to Firebase before accessing Firestore') + } + return uid +} + +export const getFirestoreUidRef = ( + firebaseApp: ReactNativeFirebase.FirebaseApp, + address: Address +): FirebaseFirestoreTypes.DocumentReference => + firestore(firebaseApp) + .collection(getAddressDataCollectionFromBundleId()) + .doc('address_uid_mapping') + .collection(address.toLowerCase()) + .doc('firebase') + +export const getFirestoreMetadataRef = ( + firebaseApp: ReactNativeFirebase.FirebaseApp, + address: Address, + pushId: string +): FirebaseFirestoreTypes.DocumentReference => + firestore(firebaseApp) + .collection(getAddressDataCollectionFromBundleId()) + .doc('metadata') + .collection(address.toLowerCase()) + .doc('onesignal_uids') + .collection(pushId) + .doc('data') + +export function getAddressDataCollectionFromBundleId(): string { + if (isDevBuild()) { + return DEV_ADDRESS_DATA_COLLECTION + } + if (isBetaBuild()) { + return BETA_ADDRESS_DATA_COLLECTION + } + return ADDRESS_DATA_COLLECTION +} diff --git a/apps/mobile/src/features/forceUpgrade/README.md b/apps/mobile/src/features/forceUpgrade/README.md new file mode 100644 index 0000000..ed2a86d --- /dev/null +++ b/apps/mobile/src/features/forceUpgrade/README.md @@ -0,0 +1,28 @@ +# Force Upgrade + +The ability of having users force upgrading our app allows us to stop critical UX and security bugs from reaching a wide audience. +By defining a mininum version all users need to be in, this component will block usage of the app and redirect users to the app store for +upgrading the app. + +## Schema + +This module relies on the following schema + StatSig's SDK automatically passes the version number of the app. + +```javascript +force_upgrade: { + status: 'recommended' | 'required' | 'not_required', +} +``` + +- `status`: + - A `recommended` status will display a dismissable dialog suggesting an upgrade + - A `required` status will display a fixed dialog asking for an upgrade. The user's only option will be to navigate out to the app store to upgrade the app. In the case the user doesn't want to continue using the app they can retrieve their seed phrase to get their funds back. + +## Flow + +1. At app start we first check if the `status` is either `recommended` or `required`. If it's not we stop the process. +2. We set the force upgrade modal's visibility and dismissability depending on the `status`. + +## UI + +[ForceUpgradeModal](https://github.com/Uniswap/universe/blob/main/apps/mobile/src/components/forceUpgrade/ForceUpgradeModal.tsx) diff --git a/apps/mobile/src/features/forceUpgrade/types.ts b/apps/mobile/src/features/forceUpgrade/types.ts new file mode 100644 index 0000000..407abb8 --- /dev/null +++ b/apps/mobile/src/features/forceUpgrade/types.ts @@ -0,0 +1,5 @@ +export enum UpgradeStatus { + Recommended, + Required, + NotRequired, +} diff --git a/apps/mobile/src/features/import/GenericImportForm.test.tsx b/apps/mobile/src/features/import/GenericImportForm.test.tsx new file mode 100644 index 0000000..e9c0446 --- /dev/null +++ b/apps/mobile/src/features/import/GenericImportForm.test.tsx @@ -0,0 +1,54 @@ +import React from 'react' +import { GenericImportForm } from 'src/features/import/GenericImportForm' +import { render, screen } from 'src/test/test-utils' +import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' +import { noOpFunction } from 'wallet/src/test/mocks' + +describe(GenericImportForm, () => { + it('renders a placeholder when there is no value', async () => { + const tree = render( + + + + ) + + expect(await screen.findByText('seed phrase')).toBeDefined() + expect(tree.toJSON()).toMatchSnapshot() + }) + + it('renders a value', async () => { + render( + + + + ) + + expect(await screen.queryByText('seed phrase')).toBeNull() + expect(await screen.findByDisplayValue('hello')).toBeDefined() + }) + + it('renders an error message', async () => { + render( + + + + ) + + expect(await screen.findByText('there is an error')).toBeDefined() + }) +}) diff --git a/apps/mobile/src/features/import/GenericImportForm.tsx b/apps/mobile/src/features/import/GenericImportForm.tsx new file mode 100644 index 0000000..fe88f69 --- /dev/null +++ b/apps/mobile/src/features/import/GenericImportForm.tsx @@ -0,0 +1,220 @@ +import React, { useEffect, useRef, useState } from 'react' +import { + Keyboard, + LayoutChangeEvent, + LayoutRectangle, + TextInput as NativeTextInput, + StyleSheet, +} from 'react-native' +import Trace from 'src/components/Trace/Trace' +import InputWithSuffix from 'src/features/import/InputWithSuffix' +import { Flex, Text, useMedia } from 'ui/src' +import { fonts } from 'ui/src/theme' +import PasteButton from 'wallet/src/components/buttons/PasteButton' +import { SectionName } from 'wallet/src/telemetry/constants' + +interface Props { + value: string | undefined + errorMessage: string | undefined + onChange: (text: string | undefined) => void + placeholderLabel: string + onSubmit?: () => void + inputSuffix?: string //text to auto to end of input string + liveCheck?: boolean + autoCorrect?: boolean + inputAlignment?: 'center' | 'flex-start' + onBlur?: () => void + onFocus?: () => void + beforePasteButtonPress?: () => void + afterPasteButtonPress?: () => void + blurOnSubmit?: boolean + textAlign?: 'left' | 'right' | 'center' + shouldUseMinHeight?: boolean +} + +export function GenericImportForm({ + value, + onChange, + errorMessage, + placeholderLabel, + onSubmit, + inputSuffix, + liveCheck, + autoCorrect, + onBlur, + onFocus, + beforePasteButtonPress, + afterPasteButtonPress, + blurOnSubmit, + textAlign, + inputAlignment = 'center', + shouldUseMinHeight = true, +}: Props): JSX.Element { + const [focused, setFocused] = useState(false) + const [layout, setLayout] = useState() + const textInputRef = useRef(null) + const isKeyboardVisibleRef = useRef(false) + const media = useMedia() + + const INPUT_FONT_SIZE = media.short ? fonts.subheading2.fontSize : fonts.subheading2.fontSize + const INPUT_MAX_FONT_SIZE_MULTIPLIER = fonts.subheading2.maxFontSizeMultiplier + + const handleBlur = (): void => { + setFocused(false) + onBlur?.() + } + + const handleFocus = (): void => { + setFocused(true) + onFocus?.() + // Need this to allow for focus on click on container. + textInputRef?.current?.focus() + } + + const handleSubmit = (): void => { + onSubmit && onSubmit() + } + + useEffect(() => { + const keyboardListeners = [ + Keyboard.addListener('keyboardDidShow', (): void => { + isKeyboardVisibleRef.current = true + }), + Keyboard.addListener('keyboardDidHide', (): void => { + if (!isKeyboardVisibleRef.current) { + return + } + isKeyboardVisibleRef.current = false + textInputRef?.current?.blur() + }), + ] + + return () => { + keyboardListeners.forEach((listener) => listener.remove()) + } + }, []) + + const INPUT_MIN_HEIGHT = 120 + const INPUT_MIN_HEIGHT_SHORT = 90 + + // Absolutely positioned paste button needs top padding to be vertically centered on bottom border of Flex + const PASTE_BUTTON_TOP_PADDING = INPUT_MIN_HEIGHT / 2 + 4 + const SHORT_PASTE_BUTTON_TOP_PADDING = INPUT_MIN_HEIGHT_SHORT / 2 - 12 + + return ( + + { + // Disable touch events when keyboard is visible (it prevents dismissing the keyboard + // when this component is pressed while the keyboard is visible) + return focused + }} + onTouchEnd={handleFocus}> + + {/* TODO: [MOB-225] make Box press re-focus TextInput. Fine for now since TexInput has autoFocus */} + + {!value && ( + setLayout(event.nativeEvent.layout)}> + + {placeholderLabel} + + + )} + {!value && !shouldUseMinHeight && ( + + + + )} + {!value && shouldUseMinHeight && ( + + + + + + )} + + + {errorMessage && value && (liveCheck || !focused) && ( + + + {errorMessage} + + + )} + + + + ) +} + +const styles = StyleSheet.create({ + placeholderLabelStyle: { + flexShrink: 1, + }, +}) diff --git a/apps/mobile/src/features/import/InputWithSuffix.tsx b/apps/mobile/src/features/import/InputWithSuffix.tsx new file mode 100644 index 0000000..6a915bc --- /dev/null +++ b/apps/mobile/src/features/import/InputWithSuffix.tsx @@ -0,0 +1,156 @@ +import { useCallback, useState } from 'react' +import { + LayoutRectangle, + NativeSyntheticEvent, + TextInput as NativeTextInput, + TextInputContentSizeChangeEventData, +} from 'react-native' +import { ColorTokens, Flex, useSporeColors } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { ElementName } from 'wallet/src/telemetry/constants' + +interface Props { + alwaysShowInputSuffix?: boolean + autoCorrect: boolean + blurOnSubmit: boolean + inputAlignment: 'center' | 'flex-start' + value?: string + inputFontSize: number + inputMaxFontSizeMultiplier: number + inputSuffix?: string + inputSuffixColor?: ColorTokens + multiline?: boolean + textAlign?: 'left' | 'right' | 'center' + textInputRef: React.RefObject + layout?: LayoutRectangle | null + onBlur?: () => void + onFocus?: () => void + onChangeText?: (text: string) => void + onSubmitEditing?: () => void +} + +export default function InputWithSuffix(props: Props): JSX.Element { + return isAndroid ? ( + + + + + ) : ( + + ) +} + +function Inputs({ + alwaysShowInputSuffix = false, + value, + layout, + inputSuffix, + inputSuffixColor, + inputAlignment, + inputFontSize, + inputMaxFontSizeMultiplier, + multiline = true, + textAlign, + textInputRef, + layerType, + ...inputProps +}: Props & { layerType?: 'foreground' | 'background' }): JSX.Element { + const colors = useSporeColors() + const [isMultiline, setIsMultiline] = useState(false) + + const handleContentSizeChange = useCallback( + (e: NativeSyntheticEvent) => { + if (multiline && textInputRef.current) { + setIsMultiline(Math.floor(e.nativeEvent.contentSize.height / inputFontSize) > 1) + } + }, + [textInputRef, inputFontSize, multiline] + ) + + const isInputEmpty = !value?.length + + const foregroundFallbackTextAlignment = + isMultiline || inputAlignment === 'flex-start' ? 'left' : 'center' + const foregroundTextAlignment = textAlign ?? foregroundFallbackTextAlignment + + const fallbackBackgroundTextAlignment = inputAlignment === 'flex-start' ? 'left' : 'center' + const backgroundTextAlignment = textAlign ?? fallbackBackgroundTextAlignment + + return ( + + {layerType === 'foreground' ? ( + + ) : ( + + )} + {inputSuffix && (alwaysShowInputSuffix || (value && !value.includes(inputSuffix))) ? ( + + ) : null} + + ) +} diff --git a/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap b/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap new file mode 100644 index 0000000..558b9f1 --- /dev/null +++ b/apps/mobile/src/features/import/__snapshots__/GenericImportForm.test.tsx.snap @@ -0,0 +1,295 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GenericImportForm renders a placeholder when there is no value 1`] = ` + + + + + + + + seed phrase + + + + + + + + Paste + + + + + + + + + + + + + +`; diff --git a/apps/mobile/src/features/modals/ModalsState.ts b/apps/mobile/src/features/modals/ModalsState.ts new file mode 100644 index 0000000..e4c78df --- /dev/null +++ b/apps/mobile/src/features/modals/ModalsState.ts @@ -0,0 +1,38 @@ +import { ExploreModalState } from 'src/app/modals/ExploreModalState' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' +import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' +import { Screens } from 'src/screens/Screens' +import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' +import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' +import { ModalName } from 'wallet/src/telemetry/constants' + +export interface AppModalState { + isOpen: boolean + initialState?: T +} + +export interface ModalsState { + [ModalName.AccountSwitcher]: AppModalState + [ModalName.ExchangeTransferModal]: AppModalState<{ + serviceProvider: FORTransferInstitution + }> + [ModalName.Experiments]: AppModalState + [ModalName.Explore]: AppModalState + [ModalName.FiatCurrencySelector]: AppModalState + [ModalName.FiatOnRamp]: AppModalState + [ModalName.FiatOnRampAggregator]: AppModalState + [ModalName.ReceiveCryptoModal]: AppModalState + [ModalName.LanguageSelector]: AppModalState + [ModalName.RemoveWallet]: AppModalState + [ModalName.RestoreWallet]: AppModalState + [ModalName.Scantastic]: AppModalState + [ModalName.Send]: AppModalState + [ModalName.Swap]: AppModalState + [ModalName.UnitagsIntro]: AppModalState<{ + address: Address + entryPoint: Screens.Home | Screens.Settings + }> + [ModalName.ViewOnlyExplainer]: AppModalState + [ModalName.WalletConnectScan]: AppModalState +} diff --git a/apps/mobile/src/features/modals/hooks.ts b/apps/mobile/src/features/modals/hooks.ts new file mode 100644 index 0000000..c39fa15 --- /dev/null +++ b/apps/mobile/src/features/modals/hooks.ts @@ -0,0 +1,27 @@ +import { useEffect } from 'react' +import { BackHandler } from 'react-native' +import { useAppSelector } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { useAppDispatch } from 'wallet/src/state' +import { ModalsState } from './ModalsState' +import { selectModalState } from './selectModalState' + +/* This hook is used to close the globally available modals (in Redux store) when the +back button is pressed. */ +export function useReduxModalBackHandler(modalName: keyof ModalsState): void { + const appDispatch = useAppDispatch() + const isBottomSheetOpen = useAppSelector(selectModalState(modalName)).isOpen + + useEffect(() => { + if (!isBottomSheetOpen) { + return + } + + const subscription = BackHandler.addEventListener('hardwareBackPress', () => { + appDispatch(closeModal({ name: modalName })) + return true + }) + + return subscription.remove + }, [isBottomSheetOpen, appDispatch, modalName]) +} diff --git a/apps/mobile/src/features/modals/modalSlice.test.ts b/apps/mobile/src/features/modals/modalSlice.test.ts new file mode 100644 index 0000000..3d8e24f --- /dev/null +++ b/apps/mobile/src/features/modals/modalSlice.test.ts @@ -0,0 +1,43 @@ +import { createStore, Store } from '@reduxjs/toolkit' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { + closeModal, + initialModalsState, + modalsReducer, + openModal, +} from 'src/features/modals/modalSlice' +import { ModalName } from 'wallet/src/telemetry/constants' +import { ModalsState } from './ModalsState' + +const initialState = { ...initialModalsState } +const modalName = ModalName.WalletConnectScan + +describe('modals reducer', () => { + let store: Store + + beforeEach(() => { + store = createStore(modalsReducer, initialState) + }) + + it('opens modals and sets initial state', () => { + expect(store.getState()[modalName].isOpen).toEqual(false) + + store.dispatch(openModal({ name: modalName, initialState: ScannerModalState.ScanQr })) + expect(store.getState()[modalName].isOpen).toEqual(true) + expect(store.getState()[modalName].initialState).toEqual(ScannerModalState.ScanQr) + }) + + it('closes modals', () => { + // initially closed + expect(store.getState()[modalName].isOpen).toEqual(false) + + // open it + store.dispatch(openModal({ name: modalName, initialState: ScannerModalState.ScanQr })) + expect(store.getState()[modalName].isOpen).toEqual(true) + + // now close it + store.dispatch(closeModal({ name: modalName })) + expect(store.getState()[modalName].isOpen).toEqual(false) + expect(store.getState()[modalName].initialState).toEqual(undefined) + }) +}) diff --git a/apps/mobile/src/features/modals/modalSlice.ts b/apps/mobile/src/features/modals/modalSlice.ts new file mode 100644 index 0000000..255abc2 --- /dev/null +++ b/apps/mobile/src/features/modals/modalSlice.ts @@ -0,0 +1,199 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { ExploreModalState } from 'src/app/modals/ExploreModalState' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { RemoveWalletModalState } from 'src/components/RemoveWallet/RemoveWalletModalState' +import { ExchangeTransferModalState } from 'src/features/fiatOnRamp/ExchangeTransferModalState' +import { ScantasticModalState } from 'src/features/scantastic/ScantasticModalState' +import { Screens } from 'src/screens/Screens' +import { getKeys } from 'utilities/src/primitives/objects' +import { TransactionState } from 'wallet/src/features/transactions/transactionState/types' +import { ModalName } from 'wallet/src/telemetry/constants' +import { ModalsState } from './ModalsState' + +type AccountSwitcherModalParams = { + name: typeof ModalName.AccountSwitcher + initialState?: undefined +} + +type ExchangeTransferModalParams = { + name: typeof ModalName.ExchangeTransferModal + initialState?: ExchangeTransferModalState +} + +type ExperimentsModalParams = { name: typeof ModalName.Experiments; initialState?: undefined } + +type ExploreModalParams = { + name: typeof ModalName.Explore + initialState?: ExploreModalState +} + +type FiatCurrencySelectorParams = { + name: typeof ModalName.FiatCurrencySelector + initialState?: undefined +} + +type FiatOnRampModalParams = { name: typeof ModalName.FiatOnRamp; initialState?: undefined } + +type FiatOnRampAggregatorModalParams = { + name: typeof ModalName.FiatOnRampAggregator + initialState?: undefined +} + +type ReceiveCryptoModalParams = { + name: typeof ModalName.ReceiveCryptoModal + initialState?: undefined +} + +type LanguageSelectorModalParams = { + name: typeof ModalName.LanguageSelector + initialState?: undefined +} + +type ScantasticModalParams = { + name: typeof ModalName.Scantastic + initialState: ScantasticModalState +} + +type RemoveWalletModalParams = { + name: typeof ModalName.RemoveWallet + initialState?: RemoveWalletModalState +} + +type RestoreWalletModalParams = { name: typeof ModalName.RestoreWallet; initialState?: undefined } + +type WalletConnectModalParams = { + name: typeof ModalName.WalletConnectScan + initialState: ScannerModalState +} + +type SwapModalParams = { name: typeof ModalName.Swap; initialState?: TransactionState } + +type SendModalParams = { name: typeof ModalName.Send; initialState?: TransactionState } + +type UnitagsIntroParams = { + name: typeof ModalName.UnitagsIntro + initialState?: { address: Address; entryPoint: Screens.Home | Screens.Settings } +} + +type ViewOnlyExplainerParams = { + name: typeof ModalName.ViewOnlyExplainer + initialState?: undefined +} + +export type OpenModalParams = + | AccountSwitcherModalParams + | ExchangeTransferModalParams + | ExperimentsModalParams + | ExploreModalParams + | FiatCurrencySelectorParams + | FiatOnRampModalParams + | FiatOnRampAggregatorModalParams + | ReceiveCryptoModalParams + | LanguageSelectorModalParams + | ScantasticModalParams + | RemoveWalletModalParams + | SendModalParams + | SwapModalParams + | WalletConnectModalParams + | RestoreWalletModalParams + | UnitagsIntroParams + | ViewOnlyExplainerParams + +export type CloseModalParams = { name: keyof ModalsState } + +export const initialModalsState: ModalsState = { + [ModalName.ExchangeTransferModal]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.FiatOnRamp]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.FiatOnRampAggregator]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.ReceiveCryptoModal]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.WalletConnectScan]: { + isOpen: false, + initialState: ScannerModalState.ScanQr, + }, + [ModalName.Scantastic]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Swap]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Send]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Experiments]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.Explore]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.AccountSwitcher]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.RemoveWallet]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.RestoreWallet]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.LanguageSelector]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.FiatCurrencySelector]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.UnitagsIntro]: { + isOpen: false, + initialState: undefined, + }, + [ModalName.ViewOnlyExplainer]: { + isOpen: false, + initialState: undefined, + }, +} + +const slice = createSlice({ + name: 'modals', + initialState: initialModalsState, + reducers: { + openModal: (state, action: PayloadAction) => { + const { name, initialState } = action.payload + state[name].isOpen = true + state[name].initialState = initialState + }, + closeModal: (state, action: PayloadAction) => { + const { name } = action.payload + state[name].isOpen = false + state[name].initialState = undefined + }, + closeAllModals: (state) => { + getKeys(state).forEach((modalName) => { + state[modalName].isOpen = false + state[modalName].initialState = undefined + }) + }, + }, +}) + +export const { openModal, closeModal, closeAllModals } = slice.actions +export const { reducer: modalsReducer } = slice diff --git a/apps/mobile/src/features/modals/saga.ts b/apps/mobile/src/features/modals/saga.ts new file mode 100644 index 0000000..27b0838 --- /dev/null +++ b/apps/mobile/src/features/modals/saga.ts @@ -0,0 +1,27 @@ +import { PayloadAction } from '@reduxjs/toolkit' +import { setTag } from '@sentry/react-native' +import { + closeModal, + CloseModalParams, + openModal, + OpenModalParams, +} from 'src/features/modals/modalSlice' +import { takeEvery } from 'typed-redux-saga' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function* modalWatcher() { + yield* takeEvery(openModal, handleOpenModalAction) + yield* takeEvery(closeModal, handleCloseModalAction) +} + +function handleOpenModalAction(action: PayloadAction) { + if (action.payload.name === ModalName.Swap) { + setTag('in_swap', true) + } +} + +function handleCloseModalAction(action: PayloadAction) { + if (action.payload.name === ModalName.Swap) { + setTag('in_swap', false) + } +} diff --git a/apps/mobile/src/features/modals/selectModalState.ts b/apps/mobile/src/features/modals/selectModalState.ts new file mode 100644 index 0000000..5ecfbd5 --- /dev/null +++ b/apps/mobile/src/features/modals/selectModalState.ts @@ -0,0 +1,8 @@ +import { MobileState } from 'src/app/reducer' +import { ModalsState } from './ModalsState' + +export function selectModalState( + name: T +): (state: MobileState) => ModalsState[T] { + return (state) => state.modals[name] +} diff --git a/apps/mobile/src/features/modals/selectSomeModalOpen.ts b/apps/mobile/src/features/modals/selectSomeModalOpen.ts new file mode 100644 index 0000000..7dbb85e --- /dev/null +++ b/apps/mobile/src/features/modals/selectSomeModalOpen.ts @@ -0,0 +1,5 @@ +import { MobileState } from 'src/app/reducer' + +export function selectSomeModalOpen(state: MobileState): boolean { + return Object.values(state.modals).some((modalState) => modalState.isOpen) +} diff --git a/apps/mobile/src/features/nfts/collection/ListPriceCard.tsx b/apps/mobile/src/features/nfts/collection/ListPriceCard.tsx new file mode 100644 index 0000000..7e2eef7 --- /dev/null +++ b/apps/mobile/src/features/nfts/collection/ListPriceCard.tsx @@ -0,0 +1,88 @@ +import { BlurView } from 'expo-blur' +import React from 'react' +import { StyleSheet } from 'react-native' +import { ColorTokens, Flex, FlexProps, Logos, SpaceTokens, Text, useSporeColors } from 'ui/src' +import { TextVariantTokens, borderRadii, iconSizes, spacing } from 'ui/src/theme' +import { IAmount } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { isIOS } from 'uniswap/src/utils/platform' +import { NumberType } from 'utilities/src/format/types' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' + +type ListPriceProps = FlexProps & { + price: IAmount + gap?: SpaceTokens + iconSize?: number + textVariant?: TextVariantTokens + iconColor?: ColorTokens + textColor?: ColorTokens +} + +export function ListPriceBadge({ + iconColor, + textColor, + iconSize, + price, + gap, + ...flexProps +}: ListPriceProps): JSX.Element { + const colors = useSporeColors() + const priceAmountProps = { iconColor, textColor, iconSize, price, gap } + + return ( + + {isIOS ? ( + + + + ) : ( + + + + )} + + ) +} + +export function PriceAmount({ + price, + gap = '$spacing4', + iconSize = iconSizes.icon16, + textVariant = 'buttonLabel4', + iconColor = '$neutral1', + textColor = '$neutral1', +}: ListPriceProps): JSX.Element { + const { convertFiatAmountFormatted, formatNumberOrString } = useLocalizationContext() + + const isUSD = price.currency === 'USD' + const formattedFiatValue = convertFiatAmountFormatted(price.value, NumberType.FiatTokenPrice) + const formattedAmount = isUSD + ? formattedFiatValue + : formatNumberOrString({ value: price.value, type: NumberType.NFTTokenFloorPrice }) + + return ( + + {!isUSD && ( + // @ts-expect-error TODO(MOB-1566) convert to specific icon size token, avoiding doing too big of a change in this PR + + )} + + {formattedAmount} + + + ) +} + +const styles = StyleSheet.create({ + background: { + alignItems: 'center', + backgroundColor: 'transparent', + flex: 1, + justifyContent: 'center', + paddingHorizontal: spacing.spacing8, + paddingVertical: spacing.spacing2, + }, + blurWrapper: { + borderRadius: borderRadii.rounded16, + overflow: 'hidden', + }, +}) diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx new file mode 100644 index 0000000..bf11772 --- /dev/null +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionContextMenu.tsx @@ -0,0 +1,118 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, Share } from 'react-native' +import ContextMenu, { ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { TripleDot } from 'src/components/icons/TripleDot' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { disableOnPress } from 'src/utils/disableOnPress' +import { ColorTokens, Flex, TouchableArea } from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { getNftCollectionUrl, getTwitterLink, openUri } from 'wallet/src/utils/linking' +import { NFTCollectionData } from './types' + +type MenuOption = { + title: string + action: () => Promise +} + +const ICON_SIZE = iconSizes.icon16 +const ICON_PADDING = spacing.spacing8 + +export function NFTCollectionContextMenu({ + data, + collectionAddress, + showButtonOutline = false, + iconColor = '$neutral2', +}: { + data: NFTCollectionData + collectionAddress?: Maybe + showButtonOutline?: boolean + iconColor?: ColorTokens +}): Nullable { + const { t } = useTranslation() + + const twitterURL = data?.twitterName ? getTwitterLink(data.twitterName) : undefined + const homepageUrl = data?.homepageUrl + const shareURL = getNftCollectionUrl(collectionAddress) + + const onSocialPress = async (): Promise => { + if (!twitterURL) { + return + } + await openUri(twitterURL) + } + + const openExplorerLink = async (): Promise => { + if (!homepageUrl) { + return + } + await openUri(homepageUrl) + } + + const onSharePress = useCallback(async () => { + if (!shareURL) { + return + } + try { + await Share.share({ + message: shareURL, + }) + sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, { + entity: ShareableEntity.NftCollection, + url: shareURL, + }) + } catch (error) { + logger.error(error, { tags: { file: 'NFTCollectionContextMenu', function: 'onSharePress' } }) + } + }, [shareURL]) + + const menuActions: MenuOption[] = [ + twitterURL + ? { + title: 'Twitter', + action: onSocialPress, + } + : undefined, + homepageUrl + ? { + title: t('tokens.nfts.link.collection'), + action: openExplorerLink, + } + : undefined, + shareURL + ? { + title: t('common.button.share'), + action: onSharePress, + } + : undefined, + ].filter((option): option is MenuOption => !!option) + + // Only display menu if valid options from data response, otherwise return empty + // element for spacing purposes + if (!homepageUrl && !twitterURL) { + return + } + + return ( + ): Promise => { + await menuActions[e.nativeEvent.index]?.action() + }}> + + + + + + + ) +} diff --git a/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx new file mode 100644 index 0000000..7c5ba16 --- /dev/null +++ b/apps/mobile/src/features/nfts/collection/NFTCollectionHeader.tsx @@ -0,0 +1,224 @@ +import React, { ReactElement } from 'react' +import { useTranslation } from 'react-i18next' +import { ImageStyle } from 'react-native-fast-image' +import { BackButton } from 'src/components/buttons/BackButton' +import { Loader } from 'src/components/loading' +import { LongMarkdownText } from 'src/components/text/LongMarkdownText' +import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu' +import { Flex, FlexProps, Logos, Text, useDeviceInsets, useSporeColors } from 'ui/src' +import VerifiedIcon from 'ui/src/assets/icons/verified.svg' +import { iconSizes, spacing } from 'ui/src/theme' +import { NumberType } from 'utilities/src/format/types' +import { ImageUri } from 'wallet/src/features/images/ImageUri' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { useExtractedColors } from 'wallet/src/utils/colors' +import { NFTCollectionData } from './types' + +const PROFILE_IMAGE_SIZE = 72 +const PROFILE_IMAGE_WRAPPER_SIZE = PROFILE_IMAGE_SIZE + spacing.spacing4 +export const NFT_BANNER_HEIGHT = 102 + +export function NFTCollectionHeader({ + loading = false, + data, + collectionAddress, +}: { + loading: boolean + data: Maybe + collectionAddress?: Maybe +}): ReactElement { + const colors = useSporeColors() + const { t } = useTranslation() + const { formatNumberOrString } = useLocalizationContext() + + // Style based on device sizing + const { top: deviceTopPadding } = useDeviceInsets() + const adjustedBannerHeight = deviceTopPadding + NFT_BANNER_HEIGHT + + const bannerImageStyle: ImageStyle = { + height: adjustedBannerHeight, + position: 'absolute', + top: 0, + left: 0, + right: 0, + maxHeight: adjustedBannerHeight, + } + + const bannerLoadingStyle: FlexProps['style'] = { + ...bannerImageStyle, + overflow: 'hidden', + } + + const profileImageWrapperStyle: ImageStyle = { + position: 'absolute', + left: 0, + top: adjustedBannerHeight - PROFILE_IMAGE_WRAPPER_SIZE / 2, + } + + const collectionStats = data?.markets?.[0] + const bannerImageUrl = data?.bannerImage?.url + const profileImageUrl = data?.image?.url + + // Extract profile image color as a fallback background color if no banner image. + const { colors: bannerColorsFallback } = useExtractedColors(profileImageUrl, 'surface2') + + return ( + <> + + {/* Banner image*/} + {loading || !!bannerImageUrl ? ( + + ) : ( + // No uri found on collection + + )} + + {/* Banner buttons */} + + + + + + + + {/* Profile image */} + + + {data?.image?.url ? ( + + + + ) : ( + + )} + + + + {/* Collection stats */} + + + + {data?.name ?? '-'} + + {data?.isVerified ? ( + + ) : null} + + + + + + {t('tokens.nfts.collection.label.items')} + + + {formatNumberOrString({ + value: data?.numAssets, + type: NumberType.NFTCollectionStats, + })} + + + + + {t('tokens.nfts.collection.label.owners')} + + + {formatNumberOrString({ + value: collectionStats?.owners, + type: NumberType.NFTCollectionStats, + })} + + + + + {t('tokens.nfts.collection.label.priceFloor')} + + + + {`${formatNumberOrString({ + value: collectionStats?.floorPrice?.value, + type: NumberType.NFTTokenFloorPrice, + })} `} + + {collectionStats?.floorPrice?.value !== undefined ? ( + + ) : null} + + + + + {t('tokens.nfts.collection.label.swapVolume')} + + + + {`${formatNumberOrString({ + value: collectionStats?.totalVolume?.value, + type: NumberType.NFTCollectionStats, + })}`} + + {collectionStats?.totalVolume?.value !== undefined ? ( + + ) : null} + + + + + {/* Collection description */} + {data?.description ? ( + + ) : loading ? ( + + + + + + ) : null} + + + + ) +} diff --git a/apps/mobile/src/features/nfts/collection/types.tsx b/apps/mobile/src/features/nfts/collection/types.tsx new file mode 100644 index 0000000..b0d01b3 --- /dev/null +++ b/apps/mobile/src/features/nfts/collection/types.tsx @@ -0,0 +1,5 @@ +import { NftCollectionScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' + +export type NFTCollectionData = Maybe< + NonNullable['edges']>[0]['node'] +> diff --git a/apps/mobile/src/features/nfts/hooks.ts b/apps/mobile/src/features/nfts/hooks.ts new file mode 100644 index 0000000..42a0004 --- /dev/null +++ b/apps/mobile/src/features/nfts/hooks.ts @@ -0,0 +1,125 @@ +import { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent, Share } from 'react-native' +import { ContextMenuAction, ContextMenuOnPressNativeEvent } from 'react-native-context-menu-view' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { selectNftsData } from 'wallet/src/features/favorites/selectors' +import { toggleNftVisibility } from 'wallet/src/features/favorites/slice' +import { shouldHideNft } from 'wallet/src/features/nfts/hooks' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { getNftUrl } from 'wallet/src/utils/linking' + +interface NFTMenuParams { + tokenId?: string + contractAddress?: Address + owner?: Address + showNotification?: boolean + isSpam?: boolean +} + +export function useNFTMenu({ + contractAddress, + tokenId, + owner, + showNotification = false, + isSpam, +}: NFTMenuParams): { + menuActions: Array void }> + onContextMenuPress: (e: NativeSyntheticEvent) => void + onlyShare: boolean +} { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const accounts = useAccounts() + const isLocalAccount = owner && !!accounts[owner] + + const isAddressAndTokenOk = contractAddress && tokenId + const nftsData = useAppSelector(selectNftsData) + const hidden = + owner && + isAddressAndTokenOk && + shouldHideNft({ nftsData, owner, contractAddress, tokenId, isSpam }) + + const onPressShare = useCallback(async (): Promise => { + if (!contractAddress || !tokenId) { + return + } + try { + const url = getNftUrl(contractAddress, tokenId) + await Share.share({ + message: url, + }) + sendMobileAnalyticsEvent(MobileEventName.ShareButtonClicked, { + entity: ShareableEntity.NftItem, + url, + }) + } catch (error) { + logger.error(error, { tags: { file: 'nfts/hooks', function: 'useNFTMenu' } }) + } + }, [contractAddress, tokenId]) + + const onPressHiddenStatus = useCallback(() => { + if (!owner || !contractAddress || !tokenId) { + return + } + dispatch( + toggleNftVisibility({ + owner, + contractAddress, + tokenId, + isSpam, + }) + ) + if (showNotification) { + dispatch( + pushNotification({ + type: AppNotificationType.AssetVisibility, + visible: !hidden, + hideDelay: 2 * ONE_SECOND_MS, + assetName: 'NFT', + }) + ) + } + }, [contractAddress, dispatch, hidden, isSpam, owner, showNotification, tokenId]) + + const menuActions = useMemo( + () => + isAddressAndTokenOk + ? [ + { + title: t('common.button.share'), + systemIcon: 'square.and.arrow.up', + onPress: onPressShare, + }, + ...((isLocalAccount && [ + { + title: hidden + ? t('tokens.nfts.hidden.action.unhide') + : t('tokens.nfts.hidden.action.hide'), + systemIcon: hidden ? 'eye' : 'eye.slash', + destructive: !hidden, + onPress: onPressHiddenStatus, + }, + ]) || + []), + ] + : [], + [isAddressAndTokenOk, t, onPressShare, isLocalAccount, hidden, onPressHiddenStatus] + ) + + const onContextMenuPress = useCallback( + async (e: NativeSyntheticEvent): Promise => { + await menuActions[e.nativeEvent.index]?.onPress?.() + }, + [menuActions] + ) + + return { menuActions, onContextMenuPress, onlyShare: !!isAddressAndTokenOk && !isLocalAccount } +} diff --git a/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx b/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx new file mode 100644 index 0000000..ec0b7c0 --- /dev/null +++ b/apps/mobile/src/features/nfts/item/BlurredImageBackground.tsx @@ -0,0 +1,34 @@ +import { BlurView } from 'expo-blur' +import React from 'react' +import { StyleSheet, View } from 'react-native' +import { FadeIn } from 'react-native-reanimated' +import { AnimatedFlex, Flex } from 'ui/src' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' + +/** + * Renders a blurred image background combined with a color overlay for a given image uri. + */ +export const BlurredImageBackground = ({ + backgroundColor, + imageUri, +}: { + backgroundColor: string + imageUri: string | undefined +}): JSX.Element => { + return ( + + {imageUri ? ( + + + + ) : null} + + + + ) +} diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx new file mode 100644 index 0000000..1187d74 --- /dev/null +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.test.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard' +import { render } from 'src/test/test-utils' +import { NFT_COLLECTION } from 'wallet/src/test/fixtures' + +it('renders collection preview card', () => { + const tree = render( + null} /> + ) + expect(tree).toMatchSnapshot() +}) diff --git a/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx new file mode 100644 index 0000000..3c5724b --- /dev/null +++ b/apps/mobile/src/features/nfts/item/CollectionPreviewCard.tsx @@ -0,0 +1,107 @@ +import { default as React } from 'react' +import { useTranslation } from 'react-i18next' +import { Loader } from 'src/components/loading' +import { PriceAmount } from 'src/features/nfts/collection/ListPriceCard' +import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import VerifiedIcon from 'ui/src/assets/icons/verified.svg' +import { iconSizes, imageSizes, spacing } from 'ui/src/theme' +import { + Currency, + NftItemScreenQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { NFTItem } from 'wallet/src/features/nfts/types' + +export type Collection = NonNullable< + NonNullable>['edges'][0] +>['node']['collection'] + +interface CollectionPreviewCardProps { + collection: Maybe + fallbackData?: NFTItem + onPress: () => void + loading: boolean +} +export function CollectionPreviewCard({ + collection, + fallbackData, + onPress, + loading, +}: CollectionPreviewCardProps): JSX.Element { + const colors = useSporeColors() + const { t } = useTranslation() + + if (loading || (!collection && !fallbackData?.name)) { + return + } + + const isViewableCollection = Boolean(collection || fallbackData?.contractAddress) + + return ( + + + + {collection?.image?.url ? ( + + + + ) : null} + + + {/* Width chosen to ensure truncation of collection name on both small + and large screens with sufficient padding */} + + + {collection?.name || fallbackData?.collectionName || '-'} + + + {collection?.isVerified && ( + + )} + + {collection?.markets?.[0]?.floorPrice && ( + + + {t('tokens.nfts.collection.label.priceFloor')}: + + + + )} + + + {isViewableCollection ? ( + + ) : null} + + + ) +} diff --git a/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap new file mode 100644 index 0000000..3eea2fb --- /dev/null +++ b/apps/mobile/src/features/nfts/item/__snapshots__/CollectionPreviewCard.test.tsx.snap @@ -0,0 +1,253 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders collection preview card 1`] = ` + + + + + + + Content not available + + + + + + + + Test NFT 1 + + + + + + + + + + + + + + + +`; diff --git a/apps/mobile/src/features/nfts/item/__snapshots__/traits.test.tsx.snap b/apps/mobile/src/features/nfts/item/__snapshots__/traits.test.tsx.snap new file mode 100644 index 0000000..8bee7d7 --- /dev/null +++ b/apps/mobile/src/features/nfts/item/__snapshots__/traits.test.tsx.snap @@ -0,0 +1,53 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders trait card 1`] = ` + + + traitName + + + traitValue + + +`; diff --git a/apps/mobile/src/features/nfts/item/traits.test.tsx b/apps/mobile/src/features/nfts/item/traits.test.tsx new file mode 100644 index 0000000..51a1665 --- /dev/null +++ b/apps/mobile/src/features/nfts/item/traits.test.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { NFTTraitCard } from 'src/features/nfts/item/traits' +import { render } from 'src/test/test-utils' +import { NFT_ASSET_TRAIT } from 'wallet/src/test/fixtures' + +it('renders trait card', () => { + const tree = render() + expect(tree).toMatchSnapshot() +}) diff --git a/apps/mobile/src/features/nfts/item/traits.tsx b/apps/mobile/src/features/nfts/item/traits.tsx new file mode 100644 index 0000000..b45cf3c --- /dev/null +++ b/apps/mobile/src/features/nfts/item/traits.tsx @@ -0,0 +1,66 @@ +import React from 'react' +import { FlatList, ListRenderItemInfo, StyleSheet } from 'react-native' +import { Flex, Text, useSporeColors } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { NftAssetTrait } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' + +export function NFTTraitCard({ + trait, + titleTextColor, +}: { + trait: NftAssetTrait + titleTextColor?: string +}): JSX.Element { + const colors = useSporeColors() + return ( + + + {trait.name} + + + {trait.value} + + + ) +} + +export function NFTTraitList({ + traits, + titleTextColor = 'neutral1', +}: { + traits: NftAssetTrait[] + titleTextColor?: string +}): JSX.Element { + function renderItem(item: ListRenderItemInfo): JSX.Element { + return + } + + return ( + + ) +} + +function Separator(): JSX.Element { + return +} + +const Styles = StyleSheet.create({ + listContainer: { + paddingHorizontal: spacing.spacing24, + }, +}) diff --git a/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx b/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx new file mode 100644 index 0000000..c6574ec --- /dev/null +++ b/apps/mobile/src/features/notifications/NotificationToastWrapper.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { useAppSelector } from 'src/app/hooks' +import { ScantasticCompleteNotification } from 'src/features/notifications/ScantasticCompleteNotification' +import { WCNotification } from 'src/features/notifications/WCNotification' +import { SharedNotificationToastRouter } from 'wallet/src/features/notifications/components/SharedNotificationToastRouter' +import { selectActiveAccountNotifications } from 'wallet/src/features/notifications/selectors' +import { AppNotification, AppNotificationType } from 'wallet/src/features/notifications/types' + +export function NotificationToastWrapper(): JSX.Element | null { + const notifications = useAppSelector(selectActiveAccountNotifications) + const notification = notifications?.[0] + + if (!notification) { + return null + } + + return +} + +function NotificationToastRouter({ + notification, +}: { + notification: AppNotification +}): JSX.Element | null { + // Insert Mobile-only notifications here. + // Shared wallet notifications should go in SharedNotificationToastRouter. + switch (notification.type) { + case AppNotificationType.WalletConnect: + return + case AppNotificationType.ScantasticComplete: + return + } + + return +} diff --git a/apps/mobile/src/features/notifications/Onesignal.ts b/apps/mobile/src/features/notifications/Onesignal.ts new file mode 100644 index 0000000..d1f6987 --- /dev/null +++ b/apps/mobile/src/features/notifications/Onesignal.ts @@ -0,0 +1,70 @@ +import { Linking } from 'react-native' +import OneSignal, { NotificationReceivedEvent, OpenedEvent } from 'react-native-onesignal' +import { apolloClientRef } from 'src/data/usePersistedApolloClient' +import { config } from 'uniswap/src/config' +import { GQLQueries } from 'uniswap/src/data/graphql/uniswap-data-api/queries' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' + +export const initOneSignal = (): void => { + OneSignal.setAppId(config.onesignalAppId) + + OneSignal.setNotificationWillShowInForegroundHandler((event: NotificationReceivedEvent) => { + // Complete with undefined means don't show OS notifications while app is in foreground + event.complete() + }) + + OneSignal.setNotificationOpenedHandler((event: OpenedEvent) => { + logger.debug( + 'Onesignal', + 'setNotificationOpenedHandler', + `Notification opened: ${event.notification}` + ) + + setTimeout( + () => + apolloClientRef.current?.refetchQueries({ + include: [GQLQueries.PortfolioBalances, GQLQueries.TransactionList], + }), + ONE_SECOND_MS // Delay by 1s to give a buffer for data sources to synchronize + ) + + // This emits a url event when coldStart = false. Don't call openURI because that will + // send the user to Safari to open the universal link. When coldStart = true, OneSignal + // handles the url event and navigates correctly. + if (event.notification.launchURL) { + Linking.emit('url', { url: event.notification.launchURL }) + } + }) +} + +export const promptPushPermission = ( + successCallback?: () => void, + failureCallback?: () => void +): void => { + OneSignal.promptForPushNotificationsWithUserResponse((response) => { + logger.debug( + 'Onesignal', + 'promptForPushNotificationsWithUserResponse', + `Prompt response: ${response}` + ) + if (response) { + successCallback?.() + } else { + failureCallback?.() + } + }) +} + +export const getOneSignalUserIdOrError = async (): Promise => { + const onesignalUserId = (await OneSignal.getDeviceState())?.userId + if (!onesignalUserId) { + throw new Error('Onesignal user ID is not defined') + } + return onesignalUserId +} + +export const getOneSignalPushToken = async (): Promise => { + const onesignalPushToken = (await OneSignal.getDeviceState())?.pushToken + return onesignalPushToken +} diff --git a/apps/mobile/src/features/notifications/PendingNotificationBadge.tsx b/apps/mobile/src/features/notifications/PendingNotificationBadge.tsx new file mode 100644 index 0000000..37919e9 --- /dev/null +++ b/apps/mobile/src/features/notifications/PendingNotificationBadge.tsx @@ -0,0 +1,100 @@ +import React from 'react' +import { useAppSelector } from 'src/app/hooks' +import { useEagerActivityNavigation } from 'src/app/navigation/hooks' +import { Flex, TouchableArea, useSporeColors } from 'ui/src' +import AlertCircle from 'ui/src/assets/icons/alert-circle.svg' +import { iconSizes } from 'ui/src/theme' +import { CheckmarkCircle } from 'wallet/src/components/icons/CheckmarkCircle' +import { SpinningLoader } from 'wallet/src/components/loading/SpinningLoader' +import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' +import { selectActiveAccountNotifications } from 'wallet/src/features/notifications/selectors' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useSortedPendingTransactions } from 'wallet/src/features/transactions/hooks' +import { TransactionStatus } from 'wallet/src/features/transactions/types' +import { selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' + +const PENDING_TX_TIME_LIMIT = 60_000 * 5 // 5 mins +const LOADING_SPINNER_SIZE = iconSizes.icon20 + +interface Props { + size?: number +} + +export function PendingNotificationBadge({ + size = LOADING_SPINNER_SIZE, +}: Props): JSX.Element | null { + const colors = useSporeColors() + const activeAccountAddress = useAppSelector(selectActiveAccountAddress) + const notifications = useAppSelector(selectActiveAccountNotifications) + const sortedPendingTransactions = useSortedPendingTransactions(activeAccountAddress) + const hasNotifications = useSelectAddressHasNotifications(activeAccountAddress) + + const { preload, navigate } = useEagerActivityNavigation() + + /*************** In-app txn confirmed **************/ + + const currentNotification = notifications?.[0] + if (currentNotification?.type === AppNotificationType.Transaction) { + const { txStatus } = currentNotification + if (txStatus === TransactionStatus.Success) { + return ( + + ) + } + + return + } + + /*************** Pending in-app txn **************/ + + const swapPendingNotificationActive = + currentNotification?.type === AppNotificationType.SwapPending + const pendingTransactionCount = (sortedPendingTransactions ?? []).length + const txPendingLongerThanLimit = + sortedPendingTransactions?.[0] && + Date.now() - sortedPendingTransactions[0].addedTime > PENDING_TX_TIME_LIMIT + + // If a transaction has been pending for longer than 5 mins, then don't show the pending icon anymore + // Dont show the loader if the swap pending toast is on screen + if ( + !swapPendingNotificationActive && + pendingTransactionCount >= 1 && + pendingTransactionCount <= 99 && + !txPendingLongerThanLimit + ) { + return ( + undefined} + onPressIn={async (): Promise => + activeAccountAddress ? await preload(activeAccountAddress) : null + }> + + + ) + } + + /** + Has unchecked notification status (triggered by Transaction history updater or transaction watcher saga). + Aka, will flip status to true when any local or remote transaction is confirmed. + **/ + + if (hasNotifications) { + return ( + + ) + } + + return null +} diff --git a/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx new file mode 100644 index 0000000..2f882df --- /dev/null +++ b/apps/mobile/src/features/notifications/ScantasticCompleteNotification.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Icons } from 'ui/src' +import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' +import { ScantasticCompleteNotification as ScantasticCompleteNotificationType } from 'wallet/src/features/notifications/types' + +export function ScantasticCompleteNotification({ + notification: { hideDelay }, +}: { + notification: ScantasticCompleteNotificationType +}): JSX.Element { + const { t } = useTranslation() + return ( + + + + + + + + + } + subtitle={t('notifications.scantastic.subtitle')} + title={t('notifications.scantastic.title')} + /> + ) +} diff --git a/apps/mobile/src/features/notifications/WCNotification.tsx b/apps/mobile/src/features/notifications/WCNotification.tsx new file mode 100644 index 0000000..b7e89ca --- /dev/null +++ b/apps/mobile/src/features/notifications/WCNotification.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { openModal } from 'src/features/modals/modalSlice' +import { iconSizes } from 'ui/src/theme' +import { DappLogoWithTxStatus } from 'wallet/src/components/CurrencyLogo/LogoWithTxStatus' +import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { NotificationToast } from 'wallet/src/features/notifications/components/NotificationToast' +import { NOTIFICATION_ICON_SIZE } from 'wallet/src/features/notifications/constants' +import { WalletConnectNotification } from 'wallet/src/features/notifications/types' +import { formWCNotificationTitle } from 'wallet/src/features/notifications/utils' +import { WalletConnectEvent } from 'wallet/src/features/walletConnect/types' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function WCNotification({ + notification, +}: { + notification: WalletConnectNotification +}): JSX.Element { + const { imageUrl, chainId, address, event, hideDelay, dappName } = notification + const dispatch = useAppDispatch() + const validChainId = toSupportedChainId(chainId) + const title = formWCNotificationTitle(notification) + + const smallToastEvents = [ + WalletConnectEvent.Connected, + WalletConnectEvent.Disconnected, + WalletConnectEvent.NetworkChanged, + ] + const smallToast = smallToastEvents.includes(event) + + const icon = ( + + ) + + const onPressNotification = (): void => { + dispatch( + openModal({ + name: ModalName.WalletConnectScan, + initialState: ScannerModalState.ConnectedDapps, + }) + ) + } + + return ( + + ) +} diff --git a/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts b/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts new file mode 100644 index 0000000..fcf1b3e --- /dev/null +++ b/apps/mobile/src/features/notifications/hooks/useNavigateToProfileTab.ts @@ -0,0 +1,33 @@ +import { useEagerActivityNavigation } from 'src/app/navigation/hooks' +import { store } from 'src/app/store' +import { closeAllModals } from 'src/features/modals/modalSlice' + +// Helpers to preload profile data, and dismiss modals and navigate +export const useNavigateToProfileTab = ( + address: string | undefined +): { + onPressIn: () => Promise + onPress: () => void +} => { + const { preload, navigate } = useEagerActivityNavigation() + + const onPressIn = async (): Promise => { + if (!address) { + return + } + await preload(address) + } + + const onPress = (): void => { + if (!address) { + return + } + navigate() + store.dispatch(closeAllModals()) + } + + return { + onPressIn, + onPress, + } +} diff --git a/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts b/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts new file mode 100644 index 0000000..874761c --- /dev/null +++ b/apps/mobile/src/features/notifications/hooks/useNotificationOSPermissionsEnabled.ts @@ -0,0 +1,30 @@ +import { useFocusEffect } from '@react-navigation/core' +import { useState } from 'react' +import { checkNotifications } from 'react-native-permissions' +import { useAppStateTrigger } from 'src/utils/useAppStateTrigger' + +export enum NotificationPermission { + Enabled = 'enabled', + Disabled = 'disabled', + Loading = 'loading', +} + +export function useNotificationOSPermissionsEnabled(): NotificationPermission { + const [notificationPermissionsEnabled, setNotificationPermissionsEnabled] = + useState(NotificationPermission.Loading) + + const checkNotificationPermissions = async (): Promise => { + const { status } = await checkNotifications() + const permission = + status === 'granted' ? NotificationPermission.Enabled : NotificationPermission.Disabled + setNotificationPermissionsEnabled(permission) + } + + useFocusEffect(() => { + checkNotificationPermissions().catch(() => undefined) + }) + + useAppStateTrigger('background', 'active', checkNotificationPermissions) + + return notificationPermissionsEnabled +} diff --git a/apps/mobile/src/features/onboarding/OnboardingScreen.tsx b/apps/mobile/src/features/onboarding/OnboardingScreen.tsx new file mode 100644 index 0000000..ab57fb6 --- /dev/null +++ b/apps/mobile/src/features/onboarding/OnboardingScreen.tsx @@ -0,0 +1,85 @@ +import { useHeaderHeight } from '@react-navigation/elements' +import React, { PropsWithChildren } from 'react' +import { KeyboardAvoidingView, StyleSheet } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { SHORT_SCREEN_HEADER_HEIGHT_RATIO, Screen } from 'src/components/layout/Screen' +import { AnimatedFlex, Flex, SpaceTokens, Text, useDeviceInsets, useMedia } from 'ui/src' +import { fonts } from 'ui/src/theme' +import { isIOS } from 'uniswap/src/utils/platform' + +type OnboardingScreenProps = { + subtitle?: string + title?: string + paddingTop?: SpaceTokens + childrenGap?: SpaceTokens + keyboardAvoidingViewEnabled?: boolean +} + +export function OnboardingScreen({ + title, + subtitle, + children, + paddingTop = '$none', + keyboardAvoidingViewEnabled = true, +}: PropsWithChildren): JSX.Element { + const headerHeight = useHeaderHeight() + const insets = useDeviceInsets() + const media = useMedia() + + const gapSize = media.short ? '$none' : '$spacing16' + + return ( + + + + {/* Text content */} + + {title && ( + + {title} + + )} + {subtitle ? ( + + {subtitle} + + ) : null} + + {/* page content */} + + {children} + + + + + ) +} + +const WrapperStyle = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, +}) diff --git a/apps/mobile/src/features/onboarding/OptionCard.tsx b/apps/mobile/src/features/onboarding/OptionCard.tsx new file mode 100644 index 0000000..acbe228 --- /dev/null +++ b/apps/mobile/src/features/onboarding/OptionCard.tsx @@ -0,0 +1,81 @@ +import React from 'react' +import Trace from 'src/components/Trace/Trace' +import { Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { ElementNameType } from 'wallet/src/telemetry/constants' + +export function OptionCard({ + title, + blurb, + icon, + onPress, + elementName, + disabled, + opacity, + badgeText, + hapticFeedback, +}: { + title: string + blurb: string + icon: React.ReactNode + onPress: () => void + elementName: ElementNameType + disabled?: boolean + opacity?: number + badgeText?: string | undefined + hapticFeedback?: boolean | undefined +}): JSX.Element { + const isDarkMode = useIsDarkMode() + + return ( + + + + + {icon} + + + + + + {title} + + {badgeText && ( + + + {badgeText} + + + )} + + + {blurb} + + + + + + + ) +} diff --git a/apps/mobile/src/features/onboarding/PasswordError.tsx b/apps/mobile/src/features/onboarding/PasswordError.tsx new file mode 100644 index 0000000..4ac7c10 --- /dev/null +++ b/apps/mobile/src/features/onboarding/PasswordError.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { StyleProp, ViewStyle } from 'react-native' +import { AnimatedFlex, Flex, Text } from 'ui/src' + +interface PasswordErrorProps { + errorText: string + style?: StyleProp +} + +export function PasswordError({ errorText, style }: PasswordErrorProps): JSX.Element { + return ( + + + + {errorText} + + + + ) +} diff --git a/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx b/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx new file mode 100644 index 0000000..902db00 --- /dev/null +++ b/apps/mobile/src/features/onboarding/SafeKeyboardOnboardingScreen.tsx @@ -0,0 +1,117 @@ +import { useHeaderHeight } from '@react-navigation/elements' +import { LinearGradient } from 'expo-linear-gradient' +import React, { PropsWithChildren } from 'react' +import { KeyboardAvoidingView, ScrollView, StyleSheet } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { Screen } from 'src/components/layout/Screen' +import { AnimatedFlex, Flex, SpaceTokens, Text, flexStyles, useMedia, useSporeColors } from 'ui/src' +import { opacify, spacing } from 'ui/src/theme' +import { isIOS } from 'uniswap/src/utils/platform' +import { useKeyboardLayout } from 'wallet/src/utils/useKeyboardLayout' + +type OnboardingScreenProps = { + subtitle?: string + title: string + paddingTop?: SpaceTokens +} + +export function SafeKeyboardOnboardingScreen({ + title, + subtitle, + children, + paddingTop = '$none', +}: PropsWithChildren): JSX.Element { + const headerHeight = useHeaderHeight() + const colors = useSporeColors() + const media = useMedia() + const keyboard = useKeyboardLayout() + + const header = ( + + + {title} + + {subtitle ? ( + + {subtitle} + + ) : null} + + ) + + const page = ( + + {children} + + ) + + const normalGradientPadding = 1.5 + const responsiveGradientPadding = media.short ? 1.25 : normalGradientPadding + + const topGradient = ( + + ) + + const compact = keyboard.isVisible && keyboard.containerHeight !== 0 + const containerStyle = compact ? styles.compact : styles.expand + + // This makes sure this component behaves just like `behavior="padding"` when + // there's enough space on the screen to show all components. + const minHeight = compact ? keyboard.containerHeight : 0 + + return ( + + + + + {header} + {page} + + + + {topGradient} + + ) +} + +const styles = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, + compact: { + flexGrow: 0, + }, + container: { + paddingBottom: spacing.spacing12, + }, + expand: { + flexGrow: 1, + }, + gradient: { + left: 0, + position: 'absolute', + right: 0, + top: 0, + }, +}) diff --git a/apps/mobile/src/features/onboarding/hooks.ts b/apps/mobile/src/features/onboarding/hooks.ts new file mode 100644 index 0000000..b88d12d --- /dev/null +++ b/apps/mobile/src/features/onboarding/hooks.ts @@ -0,0 +1,115 @@ +import { SharedEventName } from '@uniswap/analytics-events' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackBaseParams, useOnboardingStackNavigation } from 'src/app/navigation/types' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { Screens } from 'src/screens/Screens' +import { useTrace } from 'utilities/src/telemetry/trace/TraceContext' +import { + setHasSkippedUnitagPrompt, + setHasViewedUniconV2IntroModal, +} from 'wallet/src/features/behaviorHistory/slice' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { useClaimUnitag } from 'wallet/src/features/unitags/hooks' +import { Account, BackupType } from 'wallet/src/features/wallet/accounts/types' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' +import { setFinishedOnboarding } from 'wallet/src/features/wallet/slice' +import { sendWalletAnalyticsEvent, sendWalletAppsFlyerEvent } from 'wallet/src/telemetry' +import { WalletAppsFlyerEvents } from 'wallet/src/telemetry/constants' + +export type OnboardingCompleteProps = OnboardingStackBaseParams + +/** + * Bundles various actions that should be performed to complete onboarding. + * + * Used within the final screen of various onboarding flows. + */ +export function useCompleteOnboardingCallback({ + entryPoint, + importType, + unitagClaim, +}: OnboardingStackBaseParams): () => Promise { + const dispatch = useAppDispatch() + const pendingAccounts = usePendingAccounts() + const pendingWalletAddresses = Object.keys(pendingAccounts) + const parentTrace = useTrace() + const navigation = useOnboardingStackNavigation() + + const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) + const claimUnitag = useClaimUnitag() + + const uniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + + return async () => { + sendMobileAnalyticsEvent( + entryPoint === OnboardingEntryPoint.Sidebar + ? MobileEventName.WalletAdded + : MobileEventName.OnboardingCompleted, + { + wallet_type: importType, + accounts_imported_count: pendingWalletAddresses.length, + wallets_imported: pendingWalletAddresses, + cloud_backup_used: Object.values(pendingAccounts).some((acc: Account) => + acc.backups?.includes(BackupType.Cloud) + ), + ...parentTrace, + } + ) + + // Log TOS acceptance for new wallets before they are activated + if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) { + pendingWalletAddresses.forEach((address: string) => { + sendWalletAnalyticsEvent(SharedEventName.TERMS_OF_SERVICE_ACCEPTED, { address }) + }) + } + + // Claim unitag if there's a claim to process + if (unitagClaim) { + const { claimError } = await claimUnitag(unitagClaim, { + source: 'onboarding', + hasENSAddress: false, + }) + if (claimError) { + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: claimError, + }) + ) + } + } + + // Remove pending flag from all new accounts. + dispatch(pendingAccountActions.trigger(PendingAccountActions.Activate)) + + // Dismiss unitags prompt if: + // - the feature was enabled + // - the onboarding method prompts for unitags (create new) + if (unitagsFeatureFlagEnabled && importType === ImportType.CreateNew) { + dispatch(setHasSkippedUnitagPrompt(true)) + } + + if (uniconsV2Enabled) { + // Don't show Unicon V2 intro modal to new users + dispatch(setHasViewedUniconV2IntroModal(true)) + } + + // Exit flow + dispatch(setFinishedOnboarding({ finishedOnboarding: true })) + if (entryPoint === OnboardingEntryPoint.Sidebar) { + navigation.navigate(Screens.Home) + } + + if (entryPoint === OnboardingEntryPoint.FreshInstallOrReplace) { + await sendWalletAppsFlyerEvent(WalletAppsFlyerEvents.OnboardingCompleted, { importType }) + } + } +} diff --git a/apps/mobile/src/features/scantastic/ScantasticEncryption.ts b/apps/mobile/src/features/scantastic/ScantasticEncryption.ts new file mode 100644 index 0000000..349aeba --- /dev/null +++ b/apps/mobile/src/features/scantastic/ScantasticEncryption.ts @@ -0,0 +1,16 @@ +interface ScantasticEncryption { + getEncryptedMnemonic: (mnemonicId: string, n: string, e: string) => Promise +} + +declare module 'react-native' { + interface NativeModulesStatic { + ScantasticEncryption: ScantasticEncryption + } +} +import { NativeModules } from 'react-native' + +const { ScantasticEncryption } = NativeModules + +export function getEncryptedMnemonic(mnemonicId: string, n: string, e: string): Promise { + return ScantasticEncryption.getEncryptedMnemonic(mnemonicId, n, e) +} diff --git a/apps/mobile/src/features/scantastic/ScantasticModal.tsx b/apps/mobile/src/features/scantastic/ScantasticModal.tsx new file mode 100644 index 0000000..854191e --- /dev/null +++ b/apps/mobile/src/features/scantastic/ScantasticModal.tsx @@ -0,0 +1,331 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { closeAllModals } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { Button, Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { ONE_MINUTE_MS, ONE_SECOND_MS } from 'utilities/src/time/time' +import { useInterval } from 'utilities/src/time/timing' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { getOtpDurationString } from 'wallet/src/utils/duration' +import { getEncryptedMnemonic } from './ScantasticEncryption' + +enum OtpState { + Pending = 'pending', + Redeemed = 'redeemed', + Expired = 'expired', +} +interface OtpStateApiResponse { + otp?: OtpState + expiresAtInSeconds?: number +} + +export function ScantasticModal(): JSX.Element | null { + const { t } = useTranslation() + const colors = useSporeColors() + const dispatch = useAppDispatch() + + const account = useActiveAccount() + + const { initialState } = useAppSelector(selectModalState(ModalName.Scantastic)) + const params = initialState?.params + + const [OTP, setOTP] = useState('') + // Once a user has scanned a QR they have 6 minutes to correctly input the OTP + const [expirationTimestamp, setExpirationTimestamp] = useState( + Date.now() + 6 * ONE_MINUTE_MS + ) + const pubKey = params?.publicKey + const uuid = params?.uuid + const device = (params?.vendor + ' ' + params?.model || '').trim() + const browser = params?.browser || '' + + const [expired, setExpired] = useState(false) + const [redeemed, setRedeemed] = useState(false) + const [error, setError] = useState('') + + const [expiryText, setExpiryText] = useState('') + const setExpirationText = useCallback(() => { + const expirationString = getOtpDurationString(expirationTimestamp) + setExpiryText(expirationString) + }, [expirationTimestamp]) + useInterval(setExpirationText, ONE_SECOND_MS) + + if (redeemed) { + dispatch( + pushNotification({ + type: AppNotificationType.ScantasticComplete, + hideDelay: 6 * ONE_SECOND_MS, + }) + ) + dispatch(closeAllModals()) + } + + useEffect(() => { + const interval = setInterval(() => { + const timeLeft = expirationTimestamp - Date.now() + setExpired(timeLeft <= 0) + }, ONE_SECOND_MS) + + return () => clearInterval(interval) + }, [expirationTimestamp, t]) + + const onClose = useCallback((): void => { + dispatch(closeAllModals()) + }, [dispatch]) + + const onEncryptSeedphrase = async (): Promise => { + if (!pubKey) { + return + } + + setError('') + let encryptedSeedphrase = '' + const { n, e } = pubKey + try { + encryptedSeedphrase = await getEncryptedMnemonic(account?.address || '', n, e) + } catch (err) { + setError(t('scantastic.error.encryption')) + logger.error(err, { + tags: { + file: 'ScantasticModal', + function: 'onEncryptSeedphrase->getEncryptedMnemonic', + }, + extra: { + address: account?.address, + n, + e, + }, + }) + } + + try { + // submit encrypted blob + const response = await fetch(`${uniswapUrls.apiBaseExtensionUrl}/scantastic/blob`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + Origin: 'https://uniswap.org', + }, + body: JSON.stringify({ + uuid, + blob: encryptedSeedphrase, + }), + }) + if (!response.ok) { + throw new Error(`Failed to post blob: ${await response.text()}`) + } + const data = await response.json() + if (!data?.otp) { + throw new Error('OTP unavailable') + } else { + setExpirationTimestamp(Date.now() + ONE_MINUTE_MS * 2) + setOTP(data.otp) + } + } catch (err) { + setError(t('scantastic.error.noCode')) + logger.error(err, { + tags: { + file: 'ScantasticModal', + function: `onEncryptSeedphrase->fetch`, + }, + extra: { uuid }, + }) + } + } + + const { trigger: biometricTrigger } = useBiometricPrompt(onEncryptSeedphrase) + const { + requiredForAppAccess: biometricAuthRequiredForAppAccess, + requiredForTransactions: biometricAuthRequiredForTransactions, + } = useBiometricAppSettings() + + const onConfirmSync = async (): Promise => { + if (biometricAuthRequiredForAppAccess || biometricAuthRequiredForTransactions) { + await biometricTrigger() + } else { + await onEncryptSeedphrase() + } + } + + const checkOTPState = useCallback(async (): Promise => { + if (!OTP || !uuid) { + return + } + try { + const response = await fetch( + `${uniswapUrls.apiBaseExtensionUrl}/scantastic/otp-state/${uuid}`, + { + method: 'POST', + headers: { + Accept: 'application/json', + Origin: 'https://uniswap.org', + }, + } + ) + if (!response.ok) { + throw new Error(`Failed to check OTP state: ${await response.text()}`) + } + const data: OtpStateApiResponse = await response.json() + const otpState = data.otp + if (!otpState) { + throw new Error('No OTP state received.') + } + if (data.expiresAtInSeconds) { + setExpirationTimestamp(data.expiresAtInSeconds * ONE_SECOND_MS) + } + if (otpState === OtpState.Redeemed) { + setRedeemed(true) + } + if (otpState === OtpState.Expired) { + setExpired(true) + } + } catch (e) { + logger.error(e, { + tags: { + file: 'ScantasticModal', + function: `checkOTPState`, + }, + extra: { uuid }, + }) + } + }, [OTP, uuid]) + + useInterval(checkOTPState, 6000, true) + + if (expired) { + return ( + + + + + + {t('scantastic.error.timeout.title')} + + {t('scantastic.error.timeout.message')} + + + + + ) + } + + if (OTP) { + return ( + + + + + + {t('scantastic.code.title')} + + {t('scantastic.code.subtitle')} + + + {OTP.substring(0, 3).split('').join(' ')} + {OTP.substring(3).split('').join(' ')} + + + {expiryText} + + + + ) + } + + if (error) { + return ( + + + + + + {t('common.text.error')} + + {error} + + + + + + + ) + } + + return ( + + + + + + {t('scantastic.confirmation.title')} + + {t('scantastic.confirmation.subtitle')} + + + {device && ( + + + {t('scantastic.confirmation.label.device')} + + {device} + + )} + {browser && ( + + + {t('scantastic.confirmation.label.browser')} + + {browser} + + )} + + + + + + {t('common.button.cancel')} + + + + + + ) +} diff --git a/apps/mobile/src/features/scantastic/ScantasticModalState.ts b/apps/mobile/src/features/scantastic/ScantasticModalState.ts new file mode 100644 index 0000000..9c3ce9d --- /dev/null +++ b/apps/mobile/src/features/scantastic/ScantasticModalState.ts @@ -0,0 +1,5 @@ +import { ScantasticParams } from 'wallet/src/features/scantastic/types' + +export interface ScantasticModalState { + params: ScantasticParams +} diff --git a/apps/mobile/src/features/send/hooks.ts b/apps/mobile/src/features/send/hooks.ts new file mode 100644 index 0000000..ba23e59 --- /dev/null +++ b/apps/mobile/src/features/send/hooks.ts @@ -0,0 +1,34 @@ +import { useCallback } from 'react' +import { openModal } from 'src/features/modals/modalSlice' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { useAppDispatch } from 'wallet/src/state' +import { ModalName } from 'wallet/src/telemetry/constants' + +export const useNavigateToSend: () => ( + currencyAddress: Address, + currencyChainId: ChainId +) => void = () => { + const dispatch = useAppDispatch() + return useCallback( + (currencyAddress, currencyChainId) => { + const initialSendState: TransactionState = { + exactCurrencyField: CurrencyField.INPUT, + exactAmountToken: '', + [CurrencyField.INPUT]: { + address: currencyAddress, + chainId: currencyChainId, + type: AssetType.Currency, + }, + [CurrencyField.OUTPUT]: null, + showRecipientSelector: true, + } + dispatch(openModal({ name: ModalName.Send, initialState: initialSendState })) + }, + [dispatch] + ) +} diff --git a/apps/mobile/src/features/swap/hooks.ts b/apps/mobile/src/features/swap/hooks.ts new file mode 100644 index 0000000..a988f1e --- /dev/null +++ b/apps/mobile/src/features/swap/hooks.ts @@ -0,0 +1,48 @@ +import { useCallback } from 'react' +import { openModal } from 'src/features/modals/modalSlice' +import { getNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType, CurrencyAsset } from 'wallet/src/entities/assets' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { useAppDispatch } from 'wallet/src/state' +import { ModalName } from 'wallet/src/telemetry/constants' +import { areAddressesEqual } from 'wallet/src/utils/addresses' + +export const useNavigateToSwap: () => ( + currencyField: CurrencyField, + currencyAddress: Address, + currencyChainId: ChainId +) => void = () => { + const dispatch = useAppDispatch() + return useCallback( + (currencyField, currencyAddress, currencyChainId) => { + const nativeTokenAddress = getNativeAddress(currencyChainId) + const nativeToken: CurrencyAsset = { + address: nativeTokenAddress, + chainId: currencyChainId, + type: AssetType.Currency, + } + const chosenToken: CurrencyAsset = { + address: currencyAddress, + chainId: currencyChainId, + type: AssetType.Currency, + } + + const opposedToken = areAddressesEqual(nativeTokenAddress, currencyAddress) + ? null + : nativeToken + + const swapFormState: TransactionState = { + exactCurrencyField: currencyField, + exactAmountToken: '', + [CurrencyField.INPUT]: currencyField === CurrencyField.INPUT ? chosenToken : opposedToken, + [CurrencyField.OUTPUT]: currencyField === CurrencyField.OUTPUT ? chosenToken : opposedToken, + } + dispatch(openModal({ name: ModalName.Swap, initialState: swapFormState })) + }, + [dispatch] + ) +} diff --git a/apps/mobile/src/features/telemetry/constants.ts b/apps/mobile/src/features/telemetry/constants.ts new file mode 100644 index 0000000..e74e78f --- /dev/null +++ b/apps/mobile/src/features/telemetry/constants.ts @@ -0,0 +1,116 @@ +import { RootParamList } from 'src/app/navigation/types' +import { AppScreen, Screens } from 'src/screens/Screens' + +export function getAuthMethod( + isSettingEnabled: boolean, + isTouchIdSupported: boolean, + isFaceIdSupported: boolean +): AuthMethod { + if (!isSettingEnabled) { + return AuthMethod.None + } + + // both cannot be true since no iOS device supports both + if (isFaceIdSupported) { + return AuthMethod.FaceId + } + if (isTouchIdSupported) { + return AuthMethod.TouchId + } + + return AuthMethod.None +} + +export function getEventParams( + screen: AppScreen, + params: RootParamList[AppScreen] +): Record | undefined { + switch (screen) { + case Screens.SettingsWallet: + return { + address: (params as RootParamList[Screens.SettingsWallet]).address, + } + case Screens.SettingsWalletEdit: + return { + address: (params as RootParamList[Screens.SettingsWalletEdit]).address, + } + default: + return undefined + } +} + +/** + * Event names that occur in this specific application + */ +export enum MobileEventName { + AppRating = 'App Rating', + BalancesReport = 'Balances Report', + DeepLinkOpened = 'Deep Link Opened', + ExploreFilterSelected = 'Explore Filter Selected', + ExploreSearchResultClicked = 'Explore Search Result Clicked', + ExploreTokenItemSelected = 'Explore Token Item Selected', + FavoriteItem = 'Favorite Item', + FiatOnRampQuickActionButtonPressed = 'Fiat OnRamp QuickAction Button Pressed', + FiatOnRampWidgetOpened = 'Fiat OnRamp Widget Opened', + NotificationsToggled = 'Notifications Toggled', + OnboardingCompleted = 'Onboarding Completed', + PerformanceGraphql = 'Performance GraphQL', + PerformanceReport = 'Performance Report', + ShareButtonClicked = 'Share Button Clicked', + ShareLinkOpened = 'Share Link Opened', + TokenDetailsOtherChainButtonPressed = 'Token Details Other Chain Button Pressed', + WalletAdded = 'Wallet Added', + WalletConnectSheetCompleted = 'Wallet Connect Sheet Completed', + WidgetClicked = 'Widget Clicked', + WidgetConfigurationUpdated = 'Widget Configuration Updated', + // alphabetize additional values. +} + +/** + * Views not within the navigation stack that we still want to + * log Pageview events for. (Usually presented as nested views within another screen) + */ +export const enum ManualPageViewScreen { + WriteDownRecoveryPhrase = 'WriteDownRecoveryPhrase', + ConfirmRecoveryPhrase = 'ConfirmRecoveryPhrase', +} + +/** + * User properties tied to user rather than events + */ +export enum UserPropertyName { + ActiveWalletAddress = 'active_wallet_address', + ActiveWalletType = 'active_wallet_type', + AndroidPerfClass = 'android_perf_class', + AppOpenAuthMethod = 'app_open_auth_method', + AppVersion = 'app_version', + DarkMode = 'is_dark_mode', + HasLoadedENS = 'has_loaded_ens', + HasLoadedUnitag = 'has_loaded_unitag', + IsCloudBackedUp = 'is_cloud_backed_up', + IsHideSmallBalancesEnabled = 'is_hide_small_balances_enabled', + IsHideSpamTokensEnabled = 'is_hide_spam_tokens_enabled', + IsPushEnabled = 'is_push_enabled', + Language = 'language', + Currency = 'currency', + TransactionAuthMethod = 'transaction_auth_method', + WalletSignerAccounts = `wallet_signer_accounts`, + WalletSignerCount = 'wallet_signer_count', + WalletSwapProtectionSetting = 'wallet_swap_protection_setting', + WalletViewOnlyCount = 'wallet_view_only_count', + // alphabetize additional values. +} + +export enum AuthMethod { + FaceId = 'FaceId', + None = 'None', + TouchId = 'TouchId', + // alphabetize additional values. +} + +export enum ShareableEntity { + NftItem = 'NftItem', + NftCollection = 'NftCollection', + Token = 'Token', + Wallet = 'Wallet', +} diff --git a/apps/mobile/src/features/telemetry/directLogScreens.ts b/apps/mobile/src/features/telemetry/directLogScreens.ts new file mode 100644 index 0000000..3b2c0c8 --- /dev/null +++ b/apps/mobile/src/features/telemetry/directLogScreens.ts @@ -0,0 +1,15 @@ +import { Screens } from 'src/screens/Screens' + +export const DIRECT_LOG_ONLY_SCREENS: string[] = [ + Screens.TokenDetails, + Screens.ExternalProfile, + Screens.NFTItem, + Screens.NFTCollection, +] + +export function shouldLogScreen( + directFromPage: boolean | undefined, + screen: string | undefined +): boolean { + return directFromPage || screen === undefined || !DIRECT_LOG_ONLY_SCREENS.includes(screen) +} diff --git a/apps/mobile/src/features/telemetry/hooks.ts b/apps/mobile/src/features/telemetry/hooks.ts new file mode 100644 index 0000000..451d4c6 --- /dev/null +++ b/apps/mobile/src/features/telemetry/hooks.ts @@ -0,0 +1,111 @@ +import { useCallback, useMemo } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { + selectAllowAnalytics, + selectLastBalancesReport, + selectLastBalancesReportValue, + selectLastHeartbeat, + selectWalletIsFunded, +} from 'src/features/telemetry/selectors' +import { + recordBalancesReport, + recordHeartbeat, + recordWalletFunded, + shouldReportBalances, +} from 'src/features/telemetry/slice' +import { useAsyncData } from 'utilities/src/react/hooks' +import { areSameDays } from 'utilities/src/time/date' +import { useAccountList } from 'wallet/src/features/accounts/hooks' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { sendWalletAppsFlyerEvent } from 'wallet/src/telemetry' +import { WalletAppsFlyerEvents } from 'wallet/src/telemetry/constants' + +export function useLastBalancesReporter(): () => void { + const dispatch = useAppDispatch() + + const accounts = useAccounts() + const lastBalancesReport = useAppSelector(selectLastBalancesReport) + const lastBalancesReportValue = useAppSelector(selectLastBalancesReportValue) + const walletIsFunded = useAppSelector(selectWalletIsFunded) + + const signerAccountAddresses = useMemo(() => { + return Object.values(accounts) + .filter((a: Account) => a.type === AccountType.SignerMnemonic) + .map((a) => a.address) + }, [accounts]) + + const { data } = useAccountList({ + addresses: signerAccountAddresses, + fetchPolicy: 'cache-first', + }) + + const signerAccountValues = useMemo(() => { + const valuesUnfiltered = data?.portfolios + ?.map((p) => p?.tokensTotalDenominatedValue?.value) + .filter((v) => v !== undefined) + + if (valuesUnfiltered === undefined) { + return [] + } + + return valuesUnfiltered as number[] + }, [data?.portfolios]) + + const triggerAppFundedEvent = useCallback(async (): Promise => { + const sumOfFunds = signerAccountValues.reduce((a, b) => a + b, 0) + if (!walletIsFunded && sumOfFunds) { + // Only trigger the first time a funded wallet is detected + await sendWalletAppsFlyerEvent(WalletAppsFlyerEvents.WalletFunded, { sumOfFunds }) + dispatch(recordWalletFunded()) + } + }, [dispatch, signerAccountValues, walletIsFunded]) + + useAsyncData(triggerAppFundedEvent) + + const reporter = (): void => { + if ( + shouldReportBalances( + lastBalancesReport, + lastBalancesReportValue, + signerAccountAddresses, + signerAccountValues + ) + ) { + const totalBalance = signerAccountValues.reduce((a, b) => a + b, 0) + + sendMobileAnalyticsEvent(MobileEventName.BalancesReport, { + total_balances_usd: totalBalance, + wallets: signerAccountAddresses, + balances: signerAccountValues, + }) + // record that a report has been sent + dispatch(recordBalancesReport({ totalBalance })) + } + } + + return reporter +} + +// Returns a function that checks if the app needs to send a heartbeat action to record anonymous DAU +// Only logs when the user has allowing product analytics off and a heartbeat has not been sent for the user's local day +export function useHeartbeatReporter(): () => void { + const dispatch = useAppDispatch() + const allowAnalytics = useAppSelector(selectAllowAnalytics) + const lastHeartbeat = useAppSelector(selectLastHeartbeat) + + const nowDate = new Date(Date.now()) + const lastHeartbeatDate = new Date(lastHeartbeat) + + const heartbeatDue = !areSameDays(nowDate, lastHeartbeatDate) + + const reporter = (): void => { + if (!allowAnalytics && heartbeatDue) { + dispatch(recordHeartbeat()) + } + } + + return reporter +} diff --git a/apps/mobile/src/features/telemetry/index.ts b/apps/mobile/src/features/telemetry/index.ts new file mode 100644 index 0000000..11711c5 --- /dev/null +++ b/apps/mobile/src/features/telemetry/index.ts @@ -0,0 +1,16 @@ +import { UserPropertyName } from 'src/features/telemetry/constants' +import { MobileEventProperties } from 'src/features/telemetry/types' +import { analytics, UserPropertyValue } from 'utilities/src/telemetry/analytics/analytics' + +export function sendMobileAnalyticsEvent( + ...args: undefined extends MobileEventProperties[EventName] + ? [EventName] | [EventName, MobileEventProperties[EventName]] + : [EventName, MobileEventProperties[EventName]] +): void { + const [eventName, eventProperties] = args + analytics.sendEvent(eventName, eventProperties as Record) +} + +export function setUserProperty(property: UserPropertyName, value: UserPropertyValue): void { + analytics.setUserProperty(property, value) +} diff --git a/apps/mobile/src/features/telemetry/saga.ts b/apps/mobile/src/features/telemetry/saga.ts new file mode 100644 index 0000000..1427d1b --- /dev/null +++ b/apps/mobile/src/features/telemetry/saga.ts @@ -0,0 +1,30 @@ +import { OriginApplication } from '@uniswap/analytics' +import DeviceInfo from 'react-native-device-info' +import { selectAllowAnalytics } from 'src/features/telemetry/selectors' +import { call, delay, fork, select, takeEvery } from 'typed-redux-saga' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ApplicationTransport } from 'utilities/src/telemetry/analytics/ApplicationTransport' +import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { transactionActions } from 'wallet/src/features/transactions/slice' +import { logTransactionEvent } from 'wallet/src/features/transactions/transactionWatcherSaga' + +export function* telemetrySaga() { + yield* delay(1) + const allowAnalytics = yield* select(selectAllowAnalytics) + yield* call( + analytics.init, + new ApplicationTransport( + uniswapUrls.amplitudeProxyUrl, + OriginApplication.MOBILE, + uniswapUrls.apiBaseUrl, + DeviceInfo.getBundleId() + ), + allowAnalytics + ) + yield* fork(watchTransactionEvents) +} + +function* watchTransactionEvents() { + // Watch for finalized transactions to send analytics events + yield* takeEvery(transactionActions.finalizeTransaction.type, logTransactionEvent) +} diff --git a/apps/mobile/src/features/telemetry/selectors.ts b/apps/mobile/src/features/telemetry/selectors.ts new file mode 100644 index 0000000..d23c517 --- /dev/null +++ b/apps/mobile/src/features/telemetry/selectors.ts @@ -0,0 +1,13 @@ +import { MobileState } from 'src/app/reducer' + +export const selectLastBalancesReport = (state: MobileState): number => + state.telemetry.lastBalancesReport + +export const selectLastBalancesReportValue = (state: MobileState): number | undefined => + state.telemetry.lastBalancesReportValue + +export const selectLastHeartbeat = (state: MobileState): number => state.telemetry.lastHeartbeat + +export const selectWalletIsFunded = (state: MobileState): boolean => state.telemetry.walletIsFunded + +export const selectAllowAnalytics = (state: MobileState): boolean => state.telemetry.allowAnalytics diff --git a/apps/mobile/src/features/telemetry/slice.ts b/apps/mobile/src/features/telemetry/slice.ts new file mode 100644 index 0000000..8afa13b --- /dev/null +++ b/apps/mobile/src/features/telemetry/slice.ts @@ -0,0 +1,91 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { SharedEventName } from '@uniswap/analytics-events' +import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { ONE_MINUTE_MS } from 'utilities/src/time/time' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' + +const balanceReportFrequency = ONE_MINUTE_MS * 5 + +export interface TelemetryState { + // if the user has opted in/out of analytics + allowAnalytics: boolean + // anonymous user heartbeat, epoch time in milliseconds + lastHeartbeat: number + // epoch time in milliseconds + lastBalancesReport: number + // the USD balance last reported + lastBalancesReportValue?: number + walletIsFunded: boolean +} + +export const initialTelemetryState: TelemetryState = { + allowAnalytics: true, + lastHeartbeat: 0, + lastBalancesReport: 0, + lastBalancesReportValue: 0, + walletIsFunded: false, +} + +export const slice = createSlice({ + name: 'telemetry', + initialState: initialTelemetryState, + reducers: { + recordHeartbeat: (state) => { + sendWalletAnalyticsEvent(SharedEventName.HEARTBEAT) + state.lastHeartbeat = Date.now() + }, + recordBalancesReport: ( + state, + { payload: { totalBalance } }: PayloadAction<{ totalBalance: number }> + ) => { + state.lastBalancesReport = Date.now() + state.lastBalancesReportValue = totalBalance + }, + recordWalletFunded: (state) => { + state.walletIsFunded = true + }, + setAllowAnalytics: (state, { payload: { enabled } }: PayloadAction<{ enabled: boolean }>) => { + const logToggleEvent = (): void => { + sendWalletAnalyticsEvent(SharedEventName.ANALYTICS_SWITCH_TOGGLED, { enabled }) + analytics.flushEvents() + } + + // If turning off, log toggle event before turning off analytics + if (!enabled) { + logToggleEvent() + } + + analytics + .setAllowAnalytics(enabled) + .then(() => { + // If turned on, log toggle event after turning on analytics + if (enabled) { + logToggleEvent() + } + }) + .catch(() => undefined) + + // Set enabled in user state + state.allowAnalytics = enabled + }, + }, +}) + +export function shouldReportBalances( + lastBalancesReport: number | undefined, + lastBalancesReportValue: number | undefined, + signerAccountAddresses: string[], + signerAccountValues: number[] +): boolean { + const currentBalance = signerAccountValues.reduce((a, b) => a + b, 0) + + const didWalletGetFunded = currentBalance > 0 && lastBalancesReportValue === 0 + const balanceReportDue = (lastBalancesReport ?? 0) + balanceReportFrequency < Date.now() + const validAccountInfo = signerAccountAddresses.length === signerAccountValues.length + + return validAccountInfo && (didWalletGetFunded || balanceReportDue) +} + +export const { recordHeartbeat, recordBalancesReport, recordWalletFunded, setAllowAnalytics } = + slice.actions +export const { reducer: telemetryReducer } = slice diff --git a/apps/mobile/src/features/telemetry/types.ts b/apps/mobile/src/features/telemetry/types.ts new file mode 100644 index 0000000..4bceeea --- /dev/null +++ b/apps/mobile/src/features/telemetry/types.ts @@ -0,0 +1,108 @@ +import { RenderPassReport } from '@shopify/react-native-performance' +import { SharedEventName } from '@uniswap/analytics-events' +import { MobileEventName, ShareableEntity } from 'src/features/telemetry/constants' +import { WidgetEvent, WidgetType } from 'src/features/widgets/widgets' +import { TraceProps } from 'utilities/src/telemetry/trace/Trace' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { EthMethod, WCEventType, WCRequestOutcome } from 'wallet/src/features/walletConnect/types' + +// Events related to Moonpay internal transactions +// NOTE: we do not currently have access to the full life cycle of these txs +// because we do not yet use Moonpay's webhook +export type MoonpayTransactionEventProperties = TraceProps & + // allow any object of strings for now + Record + +export type AssetDetailsBaseProperties = { + name?: string + domain?: string + address: string + chain?: number +} + +export type SearchResultContextProperties = { + category?: string + query?: string + suggestion_count?: number + position?: number + isHistory?: boolean +} + +type OnboardingCompletedProps = { + wallet_type: ImportType + accounts_imported_count: number + wallets_imported: string[] + cloud_backup_used: boolean +} + +export type MobileEventProperties = { + [MobileEventName.AppRating]: { + type: 'store-review' | 'feedback-form' | 'remind' + appRatingPromptedMs?: number + appRatingProvidedMs?: number + } + [MobileEventName.BalancesReport]: { + total_balances_usd: number + wallets: string[] + balances: number[] + } + [MobileEventName.DeepLinkOpened]: { + url: string + screen: 'swap' | 'transaction' + is_cold_start: boolean + } + [MobileEventName.ExploreFilterSelected]: { + filter_type: string + } + [MobileEventName.ExploreSearchResultClicked]: SearchResultContextProperties & + AssetDetailsBaseProperties & { + type: 'collection' | 'token' | 'address' + } + [MobileEventName.ExploreTokenItemSelected]: AssetDetailsBaseProperties & { + position: number + } + [MobileEventName.FavoriteItem]: AssetDetailsBaseProperties & { + type: 'token' | 'wallet' + } + [MobileEventName.FiatOnRampQuickActionButtonPressed]: TraceProps + [MobileEventName.FiatOnRampWidgetOpened]: TraceProps & { externalTransactionId: string } + [MobileEventName.NotificationsToggled]: TraceProps & { + enabled: boolean + } + [MobileEventName.OnboardingCompleted]: OnboardingCompletedProps & TraceProps + [MobileEventName.PerformanceReport]: RenderPassReport + [MobileEventName.PerformanceGraphql]: { + dataSize: number + duration: number + operationName: string + operationType?: string + } + [MobileEventName.ShareButtonClicked]: { + entity: ShareableEntity + url: string + } + [MobileEventName.ShareLinkOpened]: { + entity: ShareableEntity + url: string + } + [MobileEventName.TokenDetailsOtherChainButtonPressed]: TraceProps + [MobileEventName.WalletAdded]: OnboardingCompletedProps & TraceProps + [MobileEventName.WalletConnectSheetCompleted]: { + request_type: WCEventType + eth_method?: EthMethod + dapp_url: string + dapp_name: string + wc_version: string + connection_chain_ids?: number[] + chain_id?: number + outcome: WCRequestOutcome + } + [MobileEventName.WidgetConfigurationUpdated]: WidgetEvent + [MobileEventName.WidgetClicked]: { + widget_type: WidgetType + url: string + } + [SharedEventName.APP_LOADED]: TraceProps | undefined + [SharedEventName.ELEMENT_CLICKED]: TraceProps + [SharedEventName.PAGE_VIEWED]: TraceProps +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.stories.tsx new file mode 100644 index 0000000..aca4d3f --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem.stories.tsx @@ -0,0 +1,209 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { ApproveSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/ApproveSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + ApproveTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39', + }, + }, + result: { + data: { + token: { + __typename: 'Token', + id: 'VG9rZW46RVRIRVJFVU1fMHgyYjU5MWU5OWFmZTlmMzJlYWE2MjE0ZjdiNzYyOTc2OGM0MGVlYjM5', + name: 'HEX', + symbol: 'HEX', + decimals: 8, + chain: 'ETHEREUM', + address: '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNXzB4NWZmNzcyYTM1MjkxQkZBOTJkNTYxNDQ3MzVjMEEzNzhlNjQyM0Y4NA==', + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png', + safetyLevel: 'MEDIUM_WARNING', + isSpam: false, + }, + }, + }, + }, + }, + { + request: { + query: TokenDocument, + variables: { + chain: 'OPTIMISM', + address: '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39', + }, + }, + result: { + data: { + token: { + __typename: 'Token', + id: 'VG9rZW46RVRIRVJFVU1fMHgyYjU5MWU5OWFmZTlmMzJlYWE2MjE0ZjdiNzYyOTc2OGM0MGVlYjM5', + name: 'HEX', + symbol: 'HEX', + decimals: 8, + chain: 'ETHEREUM', + address: '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNXzB4NWZmNzcyYTM1MjkxQkZBOTJkNTYxNDQ3MzVjMEEzNzhlNjQyM0Y4NA==', + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png', + safetyLevel: 'MEDIUM_WARNING', + isSpam: false, + }, + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseApproveTx: Omit & { + typeInfo: ApproveTransactionInfo +} = { + from: '', + addedTime: Date.now() - 30000, + hash: '', + options: { request: {} }, + chainId: 1, + id: '', + typeInfo: { + type: TransactionType.Approve, + spender: '', + approvalAmount: '1.0', + tokenAddress: '0x2b591e99afe9f32eaa6214f7b7629768c40eeb39', + }, +} + +const baseApproveUnlimitedTx = { + ...baseApproveTx, + typeInfo: { + ...baseApproveTx.typeInfo, + approvalAmount: 'INF', + }, +} + +const baseRevokeTx = { + ...baseApproveTx, + typeInfo: { + ...baseApproveTx.typeInfo, + approvalAmount: '0.0', + }, +} + +export const Approve: StoryObj = { + render: () => ( + <> + + + + + + + + + ), +} + +export const Revoke: StoryObj = { + render: () => ( + <> + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx new file mode 100644 index 0000000..61b8d95 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem.stories.tsx @@ -0,0 +1,157 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { + Chain, + TokenDocument, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { getNativeAddress } from 'wallet/src/constants/addresses' +import { ChainId } from 'wallet/src/constants/chains' +import { FiatPurchaseSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/FiatPurchaseSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + FiatPurchaseTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseFaitPurchaseTx: Omit & { + typeInfo: FiatPurchaseTransactionInfo +} = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.FiatPurchase, + inputCurrency: { + type: 'fiat', + code: 'USD', + }, + inputCurrencyAmount: 123, + outputCurrency: { + type: 'crypto', + metadata: { + contractAddress: getNativeAddress(ChainId.Mainnet), + chainId: Chain.Ethereum, + }, + }, + outputCurrencyAmount: 123, + syncedWithBackend: false, + }, +} + +export const FiatPurchase: StoryObj = { + render: () => ( + <> + + + + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx new file mode 100644 index 0000000..7b597b5 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem.stories.tsx @@ -0,0 +1,75 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import { NFTApproveSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/NFTApproveSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + NFTApproveTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', +} + +export default meta + +const baseApproveTx: Omit & { + typeInfo: NFTApproveTransactionInfo +} = { + from: '', + addedTime: Date.now() - 30000, + hash: '', + options: { request: {} }, + chainId: 1, + id: '', + typeInfo: { + type: TransactionType.NFTApprove, + spender: '', + nftSummaryInfo: { + collectionName: 'Froggy Friends Official', + imageURL: + 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', + name: 'Froggy Friend #1777', + tokenId: '1777', + }, + }, +} + +export const NFTApprove: StoryObj = { + render: () => ( + <> + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx new file mode 100644 index 0000000..bd33c81 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem.stories.tsx @@ -0,0 +1,77 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { NFTMintSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/NFTMintSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + NFTMintTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', +} + +export default meta + +const baseNFTMintTx: Omit & { typeInfo: NFTMintTransactionInfo } = { + from: '', + addedTime: Date.now() - 30000, + hash: '', + options: { request: {} }, + chainId: 1, + id: '', + typeInfo: { + type: TransactionType.NFTMint, + nftSummaryInfo: { + collectionName: 'Froggy Friends Official', + imageURL: + 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', + name: 'Froggy Friend #1777', + tokenId: '1777', + }, + }, +} + +export const NFTMint: StoryObj = { + render: () => ( + <> + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx new file mode 100644 index 0000000..b7ffbfc --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem.stories.tsx @@ -0,0 +1,133 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import { NFTTradeSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/NFTTradeSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + NFTTradeTransactionInfo, + NFTTradeType, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { buildNativeCurrencyId } from 'wallet/src/utils/currencyId' + +const meta: Meta = { + title: 'WIP/Activity Items', +} + +export default meta + +const baseNFTBuyTx: Omit & { typeInfo: NFTTradeTransactionInfo } = { + from: '', + addedTime: Date.now() - 30000, + hash: '', + options: { request: {} }, + chainId: 1, + id: '', + typeInfo: { + type: TransactionType.NFTTrade, + tradeType: NFTTradeType.BUY, + nftSummaryInfo: { + collectionName: 'Froggy Friends Official', + imageURL: + 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', + name: 'Froggy Friend #1777', + tokenId: '1777', + }, + purchaseCurrencyId: buildNativeCurrencyId(ChainId.Mainnet), + purchaseCurrencyAmountRaw: '1000000000000000000', + }, +} + +const baseNFTSellTx: Omit & { typeInfo: NFTTradeTransactionInfo } = { + ...baseNFTBuyTx, + typeInfo: { + ...baseNFTBuyTx.typeInfo, + tradeType: NFTTradeType.SELL, + }, +} + +export const NFTBuy: StoryObj = { + render: () => ( + <> + + + + + + + ), +} + +export const NFTSell: StoryObj = { + render: () => ( + <> + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx new file mode 100644 index 0000000..4e0ee25 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem.stories.tsx @@ -0,0 +1,165 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { ReceiveSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/ReceiveSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + ReceiveTokenTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + { + request: { + query: TokenDocument, + variables: { + chain: 'OPTIMISM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'OPTIMISM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseReceiveTx: Omit & { + typeInfo: ReceiveTokenTransactionInfo +} = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.Receive, + currencyAmountRaw: '50000000000000000', + sender: '0xa0c68c638235ee32657e8f720a23cec1bfc77c77', + tokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + assetType: AssetType.Currency, + transactedUSDValue: 105.21800000000002, + }, +} + +const baseNFTReceiveTx: Omit & { + typeInfo: ReceiveTokenTransactionInfo +} = { + ...baseReceiveTx, + typeInfo: { + type: TransactionType.Receive, + sender: '0xa0c68c638235ee32657e8f720a23cec1bfc77c77', + tokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + assetType: AssetType.ERC721, + nftSummaryInfo: { + collectionName: 'Froggy Friends Official', + imageURL: + 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', + name: 'Froggy Friend #1777', + tokenId: '1777', + }, + }, +} + +export const Receive: StoryObj = { + render: () => ( + <> + + + + ), +} + +export const NFTReceive: StoryObj = { + render: () => ( + <> + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx new file mode 100644 index 0000000..7e86337 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem.stories.tsx @@ -0,0 +1,217 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { AssetType } from 'wallet/src/entities/assets' +import { SendSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/SendSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + SendTokenTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + { + request: { + query: TokenDocument, + variables: { + chain: 'OPTIMISM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'OPTIMISM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseSendTx: Omit & { typeInfo: SendTokenTransactionInfo } = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.Send, + currencyAmountRaw: '50000000000000000', + recipient: '0xa0c68c638235ee32657e8f720a23cec1bfc77c77', + tokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + assetType: AssetType.Currency, + transactedUSDValue: 105.21800000000002, + }, +} + +const baseNFTSendTx: Omit & { typeInfo: SendTokenTransactionInfo } = { + ...baseSendTx, + typeInfo: { + type: TransactionType.Send, + recipient: '0xa0c68c638235ee32657e8f720a23cec1bfc77c77', + tokenAddress: '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee', + assetType: AssetType.ERC721, + nftSummaryInfo: { + collectionName: 'Froggy Friends Official', + imageURL: + 'https://lh3.googleusercontent.com/9LokgAuB0Xqkio273GE0pY0WSJwOExFtFI1SkJT2jK-USvqFc-5if7ZP5PQ1h8s5YPimyJG5cSOdGGR2UaD3gTYMKAhj6yikYaw=s250', + name: 'Froggy Friend #1777', + tokenId: '1777', + }, + }, +} + +export const Send: StoryObj = { + render: () => ( + <> + + + + + + + + ), +} + +export const NFTSend: StoryObj = { + render: () => ( + <> + + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx new file mode 100644 index 0000000..8f13044 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem.stories.tsx @@ -0,0 +1,160 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { TradeType } from '@uniswap/sdk-core' +import React from 'react' +import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { ChainId } from 'wallet/src/constants/chains' +import { SwapSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/SwapSummaryItem' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { buildCurrencyId, buildNativeCurrencyId } from 'wallet/src/utils/currencyId' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: '0x6b175474e89094c44da98b954eedeac495271d0f', + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fMHg2QjE3NTQ3NEU4OTA5NEM0NERhOThiOTU0RWVkZUFDNDk1MjcxZDBG', + name: 'Dai Stablecoin', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNXzB4REExMDAwOWNCZDVEMDdkZDBDZUNjNjYxNjFGQzkzRDdjOTAwMGRhMQ==', + isSpam: false, + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'DAI', + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseSwapTx: Omit & { + typeInfo: ExactOutputSwapTransactionInfo | ExactInputSwapTransactionInfo +} = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.Swap, + tradeType: TradeType.EXACT_OUTPUT, + outputCurrencyAmountRaw: '50000000000000000', + expectedInputCurrencyAmountRaw: '50000000000000000', + maximumInputCurrencyAmountRaw: '50000000000000000', + inputCurrencyId: buildNativeCurrencyId(ChainId.Mainnet), + outputCurrencyId: buildCurrencyId( + ChainId.Mainnet, + '0x6b175474e89094c44da98b954eedeac495271d0f' + ), + transactedUSDValue: 105.21800000000002, + }, +} + +export const Swap: StoryObj = { + render: () => ( + <> + + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.stories.tsx new file mode 100644 index 0000000..790a039 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem.stories.tsx @@ -0,0 +1,65 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { UnknownSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/UnknownSummaryItem' +import { + TransactionDetails, + TransactionStatus, + TransactionType, + UnknownTransactionInfo, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', +} + +export default meta + +const baseUnknownTx: Omit & { typeInfo: UnknownTransactionInfo } = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.Unknown, + }, +} + +export const Unknown: StoryObj = { + render: () => ( + <> + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.stories.tsx new file mode 100644 index 0000000..ccd1833 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem.stories.tsx @@ -0,0 +1,71 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { ChainId } from 'wallet/src/constants/chains' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { WCSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/WCSummaryItem' +import { + TransactionDetails, + TransactionStatus, + TransactionType, + WCConfirmInfo, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', +} + +export default meta + +const baseUnknownItem: Omit & { typeInfo: WCConfirmInfo } = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + type: TransactionType.WCConfirm, + dapp: { + icon: 'https://synapseprotocol.com/favicon.ico', + name: 'Synapse', + url: 'https://synapseprotocol.com', + source: 'walletconnect', + }, + }, +} + +export const WalletConnect: StoryObj = { + render: () => ( + <> + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx new file mode 100644 index 0000000..1a7dd85 --- /dev/null +++ b/apps/mobile/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem.stories.tsx @@ -0,0 +1,188 @@ +import type { Meta, StoryObj } from '@storybook/react' +import React from 'react' +import { TokenDocument } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import TransactionSummaryLayout from 'wallet/src/features/transactions/SummaryCards/SummaryItems/TransactionSummaryLayout' +import { WrapSummaryItem } from 'wallet/src/features/transactions/SummaryCards/SummaryItems/WrapSummaryItem' +import { + TransactionDetails, + TransactionStatus, + TransactionType, + WrapTransactionInfo, +} from 'wallet/src/features/transactions/types' + +const meta: Meta = { + title: 'WIP/Activity Items', + parameters: { + apolloClient: { + mocks: [ + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: null, + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: null, + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fbnVsbA==', + name: 'Ethereum', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNX251bGw=', + isSpam: false, + logoUrl: 'https://token-icons.s3.amazonaws.com/eth.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'ETH', + }, + }, + }, + }, + { + request: { + query: TokenDocument, + variables: { + chain: 'ETHEREUM', + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + }, + }, + result: { + data: { + token: { + __typename: 'Token', + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + chain: 'ETHEREUM', + decimals: 18, + id: 'VG9rZW46RVRIRVJFVU1fMHhjMDJhYWEzOWIyMjNmZThkMGEwZTVjNGYyN2VhZDkwODNjNzU2Y2My', + name: 'Wrapped Ether', + project: { + __typename: 'TokenProject', + id: 'VG9rZW5Qcm9qZWN0OlRva2VuOkFSQklUUlVNXzB4ODJhRjQ5NDQ3RDhhMDdlM2JkOTVCRDBkNTZmMzUyNDE1MjNmQmFiMQ==', + isSpam: false, + logoUrl: + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + safetyLevel: 'VERIFIED', + }, + symbol: 'WETH', + }, + }, + }, + }, + ], + }, + }, +} + +export default meta + +const baseWrapTx: Omit & { typeInfo: WrapTransactionInfo } = { + from: '0x76e4de46c21603545eaaf7daf25e54c0d06bafa9', + addedTime: Date.now() - 30000, + hash: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + id: '0x3ba4b82fb3bcb237cff0180b4fb4f94902cde2cfa56c57567b59b5608590d077', + options: { request: {} }, + chainId: 1, + typeInfo: { + currencyAmountRaw: '10000000000000000', + type: TransactionType.Wrap, + unwrapped: false, + }, +} + +const baseUnwrapTx: Omit & { typeInfo: WrapTransactionInfo } = { + ...baseWrapTx, + typeInfo: { + ...baseWrapTx.typeInfo, + unwrapped: true, + }, +} + +export const Wrap: StoryObj = { + render: () => ( + <> + + + + + + + ), +} + +export const Unwrap: StoryObj = { + render: () => ( + <> + + + + + + + ), +} diff --git a/apps/mobile/src/features/transactions/TransactionPending/StatusAnimation.tsx b/apps/mobile/src/features/transactions/TransactionPending/StatusAnimation.tsx new file mode 100644 index 0000000..2ceb3f2 --- /dev/null +++ b/apps/mobile/src/features/transactions/TransactionPending/StatusAnimation.tsx @@ -0,0 +1,41 @@ +import React, { useEffect, useRef } from 'react' +import Rive, { Alignment, Fit, RiveRef } from 'rive-react-native' +import { TransactionStatus } from 'wallet/src/features/transactions/types' + +const ANIMATION_WIDTH = 250 +const ANIMATION_HEIGHT = 250 +const stateMachineName = 'State Machine 1' + +export function StatusAnimation({ + status, + transactionType, +}: { + status?: TransactionStatus + transactionType: 'swap' | 'send' +}): JSX.Element { + const animationRef = useRef(null) + + useEffect(() => { + if (status === TransactionStatus.Success) { + animationRef.current?.setInputState(stateMachineName, 'isSuccess', true) + } else if (status === TransactionStatus.Failed) { + animationRef.current?.setInputState(stateMachineName, 'isFailure', true) + } + }, [status]) + + return ( + + ) +} diff --git a/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx new file mode 100644 index 0000000..d070423 --- /dev/null +++ b/apps/mobile/src/features/transactions/TransactionPending/TransactionPending.tsx @@ -0,0 +1,75 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { StatusAnimation } from 'src/features/transactions/TransactionPending/StatusAnimation' +import { AnimatedFlex, Button, Flex, Text, TouchableArea } from 'ui/src' +import { ChainId } from 'wallet/src/constants/chains' +import { + TransactionDetails, + TransactionStatus, + isFinalizedTx, +} from 'wallet/src/features/transactions/types' +import { ElementName } from 'wallet/src/telemetry/constants' +import { openTransactionLink } from 'wallet/src/utils/linking' + +type TransactionStatusProps = { + transaction: TransactionDetails | undefined + chainId: ChainId + title: string + description: string + onNext: () => void + onTryAgain: () => void + transactionType: 'swap' | 'send' +} + +export function TransactionPending({ + transaction, + title, + description, + onNext, + onTryAgain, + transactionType, +}: TransactionStatusProps): JSX.Element { + const { t } = useTranslation() + + const onPressViewTransaction = async (): Promise => { + if (transaction) { + await openTransactionLink(transaction?.hash, transaction.chainId) + } + } + + return ( + + + + + + + {title} + + {description} + + {transaction?.status === TransactionStatus.Failed ? ( + + + {t('common.button.tryAgain')} + + + ) : null} + + + + {transaction && isFinalizedTx(transaction) ? ( + + ) : null} + + + + ) +} diff --git a/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx b/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx new file mode 100644 index 0000000..0a946ef --- /dev/null +++ b/apps/mobile/src/features/transactions/swap/hooks/useOnCloseSendModal.tsx @@ -0,0 +1,14 @@ +import { useCallback } from 'react' +import { useAppDispatch } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function useOnCloseSendModal(): () => void { + const appDispatch = useAppDispatch() + + const onClose = useCallback((): void => { + appDispatch(closeModal({ name: ModalName.Send })) + }, [appDispatch]) + + return onClose +} diff --git a/apps/mobile/src/features/transactions/swap/utils.ts b/apps/mobile/src/features/transactions/swap/utils.ts new file mode 100644 index 0000000..50cbd6b --- /dev/null +++ b/apps/mobile/src/features/transactions/swap/utils.ts @@ -0,0 +1,25 @@ +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' + +export function getFocusOnCurrencyFieldFromInitialState({ + focusOnCurrencyField, + input, + output, + exactCurrencyField, +}: TransactionState): CurrencyField | undefined { + if (focusOnCurrencyField) { + return focusOnCurrencyField + } + + if (input && exactCurrencyField === CurrencyField.INPUT) { + return CurrencyField.INPUT + } + + if (output && exactCurrencyField === CurrencyField.OUTPUT) { + return CurrencyField.OUTPUT + } + + return undefined +} diff --git a/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx new file mode 100644 index 0000000..36ed8eb --- /dev/null +++ b/apps/mobile/src/features/transactions/transfer/TransferFlow.tsx @@ -0,0 +1,333 @@ +import { providers } from 'ethers' +import { default as React, useCallback, useEffect, useMemo, useReducer, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { TouchableWithoutFeedback } from 'react-native' +import { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated' +import { useShouldShowNativeKeyboard } from 'src/app/hooks' +import { RecipientSelect } from 'src/components/RecipientSelect/RecipientSelect' +import Trace from 'src/components/Trace/Trace' +import { Screen } from 'src/components/layout/Screen' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { TransferHeader } from 'src/features/transactions/transfer/TransferHeader' +import { TransferStatus } from 'src/features/transactions/transfer/TransferStatus' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { AnimatedFlex, Flex, useDeviceDimensions, useDeviceInsets, useSporeColors } from 'ui/src' +import EyeIcon from 'ui/src/assets/icons/eye.svg' +import { iconSizes } from 'ui/src/theme' +import { + TokenSelectorModal, + TokenSelectorVariation, +} from 'wallet/src/components/TokenSelector/TokenSelector' +import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { useTransactionGasFee } from 'wallet/src/features/gas/hooks' +import { GasFeeResult, GasSpeed } from 'wallet/src/features/gas/types' +import { WarningAction, WarningSeverity } from 'wallet/src/features/transactions/WarningModal/types' +import { useTokenSelectorActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenSelectorActionHandlers' +import { useTransactionGasWarning } from 'wallet/src/features/transactions/hooks/useTransactionGasWarning' +import { + initialState as emptyState, + transactionStateReducer, +} from 'wallet/src/features/transactions/transactionState/transactionState' +import { + CurrencyField, + TransactionState, +} from 'wallet/src/features/transactions/transactionState/types' +import { TransferReview } from 'wallet/src/features/transactions/transfer/TransferReview' +import { TransferTokenForm } from 'wallet/src/features/transactions/transfer/TransferTokenForm' +import { useDerivedTransferInfo } from 'wallet/src/features/transactions/transfer/hooks/useDerivedTransferInfo' +import { useOnSelectRecipient } from 'wallet/src/features/transactions/transfer/hooks/useOnSelectRecipient' +import { useOnToggleShowRecipientSelector } from 'wallet/src/features/transactions/transfer/hooks/useOnToggleShowRecipientSelector' +import { + useTransferERC20Callback, + useTransferNFTCallback, +} from 'wallet/src/features/transactions/transfer/hooks/useTransferCallback' +import { useTransferTransactionRequest } from 'wallet/src/features/transactions/transfer/hooks/useTransferTransactionRequest' +import { useTransferWarnings } from 'wallet/src/features/transactions/transfer/hooks/useTransferWarnings' +import { + DerivedTransferInfo, + TokenSelectorFlow, +} from 'wallet/src/features/transactions/transfer/types' +import { TransactionStep, TransferFlowProps } from 'wallet/src/features/transactions/types' +import { ANIMATE_SPRING_CONFIG } from 'wallet/src/features/transactions/utils' +import { ModalName, SectionName } from 'wallet/src/telemetry/constants' +import { currencyAddress } from 'wallet/src/utils/currencyId' + +interface TransferFormProps { + prefilledState?: TransactionState + onClose: () => void +} + +export function TransferFlow({ prefilledState, onClose }: TransferFormProps): JSX.Element { + const insets = useDeviceInsets() + const colors = useSporeColors() + const { t } = useTranslation() + const { fullWidth } = useDeviceDimensions() + const { isSheetReady } = useBottomSheetContext() + + const [state, dispatch] = useReducer(transactionStateReducer, prefilledState || emptyState) + const derivedTransferInfo = useDerivedTransferInfo(state) + const [showViewOnlyModal, setShowViewOnlyModal] = useState(false) + const [step, setStep] = useState(TransactionStep.FORM) + + const { isFiatInput, exactAmountToken, exactAmountFiat } = derivedTransferInfo + const { showRecipientSelector } = state + + const onSelectRecipient = useOnSelectRecipient(dispatch) + const onToggleShowRecipientSelector = useOnToggleShowRecipientSelector(dispatch) + + const txRequest = useTransferTransactionRequest(derivedTransferInfo) + const warnings = useTransferWarnings(t, derivedTransferInfo) + const gasFee = useTransactionGasFee( + txRequest, + GasSpeed.Urgent, + // stop polling for gas once transaction is submitted + step === TransactionStep.SUBMITTED || + warnings.some((warning) => warning.action === WarningAction.DisableReview) + ) + + const transferTxWithGasSettings = useMemo( + (): providers.TransactionRequest | undefined => + gasFee?.params ? { ...txRequest, ...gasFee.params } : txRequest, + [gasFee?.params, txRequest] + ) + + const gasWarning = useTransactionGasWarning({ + derivedInfo: derivedTransferInfo, + gasFee: gasFee?.value, + }) + + const allWarnings = useMemo(() => { + return !gasWarning ? warnings : [...warnings, gasWarning] + }, [warnings, gasWarning]) + + const { onSelectCurrency, onHideTokenSelector } = useTokenSelectorActionHandlers( + dispatch, + TokenSelectorFlow.Transfer + ) + + // optimization for not rendering InnerContent initially, + // when modal is opened with recipient or token selector presented + const [renderInnerContentRouter, setRenderInnerContentRouter] = useState(!showRecipientSelector) + useEffect(() => { + setRenderInnerContentRouter(renderInnerContentRouter || !showRecipientSelector) + }, [renderInnerContentRouter, showRecipientSelector]) + + const screenXOffset = useSharedValue(showRecipientSelector ? -fullWidth : 0) + useEffect(() => { + const screenOffset = showRecipientSelector ? 1 : 0 + screenXOffset.value = withSpring(-(fullWidth * screenOffset), ANIMATE_SPRING_CONFIG) + }, [screenXOffset, showRecipientSelector, fullWidth]) + + const wrapperStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: screenXOffset.value }], + })) + + const onFormNext = useCallback(() => setStep(TransactionStep.REVIEW), [setStep]) + const onReviewNext = useCallback(() => setStep(TransactionStep.SUBMITTED), [setStep]) + const onReviewPrev = useCallback(() => setStep(TransactionStep.FORM), [setStep]) + const onRetrySubmit = useCallback(() => setStep(TransactionStep.FORM), [setStep]) + + const exactValue = isFiatInput ? exactAmountFiat : exactAmountToken + + return ( + <> + + + + + {/* Padding bottom must have a similar size to the handlebar + height as 100% height doesn't include the handlebar height */} + + {step !== TransactionStep.SUBMITTED && ( + + )} + {renderInnerContentRouter && isSheetReady && ( + + )} + + + + {showRecipientSelector ? ( + + ) : null} + + + {showViewOnlyModal && ( + + } + modalName={ModalName.SwapWarning} + severity={WarningSeverity.Low} + title={t('send.warning.viewOnly.title')} + onClose={(): void => setShowViewOnlyModal(false)} + onConfirm={(): void => setShowViewOnlyModal(false)} + /> + )} + + + + {!!state.selectingCurrencyField && ( + + )} + + ) +} + +type TransferInnerContentProps = { + step: number + setStep: (step: TransactionStep) => void + showingSelectorScreen: boolean + gasFee: GasFeeResult + derivedTransferInfo: DerivedTransferInfo + onFormNext: () => void + onReviewNext: () => void + onReviewPrev: () => void + onRetrySubmit: () => void +} & Pick< + TransferFlowProps, + 'derivedInfo' | 'onClose' | 'dispatch' | 'gasFee' | 'txRequest' | 'warnings' | 'exactValue' +> + +function TransferInnerContent({ + showingSelectorScreen, + derivedTransferInfo, + onClose, + dispatch, + step, + gasFee, + txRequest, + warnings, + onFormNext, + onRetrySubmit, + onReviewNext, + onReviewPrev, +}: TransferInnerContentProps): JSX.Element | null { + // TODO: move this up in the tree to mobile specific flow + const { walletNeedsRestore, openWalletRestoreModal } = useWalletRestore() + const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = + useShouldShowNativeKeyboard() + + const { currencyAmounts, recipient, currencyInInfo, nftIn, chainId, txId } = derivedTransferInfo + const transferERC20Callback = useTransferERC20Callback( + txId, + chainId, + recipient, + currencyInInfo ? currencyAddress(currencyInInfo.currency) : undefined, + currencyAmounts[CurrencyField.INPUT]?.quotient.toString(), + txRequest, + onReviewNext + ) + const transferNFTCallback = useTransferNFTCallback( + txId, + chainId, + recipient, + nftIn?.nftContract?.address, + nftIn?.tokenId, + txRequest, + onReviewNext + ) + + const onTransfer = (): void => { + onFormNext() + nftIn ? transferNFTCallback?.() : transferERC20Callback?.() + } + + const { trigger: biometricAuthAndTransfer } = useBiometricPrompt(onTransfer) + const { requiredForTransactions: biometricRequired } = useBiometricAppSettings() + + const onReviewSubmit = async (): Promise => { + if (biometricRequired) { + await biometricAuthAndTransfer() + } else { + onTransfer() + } + } + + switch (step) { + case TransactionStep.SUBMITTED: + return ( + + + + ) + case TransactionStep.FORM: + return ( + + + + ) + case TransactionStep.REVIEW: + return ( + + + + ) + default: + return null + } +} diff --git a/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx new file mode 100644 index 0000000..8e1c46b --- /dev/null +++ b/apps/mobile/src/features/transactions/transfer/TransferHeader.tsx @@ -0,0 +1,89 @@ +import React, { Dispatch, SetStateAction } from 'react' +import { useTranslation } from 'react-i18next' +import { Flex, Text, TouchableArea, useSporeColors } from 'ui/src' +import EyeIcon from 'ui/src/assets/icons/eye.svg' +import { iconSizes } from 'ui/src/theme' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useTokenFormActionHandlers } from 'wallet/src/features/transactions/hooks/useTokenFormActionHandlers' +import { TransactionStep, TransferFlowProps } from 'wallet/src/features/transactions/types' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccountWithThrow } from 'wallet/src/features/wallet/hooks' + +type HeaderContentProps = Pick< + TransferFlowProps, + 'dispatch' | 'flowName' | 'step' | 'showFiatToggle' | 'isFiatInput' +> & { + setShowViewOnlyModal: Dispatch> +} + +export function TransferHeader({ + dispatch, + flowName, + step, + showFiatToggle, + isFiatInput, + setShowViewOnlyModal, +}: HeaderContentProps): JSX.Element { + const colors = useSporeColors() + const account = useActiveAccountWithThrow() + const { t } = useTranslation() + const { onToggleFiatInput } = useTokenFormActionHandlers(dispatch) + const currency = useAppFiatCurrencyInfo() + + const isViewOnlyWallet = account?.type === AccountType.Readonly + + return ( + + + {flowName} + + + {step === TransactionStep.FORM && showFiatToggle ? ( + onToggleFiatInput(!isFiatInput)}> + + + {currency.symbol} + + + {currency.code} + + + + ) : null} + {isViewOnlyWallet ? ( + setShowViewOnlyModal(true)}> + + + + {t('swap.header.viewOnly')} + + + + ) : null} + + + ) +} diff --git a/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx new file mode 100644 index 0000000..237d37c --- /dev/null +++ b/apps/mobile/src/features/transactions/transfer/TransferStatus.tsx @@ -0,0 +1,138 @@ +import React, { useCallback, useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { goBack } from 'src/app/navigation/rootNavigation' +import { TransactionPending } from 'src/features/transactions/TransactionPending/TransactionPending' +import { AppTFunction } from 'ui/src/i18n/types' +import { NumberType } from 'utilities/src/format/types' +import { FiatCurrencyInfo, useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { + LocalizationContextState, + useLocalizationContext, +} from 'wallet/src/features/language/LocalizationContext' +import { useSelectTransaction } from 'wallet/src/features/transactions/hooks' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { DerivedTransferInfo } from 'wallet/src/features/transactions/transfer/types' +import { + TransactionDetails, + TransactionStatus, + TransactionType, +} from 'wallet/src/features/transactions/types' +import { useActiveAccountAddressWithThrow, useDisplayName } from 'wallet/src/features/wallet/hooks' + +type TransferStatusProps = { + derivedTransferInfo: DerivedTransferInfo + onNext: () => void + onTryAgain: () => void +} + +const getTextFromTransferStatus = ( + t: AppTFunction, + formatter: LocalizationContextState, + fiatCurrencyInfo: FiatCurrencyInfo, + derivedTransferInfo: DerivedTransferInfo, + recipient: string | undefined, + transactionDetails?: TransactionDetails +): { + title: string + description: string +} => { + const { currencyInInfo, nftIn, currencyAmounts, isFiatInput, exactAmountFiat } = + derivedTransferInfo + if ( + !transactionDetails || + transactionDetails.typeInfo.type !== TransactionType.Send || + !recipient || + (!currencyInInfo && !nftIn) + ) { + // TODO: [MOB-240] should never go into this state but should probably do some + // error display here as well as log to sentry or amplitude + return { + title: t('send.status.inProgress.title'), + description: t('send.status.inProgress.description'), + } + } + const status = transactionDetails.status + if (status === TransactionStatus.Success) { + const formattedFiatValue = formatter.addFiatSymbolToNumber({ + value: exactAmountFiat, + currencyCode: fiatCurrencyInfo.code, + currencySymbol: fiatCurrencyInfo.symbol, + }) + return { + title: t('send.status.success.title'), + description: t('send.status.success.description', { + currencyAmount: nftIn + ? '' + : formatter.formatCurrencyAmount({ + value: currencyAmounts[CurrencyField.INPUT], + type: NumberType.TokenTx, + }), + fiatValue: isFiatInput ? ` (${formattedFiatValue})` : '', + tokenName: nftIn?.name ?? ` ${currencyInInfo?.currency.symbol}` ?? ' tokens', + recipient, + }), + } + } + + if (status === TransactionStatus.Failed) { + return { + title: t('send.status.failed.title'), + description: t('send.status.fail.description'), + } + } + + // TODO: [MOB-241] handle TransactionStatus.Unknown state + return { + title: t('send.status.inProgress.title'), + description: t('send.status.inProgress.description'), + } +} + +export function TransferStatus({ + derivedTransferInfo, + onNext, + onTryAgain, +}: TransferStatusProps): JSX.Element | null { + const { t } = useTranslation() + const formatter = useLocalizationContext() + const appFiatCurrencyInfo = useAppFiatCurrencyInfo() + const activeAddress = useActiveAccountAddressWithThrow() + + const { recipient, chainId, txId } = derivedTransferInfo + + const transaction = useSelectTransaction(activeAddress, chainId, txId) + + const displayName = useDisplayName(recipient, { includeUnitagSuffix: true }) + const recipientName = displayName?.name ?? recipient + const { title, description } = useMemo(() => { + return getTextFromTransferStatus( + t, + formatter, + appFiatCurrencyInfo, + derivedTransferInfo, + recipientName, + transaction + ) + }, [t, formatter, appFiatCurrencyInfo, derivedTransferInfo, recipientName, transaction]) + + const onClose = useCallback(() => { + onNext() + goBack() + }, [onNext]) + + if (!chainId) { + return null + } + + return ( + + ) +} diff --git a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx new file mode 100644 index 0000000..e50720c --- /dev/null +++ b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFlow.tsx @@ -0,0 +1,108 @@ +import { Dispatch, ReactNode, SetStateAction, useEffect, useMemo, useState } from 'react' +import { useAppSelector } from 'src/app/hooks' +import { selectModalState } from 'src/features/modals/selectModalState' +import { useOnCloseSendModal } from 'src/features/transactions/swap/hooks/useOnCloseSendModal' +import { getFocusOnCurrencyFieldFromInitialState } from 'src/features/transactions/swap/utils' +import { TransferFormScreen } from 'src/features/transactions/transfer/transferRewrite/TransferFormScreen' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { Trace } from 'utilities/src/telemetry/trace/Trace' +import { + SwapFormContextProvider, + SwapFormState, +} from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { + TransferScreen, + TransferScreenContextProvider, + useTransferScreenContext, +} from 'wallet/src/features/transactions/contexts/TransferScreenContext' +import { TransactionModal } from 'wallet/src/features/transactions/swap/TransactionModal' +import { ModalName, SectionName } from 'wallet/src/telemetry/constants' + +/** + * @todo: The screens within this flow are not implemented. + * MOB-555 https://linear.app/uniswap/issue/MOB-555/implement-updated-send-flow + */ +export function TransferFlow(): JSX.Element { + // We need this additional `screen` state outside of the `SwapScreenContext` because the `TransferContextProvider` needs to be inside the `BottomSheetModal`'s `Container`. + const [screen, setScreen] = useState(TransferScreen.TransferForm) + const fullscreen = screen === TransferScreen.TransferForm + const onClose = useOnCloseSendModal() + + const { walletNeedsRestore, openWalletRestoreModal } = useWalletRestore() + + return ( + + + + + + ) +} + +function CurrentScreen({ + screen, + setScreen, +}: { + screen: TransferScreen + setScreen: Dispatch> +}): JSX.Element { + const { screen: contextScreen, screenRef: contextScreenRef } = useTransferScreenContext() + + useEffect(() => { + setScreen(contextScreen) + }, [contextScreen, contextScreenRef, setScreen]) + + switch (screen) { + case TransferScreen.TransferForm: + return ( + + + + ) + default: + throw new Error(`Unknown screen: ${screen}`) + } +} + +function TransferFormScreenDelayedRender(): JSX.Element { + const [hideContent, setHideContent] = useState(true) + useEffect(() => { + setTimeout(() => setHideContent(false), 25) + }, []) + return +} + +function TransferContextsContainer({ children }: { children?: ReactNode }): JSX.Element { + const { initialState } = useAppSelector(selectModalState(ModalName.Send)) + + const prefilledState = useMemo( + (): SwapFormState | undefined => + initialState + ? { + customSlippageTolerance: initialState.customSlippageTolerance, + exactAmountFiat: initialState.exactAmountFiat, + exactAmountToken: initialState.exactAmountToken, + exactCurrencyField: initialState.exactCurrencyField, + focusOnCurrencyField: getFocusOnCurrencyFieldFromInitialState(initialState), + input: initialState.input ?? undefined, + output: initialState.output ?? undefined, + selectingCurrencyField: initialState.selectingCurrencyField, + txId: initialState.txId, + isFiatMode: false, + isSubmitting: false, + } + : undefined, + [initialState] + ) + + return ( + + {children} + + ) +} diff --git a/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFormScreen.tsx b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFormScreen.tsx new file mode 100644 index 0000000..17e9456 --- /dev/null +++ b/apps/mobile/src/features/transactions/transfer/transferRewrite/TransferFormScreen.tsx @@ -0,0 +1,24 @@ +import React from 'react' +import { Text } from 'ui/src' +import { useSwapFormContext } from 'wallet/src/features/transactions/contexts/SwapFormContext' +import { useTransactionModalContext } from 'wallet/src/features/transactions/contexts/TransactionModalContext' +import { TransactionModalInnerContainer } from 'wallet/src/features/transactions/swap/TransactionModal' + +export function TransferFormScreen({ hideContent }: { hideContent: boolean }): JSX.Element { + const { bottomSheetViewStyles } = useTransactionModalContext() + const { selectingCurrencyField } = useSwapFormContext() + + return ( + + {!hideContent && !!selectingCurrencyField && } + + TODO: transfer form content + + + ) +} + +function TokenSelector(): JSX.Element | null { + // TODO: implement. See `wallet/.../SwapTokenSelector.tsx` for reference. + return null +} diff --git a/apps/mobile/src/features/tweaks/selectors.ts b/apps/mobile/src/features/tweaks/selectors.ts new file mode 100644 index 0000000..1d506e7 --- /dev/null +++ b/apps/mobile/src/features/tweaks/selectors.ts @@ -0,0 +1,5 @@ +import { TweaksState } from 'src/features/tweaks/slice' +import { CustomEndpoint } from 'wallet/src/data/links' + +export const selectCustomEndpoint = (state: { tweaks: TweaksState }): CustomEndpoint | undefined => + state.tweaks.customEndpoint diff --git a/apps/mobile/src/features/tweaks/slice.ts b/apps/mobile/src/features/tweaks/slice.ts new file mode 100644 index 0000000..0a6bdd4 --- /dev/null +++ b/apps/mobile/src/features/tweaks/slice.ts @@ -0,0 +1,24 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { CustomEndpoint } from 'wallet/src/data/links' + +export interface TweaksState { + customEndpoint?: CustomEndpoint +} + +export const initialTweaksState: TweaksState = {} + +export const slice = createSlice({ + name: 'tweaks', + initialState: initialTweaksState, + reducers: { + setCustomEndpoint: ( + state, + { payload: { customEndpoint } }: PayloadAction<{ customEndpoint?: CustomEndpoint }> + ) => { + state.customEndpoint = customEndpoint + }, + }, +}) + +export const { setCustomEndpoint } = slice.actions +export const { reducer: tweaksReducer } = slice diff --git a/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx new file mode 100644 index 0000000..4a79acc --- /dev/null +++ b/apps/mobile/src/features/unitags/ChooseProfilePictureScreen.tsx @@ -0,0 +1,198 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ActivityIndicator } from 'react-native' +import { navigate } from 'src/app/navigation/rootNavigation' +import { UnitagEntryPoint, UnitagStackScreenProp } from 'src/app/navigation/types' +import { useAvatarSelectionHandler } from 'src/components/unitags/AvatarSelection' +import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' +import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { UnitagName } from 'src/features/unitags/UnitagName' +import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { Button, Flex, Icons, Text, useIsDarkMode, useSporeColors } from 'ui/src' +import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' +import { UnitagClaimSource } from 'uniswap/src/features/unitags/types' +import { ChainId } from 'wallet/src/constants/chains' +import { useENSName } from 'wallet/src/features/ens/api' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { useClaimUnitag } from 'wallet/src/features/unitags/hooks' +import { ElementName } from 'wallet/src/telemetry/constants' + +function convertEntryPointToAnalyticsSource(entryPoint: UnitagEntryPoint): UnitagClaimSource { + switch (entryPoint) { + case Screens.Home: + return 'home' + case Screens.Settings: + return 'settings' + case OnboardingScreens.Landing: + return 'onboarding' + default: + throw new Error(`unhandled entryPoint for ChooseProfilePictureScreen: ${entryPoint}`) + } +} + +export function ChooseProfilePictureScreen({ + route, +}: UnitagStackScreenProp): JSX.Element { + const { entryPoint, unitag, unitagFontSize, address } = route.params + + const { t } = useTranslation() + const colors = useSporeColors() + const { data: ensName } = useENSName(address, ChainId.Mainnet) + const claimUnitag = useClaimUnitag() + const isDarkMode = useIsDarkMode() + + const [imageUri, setImageUri] = useState() + const [showModal, setShowModal] = useState(false) + const [claimError, setClaimError] = useState() + const [isClaiming, setIsClaiming] = useState(false) + + const openModal = (): void => { + setShowModal(true) + } + + const onCloseModal = (): void => { + setShowModal(false) + } + + const { avatarSelectionHandler, hasNFTs } = useAvatarSelectionHandler({ + address, + avatarImageUri: imageUri, + setAvatarImageUri: setImageUri, + showModal: openModal, + }) + + const onPressContinue = async (): Promise => { + if (entryPoint === OnboardingScreens.Landing) { + // Handle case navigating from onboarding + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.WelcomeWallet, + params: { + importType: ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + unitagClaim: { + address, + username: unitag, + avatarUri: imageUri, + }, + }, + }) + } else { + return attemptClaimUnitag() + } + } + + const attemptClaimUnitag = async (): Promise => { + setIsClaiming(true) + const source = convertEntryPointToAnalyticsSource(entryPoint) + const { claimError: attemptClaimError } = await claimUnitag( + { + address, + username: unitag, + avatarUri: imageUri, + }, + { + source, + hasENSAddress: !!ensName, + } + ) + setIsClaiming(false) + setClaimError(attemptClaimError) + + // Navigate to confirmation screen when a claim has been made + if (attemptClaimError === undefined) { + navigate(Screens.UnitagStack, { + screen: UnitagScreens.UnitagConfirmation, + params: { + unitag, + address, + profilePictureUri: imageUri, + }, + }) + } + } + + return ( + + + + + + + + + + + + + + {!!claimError && ( + + {claimError} + + )} + + + {showModal && ( + + )} + + ) +} + +function ProfilePicture({ + address, + imageUri, +}: { + address: Maybe
+ imageUri?: string +}): JSX.Element { + if (address) { + return ( + + ) + } + return ( + + ) +} diff --git a/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx new file mode 100644 index 0000000..aeeb6a2 --- /dev/null +++ b/apps/mobile/src/features/unitags/ClaimUnitagScreen.tsx @@ -0,0 +1,504 @@ +/* eslint-disable max-lines */ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { ADDRESS_ZERO } from '@uniswap/v3-sdk' +import { default as React, useCallback, useEffect, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { ActivityIndicator, Keyboard } from 'react-native' +import { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated' +import { navigate } from 'src/app/navigation/rootNavigation' +import { UnitagStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { UnitagName } from 'src/features/unitags/UnitagName' +import { OnboardingScreens, Screens, UnitagScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { + AnimatePresence, + AnimatedFlex, + Button, + Flex, + Icons, + Image, + Text, + TouchableArea, + useSporeColors, +} from 'ui/src' +import { ENS_LOGO } from 'ui/src/assets' +import { fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { LearnMoreLink } from 'wallet/src/components/text/LearnMoreLink' +import { Pill } from 'wallet/src/components/text/Pill' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { + UNITAG_SUFFIX, + UNITAG_SUFFIX_NO_LEADING_DOT, + UNITAG_VALID_REGEX, +} from 'wallet/src/features/unitags/constants' +import { useCanClaimUnitagName } from 'wallet/src/features/unitags/hooks' +import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { ElementName, ModalName, UnitagEventName } from 'wallet/src/telemetry/constants' +import { shortenAddress } from 'wallet/src/utils/addresses' +import { useDynamicFontSizing } from 'wallet/src/utils/useDynamicFontSizing' + +const MAX_UNITAG_CHAR_LENGTH = 20 + +const MAX_INPUT_FONT_SIZE = 36 +const MIN_INPUT_FONT_SIZE = 22 +const MAX_CHAR_PIXEL_WIDTH = 20 + +const FIXED_INFO_PILL_WIDTH = 128 + +// Used in dynamic font size width calculation to ignore `.` characters +const UNITAG_SUFFIX_CHARS_ONLY = UNITAG_SUFFIX.replaceAll('.', '') + +// Accounts for height of image, gap between image and name, and spacing from top of titles +const UNITAG_NAME_ANIMATE_DISTANCE_Y = imageSizes.image100 + spacing.spacing48 + spacing.spacing24 + +type Props = NativeStackScreenProps + +export function ClaimUnitagScreen({ navigation, route }: Props): JSX.Element { + const { entryPoint, address } = route.params + + useAddBackButton(navigation) + const { t } = useTranslation() + const colors = useSporeColors() + + const inputPlaceholder = getYourNameString(t('unitags.claim.username.default')) + + // In onboarding flow, delete pending accounts and create account actions happen right before navigation + // So pendingAccountAddress must be fetched in this component and can't be passed in params + const pendingAccountAddress = Object.values(usePendingAccounts())?.[0]?.address + const unitagAddress = address || pendingAccountAddress + + const [showInfoModal, setShowInfoModal] = useState(false) + const [showClaimPeriodInfoModal, setShowClaimPeriodInfoModal] = useState(false) + + const [showTextInputView, setShowTextInputView] = useState(true) + const [unitagInputValue, setUnitagInputValue] = useState(undefined) + const [isCheckingUnitag, setIsCheckingUnitag] = useState(false) + const [shouldBlockContinue, setShouldBlockContinue] = useState(false) + const [unitagToCheck, setUnitagToCheck] = useState(undefined) + + const addressViewOpacity = useSharedValue(1) + const unitagInputContainerTranslateY = useSharedValue(0) + const addressViewAnimatedStyle = useAnimatedStyle(() => { + return { + opacity: addressViewOpacity.value, + } + }) + + const { + error: canClaimUnitagNameError, + loading: loadingUnitagErrorCheck, + requiresENSMatch, + } = useCanClaimUnitagName(unitagAddress, unitagToCheck) + + const { onLayout, fontSize, onSetFontSize } = useDynamicFontSizing( + MAX_CHAR_PIXEL_WIDTH, + MAX_INPUT_FONT_SIZE, + MIN_INPUT_FONT_SIZE + ) + + useEffect(() => { + const unsubscribe = navigation.addListener('focus', () => { + // Reset the Unitag to check + setUnitagToCheck(undefined) + + // When returning back to this screen, handle animating the Unitag logo out and text input in + if (showTextInputView) { + return + } + + unitagInputContainerTranslateY.value = withTiming( + unitagInputContainerTranslateY.value - UNITAG_NAME_ANIMATE_DISTANCE_Y, + { + duration: ONE_SECOND_MS / 2, + } + ) + setTimeout(() => { + setShowTextInputView(true) + addressViewOpacity.value = withTiming(1, { duration: ONE_SECOND_MS / 2 }) + }, ONE_SECOND_MS) + }) + + return unsubscribe + }, [ + navigation, + showTextInputView, + setShowTextInputView, + addressViewOpacity, + unitagInputContainerTranslateY, + ]) + + const onChangeTextInput = useCallback( + (text: string): void => { + setShouldBlockContinue(false) + + if (text.length > MAX_UNITAG_CHAR_LENGTH) { + return + } + + if (text.length === 0) { + onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY) + } else { + onSetFontSize(text + UNITAG_SUFFIX_CHARS_ONLY) + } + + setUnitagInputValue(text?.trim()) + }, + [inputPlaceholder, onSetFontSize] + ) + + const onPressAddressTooltip = (): void => { + Keyboard.dismiss() + setShowInfoModal(true) + } + + const onPressMaybeLater = (): void => { + sendWalletAnalyticsEvent(UnitagEventName.UnitagOnboardingActionTaken, { action: 'later' }) + // Navigate to next screen if in onboarding + navigate(Screens.OnboardingStack, { + screen: OnboardingScreens.WelcomeWallet, + params: { + importType: ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + }, + }) + } + + const navigateWithAnimation = useCallback( + (unitag: string) => { + if (!unitagAddress) { + const err = new Error('unitagAddress should always be defined') + logger.error(err, { + tags: { file: 'ClaimUnitagScreen', function: 'navigateWithAnimation' }, + }) + throw err + } + + // Log claim display and action taken + sendWalletAnalyticsEvent(UnitagEventName.UnitagClaimAvailabilityDisplayed, { + result: 'available', + }) + sendWalletAnalyticsEvent(UnitagEventName.UnitagOnboardingActionTaken, { action: 'select' }) + + // Animate the Unitag logo in and text input out + setShowTextInputView(false) + + const initialDelay = ONE_SECOND_MS + const translateYDuration = ONE_SECOND_MS / 2 + + addressViewOpacity.value = withTiming(0, { duration: ONE_SECOND_MS / 2 }) + // Intentionally delay 1s to allow enter/exit animations to finish + unitagInputContainerTranslateY.value = withDelay( + initialDelay, + withTiming(unitagInputContainerTranslateY.value + UNITAG_NAME_ANIMATE_DISTANCE_Y, { + duration: translateYDuration, + }) + ) + // Navigate to ChooseProfilePicture screen after initial delay + translation to allow animations to finish + setTimeout(() => { + navigate( + entryPoint === OnboardingScreens.Landing ? Screens.OnboardingStack : Screens.UnitagStack, + { + screen: UnitagScreens.ChooseProfilePicture, + params: { unitag, entryPoint, address: unitagAddress, unitagFontSize: fontSize }, + } + ) + }, initialDelay + translateYDuration) + }, + [addressViewOpacity, entryPoint, unitagAddress, unitagInputContainerTranslateY, fontSize] + ) + + // Handle when useUnitagError completes loading and returns a result after onPressContinue is called + useEffect(() => { + if (isCheckingUnitag && !!unitagToCheck && !loadingUnitagErrorCheck) { + setIsCheckingUnitag(false) + // If unitagError is defined, it's rendered in UI + if (!canClaimUnitagNameError) { + navigateWithAnimation(unitagToCheck) + } else { + sendWalletAnalyticsEvent(UnitagEventName.UnitagClaimAvailabilityDisplayed, { + result: requiresENSMatch ? 'restricted' : 'unavailable', + }) + setShouldBlockContinue(true) + } + } + }, [ + canClaimUnitagNameError, + loadingUnitagErrorCheck, + unitagToCheck, + isCheckingUnitag, + navigateWithAnimation, + requiresENSMatch, + ]) + + const onPressContinue = (): void => { + if (unitagInputValue !== unitagToCheck) { + setIsCheckingUnitag(true) + setUnitagToCheck(unitagInputValue) + } + } + + const onPressClaimPeriodLearnMore = (): void => { + Keyboard.dismiss() + setShowClaimPeriodInfoModal(true) + } + + const title = + entryPoint === Screens.Home + ? t('unitags.onboarding.claim.title.claim') + : t('unitags.onboarding.claim.title.choose') + + return ( + + { + onLayout(event) + onSetFontSize(inputPlaceholder + UNITAG_SUFFIX_CHARS_ONLY) + }}> + {/* Fixed text that animates in when TextInput is animated out */} + + {!showTextInputView && ( + + + + )} + + {showTextInputView && ( + + + + {UNITAG_SUFFIX} + + + )} + + + + + {shortenAddress(unitagAddress ?? ADDRESS_ZERO)} + + { + Keyboard.dismiss() + setShowInfoModal(true) + }}> + + + + {canClaimUnitagNameError && unitagToCheck === unitagInputValue && ( + + + {canClaimUnitagNameError}{' '} + {requiresENSMatch && ( + + ), + }} + i18nKey="unitags.onboarding.claimPeriod.link" + /> + )} + + + )} + + + {entryPoint === OnboardingScreens.Landing && ( + + + + {t('common.button.later')} + + + + )} + + + {showInfoModal && ( + setShowInfoModal(false)} /> + )} + {showClaimPeriodInfoModal && ( + setShowClaimPeriodInfoModal(false)} + /> + )} + + ) +} + +const InfoModal = ({ + unitagAddress, + onClose, +}: { + unitagAddress: string | undefined + onClose: () => void +}): JSX.Element => { + const colors = useSporeColors() + const { t } = useTranslation() + const usernamePlaceholder = getYourNameString(t('unitags.claim.username.default')) + + return ( + + + + + + + + {usernamePlaceholder} + + {UNITAG_SUFFIX} + + + + + } + modalName={ModalName.TooltipContent} + title={t('unitags.onboarding.info.title')} + onClose={onClose} + /> + ) +} + +const ClaimPeriodInfoModal = ({ + onClose, + username, +}: { + onClose: () => void + username: string +}): JSX.Element => { + const colors = useSporeColors() + const { t } = useTranslation() + + return ( + + } + modalName={ModalName.ENSClaimPeriod} + title={t('unitags.onboarding.claimPeriod.title')} + onClose={onClose}> + + + ) +} + +// Util to handle translations of `yourname` +// If translated string only contains valid Unitag characters, return it lowercased and without spaces +// Otherwise, return 'yourname' +const getYourNameString = (yourname: string): string => { + const noSpacesLowercase = yourname.replaceAll(' ', '').toLowerCase() + if (UNITAG_VALID_REGEX.test(noSpacesLowercase)) { + return noSpacesLowercase + } + return 'yourname' +} diff --git a/apps/mobile/src/features/unitags/ConfirmationElements.tsx b/apps/mobile/src/features/unitags/ConfirmationElements.tsx new file mode 100644 index 0000000..3bd8fb7 --- /dev/null +++ b/apps/mobile/src/features/unitags/ConfirmationElements.tsx @@ -0,0 +1,177 @@ +import { Flex, Image, Text, useSporeColors } from 'ui/src' +import { DAI_LOGO, ENS_LOGO, ETH_LOGO, FROGGY, OPENSEA_LOGO, USDC_LOGO } from 'ui/src/assets' +import HeartIcon from 'ui/src/assets/icons/heart.svg' +import SendIcon from 'ui/src/assets/icons/send-action.svg' +import { colors, iconSizes, imageSizes, opacify } from 'ui/src/theme' +import { Arrow } from 'wallet/src/components/icons/Arrow' + +export const FroggyElement = (): JSX.Element => { + return ( + + + + ) +} + +export const OpenseaElement = (): JSX.Element => { + return ( + + + + ) +} + +export const SwapElement = (): JSX.Element => { + const sporeColors = useSporeColors() + return ( + + + + + ETH + + + + + + + DAI + + + + ) +} + +export const ENSElement = (): JSX.Element => { + return ( + + + + ) +} + +export const ReceiveUSDCElement = (): JSX.Element => { + return ( + + + +100 + + + + ) +} + +export const SendElement = (): JSX.Element => { + return ( + + + + ) +} + +export const HeartElement = (): JSX.Element => { + return ( + + + + ) +} + +export const TextElement = ({ text }: { text: string }): JSX.Element => { + return ( + + + {text} + + + ) +} + +export const EmojiElement = ({ emoji }: { emoji: string }): JSX.Element => { + return ( + + + {emoji} + + + ) +} diff --git a/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx new file mode 100644 index 0000000..bac0bfd --- /dev/null +++ b/apps/mobile/src/features/unitags/EditUnitagProfileScreen.tsx @@ -0,0 +1,504 @@ +/* eslint-disable max-lines */ +import React, { useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard, KeyboardAvoidingView, StyleSheet } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { navigate } from 'src/app/navigation/rootNavigation' +import { UnitagStackScreenProp } from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { useAvatarSelectionHandler } from 'src/components/unitags/AvatarSelection' +import { ChangeUnitagModal } from 'src/components/unitags/ChangeUnitagModal' +import { ChoosePhotoOptionsModal } from 'src/components/unitags/ChoosePhotoOptionsModal' +import { DeleteUnitagModal } from 'src/components/unitags/DeleteUnitagModal' +import { UnitagProfilePicture } from 'src/components/unitags/UnitagProfilePicture' +import { HeaderRadial, solidHeaderProps } from 'src/features/externalProfile/ProfileHeader' +import { Screens, UnitagScreens } from 'src/screens/Screens' +import { + Button, + Flex, + Icons, + LinearGradient, + ScrollView, + Text, + getUniconV2Colors, + useIsDarkMode, + useSporeColors, + useUniconColors, +} from 'ui/src' +import { borderRadii, fonts, iconSizes, imageSizes, spacing } from 'ui/src/theme' +import { useUnitagUpdater } from 'uniswap/src/features/unitags/context' +import { ProfileMetadata } from 'uniswap/src/features/unitags/types' +import { isIOS } from 'uniswap/src/utils/platform' +import { logger } from 'utilities/src/logger/logger' +import { normalizeTwitterUsername } from 'utilities/src/primitives/string' +import { DisplayNameText } from 'wallet/src/components/accounts/DisplayNameText' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { ChainId } from 'wallet/src/constants/chains' +import { useENS } from 'wallet/src/features/ens/useENS' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { updateUnitagMetadata } from 'wallet/src/features/unitags/api' +import { tryUploadAvatar } from 'wallet/src/features/unitags/avatars' +import { + useAvatarUploadCredsWithRefresh, + useUnitagByAddress, +} from 'wallet/src/features/unitags/hooks' +import { useWalletSigners } from 'wallet/src/features/wallet/context' +import { useAccount } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { useAppDispatch } from 'wallet/src/state' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { UnitagEventName } from 'wallet/src/telemetry/constants' +import { shortenAddress } from 'wallet/src/utils/addresses' +import { useExtractedColors } from 'wallet/src/utils/colors' + +const BIO_TEXT_INPUT_LINES = 6 + +const isProfileMetadataEdited = ( + loading: boolean, + updatedMetadata: ProfileMetadata, + initialMetadata?: ProfileMetadata +): boolean => { + return ( + !loading && + (isFieldEdited(initialMetadata?.avatar, updatedMetadata.avatar) || + isFieldEdited(initialMetadata?.description, updatedMetadata.description) || + isFieldEdited(initialMetadata?.twitter, updatedMetadata.twitter)) + ) +} + +function isFieldEdited(a: string | undefined, b: string | undefined): boolean { + const aNonValue = a === undefined || a === '' + const bNonValue = b === undefined || b === '' + + if (aNonValue && bNonValue) { + return false + } else { + return a !== b + } +} + +export function EditUnitagProfileScreen({ + route, +}: UnitagStackScreenProp): JSX.Element { + const { address, unitag, entryPoint } = route.params + const { t } = useTranslation() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const dispatch = useAppDispatch() + const account = useAccount(address) + const signerManager = useWalletSigners() + + const { name: ensName } = useENS(ChainId.Mainnet, address) + const { triggerRefetchUnitags } = useUnitagUpdater() + const { unitag: retrievedUnitag, loading } = useUnitagByAddress(address) + const unitagMetadata = retrievedUnitag?.metadata + + const [showAvatarModal, setShowAvatarModal] = useState(false) + const [avatarImageUri, setAvatarImageUri] = useState() + const [bioInput, setBioInput] = useState() + const [twitterInput, setTwitterInput] = useState() + const [showDeleteUnitagModal, setShowDeleteUnitagModal] = useState(false) + const [showChangeUnitagModal, setShowChangeUnitagModal] = useState(false) + const [updateResponseLoading, setUpdateResponseLoading] = useState(false) + const { avatarUploadUrlResponse, avatarUploadUrlLoading } = useAvatarUploadCredsWithRefresh({ + unitag, + account, + signerManager, + }) + + const onSetTwitterInput = (input: string): void => { + const normalizedInput = normalizeTwitterUsername(input) + setTwitterInput(normalizedInput) + } + + const updatedMetadata: ProfileMetadata = { + ...(avatarImageUri ? { avatar: avatarImageUri } : {}), + description: bioInput, + twitter: twitterInput, + } + + const profileMetadataEdited = isProfileMetadataEdited( + updateResponseLoading, + updatedMetadata, + unitagMetadata + ) + + useEffect(() => { + // Only want to set values on first time unitag loads, when we have not yet made the PUT request + if (unitagMetadata) { + setAvatarImageUri(unitagMetadata.avatar) + setBioInput(unitagMetadata.description) + setTwitterInput(unitagMetadata.twitter) + setUpdateResponseLoading(false) + } + }, [unitagMetadata]) + + const { colors: avatarColors } = useExtractedColors(avatarImageUri) + + const uniconV1Colors = useUniconColors(address) + const { color: uniconV2Color } = getUniconV2Colors(address) + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + const uniconColors = isUniconsV2Enabled + ? { gradientStart: uniconV2Color, gradientEnd: uniconV2Color, glow: uniconV2Color } + : uniconV1Colors + + const uniconGradientStart = uniconColors.gradientStart + const uniconGradientEnd = uniconColors.gradientEnd + + // Wait for avatar, then render avatar extracted colors or unicon colors if no avatar + const fixedGradientColors = useMemo(() => { + if (avatarImageUri || (avatarImageUri && !avatarColors)) { + return [colors.surface1.val, colors.surface1.val] + } + if (avatarImageUri && avatarColors && avatarColors.base) { + return [avatarColors.base, avatarColors.base] + } + return [uniconGradientStart, uniconGradientEnd] + }, [avatarColors, avatarImageUri, uniconGradientEnd, uniconGradientStart, colors.surface1.val]) + + const openAvatarModal = (): void => { + Keyboard.dismiss() + setShowAvatarModal(true) + } + + const onCloseAvatarModal = (): void => { + setShowAvatarModal(false) + } + + const { avatarSelectionHandler, hasNFTs } = useAvatarSelectionHandler({ + address, + avatarImageUri, + setAvatarImageUri, + showModal: openAvatarModal, + }) + + const onPressSaveChanges = async (): Promise => { + Keyboard.dismiss() + + // Try to upload avatar or skip avatar upload if not needed + try { + const { success, skipped } = await tryUploadAvatar({ + avatarImageUri, + avatarUploadUrlResponse, + avatarUploadUrlLoading, + }) + + // Display error if avatar upload failed + if (!success) { + handleUpdateError() + return + } + + const uploadedNewAvatar = success && !skipped + await updateProfileMetadata(uploadedNewAvatar) + } catch (e) { + logger.error(e, { + tags: { file: 'EditUnitagProfileScreen', function: 'onPressSaveChanges' }, + }) + handleUpdateError() + } + } + + const updateProfileMetadata = async (uploadedNewAvatar: boolean): Promise => { + // If new avatar was uploaded, update metadata.avatar to be the S3 file location + const metadata = uploadedNewAvatar + ? { + ...updatedMetadata, + // Add Date.now() to the end to ensure the resulting URL is not cached by devices + avatar: avatarUploadUrlResponse?.avatarUrl + ? avatarUploadUrlResponse.avatarUrl + `?${Date.now()}` + : undefined, + } + : updatedMetadata + + setUpdateResponseLoading(true) + const { data: updateResponse } = await updateUnitagMetadata({ + username: unitag, + metadata, + clearAvatar: metadata.avatar === undefined, + account, + signerManager, + }) + + if (!updateResponse.success) { + handleUpdateError() + return + } + + // Log changed metadata + sendWalletAnalyticsEvent(UnitagEventName.UnitagMetadataUpdated, { + avatar: uploadedNewAvatar, + description: isFieldEdited(unitagMetadata?.description, updatedMetadata.description), + twitter: isFieldEdited(unitagMetadata?.twitter, updatedMetadata.twitter), + }) + + dispatch( + pushNotification({ + type: AppNotificationType.Success, + title: t('unitags.notification.profile.title'), + }) + ) + triggerRefetchUnitags() + if (uploadedNewAvatar) { + setAvatarImageUri(avatarUploadUrlResponse?.avatarUrl) + } + + // If entered from claim flow confirmation screen, navigate back to home on update success + if (entryPoint === UnitagScreens.UnitagConfirmation) { + navigate(Screens.Home) + } + } + + const handleUpdateError = (): void => { + setUpdateResponseLoading(false) + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: t('unitags.notification.profile.error'), + }) + ) + } + + const menuActions = useMemo(() => { + return [ + { title: t('unitags.profile.action.edit'), systemIcon: 'pencil' }, + { title: t('unitags.profile.action.delete'), systemIcon: 'trash', destructive: true }, + ] + }, [t]) + + return ( + + + { + Keyboard.dismiss() + // Emitted index based on order of menu action array + // Edit username + if (e.nativeEvent.index === 0) { + setShowChangeUnitagModal(true) + } + // Delete username + if (e.nativeEvent.index === 1) { + setShowDeleteUnitagModal(true) + } + }}> + + + + + ) : undefined + } + p="$spacing16" + onPressBack={ + // If entering from confirmation screen, back btn navigates to home + entryPoint === UnitagScreens.UnitagConfirmation + ? (): void => navigate(Screens.Home) + : undefined + }> + {t('settings.setting.wallet.action.editProfile')} + + + + + + + + + {avatarImageUri && avatarColors?.primary ? ( + + ) : null} + + + + + + + + + + + + + + + + + {shortenAddress(address)} + + + + + + + {t('unitags.profile.bio.label')} + + {!loading ? ( + + ) : null} + + + + {t('unitags.profile.links.twitter')} + + {!loading ? ( + + @ + + + ) : null} + + {ensName && ( + + + ENS + + + {ensName} + + + )} + + + + + + {showAvatarModal && ( + + )} + + {showDeleteUnitagModal && ( + setShowDeleteUnitagModal(false)} + /> + )} + {showChangeUnitagModal && ( + setShowChangeUnitagModal(false)} + /> + )} + + ) +} + +const styles = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, + expand: { + flexGrow: 1, + }, + headerGradient: { + borderRadius: borderRadii.rounded20, + flex: 1, + opacity: 0.2, + }, +}) diff --git a/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx new file mode 100644 index 0000000..2e8561a --- /dev/null +++ b/apps/mobile/src/features/unitags/UnitagConfirmationScreen.tsx @@ -0,0 +1,170 @@ +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { navigate } from 'src/app/navigation/rootNavigation' +import { UnitagStackScreenProp } from 'src/app/navigation/types' +import { AnimateInOrder } from 'src/components/animation/AnimateInOrder' +import { Screen } from 'src/components/layout/Screen' +import { UnitagWithProfilePicture } from 'src/components/unitags/UnitagWithProfilePicture' +import { + EmojiElement, + ENSElement, + FroggyElement, + HeartElement, + OpenseaElement, + ReceiveUSDCElement, + SendElement, + SwapElement, + TextElement, +} from 'src/features/unitags/ConfirmationElements' +import { Screens, UnitagScreens } from 'src/screens/Screens' +import { AnimatePresence, Button, Flex, Text, useDeviceDimensions, useDeviceInsets } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { UNITAG_SUFFIX } from 'wallet/src/features/unitags/constants' + +export function UnitagConfirmationScreen({ + route, +}: UnitagStackScreenProp): JSX.Element { + const { unitag, address, profilePictureUri } = route.params + const dimensions = useDeviceDimensions() + const insets = useDeviceInsets() + const { t } = useTranslation() + + const boxWidth = dimensions.fullWidth - insets.left - insets.right - spacing.spacing32 + + const onPressCustomize = (): void => { + navigate(Screens.UnitagStack, { + screen: UnitagScreens.EditProfile, + params: { + address, + unitag, + entryPoint: UnitagScreens.UnitagConfirmation, + }, + }) + } + + const onPressDone = (): void => { + navigate(Screens.Home) + } + + const elementsToAnimate = useMemo( + () => [ + { element: , coordinates: { x: 5, y: 0 } }, + { element: , coordinates: { x: 10, y: 2 } }, + { element: , coordinates: { x: 8.2, y: 4 } }, + { element: , coordinates: { x: 9, y: 7 } }, + { element: , coordinates: { x: 10, y: 10 } }, + { element: , coordinates: { x: 1, y: 8.5 } }, + { + element: , + coordinates: { x: 0, y: 5 }, + }, + { element: , coordinates: { x: 1, y: 2 } }, + { element: , coordinates: { x: 3.5, y: 2.5 } }, + ], + [t] + ) + + return ( + + + + + + + + + + + {elementsToAnimate.map(({ element, coordinates }, index) => ( + + {element} + + ))} + + + + + + + + {t('unitags.claim.confirmation.success.long')} + + + {t('unitags.claim.confirmation.description', { + unitagAddress: `${unitag}${UNITAG_SUFFIX}`, + })} + + + + + + + + + ) +} + +// Calculates top and left insets for absolute positioned element based +// on a 10x10 coordinate system where top left is 0,0. +const getInsetPropsForCoordinates = ( + boxWidth: number, + x: number, + y: number +): { top?: number; right?: number; bottom?: number; left?: number } => { + const unitSize = 10 + const unit = boxWidth / unitSize + + let top + let bottom + let left + let right + + if (x < unitSize / 2) { + left = x * unit + } else if (x > unitSize / 2) { + right = (unitSize - x) * unit + } + + if (y < unitSize / 2) { + top = y * unit + } else if (y > unitSize / 2) { + bottom = (unitSize - y) * unit + } + + return { top, right, bottom, left } +} diff --git a/apps/mobile/src/features/unitags/UnitagName.tsx b/apps/mobile/src/features/unitags/UnitagName.tsx new file mode 100644 index 0000000..e75ba87 --- /dev/null +++ b/apps/mobile/src/features/unitags/UnitagName.tsx @@ -0,0 +1,44 @@ +import { Flex, Icons, Text } from 'ui/src' +import { fonts, spacing } from 'ui/src/theme' + +export function UnitagName({ + name, + fontSize, + opacity = 1, + animateIcon = false, +}: { + name?: string + fontSize: number + opacity?: number + animateIcon?: boolean +}): JSX.Element { + return ( + + + {name} + + + + + + ) +} diff --git a/apps/mobile/src/features/wallet/hooks.ts b/apps/mobile/src/features/wallet/hooks.ts new file mode 100644 index 0000000..a00f300 --- /dev/null +++ b/apps/mobile/src/features/wallet/hooks.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useState } from 'react' +import { useAppSelector } from 'src/app/hooks' +import { openModal } from 'src/features/modals/modalSlice' +import { selectModalState } from 'src/features/modals/selectModalState' +import { logger } from 'utilities/src/logger/logger' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' +import { useAppDispatch } from 'wallet/src/state' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function useWalletRestore(params?: { openModalImmediately?: boolean }): { + walletNeedsRestore: undefined | boolean + openWalletRestoreModal: () => void + isModalOpen: boolean +} { + const dispatch = useAppDispatch() + const openModalImmediately = params?.openModalImmediately + // Means that no private key found for mnemonic wallets + const [walletNeedsRestore, setWalletNeedsRestore] = useState(false) + const hasImportedSeedPhrase = useNativeAccountExists() + const isRestoreWalletEnabled = useFeatureFlag(FEATURE_FLAGS.RestoreWallet) + + const openWalletRestoreModal = useCallback((): void => { + dispatch(openModal({ name: ModalName.RestoreWallet })) + }, [dispatch]) + + useEffect(() => { + if (!hasImportedSeedPhrase || !isRestoreWalletEnabled) { + return + } + + const openRestoreWalletModalIfNeeded = async (): Promise => { + const addresses = await Keyring.getAddressesForStoredPrivateKeys() + setWalletNeedsRestore(hasImportedSeedPhrase && !addresses.length) + } + openRestoreWalletModalIfNeeded().catch((error) => + logger.error(error, { tags: { file: 'wallet/hooks', function: 'useWalletRestore' } }) + ) + }, [dispatch, hasImportedSeedPhrase, isRestoreWalletEnabled]) + + useEffect(() => { + if (openModalImmediately && walletNeedsRestore) { + openWalletRestoreModal() + } + }, [openModalImmediately, openWalletRestoreModal, walletNeedsRestore]) + + const isModalOpen = useAppSelector(selectModalState(ModalName.RestoreWallet)).isOpen + + return { walletNeedsRestore, openWalletRestoreModal, isModalOpen } +} diff --git a/apps/mobile/src/features/wallet/saga.ts b/apps/mobile/src/features/wallet/saga.ts new file mode 100644 index 0000000..7a1148e --- /dev/null +++ b/apps/mobile/src/features/wallet/saga.ts @@ -0,0 +1,25 @@ +import { StackActions } from '@react-navigation/core' +import { dispatchNavigationAction } from 'src/app/navigation/rootNavigation' +import { Screens } from 'src/screens/Screens' +import { call, put, takeEvery } from 'typed-redux-saga' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { restoreMnemonicComplete } from 'wallet/src/features/wallet/slice' +import i18n from 'wallet/src/i18n/i18n' + +/** + * Watch when we've restored a mnemonic (new phone migration) + */ +export function* restoreMnemonicCompleteWatcher() { + yield* takeEvery(restoreMnemonicComplete, onRestoreMnemonicComplete) +} + +function* onRestoreMnemonicComplete() { + yield* put( + pushNotification({ + type: AppNotificationType.Success, + title: i18n.t('notification.restore.success'), + }) + ) + yield* call(dispatchNavigationAction, StackActions.replace(Screens.Home)) +} diff --git a/apps/mobile/src/features/walletConnect/WalletConnect.ts b/apps/mobile/src/features/walletConnect/WalletConnect.ts new file mode 100644 index 0000000..d0c37a0 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/WalletConnect.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +import { NativeModules } from 'react-native' +import { isAndroid } from 'uniswap/src/utils/platform' + +const { RNWalletConnect } = NativeModules + +export const returnToPreviousApp = (): boolean => { + // TOOD(MOB-1680): Implement return to previous app for Android + if (isAndroid) { + return false + } + return RNWalletConnect.returnToPreviousApp() +} diff --git a/apps/mobile/src/features/walletConnect/api.ts b/apps/mobile/src/features/walletConnect/api.ts new file mode 100644 index 0000000..14d4857 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/api.ts @@ -0,0 +1,45 @@ +import { getOneSignalPushToken } from 'src/features/notifications/Onesignal' +import { config } from 'uniswap/src/config' +import { isAndroid } from 'uniswap/src/utils/platform' +import { isJestRun } from 'utilities/src/environment' +import { logger } from 'utilities/src/logger/logger' + +const WC_HOSTED_PUSH_SERVER_URL = `https://echo.walletconnect.com/${config.walletConnectProjectId}` + +/** + * Registers client and device push token with hosted WalletConnect 2.0 Echo Server. + * The echo server listens to incoming signing requests and delivers push notifications via APNS. + * See https://docs.walletconnect.com/2.0/specs/servers/echo/spec + * + * @param clientId WalletConnect 2.0 clientId + */ +export async function registerWCClientForPushNotifications(clientId: string): Promise { + try { + const pushToken = await getOneSignalPushToken() + if (!pushToken) { + return + } + + // WC requests we use the `fcm` push server for Android and the `apns` server for prod iOS + // and `apns-sandbox` for dev iOS + const pushServer = isAndroid ? 'fcm' : __DEV__ ? 'apns-sandbox' : 'apns' + const request = { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + client_id: clientId, + type: pushServer, + token: pushToken, + }), + } + + await fetch(`${WC_HOSTED_PUSH_SERVER_URL}/clients`, request) + } catch (error) { + // Shouldn't log if this is a jest run to avoid logging after a test completes + if (!isJestRun) { + logger.error(error, { + tags: { file: 'walletConnectApi', function: 'registerWCv2ClientForPushNotifications' }, + }) + } + } +} diff --git a/apps/mobile/src/features/walletConnect/saga.ts b/apps/mobile/src/features/walletConnect/saga.ts new file mode 100644 index 0000000..a65e5b9 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/saga.ts @@ -0,0 +1,373 @@ +import { AnyAction } from '@reduxjs/toolkit' +import { Core } from '@walletconnect/core' +import '@walletconnect/react-native-compat' +import { PendingRequestTypes, ProposalTypes } from '@walletconnect/types' +import { buildApprovedNamespaces, getSdkError } from '@walletconnect/utils' +import { IWeb3Wallet, Web3Wallet, Web3WalletTypes } from '@walletconnect/web3wallet' +import { Alert } from 'react-native' +import { EventChannel, eventChannel } from 'redux-saga' +import { appSelect } from 'src/app/hooks' +import { registerWCClientForPushNotifications } from 'src/features/walletConnect/api' +import { + getAccountAddressFromEIP155String, + getChainIdFromEIP155String, + getSupportedWalletConnectChains, + parseSignRequest, + parseTransactionRequest, +} from 'src/features/walletConnect/utils' +import { + addPendingSession, + addRequest, + addSession, + removeSession, + setHasPendingSessionError, +} from 'src/features/walletConnect/walletConnectSlice' +import { call, fork, put, take } from 'typed-redux-saga' +import { config } from 'uniswap/src/config' +import { logger } from 'utilities/src/logger/logger' +import { ALL_SUPPORTED_CHAIN_IDS, CHAIN_INFO, ChainId } from 'wallet/src/constants/chains' +import { selectAccounts, selectActiveAccountAddress } from 'wallet/src/features/wallet/selectors' +import { EthEvent, EthMethod } from 'wallet/src/features/walletConnect/types' +import i18n from 'wallet/src/i18n/i18n' + +export let wcWeb3Wallet: IWeb3Wallet + +let wcWeb3WalletReadyResolve: () => void +let wcWeb3WalletReadyReject: (e: unknown) => void +const wcWeb3WalletReady = new Promise((resolve, reject) => { + wcWeb3WalletReadyResolve = resolve + wcWeb3WalletReadyReject = reject +}) + +export const waitForWcWeb3WalletIsReady = () => wcWeb3WalletReady + +export async function initializeWeb3Wallet(): Promise { + try { + const wcCore = new Core({ + projectId: config.walletConnectProjectId, + }) + + wcWeb3Wallet = await Web3Wallet.init({ + core: wcCore, + metadata: { + name: 'Uniswap Wallet', + description: + 'Built by the most trusted team in DeFi, Uniswap Wallet allows you to maintain full custody and control of your assets.', + url: 'https://uniswap.org/app', + icons: ['https://gateway.pinata.cloud/ipfs/QmR1hYqhDMoyvJtwrQ6f1kVyfEKyK65XH3nbCimXBMkHJg'], + }, + }) + + const clientId = await wcCore.crypto.getClientId() + await registerWCClientForPushNotifications(clientId) + wcWeb3WalletReadyResolve?.() + } catch (e) { + wcWeb3WalletReadyReject(e) + } +} + +function createWalletConnectChannel(): EventChannel { + return eventChannel((emit) => { + /* + * Handle incoming `session_proposal` events that contain the dapp attempting to pair + * and the proposal namespaces (chains, methods, events) + */ + const sessionProposalHandler = async ( + proposalEvent: Omit, 'topic'> + ): Promise => { + const { params: proposal } = proposalEvent + emit({ type: 'session_proposal', proposal }) + } + + const sessionRequestHandler = async ( + request: Web3WalletTypes.SessionRequest + ): Promise => { + emit({ type: 'session_request', request }) + } + + const sessionDeleteHandler = async (session: Web3WalletTypes.SessionDelete): Promise => { + emit({ type: 'session_delete', session }) + } + + wcWeb3Wallet.on('session_proposal', sessionProposalHandler) + wcWeb3Wallet.on('session_request', sessionRequestHandler) + wcWeb3Wallet.on('session_delete', sessionDeleteHandler) + + const unsubscribe = (): void => { + wcWeb3Wallet.off('session_proposal', sessionProposalHandler) + wcWeb3Wallet.off('session_request', sessionRequestHandler) + wcWeb3Wallet.off('session_delete', sessionDeleteHandler) + } + + return unsubscribe + }) +} + +function* watchWalletConnectEvents() { + const wcChannel = yield* call(createWalletConnectChannel) + + while (true) { + try { + const event = yield* take(wcChannel) + if (event.type === 'session_proposal') { + yield* call(handleSessionProposal, event.proposal) + } else if (event.type === 'session_request') { + yield* call(handleSessionRequest, event.request) + } else if (event.type === 'session_delete') { + yield* call(handleSessionDelete, event.session) + } + } catch (error) { + logger.error(error, { + tags: { file: 'walletConnect/saga', function: 'watchWalletConnectEvents' }, + }) + } + } +} + +function showAlert(title: string, message: string): Promise { + return new Promise((resolve) => { + Alert.alert(title, message, [ + { + text: 'OK', + onPress: () => resolve(true), + }, + ]) + }) +} + +function* handleSessionProposal(proposal: ProposalTypes.Struct) { + const activeAccountAddress = yield* appSelect(selectActiveAccountAddress) + + const { + id, + proposer: { metadata: dapp }, + } = proposal + + try { + const supportedEip155Chains = ALL_SUPPORTED_CHAIN_IDS.map((chainId) => `eip155:${chainId}`) + const accounts = supportedEip155Chains.map((chain) => `${chain}:${activeAccountAddress}`) + + const namespaces = buildApprovedNamespaces({ + proposal, + supportedNamespaces: { + eip155: { + chains: supportedEip155Chains, + methods: [ + EthMethod.EthSign, + EthMethod.EthSendTransaction, + EthMethod.PersonalSign, + EthMethod.SignTypedData, + EthMethod.SignTypedDataV4, + ], + events: [EthEvent.AccountsChanged, EthEvent.ChainChanged], + accounts, + }, + }, + }) + + // Extract chains from approved namespaces to show in UI for pending session + const proposalChainIds: ChainId[] = [] + Object.entries(namespaces).forEach(([key, namespace]) => { + const { chains } = namespace + // EVM chain(s) are specified in either `eip155:CHAIN` or chains array + const eip155Chains = key.includes(':') ? [key] : chains + proposalChainIds.push(...(getSupportedWalletConnectChains(eip155Chains) ?? [])) + }) + + yield* put( + addPendingSession({ + wcSession: { + id: id.toString(), + proposalNamespaces: namespaces, + chains: proposalChainIds, + dapp: { + name: dapp.name, + url: dapp.url, + icon: dapp.icons[0] ?? null, + source: 'walletconnect', + }, + }, + }) + ) + } catch (e) { + // Reject pending session if required namespaces includes non-EVM chains or unsupported EVM chains + yield* call([wcWeb3Wallet, wcWeb3Wallet.rejectSession], { + id: proposal.id, + reason: getSdkError('UNSUPPORTED_CHAINS'), + }) + + const chainLabels = ALL_SUPPORTED_CHAIN_IDS.map((chainId) => CHAIN_INFO[chainId].label).join( + ', ' + ) + + const confirmed = yield* call( + showAlert, + i18n.t('walletConnect.error.connection.title'), + i18n.t('walletConnect.error.connection.message', { + chainNames: chainLabels, + dappName: dapp.name, + }) + ) + if (confirmed) { + yield* put(setHasPendingSessionError(false)) + } + + // Set error state to cancel loading state in WalletConnectModal UI + yield* put(setHasPendingSessionError(true)) + + logger.debug( + 'WalletConnectSaga', + 'sessionProposalHandler', + 'Rejected session proposal due to invalid proposal namespaces: ', + e + ) + } +} + +function* handleSessionRequest(sessionRequest: PendingRequestTypes.Struct) { + const { topic, params, id } = sessionRequest + const { request: wcRequest, chainId: wcChainId } = params + const { method, params: requestParams } = wcRequest + + const chainId = getChainIdFromEIP155String(wcChainId) + const requestSession = wcWeb3Wallet.engine.signClient.session.get(topic) + const dapp = requestSession.peer.metadata + + if (!chainId) { + throw new Error('WalletConnect 2.0 session request has invalid chainId') + } + + switch (method) { + case EthMethod.EthSign: + case EthMethod.PersonalSign: + case EthMethod.SignTypedData: + case EthMethod.SignTypedDataV4: { + const { account, request } = parseSignRequest(method, topic, id, chainId, dapp, requestParams) + yield* put( + addRequest({ + account, + request, + }) + ) + + break + } + case EthMethod.EthSendTransaction: { + const { account, request } = parseTransactionRequest( + method, + topic, + id, + chainId, + dapp, + requestParams + ) + yield* put( + addRequest({ + account, + request, + }) + ) + + break + } + default: + // Reject request for an invalid method + logger.warn( + 'WalletConnectSaga', + 'sessionRequestHandler', + `Session request method is unsupported: ${method}` + ) + yield* call([wcWeb3Wallet, wcWeb3Wallet.respondSessionRequest], { + topic, + response: { + id, + jsonrpc: '2.0', + error: getSdkError('WC_METHOD_UNSUPPORTED'), + }, + }) + } +} + +function* handleSessionDelete(event: Web3WalletTypes.SessionDelete) { + const { topic } = event + + yield* put(removeSession({ sessionId: topic })) +} + +function* populateActiveSessions() { + // Fetch all active sessions and add to store + const sessions = wcWeb3Wallet.getActiveSessions() + + const accounts = yield* appSelect(selectAccounts) + + for (const session of Object.values(sessions)) { + // Get account address connected to the session from first namespace + const namespaces = Object.values(session.namespaces) + const eip155Account = namespaces[0]?.accounts[0] + if (!eip155Account) { + continue + } + + const accountAddress = getAccountAddressFromEIP155String(eip155Account) + + if (!accountAddress) { + continue + } + + // Verify account address for session exists in wallet's accounts + const matchingAccount = Object.values(accounts).find( + (account) => account.address.toLowerCase() === accountAddress.toLowerCase() + ) + if (!matchingAccount) { + continue + } + + // Get all chains for session namespaces, supporting `eip155:CHAIN_ID` and `eip155` namespace formats + const chains: ChainId[] = [] + Object.entries(session.namespaces).forEach(([key, namespace]) => { + const eip155Chains = key.includes(':') ? [key] : namespace.chains + chains.push(...(getSupportedWalletConnectChains(eip155Chains) ?? [])) + }) + + yield* put( + addSession({ + wcSession: { + id: session.topic, + dapp: { + name: session.peer.metadata.name, + url: session.peer.metadata.url, + icon: session.peer.metadata.icons[0] ?? null, + source: 'walletconnect', + }, + chains, + namespaces: session.namespaces, + }, + account: accountAddress, + }) + ) + } +} + +// Load any existing pending session proposals from the WC connection +function* fetchPendingSessionProposals() { + const pendingSessionProposals = wcWeb3Wallet.getPendingSessionProposals() + for (const proposal of Object.values(pendingSessionProposals)) { + yield* call(handleSessionProposal, proposal) + } +} + +// Load any existing pending session requests from the WC connection +function* fetchPendingSessionRequests() { + const pendingSessionRequests = wcWeb3Wallet.getPendingSessionRequests() + for (const sessionRequest of Object.values(pendingSessionRequests)) { + yield* call(handleSessionRequest, sessionRequest) + } +} + +export function* walletConnectSaga() { + yield* call(initializeWeb3Wallet) + yield* call(populateActiveSessions) + yield* fork(fetchPendingSessionProposals) + yield* fork(fetchPendingSessionRequests) + yield* fork(watchWalletConnectEvents) +} diff --git a/apps/mobile/src/features/walletConnect/selectors.ts b/apps/mobile/src/features/walletConnect/selectors.ts new file mode 100644 index 0000000..e06d676 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/selectors.ts @@ -0,0 +1,60 @@ +import { createSelector, Selector } from '@reduxjs/toolkit' +import { MobileState } from 'src/app/reducer' +import { + WalletConnectPendingSession, + WalletConnectRequest, + WalletConnectSession, +} from 'src/features/walletConnect/walletConnectSlice' + +export const selectSessions = + (address: Maybe) => + (state: MobileState): WalletConnectSession[] | undefined => { + if (!address) { + return + } + + const wcAccount = state.walletConnect.byAccount[address] + if (!wcAccount) { + return + } + + return Object.values(wcAccount.sessions) + } + +export const makeSelectSessions = (): Selector< + MobileState, + WalletConnectSession[] | undefined, + [Maybe
] +> => + createSelector( + (state: MobileState) => state.walletConnect.byAccount, + (_: MobileState, address: Maybe
) => address, + (sessionsByAccount, address) => { + if (!address) { + return + } + + const wcAccount = sessionsByAccount[address] + if (!wcAccount) { + return + } + + return Object.values(wcAccount.sessions) + } + ) + +export const selectPendingRequests = (state: MobileState): WalletConnectRequest[] => { + return state.walletConnect.pendingRequests +} + +export const selectPendingSession = (state: MobileState): WalletConnectPendingSession | null => { + return state.walletConnect.pendingSession +} + +export const selectDidOpenFromDeepLink = (state: MobileState): boolean => { + return state.walletConnect.didOpenFromDeepLink ?? false +} + +export const selectHasPendingSessionError = (state: MobileState): boolean => { + return state.walletConnect.hasPendingSessionError ?? false +} diff --git a/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts new file mode 100644 index 0000000..b1bbd48 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/signWcRequestSaga.ts @@ -0,0 +1,122 @@ +import { providers } from 'ethers' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { call, put } from 'typed-redux-saga' +import { logger } from 'utilities/src/logger/logger' +import { ChainId } from 'wallet/src/constants/chains' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { + SendTransactionParams, + sendTransaction, +} from 'wallet/src/features/transactions/sendTransactionSaga' +import { TransactionType } from 'wallet/src/features/transactions/types' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { getSignerManager } from 'wallet/src/features/wallet/context' +import { signMessage, signTypedDataMessage } from 'wallet/src/features/wallet/signing/signing' +import { + DappInfo, + EthMethod, + EthSignMethod, + WalletConnectEvent, +} from 'wallet/src/features/walletConnect/types' +import { createSaga } from 'wallet/src/utils/saga' + +type SignMessageParams = { + sessionId: string + requestInternalId: string + message: string + account: Account + method: EthSignMethod + dapp: DappInfo + chainId: ChainId +} + +type SignTransactionParams = { + sessionId: string + requestInternalId: string + transaction: providers.TransactionRequest + account: Account + method: EthMethod.EthSendTransaction + dapp: DappInfo + chainId: ChainId +} + +export function* signWcRequest(params: SignMessageParams | SignTransactionParams) { + const { sessionId, requestInternalId, account, method, chainId } = params + try { + const signerManager = yield* call(getSignerManager) + let signature = '' + if (method === EthMethod.PersonalSign || method === EthMethod.EthSign) { + signature = yield* call(signMessage, params.message, account, signerManager) + } else if (method === EthMethod.SignTypedData || method === EthMethod.SignTypedDataV4) { + signature = yield* call(signTypedDataMessage, params.message, account, signerManager) + } else if (method === EthMethod.EthSendTransaction) { + const txParams: SendTransactionParams = { + chainId: params.transaction.chainId || ChainId.Mainnet, + account, + options: { + request: params.transaction, + }, + typeInfo: { + type: TransactionType.WCConfirm, + dapp: params.dapp, + }, + } + const { transactionResponse } = yield* call(sendTransaction, txParams) + signature = transactionResponse.hash + } + + if (params.dapp.source === 'walletconnect') { + yield* call(wcWeb3Wallet.respondSessionRequest, { + topic: sessionId, + response: { + id: Number(requestInternalId), + jsonrpc: '2.0', + result: signature, + }, + }) + } else if (params.dapp.source === 'uwulink' && params.dapp.webhook) { + fetch(params.dapp.webhook, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ method: 'eth_sendTransaction', response: signature }), + // TODO: consider adding analytics to track UwuLink usage + }).catch((error) => + logger.error(error, { + tags: { file: 'walletConnect/saga', function: 'signWcRequest/uwulink' }, + }) + ) + } + } catch (error) { + if (params.dapp.source === 'walletconnect') { + yield* call(wcWeb3Wallet.respondSessionRequest, { + topic: sessionId, + response: { + id: Number(requestInternalId), + jsonrpc: '2.0', + error: { code: 5000, message: `Signing error: ${error}` }, + }, + }) + } + + yield* put( + pushNotification({ + type: AppNotificationType.WalletConnect, + event: WalletConnectEvent.TransactionFailed, + dappName: params.dapp.name, + imageUrl: params.dapp.icon, + chainId, + address: account.address, + }) + ) + logger.error(error, { tags: { file: 'walletConnect/saga', function: 'signWcRequest' } }) + } +} + +export const { wrappedSaga: signWcRequestSaga, actions: signWcRequestActions } = createSaga( + signWcRequest, + 'signWalletConnect' +) diff --git a/apps/mobile/src/features/walletConnect/useWalletConnect.ts b/apps/mobile/src/features/walletConnect/useWalletConnect.ts new file mode 100644 index 0000000..1deb339 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/useWalletConnect.ts @@ -0,0 +1,36 @@ +import { useMemo } from 'react' +import { useAppSelector } from 'src/app/hooks' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import { AppModalState } from 'src/features/modals/ModalsState' +import { selectModalState } from 'src/features/modals/selectModalState' +import { + makeSelectSessions, + selectHasPendingSessionError, + selectPendingRequests, + selectPendingSession, +} from 'src/features/walletConnect/selectors' +import { + WalletConnectPendingSession, + WalletConnectRequest, + WalletConnectSession, +} from 'src/features/walletConnect/walletConnectSlice' +import { ModalName } from 'wallet/src/telemetry/constants' + +interface WalletConnect { + sessions: WalletConnectSession[] + pendingRequests: WalletConnectRequest[] + modalState: AppModalState + pendingSession: WalletConnectPendingSession | null + hasPendingSessionError: boolean +} + +export function useWalletConnect(address: Maybe): WalletConnect { + const selectSessions = useMemo(() => makeSelectSessions(), []) + const sessions = useAppSelector((state) => selectSessions(state, address)) ?? [] + const pendingRequests = useAppSelector(selectPendingRequests) + const modalState = useAppSelector(selectModalState(ModalName.WalletConnectScan)) + const pendingSession = useAppSelector(selectPendingSession) + const hasPendingSessionError = useAppSelector(selectHasPendingSessionError) + + return { sessions, pendingRequests, modalState, pendingSession, hasPendingSessionError } +} diff --git a/apps/mobile/src/features/walletConnect/utils.test.ts b/apps/mobile/src/features/walletConnect/utils.test.ts new file mode 100644 index 0000000..7ddd24a --- /dev/null +++ b/apps/mobile/src/features/walletConnect/utils.test.ts @@ -0,0 +1,59 @@ +import { + getAccountAddressFromEIP155String, + getChainIdFromEIP155String, + getSupportedWalletConnectChains, +} from 'src/features/walletConnect/utils' +import { ChainId } from 'wallet/src/constants/chains' + +const EIP155_MAINNET = 'eip155:1' +const EIP155_POLYGON = 'eip155:137' +const EIP155_OPTIMISM = 'eip155:10' +const EIP155_AVAX_UNSUPPORTED = 'eip155:43114' + +const TEST_ADDRESS = '0xdFb84E543C39ACa3c6a39ea4e3B6c40eE7d2EBdA' + +describe(getAccountAddressFromEIP155String, () => { + it('handles valid eip155 mainnet address', () => { + expect(getAccountAddressFromEIP155String(`${EIP155_MAINNET}:${TEST_ADDRESS}`)).toBe( + TEST_ADDRESS + ) + }) + + it('handles valid eip155 polygon address', () => { + expect(getAccountAddressFromEIP155String(`${EIP155_POLYGON}:${TEST_ADDRESS}`)).toBe( + TEST_ADDRESS + ) + }) + + it('handles invalid eip155 address', () => { + expect(getAccountAddressFromEIP155String(TEST_ADDRESS)).toBeNull() + }) +}) + +describe(getSupportedWalletConnectChains, () => { + it('handles list of valid chains', () => { + expect( + getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_OPTIMISM]) + ).toEqual([ChainId.Mainnet, ChainId.Polygon, ChainId.Optimism]) + }) + + it('handles list of valid chains including an invalid chain', () => { + expect( + getSupportedWalletConnectChains([EIP155_MAINNET, EIP155_POLYGON, EIP155_AVAX_UNSUPPORTED]) + ).toEqual([ChainId.Mainnet, ChainId.Polygon]) + }) +}) + +describe(getChainIdFromEIP155String, () => { + it('handles valid eip155 mainnet address', () => { + expect(getChainIdFromEIP155String(EIP155_MAINNET)).toBe(ChainId.Mainnet) + }) + + it('handles valid eip155 optimism address', () => { + expect(getChainIdFromEIP155String(EIP155_OPTIMISM)).toBe(ChainId.Optimism) + }) + + it('handles invalid eip155 address', () => { + expect(getChainIdFromEIP155String(EIP155_AVAX_UNSUPPORTED)).toBeNull() + }) +}) diff --git a/apps/mobile/src/features/walletConnect/utils.ts b/apps/mobile/src/features/walletConnect/utils.ts new file mode 100644 index 0000000..f045a93 --- /dev/null +++ b/apps/mobile/src/features/walletConnect/utils.ts @@ -0,0 +1,196 @@ +import { PairingTypes, ProposalTypes, SessionTypes, SignClientTypes } from '@walletconnect/types' +import { Web3WalletTypes } from '@walletconnect/web3wallet' +import { utils } from 'ethers' +import { wcWeb3Wallet } from 'src/features/walletConnect/saga' +import { SignRequest, TransactionRequest } from 'src/features/walletConnect/walletConnectSlice' +import { logger } from 'utilities/src/logger/logger' +import { ChainId } from 'wallet/src/constants/chains' +import { toSupportedChainId } from 'wallet/src/features/chains/utils' +import { EthMethod, EthSignMethod } from 'wallet/src/features/walletConnect/types' + +/** + * Construct WalletConnect 2.0 session namespaces to complete a new pairing. Used when approving a new pairing request. + * Assumes each namespace has been validated and is supported by the app with `validateProposalNamespaces()`. + * + * @param {Address} account address of account to complete WalletConnect pairing request + * @param {ProposalTypes.RequiredNamespaces} proposalNamespaces validated proposal namespaces that specify all supported chains, methods, events for the session + * @return {SessionTypes.Namespaces} session namespaces specifying which accounts, chains, methods, events to complete the pairing + */ +export const getSessionNamespaces = ( + account: Address, + proposalNamespaces: ProposalTypes.RequiredNamespaces +): SessionTypes.Namespaces => { + // Below inspired from https://github.com/WalletConnect/web-examples/blob/main/wallets/react-wallet-v2/src/views/SessionProposalModal.tsx#L63 + const namespaces: SessionTypes.Namespaces = {} + + Object.entries(proposalNamespaces).forEach(([key, namespace]) => { + const { chains, events, methods } = namespace + namespaces[key] = { + accounts: chains ? chains.map((chain) => `${chain}:${account}`) : [`${key}:${account}`], + events, + methods, + chains, + } + }) + + return namespaces +} + +/** + * Convert list of chains from a WalletConnect namespace to a list of supported ChainIds + * @param {string[]} chains list of chain strings as received from WalletConnect (ex. "eip155:1") + * @returns {ChainId[]} list of supported ChainIds + */ +export const getSupportedWalletConnectChains = (chains?: string[]): ChainId[] | undefined => { + if (!chains) { + return + } + + return chains + .map((chain) => getChainIdFromEIP155String(chain)) + .filter((c): c is ChainId => Boolean(c)) +} + +/** + * Convert chain from `eip155:[CHAIN_ID]` format to supported ChainId. + * Returns null if chain doesn't match correct `eip155:` format or is an unsupported chain. + */ +export const getChainIdFromEIP155String = (chain: string): ChainId | null => { + const chainStr = chain.startsWith('eip155:') ? chain.split(':')[1] : undefined + return toSupportedChainId(chainStr) +} + +/** + * Convert account from `eip155:[CHAIN_ID]:[ADDRESS]` format to account address. + * Returns null if string doesn't match correct `eip155:chainId:address` forma. + */ +export const getAccountAddressFromEIP155String = (account: string): Address | null => { + const address = account.startsWith('eip155:') ? account.split(':')[2] : undefined + if (!address) { + return null + } + return address +} + +/** + * Formats SignRequest object from WalletConnect 2.0 request parameters + * + * @param {EthSignMethod} method type of method to sign + * @param {string} topic id for the WalletConnect session + * @param {number} internalId id for the WalletConnect signature request + * @param {ChainId} chainId chain the signature is being requested on + * @param {SignClientTypes.Metadata} dapp metadata for the dapp requesting the signature + * @param {Web3WalletTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request + * @returns {{Address, SignRequest}} address of the account receiving the request and formatted SignRequest object + */ +export const parseSignRequest = ( + method: EthSignMethod, + topic: string, + internalId: number, + chainId: ChainId, + dapp: SignClientTypes.Metadata, + requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'] +): { account: Address; request: SignRequest } => { + const { address, rawMessage, message } = getAddressAndMessageToSign(method, requestParams) + return { + account: address, + request: { + type: method, + sessionId: topic, + internalId: String(internalId), + rawMessage, + message, + account: address, + chainId, + dapp: { + name: dapp.name, + url: dapp.url, + icon: dapp.icons[0] ?? null, + source: 'walletconnect', + }, + }, + } +} + +/** + * Formats TransactionRequest object from WalletConnect 2.0 request parameters. + * Only supports `eth_sendTransaction` request, `eth_signTransaction` is intentionally + * unsupported since it is difficult to support to nonce calculation and tracking. + * + * @param {EthMethod.EthSendTransaction} method type of method to sign (only support `eth_signTransaction`) + * @param {string} topic id for the WalletConnect session + * @param {number} internalId id for the WalletConnect transaction request + * @param {ChainId} chainId chain the signature is being requested on + * @param {SignClientTypes.Metadata} dapp metadata for the dapp requesting the transaction + * @param {Web3WalletTypes.SessionRequest['params']['request']['params']} requestParams parameters of the request + * @returns {{Address, TransactionRequest}} address of the account receiving the request and formatted TransactionRequest object + */ +export const parseTransactionRequest = ( + method: EthMethod.EthSendTransaction, + topic: string, + internalId: number, + chainId: ChainId, + dapp: SignClientTypes.Metadata, + requestParams: Web3WalletTypes.SessionRequest['params']['request']['params'] +): { account: Address; request: TransactionRequest } => { + // Omit gasPrice and nonce in tx sent from dapp since it is calculated later + const { from, to, data, gasLimit, value } = requestParams[0] + + return { + account: from, + request: { + type: method, + sessionId: topic, + internalId: String(internalId), + transaction: { + to, + from, + value, + data, + gasLimit, + }, + account: from, + chainId, + dapp: { + name: dapp.name, + url: dapp.url, + icon: dapp.icons[0] ?? null, + source: 'walletconnect', + }, + }, + } +} + +/** + * Gets the address receiving the request, raw message, decoded message to sign based on the EthSignMethod. + * `personal_sign` params are ordered as [message, account] + * `eth_sign` params are ordered as [account, message] + * `signTypedData` params are ordered as [account, message] + * See https://docs.walletconnect.com/2.0/advanced/rpc-reference/ethereum-rpc#personal_sign + */ +export function getAddressAndMessageToSign( + ethMethod: EthSignMethod, + params: Web3WalletTypes.SessionRequest['params']['request']['params'] +): { address: string; rawMessage: string; message: string | null } { + switch (ethMethod) { + case EthMethod.PersonalSign: + return { address: params[1], rawMessage: params[0], message: utils.toUtf8String(params[0]) } + case EthMethod.EthSign: + return { address: params[0], rawMessage: params[1], message: utils.toUtf8String(params[1]) } + case EthMethod.SignTypedData: + case EthMethod.SignTypedDataV4: + return { address: params[0], rawMessage: params[1], message: null } + } +} + +export async function pairWithWalletConnectURI(uri: string): Promise { + try { + return await wcWeb3Wallet.core.pairing.pair({ uri }) + } catch (error) { + logger.error(error, { + tags: { file: 'walletConnectV2/utils', function: 'pairWithWalletConnectURI' }, + }) + + return Promise.reject(error instanceof Error ? error.message : '') + } +} diff --git a/apps/mobile/src/features/walletConnect/walletConnectSlice.ts b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts new file mode 100644 index 0000000..cb3bc3e --- /dev/null +++ b/apps/mobile/src/features/walletConnect/walletConnectSlice.ts @@ -0,0 +1,171 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { ProposalTypes, SessionTypes } from '@walletconnect/types' +import { ChainId } from 'wallet/src/constants/chains' +import { + DappInfo, + EthMethod, + EthSignMethod, + EthTransaction, +} from 'wallet/src/features/walletConnect/types' + +export type WalletConnectPendingSession = { + id: string + chains: ChainId[] + dapp: DappInfo + proposalNamespaces: ProposalTypes.RequiredNamespaces +} + +export type WalletConnectSession = { + id: string + chains: ChainId[] + dapp: DappInfo + namespaces: SessionTypes.Namespaces +} + +interface SessionMapping { + [sessionId: string]: WalletConnectSession +} + +interface BaseRequest { + sessionId: string + internalId: string + account: string + dapp: DappInfo + chainId: ChainId +} + +export interface SignRequest extends BaseRequest { + type: EthSignMethod + message: string | null + rawMessage: string +} + +export interface TransactionRequest extends BaseRequest { + type: EthMethod.EthSendTransaction + transaction: EthTransaction +} + +export type WalletConnectRequest = SignRequest | TransactionRequest + +export const isTransactionRequest = ( + request: WalletConnectRequest +): request is TransactionRequest => request.type === EthMethod.EthSendTransaction + +export interface WalletConnectState { + byAccount: { + [accountId: string]: { + sessions: SessionMapping + } + } + pendingSession: WalletConnectPendingSession | null + pendingRequests: WalletConnectRequest[] + didOpenFromDeepLink?: boolean + hasPendingSessionError?: boolean +} + +export const initialWalletConnectState: Readonly = { + byAccount: {}, + pendingSession: null, + pendingRequests: [], +} + +const slice = createSlice({ + name: 'walletConnect', + initialState: initialWalletConnectState, + reducers: { + addSession: ( + state, + action: PayloadAction<{ account: string; wcSession: WalletConnectSession }> + ) => { + const { wcSession, account } = action.payload + state.byAccount[account] ??= { sessions: {} } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + state.byAccount[account]!.sessions[wcSession.id] = wcSession + state.pendingSession = null + }, + + updateSession: ( + state, + action: PayloadAction<{ account: string; wcSession: WalletConnectSession }> + ) => { + const { wcSession, account } = action.payload + const wcAccount = state.byAccount[account] + if (wcAccount) { + wcAccount.sessions[wcSession.id] = wcSession + } + }, + + removeSession: (state, action: PayloadAction<{ sessionId: string; account?: string }>) => { + const { sessionId, account } = action.payload + + // If account address is known, delete directly + if (account) { + const wcAccount = state.byAccount[account] + if (wcAccount) { + delete wcAccount.sessions[sessionId] + } + return + } + + // If account address is not known (handling `session_delete` events), + // iterate over each account and delete the sessionId + Object.keys(state.byAccount).forEach((accountAddress) => { + const wcAccount = state.byAccount[accountAddress] + if (wcAccount && wcAccount.sessions[sessionId]) { + delete wcAccount.sessions[sessionId] + } + }) + }, + + addPendingSession: ( + state, + action: PayloadAction<{ wcSession: WalletConnectPendingSession }> + ) => { + const { wcSession } = action.payload + state.pendingSession = wcSession + }, + + removePendingSession: (state) => { + state.pendingSession = null + }, + + addRequest: ( + state, + action: PayloadAction<{ request: WalletConnectRequest; account: string }> + ) => { + const { request } = action.payload + state.pendingRequests.push(request) + }, + + removeRequest: ( + state, + action: PayloadAction<{ requestInternalId: string; account: string }> + ) => { + const { requestInternalId } = action.payload + state.pendingRequests = state.pendingRequests.filter( + (req) => req.internalId !== requestInternalId + ) + }, + + setDidOpenFromDeepLink: (state, action: PayloadAction) => { + state.didOpenFromDeepLink = action.payload + }, + + setHasPendingSessionError: (state, action: PayloadAction) => { + state.hasPendingSessionError = action.payload + }, + }, +}) + +export const { + addSession, + updateSession, + removeSession, + addPendingSession, + removePendingSession, + addRequest, + removeRequest, + setDidOpenFromDeepLink, + setHasPendingSessionError, +} = slice.actions +export const { reducer: walletConnectReducer } = slice diff --git a/apps/mobile/src/features/widgets/widgets.ts b/apps/mobile/src/features/widgets/widgets.ts new file mode 100644 index 0000000..569b8a0 --- /dev/null +++ b/apps/mobile/src/features/widgets/widgets.ts @@ -0,0 +1,133 @@ +import { NativeModules } from 'react-native' +import { getItem, reloadAllTimelines, setItem } from 'react-native-widgetkit' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { getBuildVariant } from 'src/utils/version' +import { isAndroid } from 'uniswap/src/utils/platform' +import { analytics } from 'utilities/src/telemetry/analytics/analytics' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { Account, AccountType } from 'wallet/src/features/wallet/accounts/types' +import { CurrencyId } from 'wallet/src/utils/currencyId' + +const APP_GROUP = 'group.com.uniswap.widgets' +const KEY_WIDGET_EVENTS = getBuildVariant() + '.widgets.configuration.events' +const KEY_WIDGET_CACHE = getBuildVariant() + '.widgets.configuration.cache' +const KEY_WIDGETS_FAVORITE = getBuildVariant() + '.widgets.favorites' +const KEY_WIDGETS_ACCOUNTS = getBuildVariant() + '.widgets.accounts' +const KEY_WIDGETS_I18N = getBuildVariant() + '.widgets.i18n' + +const { RNWidgets } = NativeModules + +export const enum WidgetType { + TokenPrice = 'token-price', +} + +type WidgetEventsData = { + events: WidgetEvent[] +} + +export type WidgetEvent = { + kind: string + family: string + change: 'added' | 'removed' +} + +type WidgetCacheData = { + configuration: WidgetConfiguration[] +} + +export type WidgetConfiguration = { + kind: string + family: string +} + +export type WidgetI18nSettings = { + locale: string + currency: string +} + +export const setUserDefaults = async (data: object, key: string): Promise => { + const dataJSON = JSON.stringify(data) + await setItem(key, dataJSON, APP_GROUP) + reloadAllTimelines() +} + +export const setFavoritesUserDefaults = (currencyIds: CurrencyId[]): void => { + const favorites: Array<{ address: Maybe; chain: string }> = [] + currencyIds.forEach((currencyId: CurrencyId) => { + const contractInput = currencyIdToContractInput(currencyId) + favorites.push({ address: contractInput.address, chain: contractInput.chain }) + }) + const data = { + favorites, + } + setUserDefaults(data, KEY_WIDGETS_FAVORITE).catch(() => undefined) +} + +export const setAccountAddressesUserDefaults = (accounts: Account[]): void => { + const userDefaultAccounts: Array<{ address: string; name: Maybe; isSigner: boolean }> = + accounts.map((account: Account) => { + return { + address: account.address, + name: account.name, + isSigner: account.type === AccountType.SignerMnemonic, + } + }) + const data = { + accounts: userDefaultAccounts, + } + setUserDefaults(data, KEY_WIDGETS_ACCOUNTS).catch(() => undefined) +} + +export const setI18NUserDefaults = (i18nSettings: WidgetI18nSettings): void => { + setUserDefaults(i18nSettings, KEY_WIDGETS_I18N).catch(() => undefined) +} + +// handles edge case where there is a widget left in the cache, +// but no configured widgets, and no widgets to call getTimeline() in order to update the cache +// and send out the last removed event +async function handleLastRemovalEvents(): Promise { + const areWidgetsInstalled = await hasWidgetsInstalled() + if (!areWidgetsInstalled) { + const widgetCacheJSONString = await getItem(KEY_WIDGET_CACHE, APP_GROUP) + if (!widgetCacheJSONString) { + return + } + const widgetCache: WidgetCacheData = JSON.parse(widgetCacheJSONString) + widgetCache.configuration.forEach((widget) => { + sendMobileAnalyticsEvent(MobileEventName.WidgetConfigurationUpdated, { + kind: widget.kind, + family: widget.family, + change: 'removed', + }) + }) + await setUserDefaults({ configuration: [] }, KEY_WIDGET_CACHE) + } +} + +export async function processWidgetEvents(): Promise { + reloadAllTimelines() + await handleLastRemovalEvents() + const widgetEventsJSONString = await getItem(KEY_WIDGET_EVENTS, APP_GROUP) + + if (!widgetEventsJSONString) { + return + } + const widgetEvents: WidgetEventsData = JSON.parse(widgetEventsJSONString) + widgetEvents.events.forEach((widget) => { + sendMobileAnalyticsEvent(MobileEventName.WidgetConfigurationUpdated, widget) + }) + + if (widgetEvents.events.length > 0) { + analytics.flushEvents() + await setUserDefaults({ events: [] }, KEY_WIDGET_EVENTS) + } +} + +async function hasWidgetsInstalled(): Promise { + if (isAndroid) { + return false + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return await RNWidgets.hasWidgetsInstalled() +} diff --git a/apps/mobile/src/index.ts b/apps/mobile/src/index.ts new file mode 100644 index 0000000..89c0480 --- /dev/null +++ b/apps/mobile/src/index.ts @@ -0,0 +1,4 @@ +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +export {} diff --git a/apps/mobile/src/lib/RNEthersRs.ts b/apps/mobile/src/lib/RNEthersRs.ts new file mode 100644 index 0000000..1837d4f --- /dev/null +++ b/apps/mobile/src/lib/RNEthersRs.ts @@ -0,0 +1,64 @@ +declare module 'react-native' { + interface NativeModulesStatic { + RNEthersRS: IKeyring + } +} + +import { NativeModules } from 'react-native' +import { IKeyring } from 'wallet/src/features/wallet/Keyring/Keyring' + +const { RNEthersRS } = NativeModules + +export function getMnemonicIds(): Promise { + return RNEthersRS.getMnemonicIds() +} + +// returns the mnemonicId (derived address at index 0) of the imported mnemonic +export function importMnemonic(mnemonic: string): Promise { + return RNEthersRS.importMnemonic(mnemonic) +} + +// returns the mnemonicId (derived address at index 0) of the stored mnemonic +export function generateAndStoreMnemonic(): Promise { + return RNEthersRS.generateAndStoreMnemonic() +} + +export function getAddressesForStoredPrivateKeys(): Promise { + return RNEthersRS.getAddressesForStoredPrivateKeys() +} + +// returns the address for the mnemonic +export function generateAddressForMnemonic( + mnemonic: string, + derivationIndex: number +): Promise { + return RNEthersRS.generateAddressForMnemonic(mnemonic, derivationIndex) +} + +// returns the address of the generated key +export function generateAndStorePrivateKey( + mnemonicId: string, + derivationIndex: number +): Promise { + return RNEthersRS.generateAndStorePrivateKey(mnemonicId, derivationIndex) +} + +export function signTransactionHashForAddress( + address: string, + hash: string, + chainId: number +): Promise { + return RNEthersRS.signTransactionHashForAddress(address, hash, chainId) +} + +export function signMessageForAddress(address: string, message: string): Promise { + return RNEthersRS.signMessageForAddress(address, message) +} + +export function signHashForAddress( + address: string, + hash: string, + chainId: number +): Promise { + return RNEthersRS.signHashForAddress(address, hash, chainId) +} diff --git a/apps/mobile/src/lib/__mocks__/RNEthersRs.ts b/apps/mobile/src/lib/__mocks__/RNEthersRs.ts new file mode 100644 index 0000000..ce07566 --- /dev/null +++ b/apps/mobile/src/lib/__mocks__/RNEthersRs.ts @@ -0,0 +1,77 @@ +import { providers, utils, Wallet } from 'ethers' + +const pathFromIndex = (index: number): string => `m/44'/60'/0'/0/${index}` + +const mnemonics: { [id: string]: string } = {} +const privateKeys: { [id: string]: string } = {} + +export function getMnemonicIds(): Promise { + return Promise.resolve(Object.keys(mnemonics)) +} + +// returns the mnemonicId (derived address at index 0) of the imported mnemonic +export function importMnemonic(mnemonic: string): Promise { + const wallet = Wallet.fromMnemonic(mnemonic) + mnemonics[wallet.address] = mnemonic + return Promise.resolve(wallet.address) +} + +// returns the mnemonicId (derived address at index 0) of the stored mnemonic +export function generateAndStoreMnemonic(): Promise { + const wallet = Wallet.createRandom() + mnemonics[wallet.address] = wallet.mnemonic.phrase + return Promise.resolve(wallet.address) +} + +export function getAddressesForStoredPrivateKeys(): Promise { + return Promise.resolve(Object.keys(privateKeys)) +} + +// returns the address for the mnemonic +export function generateAddressForMnemonic( + mnemonic: string, + derivationIndex: number +): Promise { + const wallet = Wallet.fromMnemonic(mnemonic, pathFromIndex(derivationIndex)) + return Promise.resolve(wallet.address) +} + +// returns the address of the generated key +export function generateAndStorePrivateKey( + mnemonicId: string, + derivationIndex: number +): Promise { + const mnemonic = mnemonics[mnemonicId] + if (!mnemonic) { + return Promise.reject(`No mnemonic found for ${mnemonicId}`) + } + const wallet = Wallet.fromMnemonic(mnemonic, pathFromIndex(derivationIndex)) + privateKeys[wallet.address] = wallet.privateKey + return Promise.resolve(wallet.address) +} + +export async function signTransactionForAddress( + address: string, + transaction: providers.TransactionRequest +): Promise { + const privateKey = privateKeys[address] + if (!privateKey) { + return Promise.reject(`No private key found for ${address}`) + } + const wallet = new Wallet(privateKey) + const signature = await wallet.signTransaction(transaction) + return signature +} + +export async function signMessageForAddress( + address: string, + message: string | utils.Bytes +): Promise { + const privateKey = privateKeys[address] + if (!privateKey) { + return Promise.reject(`No private key found for ${address}`) + } + const wallet = new Wallet(privateKey) + const signature = await wallet.signMessage(message) + return signature +} diff --git a/apps/mobile/src/logbox.e2e.js b/apps/mobile/src/logbox.e2e.js new file mode 100644 index 0000000..cfcffd0 --- /dev/null +++ b/apps/mobile/src/logbox.e2e.js @@ -0,0 +1,3 @@ +import { LogBox } from 'react-native' + +LogBox.ignoreAllLogs() diff --git a/apps/mobile/src/logbox.js b/apps/mobile/src/logbox.js new file mode 100644 index 0000000..bed757d --- /dev/null +++ b/apps/mobile/src/logbox.js @@ -0,0 +1,18 @@ +import { LogBox } from 'react-native' + +// Ignore errors coming from AnimatedComponent, either from React Native itself or possibly an animation lib +// https://github.com/facebook/react-native/issues/22186 +LogBox.ignoreLogs([ + 'Warning: Using UNSAFE_componentWillMount', + 'Warning: Using UNSAFE_componentWillReceiveProps', + // https://github.com/software-mansion/react-native-gesture-handler/issues/1036 + 'Warning: findNodeHandle', + // https://github.com/d3/d3-interpolate/issues/99 + 'Require cycle', + 'logException:ApolloClient [GraphQL Error]:', + 'logException:ApolloClient [Network Error]:', + // Ignore since it's difficult to filter out just these styles and they are often shared styles + 'FlashList only supports padding related props and backgroundColor in contentContainerStyle.', + // This is enabled conditionally in bash profile only for dev mode + 'The native module for Flipper seems unavailable.', +]) diff --git a/apps/mobile/src/package.json b/apps/mobile/src/package.json new file mode 100644 index 0000000..0e71c24 --- /dev/null +++ b/apps/mobile/src/package.json @@ -0,0 +1,3 @@ +{ + "name": "src" +} diff --git a/apps/mobile/src/polyfills/arrayAt.js b/apps/mobile/src/polyfills/arrayAt.js new file mode 100644 index 0000000..514df0a --- /dev/null +++ b/apps/mobile/src/polyfills/arrayAt.js @@ -0,0 +1,29 @@ +// From https://github.com/tc39/proposal-relative-indexing-method#polyfill +if (!Array.prototype.at) { + function at(n) { + // ToInteger() abstract op + n = Math.trunc(n) || 0 + // Allow negative indexing from the end + if (n < 0) { + n += this.length + } + // OOB access is guaranteed to return undefined + if (n < 0 || n >= this.length) { + return undefined + } + // Otherwise, this is just normal property access + return this[n] + } + + const TypedArray = Reflect.getPrototypeOf(Int8Array) + for (const C of [Array, String, TypedArray]) { + Object.defineProperty(C.prototype, 'at', { + value: at, + writable: true, + enumerable: false, + configurable: true, + }) + } +} + +export {} diff --git a/apps/mobile/src/polyfills/index.ts b/apps/mobile/src/polyfills/index.ts new file mode 100644 index 0000000..5366f6c --- /dev/null +++ b/apps/mobile/src/polyfills/index.ts @@ -0,0 +1,16 @@ +/** + * Ethers Shims + * https://docs.ethers.io/v5/cookbook/react-native/#cookbook-reactnative-security + */ + +// Disable sorting imports with Prettier for this file so that it doesn't change the order +// organize-imports-ignore + +// Import the crypto getRandomValues shim BEFORE ethers shims +import 'react-native-get-random-values' +// Import the the ethers shims BEFORE ethers +import '@ethersproject/shims' +// Add .at() method to Array if necessary (missing before iOS 15) +import 'src/polyfills/arrayAt' +// Import the Intl polyfills for Hermes +import 'src/polyfills/intl' diff --git a/apps/mobile/src/polyfills/intl.js b/apps/mobile/src/polyfills/intl.js new file mode 100644 index 0000000..f0ed9e5 --- /dev/null +++ b/apps/mobile/src/polyfills/intl.js @@ -0,0 +1,105 @@ +/* eslint-disable @typescript-eslint/no-unused-expressions */ +import { isAndroid } from 'uniswap/src/utils/platform' + +// TODO: [MOB-247] remove polyfill once Hermes support it +// https://github.com/facebook/hermes/issues/23 + +// Polyfills required to use Intl with Hermes engine +require('@formatjs/intl-getcanonicallocales/polyfill').default + +require('@formatjs/intl-locale/polyfill').default + +require('@formatjs/intl-pluralrules/polyfill').default + +// https://github.com/formatjs/formatjs/blob/main/packages/intl-pluralrules/supported-locales.generated.ts +require('@formatjs/intl-pluralrules/locale-data/zh').default +require('@formatjs/intl-pluralrules/locale-data/nl').default +require('@formatjs/intl-pluralrules/locale-data/en').default +require('@formatjs/intl-pluralrules/locale-data/fr').default +require('@formatjs/intl-pluralrules/locale-data/hi').default +require('@formatjs/intl-pluralrules/locale-data/id').default +require('@formatjs/intl-pluralrules/locale-data/ja').default +require('@formatjs/intl-pluralrules/locale-data/ms').default +require('@formatjs/intl-pluralrules/locale-data/pt').default +require('@formatjs/intl-pluralrules/locale-data/ru').default +require('@formatjs/intl-pluralrules/locale-data/es').default +require('@formatjs/intl-pluralrules/locale-data/th').default +require('@formatjs/intl-pluralrules/locale-data/tr').default +require('@formatjs/intl-pluralrules/locale-data/uk').default +require('@formatjs/intl-pluralrules/locale-data/ur').default +require('@formatjs/intl-pluralrules/locale-data/vi').default + +if (isAndroid) { + // Forces polyfill to replace Hermes NumberFormat due to issues with "compact" notation + // https://hermesengine.dev/docs/intl/#android-11 + require('@formatjs/intl-numberformat/polyfill-force').default +} else { + require('@formatjs/intl-numberformat/polyfill').default +} + +// https://github.com/formatjs/formatjs/blob/main/packages/intl-numberformat/supported-locales.generated.ts +require('@formatjs/intl-numberformat/locale-data/zh-Hans').default +require('@formatjs/intl-numberformat/locale-data/zh-Hant').default +require('@formatjs/intl-numberformat/locale-data/nl').default +require('@formatjs/intl-numberformat/locale-data/en').default +require('@formatjs/intl-numberformat/locale-data/fr').default +require('@formatjs/intl-numberformat/locale-data/hi').default +require('@formatjs/intl-numberformat/locale-data/id').default +require('@formatjs/intl-numberformat/locale-data/ja').default +require('@formatjs/intl-numberformat/locale-data/ms').default +require('@formatjs/intl-numberformat/locale-data/pt').default +require('@formatjs/intl-numberformat/locale-data/ru').default +require('@formatjs/intl-numberformat/locale-data/es').default +require('@formatjs/intl-numberformat/locale-data/es-US').default +require('@formatjs/intl-numberformat/locale-data/es-419').default +require('@formatjs/intl-numberformat/locale-data/th').default +require('@formatjs/intl-numberformat/locale-data/tr').default +require('@formatjs/intl-numberformat/locale-data/uk').default +require('@formatjs/intl-numberformat/locale-data/ur').default +require('@formatjs/intl-numberformat/locale-data/vi').default + +require('@formatjs/intl-datetimeformat/polyfill').default + +// https://github.com/formatjs/formatjs/blob/main/packages/intl-datetimeformat/supported-locales.generated.ts +require('@formatjs/intl-datetimeformat/locale-data/zh-Hans').default +require('@formatjs/intl-datetimeformat/locale-data/zh-Hant').default +require('@formatjs/intl-datetimeformat/locale-data/nl').default +require('@formatjs/intl-datetimeformat/locale-data/en').default +require('@formatjs/intl-datetimeformat/locale-data/fr').default +require('@formatjs/intl-datetimeformat/locale-data/hi').default +require('@formatjs/intl-datetimeformat/locale-data/id').default +require('@formatjs/intl-datetimeformat/locale-data/ja').default +require('@formatjs/intl-datetimeformat/locale-data/ms').default +require('@formatjs/intl-datetimeformat/locale-data/pt').default +require('@formatjs/intl-datetimeformat/locale-data/ru').default +require('@formatjs/intl-datetimeformat/locale-data/es').default +require('@formatjs/intl-datetimeformat/locale-data/es-US').default +require('@formatjs/intl-datetimeformat/locale-data/es-419').default +require('@formatjs/intl-datetimeformat/locale-data/th').default +require('@formatjs/intl-datetimeformat/locale-data/tr').default +require('@formatjs/intl-datetimeformat/locale-data/uk').default +require('@formatjs/intl-datetimeformat/locale-data/ur').default +require('@formatjs/intl-datetimeformat/locale-data/vi').default + +require('@formatjs/intl-relativetimeformat/polyfill').default + +// https://github.com/formatjs/formatjs/blob/main/packages/intl-relativetimeformat/supported-locales.generated.ts +require('@formatjs/intl-relativetimeformat/locale-data/zh-Hans').default +require('@formatjs/intl-relativetimeformat/locale-data/zh-Hant').default +require('@formatjs/intl-relativetimeformat/locale-data/nl').default +require('@formatjs/intl-relativetimeformat/locale-data/en').default +require('@formatjs/intl-relativetimeformat/locale-data/fr').default +require('@formatjs/intl-relativetimeformat/locale-data/hi').default +require('@formatjs/intl-relativetimeformat/locale-data/id').default +require('@formatjs/intl-relativetimeformat/locale-data/ja').default +require('@formatjs/intl-relativetimeformat/locale-data/ms').default +require('@formatjs/intl-relativetimeformat/locale-data/pt').default +require('@formatjs/intl-relativetimeformat/locale-data/ru').default +require('@formatjs/intl-relativetimeformat/locale-data/es').default +require('@formatjs/intl-relativetimeformat/locale-data/es-US').default +require('@formatjs/intl-relativetimeformat/locale-data/es-419').default +require('@formatjs/intl-relativetimeformat/locale-data/th').default +require('@formatjs/intl-relativetimeformat/locale-data/tr').default +require('@formatjs/intl-relativetimeformat/locale-data/uk').default +require('@formatjs/intl-relativetimeformat/locale-data/ur').default +require('@formatjs/intl-relativetimeformat/locale-data/vi').default diff --git a/apps/mobile/src/screens/DevScreen.tsx b/apps/mobile/src/screens/DevScreen.tsx new file mode 100644 index 0000000..5716f9a --- /dev/null +++ b/apps/mobile/src/screens/DevScreen.tsx @@ -0,0 +1,129 @@ +import React, { useState } from 'react' +import { I18nManager, ScrollView } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { UniconSampleSheet } from 'src/components/DevelopmentOnly/UniconSampleSheet' +import { BackButton } from 'src/components/buttons/BackButton' +import { Screen } from 'src/components/layout/Screen' +import { Screens } from 'src/screens/Screens' +import { Flex, Text, TouchableArea, useDeviceInsets } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { logger } from 'utilities/src/logger/logger' +import { Switch } from 'wallet/src/components/buttons/Switch' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { resetDismissedWarnings } from 'wallet/src/features/tokens/tokensSlice' +import { createAccountActions } from 'wallet/src/features/wallet/create/createAccountSaga' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { resetWallet } from 'wallet/src/features/wallet/slice' + +export function DevScreen(): JSX.Element { + const insets = useDeviceInsets() + const dispatch = useAppDispatch() + const activeAccount = useActiveAccount() + const [rtlEnabled, setRTLEnabled] = useState(I18nManager.isRTL) + + const onPressResetTokenWarnings = (): void => { + dispatch(resetDismissedWarnings()) + } + + const onPressCreate = (): void => { + dispatch(createAccountActions.trigger()) + } + + const activateWormhole = (s: Screens): void => { + navigate(s) + } + + const onPressShowError = (): void => { + const address = activeAccount?.address + if (!address) { + logger.debug( + 'DevScreen', + 'onPressShowError', + 'Cannot show error if activeAccount is undefined' + ) + return + } + + dispatch( + pushNotification({ + type: AppNotificationType.Error, + address, + errorMessage: 'A scary new error has happened. Be afraid!!', + }) + ) + } + + const onPressResetOnboarding = (): void => { + if (!activeAccount) { + return + } + + dispatch(resetWallet()) + } + + const [showUniconsModal, setShowUniconsModal] = useState(false) + + const onPressShowUniconsModal = (): void => { + setShowUniconsModal((prev) => !prev) + } + + return ( + + + + + + + + {`Your Account: ${activeAccount?.address || 'none'}`} + + + 🌀🌀Screen Stargate🌀🌀 + + + {Object.values(Screens).map((s) => ( + activateWormhole(s)}> + {s} + + ))} + + + 🌀🌀🌀🌀🌀🌀🌀🌀🌀🌀🌀 + + + Create account + + + Reset token warnings + + + Show global error + + + Reset onboarding + + + Force RTL (requires restart to apply) + { + I18nManager.forceRTL(value) + setRTLEnabled(value) + }} + /> + + + Show Unicons sheet + + + + {showUniconsModal ? setShowUniconsModal(false)} /> : null} + + ) +} diff --git a/apps/mobile/src/screens/EducationScreen.tsx b/apps/mobile/src/screens/EducationScreen.tsx new file mode 100644 index 0000000..38f6571 --- /dev/null +++ b/apps/mobile/src/screens/EducationScreen.tsx @@ -0,0 +1,28 @@ +import React, { useMemo } from 'react' +import { AppStackScreenProp } from 'src/app/navigation/types' +import { Carousel } from 'src/components/carousel/Carousel' +import { educationContent } from 'src/components/education' +import { Screen } from 'src/components/layout/Screen' +import { Screens } from 'src/screens/Screens' +import { isIOS } from 'uniswap/src/utils/platform' + +export function EducationScreen({ + route: { + params: { type, importType, entryPoint }, + }, +}: AppStackScreenProp): JSX.Element { + const content = useMemo( + () => + educationContent[type]({ + importType, + entryPoint, + }), + [entryPoint, importType, type] + ) + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/ExchangeTransferConnecting.tsx b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx new file mode 100644 index 0000000..f1f5eb3 --- /dev/null +++ b/apps/mobile/src/screens/ExchangeTransferConnecting.tsx @@ -0,0 +1,131 @@ +import { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { getCountry } from 'react-native-localize' +import { useAppDispatch } from 'src/app/hooks' +import { Screen } from 'src/components/layout/Screen' +import { + FiatOnRampConnectingView, + SERVICE_PROVIDER_ICON_BORDER_RADIUS, + SERVICE_PROVIDER_ICON_SIZE, +} from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { isAndroid } from 'uniswap/src/utils/platform' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useTimeout } from 'utilities/src/time/timing' +import { ChainId } from 'wallet/src/constants/chains' +import { useFiatOnRampAggregatorTransferWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' +import { FORTransferInstitution } from 'wallet/src/features/fiatOnRamp/types' +import { RemoteImage } from 'wallet/src/features/images/RemoteImage' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { InstitutionTransferEventName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' + +// Design decision +const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS + +const DEFAULT_TRANSFER_AMOUNT = 1 +const DEFAULT_TRANSFER_CURRENCY = 'ETH' + +export function ExchangeTransferConnecting({ + serviceProvider, + onClose, +}: { + serviceProvider: FORTransferInstitution + onClose: () => void +}): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const activeAccountAddress = useActiveAccountAddressWithThrow() + const [timeoutElapsed, setTimeoutElapsed] = useState(false) + + const initialTypeInfo = useMemo( + () => ({ institutionLogoUrl: serviceProvider.icon }), + [serviceProvider.icon] + ) + + const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( + activeAccountAddress, + ChainId.Mainnet, + initialTypeInfo + ) + + const onError = useCallback((): void => { + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: t('common.error.general'), + }) + ) + onClose() + }, [dispatch, onClose, t]) + + useTimeout(() => { + setTimeoutElapsed(true) + }, CONNECTING_TIMEOUT) + + const { + data: widgetData, + isLoading: widgetLoading, + error: widgetError, + } = useFiatOnRampAggregatorTransferWidgetQuery({ + sourceAmount: DEFAULT_TRANSFER_AMOUNT, + sourceCurrencyCode: DEFAULT_TRANSFER_CURRENCY, + countryCode: getCountry(), + institutionId: serviceProvider.id, + walletAddress: activeAccountAddress, + externalSessionId: externalTransactionId, + redirectURL: `${ + isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl + }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + }) + + useEffect(() => { + if (widgetError) { + onError() + return + } + async function navigateToWidget(widgetUrl: string): Promise { + onClose() + sendWalletAnalyticsEvent(InstitutionTransferEventName.InstitutionTransferWidgetOpened, { + externalTransactionId, + institutionName: serviceProvider.name, + }) + + await openUri(widgetUrl).catch(onError) + dispatchAddTransaction() + } + if (timeoutElapsed && !widgetLoading && widgetData) { + navigateToWidget(widgetData.widgetUrl).catch(() => undefined) + } + }, [ + dispatchAddTransaction, + onClose, + onError, + timeoutElapsed, + widgetData, + widgetLoading, + widgetError, + externalTransactionId, + serviceProvider?.name, + ]) + + return ( + + + } + serviceProviderName={serviceProvider.name} + /> + + ) +} diff --git a/apps/mobile/src/screens/ExploreScreen.tsx b/apps/mobile/src/screens/ExploreScreen.tsx new file mode 100644 index 0000000..77f66af --- /dev/null +++ b/apps/mobile/src/screens/ExploreScreen.tsx @@ -0,0 +1,111 @@ +import { useScrollToTop } from '@react-navigation/native' +import { SharedEventName } from '@uniswap/analytics-events' +import React, { useCallback, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { KeyboardAvoidingView, TextInput } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { useExploreStackNavigation } from 'src/app/navigation/types' +import { ExploreSections } from 'src/components/explore/ExploreSections' +import { SearchEmptySection } from 'src/components/explore/search/SearchEmptySection' +import { SearchResultsSection } from 'src/components/explore/search/SearchResultsSection' +import { Screen } from 'src/components/layout/Screen' +import { VirtualizedList } from 'src/components/layout/VirtualizedList' +import { useReduxModalBackHandler } from 'src/features/modals/hooks' +import { selectModalState } from 'src/features/modals/selectModalState' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { Screens } from 'src/screens/Screens' +import { AnimatedFlex, ColorTokens, Flex, flexStyles, useIsDarkMode } from 'ui/src' +import { useDebounce } from 'utilities/src/time/timing' +import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { SearchTextInput } from 'wallet/src/features/search/SearchTextInput' +import { ModalName, SectionName } from 'wallet/src/telemetry/constants' + +export function ExploreScreen(): JSX.Element { + const modalInitialState = useAppSelector(selectModalState(ModalName.Explore)).initialState + const navigation = useExploreStackNavigation() + + const { isSheetReady } = useBottomSheetContext() + + useReduxModalBackHandler(ModalName.Explore) + + // The ExploreStack is not directly accessible from outside + // (e.g., navigating from Home to NFTItem within ExploreStack), due to its mount within BottomSheetModal. + // To bypass this limitation, we use an initialState to define a specific screen within ExploreStack. + useEffect(() => { + if (modalInitialState) { + navigation.navigate(modalInitialState.screen, modalInitialState.params) + } + }, [modalInitialState, navigation]) + + const { t } = useTranslation() + const isDarkMode = useIsDarkMode() + + const listRef = useRef(null) + useScrollToTop(listRef) + + const [searchQuery, setSearchQuery] = useState('') + const debouncedSearchQuery = useDebounce(searchQuery).trim() + const [isSearchMode, setIsSearchMode] = useState(false) + const textInputRef = useRef(null) + + const onSearchChangeText = (newSearchFilter: string): void => { + setSearchQuery(newSearchFilter) + } + + const onSearchFocus = (): void => { + setIsSearchMode(true) + sendMobileAnalyticsEvent(SharedEventName.PAGE_VIEWED, { + section: SectionName.ExploreSearch, + screen: Screens.Explore, + }) + } + + const onSearchCancel = (): void => { + setIsSearchMode(false) + } + + // Handle special case with design system light colors because surface2 is the same as surface1 + const contrastBackgroundColor: ColorTokens = isDarkMode ? '$DEP_backgroundOverlay' : '$surface1' + const searchBarBackgroundColor: ColorTokens = isDarkMode ? '$DEP_backgroundOverlay' : '$surface1' + + const onScroll = useCallback(() => { + textInputRef.current?.blur() + }, []) + + return ( + + + + + + {isSearchMode ? ( + + + + + {debouncedSearchQuery.length === 0 ? ( + + ) : ( + + + + )} + + + + ) : ( + isSheetReady && + )} + + ) +} diff --git a/apps/mobile/src/screens/ExternalProfileScreen.tsx b/apps/mobile/src/screens/ExternalProfileScreen.tsx new file mode 100644 index 0000000..ffa3a76 --- /dev/null +++ b/apps/mobile/src/screens/ExternalProfileScreen.tsx @@ -0,0 +1,180 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleProp, StyleSheet, ViewStyle } from 'react-native' +import { SceneRendererProps, TabBar } from 'react-native-tab-view' +import { AppStackParamList } from 'src/app/navigation/types' +import { ActivityTab } from 'src/components/home/ActivityTab' +import { NftsTab } from 'src/components/home/NftsTab' +import { TokensTab } from 'src/components/home/TokensTab' +import { Screen } from 'src/components/layout/Screen' +import { renderTabLabel, TAB_STYLES, TabContentProps } from 'src/components/layout/TabHelpers' +import Trace from 'src/components/Trace/Trace' +import TraceTabView from 'src/components/Trace/TraceTabView' +import { ProfileHeader } from 'src/features/externalProfile/ProfileHeader' +import { ExploreModalAwareView } from 'src/screens/ModalAwareView' +import { Screens } from 'src/screens/Screens' +import { Flex, useDeviceInsets, useSporeColors } from 'ui/src' +import { spacing } from 'ui/src/theme' +import { useDisplayName } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { SectionName, SectionNameType } from 'wallet/src/telemetry/constants' + +type Props = NativeStackScreenProps & { + renderedInModal?: boolean +} + +export function ExternalProfileScreen({ + route: { + params: { address }, + }, + renderedInModal = false, +}: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const [tabIndex, setIndex] = useState(0) + const insets = useDeviceInsets() + + const displayName = useDisplayName(address) + + const tabs = useMemo( + () => [ + { key: SectionName.ProfileTokensTab, title: t('home.tokens.title') }, + { key: SectionName.ProfileNftsTab, title: t('home.nfts.title') }, + { key: SectionName.ProfileActivityTab, title: t('home.activity.title') }, + ], + [t] + ) + + const containerStyle = useMemo>( + () => ({ + ...TAB_STYLES.tabListInner, + paddingBottom: insets.bottom + TAB_STYLES.tabListInner.paddingBottom, + }), + [insets.bottom] + ) + + const emptyContainerStyle = useMemo>( + () => ({ + paddingTop: spacing.spacing60, + paddingHorizontal: spacing.spacing36, + paddingBottom: insets.bottom, + }), + [insets.bottom] + ) + + const sharedProps = useMemo( + () => ({ + contentContainerStyle: containerStyle, + loadingContainerStyle: containerStyle, + emptyContainerStyle, + }), + [containerStyle, emptyContainerStyle] + ) + + const renderTab = useCallback( + ({ + route, + }: { + route: { + key: SectionNameType + title: string + } + }) => { + switch (route?.key) { + case SectionName.ProfileActivityTab: + return ( + + ) + case SectionName.ProfileNftsTab: + return ( + + ) + case SectionName.ProfileTokensTab: + return ( + + ) + } + return null + }, + [address, sharedProps, renderedInModal] + ) + + const renderTabBar = useCallback( + (sceneProps: SceneRendererProps) => { + return ( + + renderTabLabel({ route, focused, isExternalProfile: true }) + } + style={[ + TAB_STYLES.tabBar, + { + backgroundColor: colors.surface1.get(), + borderBottomColor: colors.surface3.get(), + paddingLeft: spacing.spacing12, + }, + ]} + tabStyle={styles.tabStyle} + /> + ) + }, + [colors.surface1, colors.surface3, tabIndex, tabs] + ) + + const traceProperties = useMemo( + () => ({ + address, + walletName: displayName?.name, + displayNameType: displayName?.type ? DisplayNameType[displayName.type] : undefined, + }), + [address, displayName?.name, displayName?.type] + ) + + return ( + + + + + + + + + + + ) +} + +const styles = StyleSheet.create({ + tabStyle: { width: 'auto' }, +}) diff --git a/apps/mobile/src/screens/FiatOnRampConnecting.tsx b/apps/mobile/src/screens/FiatOnRampConnecting.tsx new file mode 100644 index 0000000..b45823e --- /dev/null +++ b/apps/mobile/src/screens/FiatOnRampConnecting.tsx @@ -0,0 +1,184 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { skipToken } from '@reduxjs/toolkit/query/react' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleSheet } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { FiatOnRampStackParamList } from 'src/app/navigation/types' +import { Screen } from 'src/components/layout/Screen' +import { + FiatOnRampConnectingView, + SERVICE_PROVIDER_ICON_SIZE, +} from 'src/features/fiatOnRamp/FiatOnRampConnecting' +import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' +import { useFiatOnRampTransactionCreator } from 'src/features/fiatOnRamp/hooks' +import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils' +import { closeModal } from 'src/features/modals/modalSlice' +import { FiatOnRampScreens } from 'src/screens/Screens' +import { Flex, useIsDarkMode } from 'ui/src' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { isAndroid } from 'uniswap/src/utils/platform' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useTimeout } from 'utilities/src/time/timing' +import { ChainId } from 'wallet/src/constants/chains' +import { useFiatOnRampAggregatorWidgetQuery } from 'wallet/src/features/fiatOnRamp/api' +import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { ImageUri } from 'wallet/src/features/images/ImageUri' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { FiatOnRampEventName, ModalName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' + +// Design decision +const CONNECTING_TIMEOUT = 2 * ONE_SECOND_MS + +type Props = NativeStackScreenProps + +export function FiatOnRampConnectingScreen({ navigation }: Props): JSX.Element | null { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const { addFiatSymbolToNumber } = useLocalizationContext() + const [timeoutElapsed, setTimeoutElapsed] = useState(false) + const activeAccountAddress = useActiveAccountAddressWithThrow() + + const { + selectedQuote, + quotesSections, + serviceProviders, + countryCode, + baseCurrencyInfo, + quoteCurrency, + amount, + } = useFiatOnRampContext() + const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) + + const initialTypeInfo = useMemo( + () => ({ serviceProviderLogo: serviceProvider?.logos }), + [serviceProvider?.logos] + ) + + const { externalTransactionId, dispatchAddTransaction } = useFiatOnRampTransactionCreator( + activeAccountAddress, + quoteCurrency.currencyInfo?.currency.chainId ?? ChainId.Mainnet, + initialTypeInfo + ) + + const onError = useCallback((): void => { + dispatch( + pushNotification({ + type: AppNotificationType.Error, + errorMessage: t('common.error.general'), + }) + ) + navigation.goBack() + }, [dispatch, navigation, t]) + + const { + data: widgetData, + isLoading: widgetLoading, + error: widgetError, + } = useFiatOnRampAggregatorWidgetQuery( + serviceProvider && quoteCurrency?.currencyInfo?.currency.symbol && baseCurrencyInfo && amount + ? { + serviceProvider: serviceProvider.serviceProvider, + countryCode, + destinationCurrencyCode: quoteCurrency?.currencyInfo?.currency.symbol, + sourceAmount: amount, + sourceCurrencyCode: baseCurrencyInfo.code, + walletAddress: activeAccountAddress, + externalSessionId: externalTransactionId, + redirectUrl: `${ + isAndroid ? uniswapUrls.appUrl : uniswapUrls.appBaseUrl + }/?screen=transaction&fiatOnRamp=true&userAddress=${activeAccountAddress}`, + } + : skipToken + ) + + useTimeout(() => { + setTimeoutElapsed(true) + }, CONNECTING_TIMEOUT) + + useEffect(() => { + if (!baseCurrencyInfo || !serviceProvider || widgetError) { + onError() + return + } + async function navigateToWidget(widgetUrl: string): Promise { + dispatch(closeModal({ name: ModalName.FiatOnRampAggregator })) + if ( + serviceProvider && + quoteCurrency?.currencyInfo?.currency.symbol && + baseCurrencyInfo && + quotesSections?.[0]?.data?.[0] + ) { + sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampWidgetOpened, { + externalTransactionId, + serviceProvider: serviceProvider.serviceProvider, + preselectedServiceProvider: serviceProvider.serviceProvider, + countryCode, + fiatCurrency: baseCurrencyInfo?.code.toLowerCase(), + cryptoCurrency: quoteCurrency?.currencyInfo?.currency.symbol?.toLowerCase(), + }) + } + await openUri(widgetUrl).catch(onError) + dispatchAddTransaction() + } + + if (timeoutElapsed && !widgetLoading && widgetData) { + navigateToWidget(widgetData.widgetUrl).catch(() => undefined) + } + }, [ + navigation, + timeoutElapsed, + widgetData, + widgetLoading, + widgetError, + onError, + dispatchAddTransaction, + baseCurrencyInfo, + serviceProvider, + dispatch, + externalTransactionId, + quoteCurrency?.currencyInfo?.currency.symbol, + quotesSections, + countryCode, + ]) + + const isDarkMode = useIsDarkMode() + const logoUrl = getServiceProviderLogo(serviceProvider?.logos, isDarkMode) + + return ( + + {baseCurrencyInfo && serviceProvider ? ( + + + + } + serviceProviderName={serviceProvider.name} + /> + ) : null} + + ) +} + +const ServiceProviderLogoStyles = StyleSheet.create({ + icon: { + height: SERVICE_PROVIDER_ICON_SIZE, + width: SERVICE_PROVIDER_ICON_SIZE, + }, +}) diff --git a/apps/mobile/src/screens/FiatOnRampScreen.tsx b/apps/mobile/src/screens/FiatOnRampScreen.tsx new file mode 100644 index 0000000..6360a10 --- /dev/null +++ b/apps/mobile/src/screens/FiatOnRampScreen.tsx @@ -0,0 +1,363 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { ComponentProps, useEffect, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { TextInput, TextInputProps } from 'react-native' +import FastImage from 'react-native-fast-image' +import { FadeIn, FadeOut, FadeOutDown } from 'react-native-reanimated' +import { useAppDispatch, useShouldShowNativeKeyboard } from 'src/app/hooks' +import { FiatOnRampStackParamList } from 'src/app/navigation/types' +import { FiatOnRampCtaButton } from 'src/components/fiatOnRamp/CtaButton' +import { Screen } from 'src/components/layout/Screen' +import { FiatOnRampAmountSection } from 'src/features/fiatOnRamp/FiatOnRampAmountSection' +import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' +import { FiatOnRampCountryListModal } from 'src/features/fiatOnRamp/FiatOnRampCountryListModal' +import { FiatOnRampCountryPicker } from 'src/features/fiatOnRamp/FiatOnRampCountryPicker' +import { FiatOnRampTokenSelectorModal } from 'src/features/fiatOnRamp/FiatOnRampTokenSelector' +import { + useFiatOnRampQuotes, + useMeldFiatCurrencySupportInfo, + useParseFiatOnRampError, +} from 'src/features/fiatOnRamp/aggregatorHooks' +import { useFiatOnRampSupportedTokens } from 'src/features/fiatOnRamp/hooks' +import { FiatOnRampCurrency, InitialQuoteSelection } from 'src/features/fiatOnRamp/types' +import { FiatOnRampScreens } from 'src/screens/Screens' +import { AnimatedFlex, Flex, Text, useIsDarkMode } from 'ui/src' +import { usePrevious } from 'utilities/src/react/hooks' +import { DEFAULT_DELAY, useDebounce } from 'utilities/src/time/timing' +import { DecimalPadLegacy } from 'wallet/src/components/legacy/DecimalPadLegacy' +import { useBottomSheetContext } from 'wallet/src/components/modals/BottomSheetContext' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { + useFiatOnRampAggregatorGetCountryQuery, + useFiatOnRampAggregatorServiceProvidersQuery, + useFiatOnRampAggregatorTransactionsQuery, +} from 'wallet/src/features/fiatOnRamp/api' +import { FORQuote, FORServiceProvider, FORTransaction } from 'wallet/src/features/fiatOnRamp/types' +import { getServiceProviderLogo } from 'wallet/src/features/fiatOnRamp/utils' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType } from 'wallet/src/features/notifications/types' +import { sendWalletAnalyticsEvent } from 'wallet/src/telemetry' +import { FiatOnRampEventName } from 'wallet/src/telemetry/constants' +import { WalletEventProperties } from 'wallet/src/telemetry/types' + +type Props = NativeStackScreenProps + +function selectInitialQuote( + quotes: FORQuote[] | undefined, + lastTransaction: FORTransaction | undefined +): { quote: FORQuote | undefined; type: InitialQuoteSelection | undefined } { + const lastUsedServiceProvider = lastTransaction?.serviceProvider + if (lastUsedServiceProvider) { + const quote = quotes?.filter((q) => q.serviceProvider === lastUsedServiceProvider)[0] + if (quote) { + return { + quote, + type: InitialQuoteSelection.MostRecent, + } + } + } + const bestQuote = quotes && quotes.length && quotes[0] + if (bestQuote) { + return { + quote: quotes.reduce((prev, curr) => { + return curr.destinationAmount > prev.destinationAmount ? curr : prev + }, bestQuote), + type: InitialQuoteSelection.Best, + } + } + return { quote: undefined, type: undefined } +} + +function preloadServiceProviderLogos( + serviceProviders: FORServiceProvider[], + isDarkMode: boolean +): void { + FastImage.preload( + serviceProviders + .map((sp) => ({ uri: getServiceProviderLogo(sp.logos, isDarkMode) })) + .filter((sp) => !!sp.uri) + ) +} + +export function FiatOnRampScreen({ navigation }: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() + const [selection, setSelection] = useState() + const [value, setValue] = useState('') + const [showTokenSelector, setShowTokenSelector] = useState(false) + const inputRef = useRef(null) + const [selectingCountry, setSelectingCountry] = useState(false) + + const { isSheetReady } = useBottomSheetContext() + + const { + selectedQuote, + setSelectedQuote, + setQuotesSections, + countryCode, + setCountryCode, + amount, + setAmount, + setBaseCurrencyInfo, + setServiceProviders, + quoteCurrency, + setQuoteCurrency, + } = useFiatOnRampContext() + + const resetSelection = (start: number, end?: number): void => { + setSelection({ start, end: end ?? start }) + } + + const { showNativeKeyboard, onDecimalPadLayout, isLayoutPending, onInputPanelLayout } = + useShouldShowNativeKeyboard() + + const { appFiatCurrencySupportedInMeld, meldSupportedFiatCurrency } = + useMeldFiatCurrencySupportInfo(countryCode) + + const debouncedAmount = useDebounce(amount, DEFAULT_DELAY * 2) + const { + error: quotesError, + loading: quotesLoading, + quotes, + } = useFiatOnRampQuotes({ + baseCurrencyAmount: debouncedAmount, + baseCurrencyCode: meldSupportedFiatCurrency.code, + quoteCurrencyCode: quoteCurrency.currencyInfo?.currency.symbol, + countryCode, + }) + + const selectTokenLoading = quotesLoading || amount !== debouncedAmount + + const { currentData: ipCountryData } = useFiatOnRampAggregatorGetCountryQuery() + + useEffect(() => { + if (ipCountryData) { + setCountryCode(ipCountryData.countryCode) + } + }, [ipCountryData, setCountryCode]) + + const { + currentData: serviceProvidersResponse, + isFetching: serviceProvidersLoading, + error: serviceProvidersError, + } = useFiatOnRampAggregatorServiceProvidersQuery() + + // preload service provider logos for given quotes for the next screen + useEffect(() => { + if (serviceProvidersResponse?.serviceProviders && quotes) { + const quotesServiceProviderNames = quotes.map((q) => q.serviceProvider) + const serviceProviders = serviceProvidersResponse.serviceProviders.filter( + (sp) => quotesServiceProviderNames.indexOf(sp.serviceProvider) !== -1 + ) + preloadServiceProviderLogos(serviceProviders, isDarkMode) + } + }, [serviceProvidersResponse, quotes, isDarkMode]) + + const { currentData: transactionsResponse } = useFiatOnRampAggregatorTransactionsQuery({ + limit: 1, + }) + + const { errorText, errorColor } = useParseFiatOnRampError( + quotesError || serviceProvidersError, + meldSupportedFiatCurrency.code + ) + + const prevQuotes = usePrevious(quotes) + useEffect(() => { + if (quotes && (!selectedQuote || prevQuotes !== quotes)) { + const { quote, type } = selectInitialQuote(quotes, transactionsResponse?.transactions[0]) + if (!quote) { + return + } + const otherQuotes = quotes.filter((item) => item !== quote) + setQuotesSections([ + { data: [quote], type }, + ...(otherQuotes.length ? [{ data: otherQuotes }] : []), + ]) + setSelectedQuote(quote) + } + }, [ + prevQuotes, + quotes, + selectedQuote, + setQuotesSections, + setSelectedQuote, + t, + transactionsResponse?.transactions, + ]) + + useEffect(() => { + if (!quotes && (quotesError || serviceProvidersError || !amount)) { + setQuotesSections(undefined) + setSelectedQuote(undefined) + } + }, [amount, quotesError, serviceProvidersError, quotes, setQuotesSections, setSelectedQuote]) + + const onSelectCountry: ComponentProps['onSelectCountry'] = ( + country + ): void => { + dispatch( + pushNotification({ + type: AppNotificationType.ChooseCountry, + countryName: country.displayName, + countryCode: country.countryCode, + }) + ) + setSelectingCountry(false) + setCountryCode(country.countryCode) + } + + const onChangeValue = + (source: WalletEventProperties[FiatOnRampEventName.FiatOnRampAmountEntered]['source']) => + (newAmount: string): void => { + sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampAmountEntered, { + source, + }) + setValue(newAmount) + setAmount(newAmount ? parseFloat(newAmount) : 0) + } + + // hide keyboard when user goes to token selector screen + useEffect(() => { + if (showTokenSelector) { + inputRef.current?.blur() + } else if (showNativeKeyboard) { + inputRef.current?.focus() + } + }, [showNativeKeyboard, showTokenSelector]) + + // we only show loading when there are no errors and quote value is not empty + const buttonDisabled = + serviceProvidersLoading || + !!serviceProvidersError || + selectTokenLoading || + !!quotesError || + !selectedQuote?.destinationAmount + + const onContinue = (): void => { + if ( + quotes && + serviceProvidersResponse?.serviceProviders && + serviceProvidersResponse?.serviceProviders.length > 0 && + quoteCurrency?.currencyInfo?.currency + ) { + setBaseCurrencyInfo(meldSupportedFiatCurrency) + setServiceProviders(serviceProvidersResponse.serviceProviders) + navigation.navigate(FiatOnRampScreens.ServiceProviders) + } + } + + const { + list: supportedTokensList, + loading: supportedTokensLoading, + error: supportedTokensError, + refetch: supportedTokensRefetch, + } = useFiatOnRampSupportedTokens({ + sourceCurrencyCode: meldSupportedFiatCurrency.code, + countryCode, + }) + + const onSelectCurrency = (newCurrency: FiatOnRampCurrency): void => { + setQuoteCurrency(newCurrency) + setShowTokenSelector(false) + if (newCurrency.currencyInfo?.currency.symbol) { + sendWalletAnalyticsEvent(FiatOnRampEventName.FiatOnRampTokenSelected, { + token: newCurrency.currencyInfo.currency.symbol.toLowerCase(), + }) + } + } + + return ( + + + + {isSheetReady && ( + + + {t('common.button.buy')} + { + setSelectingCountry(true) + }} + /> + + { + setShowTokenSelector(true) + }} + /> + + {!showNativeKeyboard && ( + + )} + + + + )} + + {selectingCountry && countryCode && ( + { + setSelectingCountry(false) + }} + onSelectCountry={onSelectCountry} + /> + )} + {showTokenSelector && countryCode && ( + setShowTokenSelector(false)} + onRetry={supportedTokensRefetch} + onSelectCurrency={onSelectCurrency} + /> + )} + + ) +} diff --git a/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx b/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx new file mode 100644 index 0000000..eac1dbe --- /dev/null +++ b/apps/mobile/src/screens/FiatOnRampServiceProviders.tsx @@ -0,0 +1,163 @@ +import { BottomSheetSectionList } from '@gorhom/bottom-sheet' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItemInfo, SectionListData } from 'react-native' +import { FadeIn, FadeOut } from 'react-native-reanimated' +import { FiatOnRampStackParamList } from 'src/app/navigation/types' +import { BackButton } from 'src/components/buttons/BackButton' +import { FORQuoteItem } from 'src/components/fiatOnRamp/QuoteItem' +import { Screen } from 'src/components/layout/Screen' +import { useFiatOnRampContext } from 'src/features/fiatOnRamp/FiatOnRampContext' +import { InitialQuoteSelection } from 'src/features/fiatOnRamp/types' +import { getServiceProviderForQuote } from 'src/features/fiatOnRamp/utils' +import { FiatOnRampScreens } from 'src/screens/Screens' +import { + AnimatedFlex, + Button, + ColorTokens, + Flex, + GeneratedIcon, + Icons, + Inset, + Separator, + Text, +} from 'ui/src' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { useBottomSheetFocusHook } from 'wallet/src/components/modals/hooks' +import { FORQuote } from 'wallet/src/features/fiatOnRamp/types' + +type Props = NativeStackScreenProps + +const key = (item: FORQuote): string => item.serviceProvider + +function SectionHeader({ + Icon, + title, + iconColor, +}: { + Icon: GeneratedIcon + title: string + iconColor: ColorTokens +}): JSX.Element { + return ( + + + + {title} + + + ) +} + +export function FiatOnRampServiceProvidersScreen({ navigation }: Props): JSX.Element { + const { t } = useTranslation() + const { + selectedQuote, + setSelectedQuote, + quotesSections, + quoteCurrency, + baseCurrencyInfo, + serviceProviders, + } = useFiatOnRampContext() + + const renderItem = ({ item }: ListRenderItemInfo): JSX.Element => { + return ( + + {baseCurrencyInfo && ( + { + setSelectedQuote(item) + }} + /> + )} + + ) + } + + const renderSectionHeader = ({ + section: { type }, + }: { + section: SectionListData + }): JSX.Element => ( + + {type === InitialQuoteSelection.Best ? ( + + ) : type === InitialQuoteSelection.MostRecent ? ( + + ) : ( + + + + {t('fiatOnRamp.quote.type.other')} + + + + )} + + ) + + const onContinue = (): void => { + const serviceProvider = getServiceProviderForQuote(selectedQuote, serviceProviders) + if (serviceProvider) { + navigation.navigate(FiatOnRampScreens.Connecting) + } + } + + return ( + + + + + + + {t('fiatOnRamp.checkout.title')} + + + + + + } + ListFooterComponent={} + focusHook={useBottomSheetFocusHook} + keyExtractor={key} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="always" + renderItem={renderItem} + renderSectionHeader={renderSectionHeader} + sections={quotesSections ?? []} + showsVerticalScrollIndicator={false} + stickySectionHeadersEnabled={false} + windowSize={5} + /> + + + + + + ) +} diff --git a/apps/mobile/src/screens/HomeScreen.tsx b/apps/mobile/src/screens/HomeScreen.tsx new file mode 100644 index 0000000..16cee3a --- /dev/null +++ b/apps/mobile/src/screens/HomeScreen.tsx @@ -0,0 +1,800 @@ +/* eslint-disable max-lines */ +import { useApolloClient } from '@apollo/client' +import { useIsFocused, useScrollToTop } from '@react-navigation/native' +import { FlashList } from '@shopify/flash-list' +import { impactAsync } from 'expo-haptics' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Freeze } from 'react-freeze' +import { useTranslation } from 'react-i18next' +import { FlatList, StyleProp, View, ViewProps, ViewStyle } from 'react-native' +import Animated, { + FadeIn, + FadeOut, + interpolateColor, + useAnimatedRef, + useAnimatedScrollHandler, + useAnimatedStyle, + useDerivedValue, + useSharedValue, +} from 'react-native-reanimated' +import { SvgProps } from 'react-native-svg' +import { SceneRendererProps, TabBar } from 'react-native-tab-view' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { UniconsV2Modal } from 'src/app/modals/UniconsV2Modal' +import { NavBar, SWAP_BUTTON_HEIGHT } from 'src/app/navigation/NavBar' +import { AppStackScreenProp } from 'src/app/navigation/types' +import { ScannerModalState } from 'src/components/QRCodeScanner/constants' +import Trace from 'src/components/Trace/Trace' +import TraceTabView from 'src/components/Trace/TraceTabView' +import { AccountHeader } from 'src/components/accounts/AccountHeader' +import { ACTIVITY_TAB_DATA_DEPENDENCIES, ActivityTab } from 'src/components/home/ActivityTab' +import { FEED_TAB_DATA_DEPENDENCIES, FeedTab } from 'src/components/home/FeedTab' +import { NFTS_TAB_DATA_DEPENDENCIES, NftsTab } from 'src/components/home/NftsTab' +import { TOKENS_TAB_DATA_DEPENDENCIES, TokensTab } from 'src/components/home/TokensTab' +import { Screen } from 'src/components/layout/Screen' +import { + HeaderConfig, + ScrollPair, + TAB_BAR_HEIGHT, + TAB_STYLES, + TAB_VIEW_SCROLL_THROTTLE, + TabContentProps, + renderTabLabel, + useScrollSync, +} from 'src/components/layout/TabHelpers' +import { UnitagBanner } from 'src/components/unitags/UnitagBanner' +import { PortfolioBalance } from 'src/features/balances/PortfolioBalance' +import { openModal } from 'src/features/modals/modalSlice' +import { selectSomeModalOpen } from 'src/features/modals/selectSomeModalOpen' +import { MobileEventName } from 'src/features/telemetry/constants' +import { useHeartbeatReporter, useLastBalancesReporter } from 'src/features/telemetry/hooks' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { removePendingSession } from 'src/features/walletConnect/walletConnectSlice' +import { Screens } from 'src/screens/Screens' +import { hideSplashScreen } from 'src/utils/splashScreen' +import { + AnimatedFlex, + Flex, + Text, + TouchableArea, + useDeviceDimensions, + useDeviceInsets, + useMedia, + useSporeColors, +} from 'ui/src' +import ReceiveIcon from 'ui/src/assets/icons/arrow-down-circle.svg' +import BuyIcon from 'ui/src/assets/icons/buy.svg' +import ScanIcon from 'ui/src/assets/icons/scan-home.svg' +import SendIcon from 'ui/src/assets/icons/send-action.svg' +import { iconSizes, spacing } from 'ui/src/theme' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useInterval, useTimeout } from 'utilities/src/time/timing' +import { + selectHasSkippedUnitagPrompt, + selectHasViewedUniconV2IntroModal, +} from 'wallet/src/features/behaviorHistory/selectors' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useSelectAddressHasNotifications } from 'wallet/src/features/notifications/hooks' +import { setNotificationStatus } from 'wallet/src/features/notifications/slice' +import { TokenBalanceListRow } from 'wallet/src/features/portfolio/TokenBalanceListContext' +import { useCanActiveAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { + useActiveAccountWithThrow, + useAvatar, + useNonPendingSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { selectFinishedOnboarding } from 'wallet/src/features/wallet/selectors' +import { + ElementName, + ElementNameType, + ModalName, + SectionName, + SectionNameType, +} from 'wallet/src/telemetry/constants' +import { HomeScreenTabIndex } from './HomeScreenTabIndex' + +const CONTENT_HEADER_HEIGHT_ESTIMATE = 270 + +/** + * Home Screen hosts both Tokens and NFTs Tab + * Manages TokensTabs and NftsTab scroll offsets when header is collapsed + * Borrowed from: https://stormotion.io/blog/how-to-create-collapsing-tab-header-using-react-native/ + */ +export function HomeScreen(props?: AppStackScreenProp): JSX.Element { + const activeAccount = useActiveAccountWithThrow() + const { t } = useTranslation() + const colors = useSporeColors() + const media = useMedia() + const insets = useDeviceInsets() + const dimensions = useDeviceDimensions() + const dispatch = useAppDispatch() + const isFocused = useIsFocused() + const isModalOpen = useAppSelector(selectSomeModalOpen) + const isHomeScreenBlur = !isFocused || isModalOpen + const { avatar, loading: avatarLoading } = useAvatar(activeAccount.address) + const hasAvatar = !!avatar && !avatarLoading + + // Ensure if a user is here and has completed onboarding, they have at least one non-pending signer account + const finishedOnboarding = useAppSelector(selectFinishedOnboarding) + const nonPendingSignerAccounts = useNonPendingSignerAccounts() + useEffect(() => { + if (finishedOnboarding && activeAccount.pending && nonPendingSignerAccounts.length === 0) { + dispatch(pendingAccountActions.trigger(PendingAccountActions.ActivateOneAndDelete)) + } + }, [activeAccount, dispatch, finishedOnboarding, nonPendingSignerAccounts.length]) + + const hasSkippedUnitagPrompt = useAppSelector(selectHasSkippedUnitagPrompt) + + const hasViewedUniconV2IntroModal = useAppSelector(selectHasViewedUniconV2IntroModal) + + const showFeedTab = useFeatureFlag(FEATURE_FLAGS.FeedTab) + // opens the wallet restore modal if recovery phrase is missing after the app is opened + useWalletRestore({ openModalImmediately: true }) + + // Record a heartbeat for anonymous user DAU + const heartbeatReporter = useHeartbeatReporter() + useInterval(heartbeatReporter, ONE_SECOND_MS * 15, true) + + // Report balances at most every 24 hours, checking every 15 seconds when app is open + const lastBalancesReporter = useLastBalancesReporter() + useInterval(lastBalancesReporter, ONE_SECOND_MS * 15, true) + + const listBottomPadding = media.short ? spacing.spacing36 : spacing.spacing12 + + const [tabIndex, setTabIndex] = useState(props?.route?.params?.tab ?? HomeScreenTabIndex.Tokens) + // Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo + const tokensTitle = t('home.tokens.title') + const nftsTitle = t('home.nfts.title') + const activityTitle = t('home.activity.title') + const feedTitle = t('home.feed.title') + + const routes = useMemo(() => { + const tabs: Array<{ key: SectionNameType; title: string }> = [ + { key: SectionName.HomeTokensTab, title: tokensTitle }, + { key: SectionName.HomeNFTsTab, title: nftsTitle }, + { key: SectionName.HomeActivityTab, title: activityTitle }, + ] + + if (showFeedTab) { + tabs.push({ key: SectionName.HomeFeedTab, title: feedTitle }) + } + + return tabs + }, [tokensTitle, nftsTitle, activityTitle, feedTitle, showFeedTab]) + + useEffect( + function syncTabIndex() { + const newTabIndex = props?.route.params?.tab + if (newTabIndex === undefined) { + return + } + setTabIndex(newTabIndex) + }, + [props?.route.params?.tab] + ) + + const [isLayoutReady, setIsLayoutReady] = useState(false) + + const [headerHeight, setHeaderHeight] = useState(CONTENT_HEADER_HEIGHT_ESTIMATE) + const headerConfig = useMemo( + () => ({ + heightCollapsed: insets.top, + heightExpanded: headerHeight, + }), + [headerHeight, insets.top] + ) + const { heightCollapsed, heightExpanded } = headerConfig + const headerHeightDiff = heightExpanded - heightCollapsed + + const handleHeaderLayout = useCallback>((event) => { + setHeaderHeight(event.nativeEvent.layout.height) + setIsLayoutReady(true) + }, []) + + const tokensTabScrollValue = useSharedValue(0) + const tokensTabScrollHandler = useAnimatedScrollHandler( + (event) => (tokensTabScrollValue.value = event.contentOffset.y) + ) + const nftsTabScrollValue = useSharedValue(0) + const nftsTabScrollHandler = useAnimatedScrollHandler( + (event) => (nftsTabScrollValue.value = event.contentOffset.y) + ) + const activityTabScrollValue = useSharedValue(0) + const activityTabScrollHandler = useAnimatedScrollHandler( + (event) => (activityTabScrollValue.value = event.contentOffset.y) + ) + const feedTabScrollValue = useSharedValue(0) + const feedTabScrollHandler = useAnimatedScrollHandler( + (event) => (feedTabScrollValue.value = event.contentOffset.y) + ) + + const tokensTabScrollRef = useAnimatedRef>() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const nftsTabScrollRef = useAnimatedRef>() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const activityTabScrollRef = useAnimatedRef>() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const feedTabScrollRef = useAnimatedRef>() + + const currentScrollValue = useDerivedValue(() => { + if (tabIndex === HomeScreenTabIndex.Tokens) { + return tokensTabScrollValue.value + } else if (tabIndex === HomeScreenTabIndex.NFTs) { + return nftsTabScrollValue.value + } else if (tabIndex === HomeScreenTabIndex.Activity) { + return activityTabScrollValue.value + } + return feedTabScrollValue.value + }, [ + activityTabScrollValue, + feedTabScrollValue, + nftsTabScrollValue, + tabIndex, + tokensTabScrollValue, + ]) + + // clear the notification indicator if the user is on the activity tab + const hasNotifications = useSelectAddressHasNotifications(activeAccount.address) + useEffect(() => { + if (tabIndex === 2 && hasNotifications) { + dispatch(setNotificationStatus({ address: activeAccount.address, hasNotifications: false })) + } + }, [dispatch, activeAccount.address, tabIndex, hasNotifications]) + + // If accounts are switched, we want to scroll to top and show full header + useEffect(() => { + nftsTabScrollValue.value = 0 + tokensTabScrollValue.value = 0 + activityTabScrollValue.value = 0 + feedTabScrollValue.value = 0 + nftsTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + tokensTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + activityTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + feedTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + }, [ + activeAccount, + activityTabScrollRef, + activityTabScrollValue, + nftsTabScrollRef, + nftsTabScrollValue, + tokensTabScrollRef, + tokensTabScrollValue, + feedTabScrollRef, + feedTabScrollValue, + ]) + + // Need to create a derived value for tab index so it can be referenced from a static ref + const currentTabIndex = useDerivedValue(() => tabIndex, [tabIndex]) + const isNftTabsAtTop = useDerivedValue(() => nftsTabScrollValue.value === 0) + const isActivityTabAtTop = useDerivedValue(() => activityTabScrollValue.value === 0) + + useScrollToTop( + useRef({ + scrollToTop: () => { + if (currentTabIndex.value === HomeScreenTabIndex.NFTs && isNftTabsAtTop.value) { + setTabIndex(HomeScreenTabIndex.Tokens) + } else if (currentTabIndex.value === HomeScreenTabIndex.NFTs) { + nftsTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + } else if ( + currentTabIndex.value === HomeScreenTabIndex.Activity && + isActivityTabAtTop.value + ) { + setTabIndex(HomeScreenTabIndex.NFTs) + } else if (currentTabIndex.value === HomeScreenTabIndex.Activity) { + activityTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + } else { + tokensTabScrollRef.current?.scrollToOffset({ offset: 0, animated: true }) + } + }, + }) + ) + + const translateY = useDerivedValue(() => { + // Allow header to scroll vertically with list + return -Math.min(currentScrollValue.value, headerHeightDiff) + }) + + const translatedStyle = useAnimatedStyle(() => ({ + transform: [{ translateY: translateY.value }], + })) + + const scrollPairs = useMemo( + () => [ + { list: tokensTabScrollRef, position: tokensTabScrollValue, index: 0 }, + { list: nftsTabScrollRef, position: nftsTabScrollValue, index: 1 }, + { list: activityTabScrollRef, position: activityTabScrollValue, index: 2 }, + { list: feedTabScrollRef, position: feedTabScrollValue, index: 3 }, + ], + [ + activityTabScrollRef, + activityTabScrollValue, + feedTabScrollRef, + feedTabScrollValue, + nftsTabScrollRef, + nftsTabScrollValue, + tokensTabScrollRef, + tokensTabScrollValue, + ] + ) + + const { sync } = useScrollSync(currentTabIndex, scrollPairs, headerConfig) + + const forAggregatorEnabled = useFeatureFlag(FEATURE_FLAGS.ForAggregator) + + const onPressBuy = useCallback( + () => + dispatch( + openModal({ + name: forAggregatorEnabled ? ModalName.FiatOnRampAggregator : ModalName.FiatOnRamp, + }) + ), + [dispatch, forAggregatorEnabled] + ) + const onPressScan = useCallback(() => { + // in case we received a pending session from a previous scan after closing modal + dispatch(removePendingSession()) + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.ScanQr }) + ) + }, [dispatch]) + const onPressSend = useCallback(() => dispatch(openModal({ name: ModalName.Send })), [dispatch]) + const onPressReceive = useCallback(() => { + if (forAggregatorEnabled) { + dispatch(openModal({ name: ModalName.ReceiveCryptoModal })) + } else { + dispatch( + openModal({ name: ModalName.WalletConnectScan, initialState: ScannerModalState.WalletQr }) + ) + } + }, [dispatch, forAggregatorEnabled]) + const onPressViewOnlyLabel = useCallback( + () => dispatch(openModal({ name: ModalName.ViewOnlyExplainer })), + [dispatch] + ) + + // Hide actions when active account isn't a signer account. + const isSignerAccount = activeAccount.type === AccountType.SignerMnemonic + // Necessary to declare these as direct dependencies due to race condition with initializing react-i18next and useMemo + const buyLabel = t('home.label.buy') + const sendLabel = t('home.label.send') + const receiveLabel = t('home.label.receive') + const scanLabel = t('home.label.scan') + + const actions = useMemo( + (): QuickAction[] => [ + { + Icon: BuyIcon, + eventName: MobileEventName.FiatOnRampQuickActionButtonPressed, + iconScale: 1.2, + label: buyLabel, + name: ElementName.Buy, + sentryLabel: 'BuyActionButton', + onPress: onPressBuy, + }, + { + Icon: SendIcon, + iconScale: 1.1, + label: sendLabel, + name: ElementName.Send, + sentryLabel: 'SendActionButton', + onPress: onPressSend, + }, + { + Icon: ReceiveIcon, + label: receiveLabel, + name: ElementName.Receive, + sentryLabel: 'ReceiveActionButton', + onPress: onPressReceive, + }, + { + Icon: ScanIcon, + label: scanLabel, + name: ElementName.WalletConnectScan, + sentryLabel: 'ScanActionButton', + onPress: onPressScan, + }, + ], + [ + buyLabel, + sendLabel, + scanLabel, + receiveLabel, + onPressBuy, + onPressScan, + onPressSend, + onPressReceive, + ] + ) + + const { canClaimUnitag } = useCanActiveAddressClaimUnitag() + + const shouldPromptUnitag = + activeAccount.type === AccountType.SignerMnemonic && !hasSkippedUnitagPrompt && canClaimUnitag + + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + const shouldShowUniconV2Modal = + isUniconsV2Enabled && !hasViewedUniconV2IntroModal && !hasAvatar && !avatarLoading + + const viewOnlyLabel = t('home.warning.viewOnly') + const contentHeader = useMemo(() => { + return ( + + + + + + {isSignerAccount ? ( + + ) : ( + + + + {viewOnlyLabel} + + + + )} + {shouldPromptUnitag && ( + + + + )} + + ) + }, [ + activeAccount.address, + isSignerAccount, + actions, + onPressViewOnlyLabel, + viewOnlyLabel, + shouldPromptUnitag, + ]) + + const contentContainerStyle = useMemo>( + () => ({ + paddingTop: headerHeight + TAB_BAR_HEIGHT + TAB_STYLES.tabListInner.paddingTop, + paddingBottom: + insets.bottom + + SWAP_BUTTON_HEIGHT + + TAB_STYLES.tabListInner.paddingBottom + + listBottomPadding, + }), + [headerHeight, insets.bottom, listBottomPadding] + ) + + const loadingContainerStyle = useMemo>( + () => ({ + paddingTop: headerHeight + TAB_BAR_HEIGHT + TAB_STYLES.tabListInner.paddingTop, + paddingBottom: insets.bottom, + }), + [headerHeight, insets.bottom] + ) + + const emptyContainerStyle = useMemo>( + () => ({ + paddingTop: media.short ? spacing.none : spacing.spacing60, + paddingBottom: insets.bottom, + paddingHorizontal: media.short ? spacing.spacing12 : spacing.spacing48, + }), + [insets.bottom, media.short] + ) + + const sharedProps = useMemo( + () => ({ + loadingContainerStyle, + emptyContainerStyle, + contentContainerStyle, + onMomentumScrollEnd: sync, + onScrollEndDrag: sync, + scrollEventThrottle: TAB_VIEW_SCROLL_THROTTLE, + }), + [contentContainerStyle, emptyContainerStyle, loadingContainerStyle, sync] + ) + + const tabBarStyle = useMemo>( + () => [{ top: headerHeight }, translatedStyle], + [headerHeight, translatedStyle] + ) + + const headerContainerStyle = useMemo>( + () => [TAB_STYLES.headerContainer, { paddingTop: insets.top }, translatedStyle], + [insets.top, translatedStyle] + ) + + const statusBarStyle = useAnimatedStyle(() => ({ + backgroundColor: interpolateColor( + currentScrollValue.value, + [0, headerHeightDiff], + [colors.surface1.val, colors.surface1.val] + ), + })) + + const apolloClient = useApolloClient() + + const renderTabBar = useCallback( + (sceneProps: SceneRendererProps) => { + const style = { width: 'auto' } + return ( + <> + + {contentHeader} + + + {isLayoutReady && ( + + => { + await impactAsync() + }} + /> + + )} + + ) + }, + [ + colors.surface1, + colors.surface3, + contentHeader, + handleHeaderLayout, + headerContainerStyle, + isLayoutReady, + routes, + tabBarStyle, + tabIndex, + ] + ) + + const [refreshing, setRefreshing] = useState(false) + + const onRefreshHomeData = useCallback(async () => { + setRefreshing(true) + + await apolloClient.refetchQueries({ + include: [ + ...TOKENS_TAB_DATA_DEPENDENCIES, + ...NFTS_TAB_DATA_DEPENDENCIES, + ...ACTIVITY_TAB_DATA_DEPENDENCIES, + ...(showFeedTab ? FEED_TAB_DATA_DEPENDENCIES : []), + ], + }) + + // Artificially delay 0.5 second to show the refresh animation + const timeout = setTimeout(() => setRefreshing(false), 500) + return () => clearTimeout(timeout) + }, [apolloClient, showFeedTab]) + + const renderTab = useCallback( + ({ + route, + }: { + route: { + key: SectionNameType + title: string + } + }) => { + switch (route?.key) { + case SectionName.HomeTokensTab: + return ( + + {isLayoutReady && ( + + + + )} + + ) + case SectionName.HomeNFTsTab: + return ( + + + + ) + case SectionName.HomeActivityTab: + return ( + + + + ) + case SectionName.HomeFeedTab: + return ( + + ) + } + return null + }, + [ + tabIndex, + isHomeScreenBlur, + isLayoutReady, + tokensTabScrollRef, + sharedProps, + headerHeight, + activeAccount?.address, + refreshing, + tokensTabScrollHandler, + onRefreshHomeData, + nftsTabScrollRef, + nftsTabScrollHandler, + activityTabScrollRef, + activityTabScrollHandler, + feedTabScrollRef, + feedTabScrollHandler, + ] + ) + + // Hides lock screen on next js render cycle, ensuring this component is loaded when the screen is hidden + useTimeout(hideSplashScreen, 1) + + return ( + + + + + + + {shouldShowUniconV2Modal && ( + <> + + {/* manual scrim so we can highlight unicon above it */} + + + )} + + ) +} + +type QuickAction = { + Icon: React.FC + eventName?: MobileEventName + iconScale?: number + label: string + name: ElementNameType + sentryLabel: string + onPress: () => void +} + +function QuickActions({ actions }: { actions: QuickAction[] }): JSX.Element { + return ( + + {actions.map((action) => ( + + ))} + + ) +} + +function ActionButton({ + eventName, + name, + Icon, + onPress, + flex, + activeScale = 0.96, + iconScale = 1, +}: { + eventName?: MobileEventName + name: ElementNameType + label: string + Icon: React.FC + onPress: () => void + flex: number + activeScale?: number + iconScale?: number +}): JSX.Element { + const colors = useSporeColors() + const media = useMedia() + const iconSize = media.short ? iconSizes.icon24 : iconSizes.icon28 + + return ( + + + + + + + + ) +} diff --git a/apps/mobile/src/screens/HomeScreenTabIndex.tsx b/apps/mobile/src/screens/HomeScreenTabIndex.tsx new file mode 100644 index 0000000..b6107fb --- /dev/null +++ b/apps/mobile/src/screens/HomeScreenTabIndex.tsx @@ -0,0 +1,6 @@ +export enum HomeScreenTabIndex { + Tokens = 0, + NFTs = 1, + Activity = 2, + Feed = 3, +} diff --git a/apps/mobile/src/screens/Import/ImportMethodScreen.tsx b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx new file mode 100644 index 0000000..abd1e55 --- /dev/null +++ b/apps/mobile/src/screens/Import/ImportMethodScreen.tsx @@ -0,0 +1,166 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { OptionCard } from 'src/features/onboarding/OptionCard' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import EyeIcon from 'ui/src/assets/icons/eye.svg' +import { useIsDarkMode } from 'ui/src/hooks/useIsDarkMode' +import { AppTFunction } from 'ui/src/i18n/types' +import { iconSizes } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { ElementName, ElementNameType } from 'wallet/src/telemetry/constants' +import { openSettings } from 'wallet/src/utils/linking' + +interface ImportMethodOption { + title: (t: AppTFunction) => string + blurb: (t: AppTFunction) => string + icon: React.ReactNode + nav: OnboardingScreens + importType: ImportType + name: ElementNameType +} + +const options: ImportMethodOption[] = [ + { + title: (t: AppTFunction) => t('onboarding.import.method.import.title'), + blurb: (t: AppTFunction) => t('onboarding.import.method.import.message'), + icon: , + nav: OnboardingScreens.SeedPhraseInput, + importType: ImportType.SeedPhrase, + name: ElementName.OnboardingImportSeedPhrase, + }, + { + title: (t: AppTFunction) => t('onboarding.import.method.restore.title'), + blurb: (t: AppTFunction) => + isAndroid + ? t(`onboarding.import.method.restore.message.android`) + : t(`onboarding.import.method.restore.message.ios`), + icon: , + nav: OnboardingScreens.RestoreCloudBackup, + importType: ImportType.Restore, + name: ElementName.RestoreFromCloud, + }, +] + +type Props = NativeStackScreenProps + +export function ImportMethodScreen({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const dispatch = useAppDispatch() + const entryPoint = params?.entryPoint + + useAddBackButton(navigation) + + const handleOnPressRestoreBackup = async (): Promise => { + const cloudStorageAvailable = await isCloudStorageAvailable() + + if (!cloudStorageAvailable) { + Alert.alert( + isAndroid + ? t('account.cloud.error.unavailable.title.android') + : t('account.cloud.error.unavailable.title.ios'), + isAndroid + ? t('account.cloud.error.unavailable.message.android') + : t('account.cloud.error.unavailable.message.ios'), + [ + { + text: t('account.cloud.error.unavailable.button.settings'), + onPress: openSettings, + style: 'default', + }, + { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, + ] + ) + return + } + + navigation.navigate({ + name: OnboardingScreens.RestoreCloudBackupLoading, + params: { importType: ImportType.Restore, entryPoint }, + merge: true, + }) + } + + const handleOnPress = async (nav: OnboardingScreens, importType: ImportType): Promise => { + // Delete any pending accounts before entering flow. + dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) + + if (importType === ImportType.Restore) { + await handleOnPressRestoreBackup() + return + } + + navigation.navigate({ + name: nav, + params: { importType, entryPoint }, + merge: true, + }) + } + + const importOptions = + entryPoint === OnboardingEntryPoint.Sidebar + ? options.filter((option) => option.name !== ElementName.RestoreFromCloud) + : options + + return ( + + + {importOptions.map(({ title, blurb, icon, nav, importType, name }, i) => ( + => handleOnPress(nav, importType)} + /> + ))} + + + + + + => + handleOnPress(OnboardingScreens.WatchWallet, ImportType.Watch) + }> + {t('account.wallet.button.watch')} + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx new file mode 100644 index 0000000..8bf2df9 --- /dev/null +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupLoadingScreen.tsx @@ -0,0 +1,180 @@ +import { useFocusEffect } from '@react-navigation/core' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { + startFetchingCloudStorageBackups, + stopFetchingCloudStorageBackups, +} from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { clearCloudBackups } from 'src/features/CloudBackup/cloudBackupSlice' +import { useCloudBackups } from 'src/features/CloudBackup/hooks' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Flex, Icons, Loader } from 'ui/src' +import { imageSizes } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { logger } from 'utilities/src/logger/logger' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' + +type Props = NativeStackScreenProps< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackupLoading +> + +const MIN_LOADING_UI_MS = ONE_SECOND_MS +// 10s timeout time for query for backups, since we don't know when the query completes +const MAX_LOADING_TIMEOUT_MS = ONE_SECOND_MS * 10 + +export function RestoreCloudBackupLoadingScreen({ + navigation, + route: { params }, +}: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const entryPoint = params.entryPoint + const importType = params.importType + + const isRestoringMnemonic = importType === ImportType.RestoreMnemonic + // inits with null before fetchCloudStorageBackups starts fetching + const [isLoading, setIsLoading] = useState(null) + const [isError, setIsError] = useState(false) + + // when we are restoring after phone migration + const signerAccounts = useNonPendingSignerAccounts() + const mnemonicId = (isRestoringMnemonic && signerAccounts[0]?.mnemonicId) || undefined + + const backups = useCloudBackups(mnemonicId) + + useAddBackButton(navigation) + + // Starts query for cloud backup files, backup files found are streamed into Redux + const fetchCloudStorageBackups = useCallback(async () => { + setIsError(false) + try { + await startFetchingCloudStorageBackups() + setIsLoading(true) + } catch (e) { + setIsError(true) + } + }, []) + + /** + * Monitors the fetching process and uses two different timeouts: + * - MAX_LOADING_TIMEOUT_MS for initial backup fetch + * - MIN_LOADING_UI_MS if subsequent backups are being fetched. + * Stops the backup fetching process and sets the loading state to false once the timeout is reached. + */ + useEffect(() => { + if (!isLoading) { + return + } + const timer = setTimeout( + () => { + if (backups.length === 0) { + logger.debug( + 'RestoreCloudBackupLoadingScreen', + 'fetchCloudStorageBackups', + `Timed out fetching cloud backups after ${MAX_LOADING_TIMEOUT_MS}ms` + ) + } + // eslint-disable-next-line @typescript-eslint/no-floating-promises + stopFetchingCloudStorageBackups() + setIsLoading(false) + }, + backups.length === 0 ? MAX_LOADING_TIMEOUT_MS : MIN_LOADING_UI_MS + ) + + return () => { + clearTimeout(timer) + } + }, [backups.length, isLoading]) + + /** + * Initiates the backup fetching process when the screen comes into focus, helping avoid potential issues with Android's consent screens. + * 1. Listens for the end of a navigation transition event. + * 2. Clears any previous fetched backups from state or redux store. + * 3. Starts the backup fetching process anew. + */ + useFocusEffect( + useCallback(() => { + return navigation.addListener('transitionEnd', async () => { + dispatch(clearCloudBackups()) + await fetchCloudStorageBackups() + }) + }, [dispatch, fetchCloudStorageBackups, navigation]) + ) + + /** + * Redirects to restore screens after loading phase. + * Waits until the loading state is false (indicating that the fetching process has ended). + * - If only one backup is found, redirects the user to enter the backup password. + * - If multiple backups are found, navigates the user to a screen to choose which backup to restore. + */ + useEffect(() => { + if (isLoading !== false || backups.length === 0) { + return + } + if (backups.length === 1 && backups[0]) { + navigation.replace(OnboardingScreens.RestoreCloudBackupPassword, { + importType, + entryPoint, + mnemonicId: backups[0].mnemonicId, + }) + } else { + navigation.replace(OnboardingScreens.RestoreCloudBackup, { + importType, + entryPoint, + }) + } + }, [backups, entryPoint, importType, isLoading, navigation]) + + if (isError) { + return ( + + } + retryButtonLabel={t('common.button.retry')} + title={t('account.cloud.error.backup.title')} + onRetry={fetchCloudStorageBackups} + /> + + ) + } + + // Handle no backups found error state + if (isLoading === false && backups.length === 0) { + if (isRestoringMnemonic) { + navigation.replace(OnboardingScreens.SeedPhraseInput, { + importType, + entryPoint, + }) + } else { + return ( + + } + retryButtonLabel={t('common.button.retry')} + title={t('account.cloud.empty.title')} + onRetry={fetchCloudStorageBackups} + /> + + ) + } + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx new file mode 100644 index 0000000..adeaf5d --- /dev/null +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.test.tsx @@ -0,0 +1,40 @@ +import { RouteProp } from '@react-navigation/core' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import React from 'react' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { RestoreCloudBackupPasswordScreen } from 'src/screens/Import/RestoreCloudBackupPasswordScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { render } from 'src/test/test-utils' +import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' + +const setOptionsSpy = jest.fn() +const routeProp = { params: {} } as RouteProp< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackupPassword +> + +describe(RestoreCloudBackupPasswordScreen, () => { + it('renders correctly', () => { + const tree = render( + + ({ + index: 0, + }), + setOptions: setOptionsSpy, + } as unknown as NativeStackNavigationProp< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackupPassword, + undefined + > + } + route={routeProp} + /> + + ).toJSON() + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx new file mode 100644 index 0000000..5f771aa --- /dev/null +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupPasswordScreen.tsx @@ -0,0 +1,191 @@ +import { useFocusEffect } from '@react-navigation/core' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard, TextInput } from 'react-native' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { PasswordInput } from 'src/components/input/PasswordInput' +import { restoreMnemonicFromCloudStorage } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { + incrementPasswordAttempts, + resetLockoutEndTime, + resetPasswordAttempts, + setLockoutEndTime, +} from 'src/features/CloudBackup/passwordLockoutSlice' +import { selectLockoutEndTime, selectPasswordAttempts } from 'src/features/CloudBackup/selectors' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { PasswordError } from 'src/features/onboarding/PasswordError' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Button, Flex, Text, TouchableArea } from 'ui/src' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { MINUTES_IN_HOUR, ONE_HOUR_MS, ONE_MINUTE_MS } from 'utilities/src/time/time' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { importAccountActions } from 'wallet/src/features/wallet/import/importAccountSaga' +import { ImportAccountType } from 'wallet/src/features/wallet/import/types' +import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' +import { ElementName } from 'wallet/src/telemetry/constants' + +type Props = NativeStackScreenProps< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackupPassword +> + +/** + * If the attempt count does not correspond to a lockout then returns undefined. Otherwise returns the lockout time based on attempts. The lockout time logic is as follows: + * after 6 attempts, lock out for 5 minutes + * after 10 attempts, lock out for 15 minutes + * after 12 attempts and any subsequent multiple of 2, lock out for another 1hr + */ +function calculateLockoutEndTime(attemptCount: number): number | undefined { + if (attemptCount < 6) { + return undefined + } + if (attemptCount === 6) { + return Date.now() + ONE_MINUTE_MS * 5 + } + if (attemptCount < 10) { + return undefined + } + if (attemptCount === 10) { + return Date.now() + ONE_MINUTE_MS * 15 + } + if (attemptCount < 12) { + return undefined + } + if (attemptCount % 2 === 0) { + return Date.now() + ONE_HOUR_MS + } + return undefined +} + +function useLockoutTimeMessage(remainingLockoutTime: number): string { + const { t } = useTranslation() + const minutes = Math.ceil(remainingLockoutTime / ONE_MINUTE_MS) + if (minutes >= MINUTES_IN_HOUR) { + return t('account.cloud.lockout.time.hours', { count: Math.floor(minutes / MINUTES_IN_HOUR) }) + } + + return t('account.cloud.lockout.time.minutes', { count: Math.floor(minutes) }) +} + +export function RestoreCloudBackupPasswordScreen({ + navigation, + route: { params }, +}: Props): JSX.Element { + const { t } = useTranslation() + const inputRef = useRef(null) + const dispatch = useAppDispatch() + + const passwordAttemptCount = useAppSelector(selectPasswordAttempts) + const lockoutEndTime = useAppSelector(selectLockoutEndTime) + + const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic + + const [enteredPassword, setEnteredPassword] = useState('') + const [errorMessage, setErrorMessage] = useState(undefined) + + const remainingLockoutTime = lockoutEndTime ? Math.max(0, lockoutEndTime - Date.now()) : 0 + const isLockedOut = remainingLockoutTime > 0 + const lockoutMessage = useLockoutTimeMessage(remainingLockoutTime) + + useFocusEffect( + useCallback(() => { + if (isLockedOut) { + setErrorMessage(lockoutMessage) + + const timer = setTimeout(() => { + setErrorMessage(undefined) + dispatch(resetLockoutEndTime()) + inputRef.current?.focus() + }, remainingLockoutTime) + + return () => clearTimeout(timer) + } + }, [isLockedOut, lockoutMessage, remainingLockoutTime, dispatch]) + ) + + useAddBackButton(navigation) + + const onPasswordSubmit = async (): Promise => { + if (isLockedOut || enteredPassword.length === 0) { + return + } + + // Attempt to restore backup with encrypted mnemonic using password + async function checkCorrectPassword(): Promise { + try { + await restoreMnemonicFromCloudStorage(params.mnemonicId, enteredPassword) + dispatch( + importAccountActions.trigger({ + type: ImportAccountType.RestoreBackup, + mnemonicId: params.mnemonicId, + indexes: Array.from(Array(NUMBER_OF_WALLETS_TO_IMPORT).keys()), + }) + ) + dispatch(resetPasswordAttempts()) + // restore flow is handled in saga after `restoreMnemonicComplete` is dispatched + if (!isRestoringMnemonic) { + navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) + } + } catch (error) { + dispatch(incrementPasswordAttempts()) + const updatedLockoutEndTime = calculateLockoutEndTime(passwordAttemptCount + 1) + if (updatedLockoutEndTime) { + dispatch(setLockoutEndTime({ lockoutEndTime: updatedLockoutEndTime })) + } else { + setErrorMessage(t('account.cloud.error.password.title')) + inputRef.current?.focus() + } + } + } + + await checkCorrectPassword() + setEnteredPassword('') + Keyboard.dismiss() + } + + const navigateToEnterRecoveryPhrase = (): void => { + navigation.replace(OnboardingScreens.SeedPhraseInput, params) + } + + return ( + + + { + if (!isLockedOut) { + setErrorMessage(undefined) + } + setEnteredPassword(newValue) + }} + onSubmitEditing={onPasswordSubmit} + /> + {errorMessage && } + + + {isRestoringMnemonic && ( + + + {t('account.cloud.password.recoveryPhrase')} + + + )} + + + + ) +} diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx new file mode 100644 index 0000000..84003ab --- /dev/null +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.test.tsx @@ -0,0 +1,37 @@ +import { RouteProp } from '@react-navigation/core' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import React from 'react' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { RestoreCloudBackupScreen } from 'src/screens/Import/RestoreCloudBackupScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { render } from 'src/test/test-utils' + +const setOptionsSpy = jest.fn() +const routeProp = { params: {} } as RouteProp< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackup +> + +describe(RestoreCloudBackupScreen, () => { + it('renders correctly', () => { + const tree = render( + ({ + index: 0, + }), + setOptions: setOptionsSpy, + } as unknown as NativeStackNavigationProp< + OnboardingStackParamList, + OnboardingScreens.RestoreCloudBackup, + undefined + > + } + route={routeProp} + /> + ).toJSON() + + expect(tree).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx new file mode 100644 index 0000000..58443b3 --- /dev/null +++ b/apps/mobile/src/screens/Import/RestoreCloudBackupScreen.tsx @@ -0,0 +1,102 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView } from 'react-native-gesture-handler' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { useCloudBackups } from 'src/features/CloudBackup/hooks' +import { CloudStorageMnemonicBackup } from 'src/features/CloudBackup/types' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Flex, Icons, Text, TouchableArea, Unicon, UniconV2, useIsDarkMode } from 'ui/src' +import { iconSizes } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { + FORMAT_DATE_TIME_SHORT, + useLocalizedDayjs, +} from 'wallet/src/features/language/localizedDayjs' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { sanitizeAddressText, shortenAddress } from 'wallet/src/utils/addresses' + +type Props = NativeStackScreenProps + +export function RestoreCloudBackupScreen({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const isDarkMode = useIsDarkMode() + const localizedDayjs = useLocalizedDayjs() + + // const backups = useMockCloudBackups(4) // returns 4 mock backups with random mnemonicIds and createdAt dates + const backups = useCloudBackups() + const sortedBackups = backups.slice().sort((a, b) => b.createdAt - a.createdAt) + const isUniconsV2Enabled = useFeatureFlag(FEATURE_FLAGS.UniconsV2) + + const onPressRestoreBackup = async (backup: CloudStorageMnemonicBackup): Promise => { + // Clear any existing pending accounts + dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) + + navigation.navigate({ + name: OnboardingScreens.RestoreCloudBackupPassword, + params: { ...params, mnemonicId: backup.mnemonicId }, + merge: true, + }) + } + + useAddBackButton(navigation) + + return ( + + + + {sortedBackups.map((backup) => { + const { mnemonicId, createdAt } = backup + return ( + => onPressRestoreBackup(backup)}> + + + {isUniconsV2Enabled ? ( + + ) : ( + + )} + + + {sanitizeAddressText(shortenAddress(mnemonicId))} + + + {localizedDayjs.unix(createdAt).format(FORMAT_DATE_TIME_SHORT)} + + + + + + + ) + })} + + + + ) +} diff --git a/apps/mobile/src/screens/Import/SeedPhraseInput.tsx b/apps/mobile/src/screens/Import/SeedPhraseInput.tsx new file mode 100644 index 0000000..3b2bf47 --- /dev/null +++ b/apps/mobile/src/screens/Import/SeedPhraseInput.tsx @@ -0,0 +1,70 @@ +import { forwardRef, RefObject, useEffect, useRef } from 'react' +import { + findNodeHandle, + NativeSyntheticEvent, + requireNativeComponent, + StyleSheet, + UIManager, +} from 'react-native' + +export type MnemonicStoredEvent = { + mnemonicId: string +} +export type InputValidatedEvent = { + canSubmit: boolean +} + +export enum StringKey { + HelpText = 'helpText', + InputPlaceholder = 'inputPlaceholder', + PasteButton = 'pasteButton', + ErrorInvalidWord = 'errorInvalidWord', + ErrorPhraseLength = 'errorPhraseLength', + ErrorWrongPhrase = 'errorWrongPhrase', + ErrorInvalidPhrase = 'errorInvalidPhrase', +} +interface NativeSeedPhraseInputProps { + targetMnemonicId?: string + strings: Record + onHelpTextPress: () => void + onInputValidated: (e: NativeSyntheticEvent) => void + onMnemonicStored: (e: NativeSyntheticEvent) => void + + // Only needed on iOS to determine when paste permission modal is open, which triggers inactive app state + // And we need to prevent splash screen from appearing + onPasteStart: () => void + onPasteEnd: () => void +} + +const NativeSeedPhraseInput = requireNativeComponent('SeedPhraseInput') +type NativeSeedPhraseInputRef = typeof NativeSeedPhraseInput & { handleSubmit: () => void } + +const styles = StyleSheet.create({ + input: { + flex: 1, + flexGrow: 1, + }, +}) + +export const useSeedPhraseInputRef = (): RefObject => { + const ref = useRef(null) + const current = ref.current + + useEffect(() => { + if (current) { + current.handleSubmit = (): void => { + // Executes in Native as an external method in iOS (RCT_EXTERN_METHOD) or a command in Android + UIManager.dispatchViewManagerCommand(findNodeHandle(current), 'handleSubmit', []) + } + } + }, [current]) + + return ref +} + +export const SeedPhraseInput = forwardRef( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (props, ref) => +) +SeedPhraseInput.displayName = 'NativeSeedPhraseInput' diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx new file mode 100644 index 0000000..37cb28d --- /dev/null +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.test.tsx @@ -0,0 +1,31 @@ +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { StackNavigationProp } from '@react-navigation/stack' +import React from 'react' +import { AppStackParamList, OnboardingStackParamList } from 'src/app/navigation/types' +import { SeedPhraseInputScreen } from 'src/screens/Import/SeedPhraseInputScreen' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { render } from 'src/test/test-utils' +import { ImportType } from 'wallet/src/features/onboarding/types' + +jest.mock('src/utils/useAddBackButton', () => ({ + useAddBackButton: (): jest.Mock => jest.fn(), +})) + +const navigationProp = {} as CompositeNavigationProp< + StackNavigationProp, + NativeStackNavigationProp +> + +const routeProp = { params: { importType: ImportType.CreateNew } } as RouteProp< + OnboardingStackParamList, + OnboardingScreens.SeedPhraseInput +> + +describe(SeedPhraseInputScreen, () => { + it.skip('seed phrase initial screen rendering', async () => { + const tree = render() + + expect(tree.toJSON()).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx new file mode 100644 index 0000000..3c087c8 --- /dev/null +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreen.tsx @@ -0,0 +1,172 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { useLockScreenOnBlur } from 'src/features/authentication/lockScreenContext' +import { GenericImportForm } from 'src/features/import/GenericImportForm' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Button, Flex, Icons, Text, TouchableArea } from 'ui/src' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { importAccountActions } from 'wallet/src/features/wallet/import/importAccountSaga' +import { ImportAccountType } from 'wallet/src/features/wallet/import/types' +import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' +import { Keyring } from 'wallet/src/features/wallet/Keyring/Keyring' +import { ElementName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' +import { + MnemonicValidationError, + translateMnemonicErrorMessage, + userFinishedTypingWord, + validateMnemonic, + validateSetOfWords, +} from 'wallet/src/utils/mnemonics' + +type Props = NativeStackScreenProps + +export function SeedPhraseInputScreen({ navigation, route: { params } }: Props): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + + /** + * If paste permission modal is open, we need to manually disable the splash screen that appears on blur, + * since the modal triggers the same `inactive` app state as does going to app switcher + * + * Technically seed phrase will be blocked if user pastes from keyboard, + * but that is an extreme edge case. + **/ + const [pastePermissionModalOpen, setPastePermissionModalOpen] = useState(false) + useLockScreenOnBlur(pastePermissionModalOpen) + + const [value, setValue] = useState(undefined) + const [errorMessage, setErrorMessage] = useState(undefined) + + const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic + + useAddBackButton(navigation) + + const signerAccounts = useNonPendingSignerAccounts() + const mnemonicId = (isRestoringMnemonic && signerAccounts[0]?.mnemonicId) || undefined + + // Add all accounts from mnemonic. + const onSubmit = useCallback(async () => { + // Check phrase validation + const { validMnemonic, error, invalidWord } = validateMnemonic(value) + + if (error) { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + return + } + + if (mnemonicId && validMnemonic) { + const generatedMnemonicId = await Keyring.generateAddressForMnemonic(validMnemonic, 0) + if (generatedMnemonicId !== mnemonicId) { + setErrorMessage(t('account.recoveryPhrase.error.wrong')) + return + } + } + + dispatch( + importAccountActions.trigger({ + type: ImportAccountType.Mnemonic, + validatedMnemonic: validMnemonic, + indexes: Array.from(Array(NUMBER_OF_WALLETS_TO_IMPORT).keys()), + }) + ) + // restore flow is handled in saga after `restoreMnemonicComplete` is dispatched + if (!isRestoringMnemonic) { + navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) + } + }, [value, mnemonicId, dispatch, isRestoringMnemonic, t, navigation, params]) + + const onBlur = useCallback(() => { + const { error, invalidWord } = validateMnemonic(value) + if (error) { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + } + }, [t, value]) + + const onChange = (text: string | undefined): void => { + const { error, invalidWord } = validateSetOfWords(text) + + // suppress error messages if the user is not done typing a word + const suppressError = + (error === MnemonicValidationError.InvalidWord && !userFinishedTypingWord(text)) || + error === MnemonicValidationError.NotEnoughWords + + if (!error || suppressError) { + setErrorMessage(undefined) + } else { + setErrorMessage(translateMnemonicErrorMessage(error, invalidWord, t)) + } + + setValue(text) + } + + const onPressRecoveryHelpButton = (): Promise => + openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHelp) + + const onPressTryAgainButton = (): void => { + navigation.replace(OnboardingScreens.RestoreCloudBackupLoading, params) + } + + return ( + + + + setPastePermissionModalOpen(false)} + beforePasteButtonPress={(): void => setPastePermissionModalOpen(true)} + errorMessage={errorMessage} + inputAlignment="flex-start" + placeholderLabel={t('account.recoveryPhrase.input')} + textAlign="left" + value={value} + onBlur={onBlur} + onChange={onChange} + /> + + + + + + + {isRestoringMnemonic + ? t('account.recoveryPhrase.helpText.restoring') + : t('account.recoveryPhrase.helpText.import')} + + + + + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx new file mode 100644 index 0000000..e53fc04 --- /dev/null +++ b/apps/mobile/src/screens/Import/SeedPhraseInputScreenV2.tsx @@ -0,0 +1,133 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { NativeSyntheticEvent } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { useLockScreenOnBlur } from 'src/features/authentication/lockScreenContext' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { + InputValidatedEvent, + MnemonicStoredEvent, + SeedPhraseInput, + StringKey, + useSeedPhraseInputRef, +} from 'src/screens/Import/SeedPhraseInput' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Button } from 'ui/src' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { useNonPendingSignerAccounts } from 'wallet/src/features/wallet/hooks' +import { importAccountActions } from 'wallet/src/features/wallet/import/importAccountSaga' +import { ImportAccountType } from 'wallet/src/features/wallet/import/types' +import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' +import { ElementName } from 'wallet/src/telemetry/constants' +import { openUri } from 'wallet/src/utils/linking' + +type Props = NativeStackScreenProps + +export function SeedPhraseInputScreenV2({ navigation, route: { params } }: Props): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + + /** + * If paste permission modal is open, we need to manually disable the splash screen that appears on blur, + * since the modal triggers the same `inactive` app state as does going to app switcher + * + * Technically seed phrase will be blocked if user pastes from keyboard, + * but that is an extreme edge case. + **/ + const [pastePermissionModalOpen, setPastePermissionModalOpen] = useState(false) + useLockScreenOnBlur(pastePermissionModalOpen) + + const [submitEnabled, setSubmitEnabled] = useState(false) + const seedPhraseInputRef = useSeedPhraseInputRef() + const isRestoringMnemonic = params.importType === ImportType.RestoreMnemonic + + useAddBackButton(navigation) + + const signerAccounts = useNonPendingSignerAccounts() + const targetMnemonicId = (isRestoringMnemonic && signerAccounts[0]?.mnemonicId) || undefined + + const handleNext = useCallback( + (storedMnemonicId: string) => { + dispatch( + importAccountActions.trigger({ + type: ImportAccountType.MnemonicNative, + mnemonicId: storedMnemonicId, + indexes: Array.from(Array(NUMBER_OF_WALLETS_TO_IMPORT).keys()), + }) + ) + + // restore flow is handled in saga after `restoreMnemonicComplete` is dispatched + if (!isRestoringMnemonic) { + navigation.navigate({ name: OnboardingScreens.SelectWallet, params, merge: true }) + } + }, + [dispatch, isRestoringMnemonic, navigation, params] + ) + + const onPressRecoveryHelpButton = (): Promise => + openUri(uniswapUrls.helpArticleUrls.recoveryPhraseHelp) + + const onPressTryAgainButton = (): void => { + navigation.replace(OnboardingScreens.RestoreCloudBackupLoading, params) + } + + return ( + + {/* */} + ): void => + setSubmitEnabled(e.nativeEvent.canSubmit) + } + onMnemonicStored={(e: NativeSyntheticEvent): void => + handleNext(e.nativeEvent.mnemonicId) + } + onPasteEnd={(): void => { + setPastePermissionModalOpen(false) + }} + onPasteStart={(): void => { + setPastePermissionModalOpen(true) + }} + /> + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/SelectWalletScreen.tsx b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx new file mode 100644 index 0000000..6f110fd --- /dev/null +++ b/apps/mobile/src/screens/Import/SelectWalletScreen.tsx @@ -0,0 +1,252 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ScrollView } from 'react-native-gesture-handler' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { Button, Flex, Loader } from 'ui/src' +import { useSelectWalletScreenQuery } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useTimeout } from 'utilities/src/time/timing' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import WalletPreviewCard from 'wallet/src/components/WalletPreviewCard/WalletPreviewCard' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' +import { NUMBER_OF_WALLETS_TO_IMPORT } from 'wallet/src/features/wallet/import/utils' +import { setAccountAsActive } from 'wallet/src/features/wallet/slice' +import { ElementName } from 'wallet/src/telemetry/constants' + +const FORCED_LOADING_DURATION = 3 * ONE_SECOND_MS // 3s + +interface ImportableAccount { + ownerAddress: string + balance: number | undefined +} + +function isImportableAccount(account: { + ownerAddress: string | undefined + balance: Maybe +}): account is ImportableAccount { + return (account as ImportableAccount).ownerAddress !== undefined +} + +type Props = NativeStackScreenProps + +export function SelectWalletScreen({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + + const pendingAccounts = usePendingAccounts() + const addresses = Object.values(pendingAccounts) + .filter((a) => a.type === AccountType.SignerMnemonic) + .sort( + (a, b) => + (a as SignerMnemonicAccount).derivationIndex - (b as SignerMnemonicAccount).derivationIndex + ) + .map((account) => account.address) + + const isImportingAccounts = addresses.length !== NUMBER_OF_WALLETS_TO_IMPORT + + const { data, loading, refetch, error } = useSelectWalletScreenQuery({ + variables: { ownerAddresses: addresses }, + /* + * Wait until all the addresses have been added to the store before querying. + * Also prevents an extra API call when user navigates back and clears pending accounts. + */ + skip: isImportingAccounts, + }) + + const onRetry = useCallback(() => refetch(), [refetch]) + + const allAddressBalances = data?.portfolios + + const initialShownAccounts = useMemo(() => { + const filteredAccounts = allAddressBalances + ?.map((address) => ({ + ownerAddress: address?.ownerAddress, + balance: address?.tokensTotalDenominatedValue?.value, + })) + .filter(isImportableAccount) + + const accountsWithBalance = filteredAccounts?.filter( + (address) => address.balance && address.balance > 0 + ) + + if (accountsWithBalance?.length) { + return accountsWithBalance + } + + // if all addresses have 0 total token value, show the first address + const firstFilteredAccount = filteredAccounts?.[0] + if (firstFilteredAccount) { + return [firstFilteredAccount] + } + + // if query for address balances returned null, show the first address + const firstPendingAddress = addresses[0] + if (firstPendingAddress) { + return [{ ownerAddress: firstPendingAddress, balance: undefined }] + } + }, [addresses, allAddressBalances]) + + const initialSelectedAddresses = useMemo( + () => + initialShownAccounts + ?.map((account) => account?.ownerAddress) + .filter((address): address is string => typeof address === 'string') ?? [], + [initialShownAccounts] + ) + + const isOnlyOneAccount = initialShownAccounts?.length === 1 + + const showError = error && !initialShownAccounts?.length + + const [selectedAddresses, setSelectedAddresses] = useReducer( + (currentAddresses: string[], addressToProcess: string) => + currentAddresses.includes(addressToProcess) + ? currentAddresses.filter((address) => address !== addressToProcess) + : [...currentAddresses, addressToProcess], + initialSelectedAddresses + ) + + useEffect(() => { + const beforeRemoveListener = (): void => { + // Remove all pending signer accounts when navigating back + dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) + } + navigation.addListener('beforeRemove', beforeRemoveListener) + return () => navigation.removeListener('beforeRemove', beforeRemoveListener) + }, [dispatch, navigation, pendingAccounts]) + + useEffect(() => { + /* + * In the event that the initial state of `selectedAddresses` is empty due to + * delay in importAccountSaga, we need to set the fallback account as selected + */ + if (isImportingAccounts || loading || selectedAddresses.length > 0) { + return + } + + initialSelectedAddresses.forEach((address) => setSelectedAddresses(address)) + }, [initialSelectedAddresses, isImportingAccounts, loading, selectedAddresses.length]) + + const onPress = (address: string): void => { + if (initialShownAccounts?.length === 1 && selectedAddresses.length === 1) { + return + } + setSelectedAddresses(address) + } + + const isFirstAccountActive = useRef(false) // to keep track of first account activated from the selected accounts + const onSubmit = useCallback(() => { + addresses.forEach((address) => { + // Remove unselected accounts from store. + if (!selectedAddresses.includes(address)) { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.Remove, + address, + notificationsEnabled: !!pendingAccounts[address]?.pushNotificationsEnabled, + }) + ) + } else { + if (!isFirstAccountActive.current) { + dispatch(setAccountAsActive(address)) + isFirstAccountActive.current = true + } + const account = pendingAccounts[address] + if (account && !account.name && account.type !== AccountType.Readonly) { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.Rename, + address, + newName: t('onboarding.wallet.defaultName', { number: account.derivationIndex + 1 }), + }) + ) + } + } + }) + + navigation.navigate({ + name: + params?.importType === ImportType.Restore + ? OnboardingScreens.Notifications + : OnboardingScreens.Backup, + params, + merge: true, + }) + }, [addresses, navigation, params, selectedAddresses, dispatch, pendingAccounts, t]) + + // Force a fixed duration loading state for smoother transition (as we show different UI for 1 vs multiple wallets) + const [isForcedLoading, setIsForcedLoading] = useState(true) + useTimeout(() => setIsForcedLoading(false), FORCED_LOADING_DURATION) + + const isLoading = loading || isForcedLoading || isImportingAccounts + + const title = isLoading + ? t('account.wallet.select.loading.title') + : t('account.wallet.select.title_one', { count: initialShownAccounts?.length ?? 0 }) + + const subtitle = isLoading ? t('account.wallet.select.loading.subtitle') : undefined + + return ( + <> + + {showError ? ( + + ) : isLoading ? ( + + + + ) : ( + + + {initialShownAccounts?.map((account, i) => { + const { ownerAddress, balance } = account + return ( + + ) + })} + + + )} + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/WatchWalletScreen.tsx b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx new file mode 100644 index 0000000..3521011 --- /dev/null +++ b/apps/mobile/src/screens/Import/WatchWalletScreen.tsx @@ -0,0 +1,205 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { SharedEventName } from '@uniswap/analytics-events' +import { TFunction } from 'i18next' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Keyboard } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { GenericImportForm } from 'src/features/import/GenericImportForm' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { Button, Flex, Icons, Text } from 'ui/src' +import { normalizeTextInput } from 'utilities/src/primitives/string' +import { ChainId } from 'wallet/src/constants/chains' +import { usePortfolioBalances } from 'wallet/src/features/dataApi/balances' +import { useENS } from 'wallet/src/features/ens/useENS' +import { useIsSmartContractAddress } from 'wallet/src/features/transactions/transfer/hooks/useIsSmartContractAddress' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { importAccountActions } from 'wallet/src/features/wallet/import/importAccountSaga' +import { ImportAccountType } from 'wallet/src/features/wallet/import/types' +import { ElementName } from 'wallet/src/telemetry/constants' +import { getValidAddress } from 'wallet/src/utils/addresses' + +type Props = NativeStackScreenProps + +const LIVE_CHECK_DELAY = 1000 + +const validateForm = ({ + isAddress, + name, + walletExists, + loading, + isSmartContractAddress, + isValidSmartContract, +}: { + isAddress: string | null + name: string | null + walletExists: boolean + loading: boolean + isSmartContractAddress: boolean + isValidSmartContract: boolean +}): boolean => { + return ( + (!!isAddress || !!name) && + !walletExists && + !loading && + (!isSmartContractAddress || isValidSmartContract) + ) +} + +const getErrorText = ({ + walletExists, + isSmartContractAddress, + loading, + t, +}: { + walletExists: boolean + isSmartContractAddress: boolean + loading: boolean + t: TFunction +}): string | undefined => { + if (walletExists) { + return t('account.wallet.watch.error.alreadyImported') + } else if (isSmartContractAddress) { + return t('account.wallet.watch.error.smartContract') + } else if (!loading) { + return t('account.wallet.watch.error.notFound') + } + return undefined +} + +export function WatchWalletScreen({ navigation, route: { params } }: Props): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + const accounts = useAccounts() + const importedAddresses = Object.keys(accounts) + + useAddBackButton(navigation) + + // Form values. + const [value, setValue] = useState(undefined) + const [showLiveCheck, setShowLiveCheck] = useState(false) + + // ENS and address parsing. + const normalizedValue = normalizeTextInput(value ?? '') + const hasSuffixIncluded = normalizedValue.includes('.') + const { address: resolvedAddress, name } = useENS( + ChainId.Mainnet, + normalizedValue, + !hasSuffixIncluded + ) + const isAddress = getValidAddress(normalizedValue, true, false) + const { isSmartContractAddress, loading } = useIsSmartContractAddress( + (isAddress || resolvedAddress) ?? undefined, + ChainId.Mainnet + ) + // Allow smart contracts with non-null balances + const { data: balancesById } = usePortfolioBalances({ + address: isSmartContractAddress ? (isAddress || resolvedAddress) ?? undefined : undefined, + fetchPolicy: 'cache-and-network', + }) + const isValidSmartContract = isSmartContractAddress && !!balancesById + + const onCompleteOnboarding = useCompleteOnboardingCallback(params) + + // Form validation. + const walletExists = + (resolvedAddress && importedAddresses.includes(resolvedAddress)) || + importedAddresses.includes(normalizedValue) + const isValid = validateForm({ + isAddress, + name, + walletExists, + loading, + isSmartContractAddress, + isValidSmartContract, + }) + + const errorText = !isValid + ? getErrorText({ walletExists, isSmartContractAddress, loading, t }) + : undefined + + const onSubmit = useCallback(async () => { + if (isValid && value) { + if (resolvedAddress) { + dispatch( + importAccountActions.trigger({ + type: ImportAccountType.Address, + address: resolvedAddress, + }) + ) + } else { + dispatch( + importAccountActions.trigger({ + type: ImportAccountType.Address, + address: normalizedValue, + }) + ) + } + sendMobileAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + screen: OnboardingScreens.WatchWallet, + element: ElementName.Continue, + }) + await onCompleteOnboarding() + } + }, [dispatch, isValid, normalizedValue, onCompleteOnboarding, resolvedAddress, value]) + + const onChange = (text: string | undefined): void => { + if (value !== text?.trim()) { + setShowLiveCheck(false) + } + setValue(text?.trim()) + } + + useEffect(() => { + const delayFn = setTimeout(() => { + setShowLiveCheck(true) + }, LIVE_CHECK_DELAY) + + return () => { + clearTimeout(delayFn) + } + }, [value]) + + return ( + + + { + isValid && Keyboard.dismiss() + }} + /> + + + + {t('account.wallet.watch.message')} + + + + + + ) +} diff --git a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap new file mode 100644 index 0000000..2e6c75a --- /dev/null +++ b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupPasswordScreen.test.tsx.snap @@ -0,0 +1,332 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RestoreCloudBackupPasswordScreen renders correctly 1`] = ` + + + + + + Enter backup password + + + This password is required to recover your recovery phrase backup from iCloud. + + + + + + + + + + + + + + + + + + + Continue + + + + + + + +`; diff --git a/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupScreen.test.tsx.snap new file mode 100644 index 0000000..e2b996b --- /dev/null +++ b/apps/mobile/src/screens/Import/__snapshots__/RestoreCloudBackupScreen.test.tsx.snap @@ -0,0 +1,143 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RestoreCloudBackupScreen renders correctly 1`] = ` + + + + + + Select a backup to restore + + + There are multiple recovery phrases backed up to your iCloud. + + + + + + + + + + + + +`; diff --git a/apps/mobile/src/screens/Import/__snapshots__/SeedPhraseInputScreen.test.tsx.snap b/apps/mobile/src/screens/Import/__snapshots__/SeedPhraseInputScreen.test.tsx.snap new file mode 100644 index 0000000..5b32491 --- /dev/null +++ b/apps/mobile/src/screens/Import/__snapshots__/SeedPhraseInputScreen.test.tsx.snap @@ -0,0 +1,696 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SeedPhraseInputScreen seed phrase initial screen rendering 1`] = ` + + + + + + + + Enter your recovery phrase + + + Your recovery phrase will only be stored locally on your device. + + + + + + + + + + + + + Type your recovery phrase + + + + + + + + Paste + + + + + + + + + + + + + + + + + + + + + + + + + How do I find my recovery phrase? + + + + + + + Continue + + + + + + + + ExpoLinearGradient + +`; diff --git a/apps/mobile/src/screens/ModalAwareView.tsx b/apps/mobile/src/screens/ModalAwareView.tsx new file mode 100644 index 0000000..6746984 --- /dev/null +++ b/apps/mobile/src/screens/ModalAwareView.tsx @@ -0,0 +1,37 @@ +import { BottomSheetDraggableView } from '@gorhom/bottom-sheet' +import React from 'react' +import { View } from 'react-native' +import { useAppSelector } from 'src/app/hooks' +import { HorizontalEdgeGestureTarget } from 'src/components/layout/screens/EdgeGestureTarget' +import { selectModalState } from 'src/features/modals/selectModalState' +import { Flex, flexStyles, useDeviceInsets, useSporeColors } from 'ui/src' +import { HandleBar } from 'wallet/src/components/modals/HandleBar' +import { ModalName } from 'wallet/src/telemetry/constants' +/** + * Wrapper view to correctly render screens within BottomSheetModal as needed. This is required + * to enable both full screen, and bottom sheet drag gestures on a screen within a modal. + * + * Note: full screen gesture must be enable in the root navigator to work. + * + * This is not needed on screens that make use of bottom sheet compatible views (like HeaderScrollScreen, which + * uses a compatible virtualized list when rendered within a bottom sheet modal). + */ +export function ExploreModalAwareView({ children }: { children: JSX.Element }): JSX.Element { + const inModal = useAppSelector(selectModalState(ModalName.Explore)).isOpen + const colors = useSporeColors() + const insets = useDeviceInsets() + + if (inModal) { + return ( + + + + + {children} + + + ) + } + + return {children} +} diff --git a/apps/mobile/src/screens/NFTCollectionScreen.tsx b/apps/mobile/src/screens/NFTCollectionScreen.tsx new file mode 100644 index 0000000..6de089e --- /dev/null +++ b/apps/mobile/src/screens/NFTCollectionScreen.tsx @@ -0,0 +1,305 @@ +import { NetworkStatus } from '@apollo/client' +import { useScrollToTop } from '@react-navigation/native' +import { ImpactFeedbackStyle } from 'expo-haptics' +import React, { ReactElement, useCallback, useMemo, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItemInfo } from 'react-native' +import { useAnimatedScrollHandler, useSharedValue, withTiming } from 'react-native-reanimated' +import { AppStackScreenProp, useAppStackNavigation } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { Screen } from 'src/components/layout/Screen' +import { ScrollHeader } from 'src/components/layout/screens/ScrollHeader' +import { Loader } from 'src/components/loading' +import { ListPriceBadge } from 'src/features/nfts/collection/ListPriceCard' +import { NFTCollectionContextMenu } from 'src/features/nfts/collection/NFTCollectionContextMenu' +import { + NFTCollectionHeader, + NFT_BANNER_HEIGHT, +} from 'src/features/nfts/collection/NFTCollectionHeader' +import { ExploreModalAwareView } from 'src/screens/ModalAwareView' +import { Screens } from 'src/screens/Screens' +import { + AnimatedBottomSheetFlashList, + AnimatedFlashList, + Flex, + Text, + TouchableArea, + useDeviceDimensions, + useDeviceInsets, +} from 'ui/src' +import { iconSizes, spacing } from 'ui/src/theme' +import { + NftCollectionScreenQuery, + useNftCollectionScreenQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { isIOS } from 'uniswap/src/utils/platform' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { isError } from 'wallet/src/data/utils' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { NFTItem } from 'wallet/src/features/nfts/types' +import { getNFTAssetKey } from 'wallet/src/features/nfts/utils' + +const PREFETCH_ITEMS_THRESHOLD = 0.5 +const ASSET_FETCH_PAGE_SIZE = 30 +const ESTIMATED_ITEM_SIZE = 104 // heuristic provided by FlashList + +const LOADING_ITEM = 'loading' +const LOADING_BUFFER_AMOUNT = 9 +const LOADING_ITEMS_ARRAY: NFTItem[] = Array(LOADING_BUFFER_AMOUNT).fill(LOADING_ITEM) + +const keyExtractor = (item: NFTItem | string, index: number): string => + typeof item === 'string' + ? `${LOADING_ITEM}-${index}` + : getNFTAssetKey(item.contractAddress ?? '', item.tokenId ?? '') + +function gqlNFTAssetToNFTItem(data: NftCollectionScreenQuery | undefined): NFTItem[] | undefined { + const items = data?.nftAssets?.edges?.flatMap((item) => item.node) + if (!items) { + return + } + + return items.map((item): NFTItem => { + return { + name: item?.name ?? undefined, + contractAddress: item?.nftContract?.address ?? undefined, + tokenId: item?.tokenId ?? undefined, + imageUrl: item?.image?.url ?? undefined, + collectionName: item?.collection?.name ?? undefined, + ownerAddress: item.ownerAddress ?? undefined, + imageDimensions: + item?.image?.dimensions?.height && item?.image?.dimensions?.width + ? { width: item.image.dimensions.width, height: item.image.dimensions.height } + : undefined, + listPrice: item?.listings?.edges?.[0]?.node?.price ?? undefined, + } + }) +} + +type NFTCollectionScreenProps = AppStackScreenProp & { + renderedInModal?: boolean +} + +export function NFTCollectionScreen({ + route: { + params: { collectionAddress }, + }, + renderedInModal = false, +}: NFTCollectionScreenProps): ReactElement { + const { t } = useTranslation() + const insets = useDeviceInsets() + const dimensions = useDeviceDimensions() + const navigation = useAppStackNavigation() + + // Collection overview data and paginated grid items + const { data, networkStatus, fetchMore, refetch } = useNftCollectionScreenQuery({ + variables: { contractAddress: collectionAddress, first: ASSET_FETCH_PAGE_SIZE }, + notifyOnNetworkStatusChange: true, + fetchPolicy: 'cache-and-network', + }) + + // Parse response for overview data and collection grid data + const collectionData = data?.nftCollections?.edges?.[0]?.node + const collectionItems = useMemo(() => gqlNFTAssetToNFTItem(data), [data]) + + // Fill in grid with loading boxes if we have incomplete data and are loading more + const extraLoadingItemAmount = + networkStatus === NetworkStatus.fetchMore || networkStatus === NetworkStatus.loading + ? LOADING_BUFFER_AMOUNT + (3 - ((collectionItems ?? []).length % 3)) + : undefined + + const onListEndReached = useCallback(async () => { + if (!data?.nftAssets?.pageInfo?.hasNextPage) { + return + } + await fetchMore({ + variables: { + first: ASSET_FETCH_PAGE_SIZE, + after: data?.nftAssets?.pageInfo?.endCursor, + }, + }) + }, [data?.nftAssets?.pageInfo?.endCursor, data?.nftAssets?.pageInfo?.hasNextPage, fetchMore]) + + // Scroll behavior for fixed scroll header + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const listRef = useRef(null) + useScrollToTop(listRef) + const scrollY = useSharedValue(0) + const scrollHandler = useAnimatedScrollHandler({ + onScroll: (event) => { + scrollY.value = event.contentOffset.y + }, + onEndDrag: (event) => { + scrollY.value = withTiming(event.contentOffset.y > 0 ? NFT_BANNER_HEIGHT : 0) + }, + }) + + const onPressItem = (asset: NFTItem): void => { + navigation.navigate(Screens.NFTItem, { + address: asset.contractAddress ?? '', + tokenId: asset.tokenId ?? '', + isSpam: asset.isSpam ?? false, + fallbackData: asset, + }) + } + + /** + * @TODO: @ianlapham We can remove these styles when FLashList supports + * columnWrapperStyle prop (from FlatList). Until then, do this to preserve full width header, + * but padded list. + */ + const renderItem = ({ item, index }: ListRenderItemInfo): JSX.Element => { + const first = index % 3 === 0 + const last = index % 3 === 2 + const middle = !first && !last + const containerStyle = { + marginLeft: middle ? spacing.spacing8 : first ? spacing.spacing16 : 0, + marginRight: middle ? spacing.spacing8 : last ? spacing.spacing16 : 0, + marginBottom: spacing.spacing8, + } + const priceColor = isIOS ? '$sporeWhite' : '$neutral1' + + return ( + + {typeof item === 'string' ? ( + + ) : ( + onPressItem(item)}> + + {item.listPrice && ( + + )} + + )} + + ) + } + + // Only show loading UI if no data and first request, otherwise render cached data + const headerDataLoading = networkStatus === NetworkStatus.loading && !collectionData + const gridDataLoading = networkStatus === NetworkStatus.loading && !collectionItems + + const gridDataWithLoadingElements = useMemo(() => { + if (gridDataLoading) { + return LOADING_ITEMS_ARRAY + } + + const extraLoadingItems: NFTItem[] = extraLoadingItemAmount + ? Array(extraLoadingItemAmount).fill(LOADING_ITEM) + : [] + + return [...(collectionItems ?? []), ...extraLoadingItems] + }, [collectionItems, extraLoadingItemAmount, gridDataLoading]) + + const traceProperties = useMemo( + () => + collectionData?.name + ? { collectionAddress, collectionName: collectionData?.name } + : undefined, + [collectionAddress, collectionData?.name] + ) + + if (isError(networkStatus, !!data)) { + return ( + + + + + + + ) + } + + const List = renderedInModal ? AnimatedBottomSheetFlashList : AnimatedFlashList + + return ( + + + + {collectionData.name} : undefined + } + listRef={listRef} + rightElement={ + + } + scrollY={scrollY} + showHeaderScrollYDistance={NFT_BANNER_HEIGHT} + /> + + ) + } + ListHeaderComponent={ + + } + contentContainerStyle={{ paddingBottom: insets.bottom }} + data={gridDataWithLoadingElements} + estimatedItemSize={ESTIMATED_ITEM_SIZE} + estimatedListSize={{ + width: dimensions.fullWidth, + height: dimensions.fullHeight, + }} + keyExtractor={keyExtractor} + numColumns={3} + renderItem={renderItem} + showsVerticalScrollIndicator={false} + onEndReached={onListEndReached} + onEndReachedThreshold={PREFETCH_ITEMS_THRESHOLD} + onScroll={scrollHandler} + /> + + + + ) +} diff --git a/apps/mobile/src/screens/NFTItemScreen.tsx b/apps/mobile/src/screens/NFTItemScreen.tsx new file mode 100644 index 0000000..61d7087 --- /dev/null +++ b/apps/mobile/src/screens/NFTItemScreen.tsx @@ -0,0 +1,453 @@ +/* eslint-disable complexity */ +import { ApolloQueryResult } from '@apollo/client' +import { isAddress } from 'ethers/lib/utils' +import { impactAsync } from 'expo-haptics' +import React, { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import { StatusBar, StyleSheet, TouchableOpacity } from 'react-native' +import ContextMenu from 'react-native-context-menu-view' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { AppStackScreenProp, useAppStackNavigation } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' +import { Loader } from 'src/components/loading' +import { LongMarkdownText } from 'src/components/text/LongMarkdownText' +import { selectModalState } from 'src/features/modals/selectModalState' +import { PriceAmount } from 'src/features/nfts/collection/ListPriceCard' +import { useNFTMenu } from 'src/features/nfts/hooks' +import { BlurredImageBackground } from 'src/features/nfts/item/BlurredImageBackground' +import { CollectionPreviewCard } from 'src/features/nfts/item/CollectionPreviewCard' +import { NFTTraitList } from 'src/features/nfts/item/traits' +import { ExploreModalAwareView } from 'src/screens/ModalAwareView' +import { Screens } from 'src/screens/Screens' +import { Flex, Text, Theme, TouchableArea, getTokenValue, useSporeColors } from 'ui/src' +import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg' +import ShareIcon from 'ui/src/assets/icons/share.svg' +import { colorsDark, fonts, iconSizes } from 'ui/src/theme' +import { + NftActivityType, + NftItemScreenQuery, + useNftItemScreenQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { isAndroid, isIOS } from 'uniswap/src/utils/platform' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { PollingInterval } from 'wallet/src/constants/misc' +import { NFTViewer } from 'wallet/src/features/images/NFTViewer' +import { GQLNftAsset } from 'wallet/src/features/nfts/hooks' +import { pushNotification } from 'wallet/src/features/notifications/slice' +import { AppNotificationType, CopyNotificationType } from 'wallet/src/features/notifications/types' +import { useActiveAccountAddressWithThrow } from 'wallet/src/features/wallet/hooks' +import { ModalName } from 'wallet/src/telemetry/constants' +import { areAddressesEqual } from 'wallet/src/utils/addresses' +import { setClipboardImage } from 'wallet/src/utils/clipboard' +import { + MIN_COLOR_CONTRAST_THRESHOLD, + passesContrast, + useNearestThemeColorFromImageUri, +} from 'wallet/src/utils/colors' + +const MAX_NFT_IMAGE_HEIGHT = 375 + +type NFTItemScreenProps = AppStackScreenProp + +export function NFTItemScreen(props: NFTItemScreenProps): JSX.Element { + return isAndroid ? ( + // display screen with theme dependent colors on Android + + ) : ( + // put Theme above the Contents so our useSporeColors() gets the right colors + + + + ) +} + +function NFTItemScreenContents({ + route: { + // ownerFromProps needed here, when nftBalances GQL query returns a user NFT, + // but nftAssets query for this NFT returns ownerAddress === null, + params: { owner: ownerFromProps, address, tokenId, isSpam, fallbackData }, + }, +}: NFTItemScreenProps): JSX.Element { + const { t } = useTranslation() + const activeAccountAddress = useActiveAccountAddressWithThrow() + const dispatch = useAppDispatch() + const colors = useSporeColors() + const navigation = useAppStackNavigation() + + const { + data, + loading: nftLoading, + refetch, + } = useNftItemScreenQuery({ + variables: { + contractAddress: address, + filter: { tokenIds: [tokenId] }, + activityFilter: { + address, + tokenId, + activityTypes: [NftActivityType.Sale], + }, + }, + pollInterval: PollingInterval.Slow, + }) + const asset = data?.nftAssets?.edges[0]?.node + const owner = (ownerFromProps || asset?.ownerAddress) ?? undefined + + const lastSaleData = data?.nftActivity?.edges[0]?.node + const listingPrice = asset?.listings?.edges?.[0]?.node?.price + + const name = useMemo(() => asset?.name ?? fallbackData?.name, [asset?.name, fallbackData?.name]) + const description = useMemo( + () => asset?.description ?? fallbackData?.description, + [asset?.description, fallbackData?.description] + ) + const imageUrl = useMemo( + () => asset?.image?.url ?? fallbackData?.imageUrl, + [asset?.image?.url, fallbackData?.imageUrl] + ) + const imageHeight = asset?.image?.dimensions?.height + const imageWidth = asset?.image?.dimensions?.width + const imageDimensionsExist = imageHeight && imageWidth + const imageDimensions = imageDimensionsExist + ? { height: imageHeight, width: imageWidth } + : undefined + const imageAspectRatio = imageDimensions ? imageDimensions.width / imageDimensions.height : 1 + const onPressCollection = (): void => { + const collectionAddress = asset?.nftContract?.address ?? fallbackData?.contractAddress + if (collectionAddress) { + navigation.navigate(Screens.NFTCollection, { collectionAddress }) + } + } + + // Disable navigation to profile if user owns NFT or invalid owner + const disableProfileNavigation = Boolean( + owner && (areAddressesEqual(owner, activeAccountAddress) || !isAddress(owner)) + ) + + const onPressOwner = (): void => { + if (owner) { + navigation.navigate(Screens.ExternalProfile, { + address: owner, + }) + } + } + + const inModal = useAppSelector(selectModalState(ModalName.Explore)).isOpen + + const traceProperties: Record> = useMemo(() => { + const baseProps = { + owner, + address, + tokenId, + } + + if (asset?.collection?.name) { + return { + ...baseProps, + collectionName: asset?.collection?.name, + isMissingData: false, + } + } + + if (fallbackData) { + return { + ...baseProps, + collectionName: fallbackData.collectionName, + isMissingData: true, + } + } + + return { ...baseProps, isMissingData: true } + }, [address, asset?.collection?.name, fallbackData, owner, tokenId]) + + const { colorLight, colorDark } = useNearestThemeColorFromImageUri(imageUrl) + // check if colorLight passes contrast against card bg color, if not use fallback + const accentTextColor = useMemo(() => { + if ( + colorLight && + passesContrast(colorLight, colors.surface1.val, MIN_COLOR_CONTRAST_THRESHOLD) + ) { + return colorLight + } + return colors.neutral2.val + }, [colorLight, colors.neutral2, colors.surface1]) + + const onLongPressNFTImage = async (): Promise => { + await setClipboardImage(imageUrl) + await impactAsync() + dispatch( + pushNotification({ + type: AppNotificationType.Copied, + copyType: CopyNotificationType.Image, + }) + ) + } + + const rightElement = useMemo( + () => , + [asset, isSpam, owner] + ) + + return ( + <> + {isIOS ? ( + + ) : null} + + + <> + {isIOS ? ( + + ) : ( + + )} + + + + ) : ( + + {name} + + ) + } + renderedInModal={inModal} + rightElement={rightElement}> + {/* Content wrapper */} + + + + {nftLoading ? ( + + + + ) : imageUrl ? ( + + + + ) : ( + + > => + refetch?.() + } + /> + + )} + + {nftLoading ? ( + + ) : name ? ( + + {name} + + ) : null} + + + + {/* Description */} + + {nftLoading ? ( + + + + ) : description ? ( + + ) : null} + + + {/* Metadata */} + + {listingPrice?.value ? ( + + } + /> + ) : null} + {lastSaleData?.price?.value ? ( + + } + /> + ) : null} + + {owner && ( + + + + } + /> + )} + + + {/* Traits */} + {asset?.traits && asset?.traits?.length > 0 ? ( + + + {t('tokens.nfts.details.traits')} + + + + ) : null} + + + + + + + ) +} + +function AssetMetadata({ + title, + valueComponent, + color, +}: { + title: string + valueComponent: JSX.Element + color: string +}): JSX.Element { + const colors = useSporeColors() + return ( + + + + {title} + + + {valueComponent} + + ) +} + +function RightElement({ + asset, + owner, + isSpam, +}: { + asset: GQLNftAsset + owner?: string + isSpam?: boolean +}): JSX.Element { + const colors = useSporeColors() + + const { menuActions, onContextMenuPress, onlyShare } = useNFTMenu({ + contractAddress: asset?.nftContract?.address, + tokenId: asset?.tokenId, + owner, + showNotification: true, + isSpam, + }) + + return ( + + {menuActions.length > 0 ? ( + onlyShare ? ( + + + + ) : ( + + + + ) + ) : undefined} + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx new file mode 100644 index 0000000..44af708 --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.test.tsx @@ -0,0 +1,51 @@ +import { CompositeNavigationProp, RouteProp } from '@react-navigation/core' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { StackNavigationProp } from '@react-navigation/stack' +import React from 'react' +import { act } from 'react-test-renderer' +import { AppStackParamList, OnboardingStackParamList } from 'src/app/navigation/types' +import { BackupScreen } from 'src/screens/Onboarding/BackupScreen' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { renderWithProviders } from 'src/test/render' +import { render } from 'src/test/test-utils' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { TamaguiProvider } from 'wallet/src/provider/tamagui-provider' +import { ACCOUNT, preloadedSharedState } from 'wallet/src/test/fixtures' + +const navigationProp = {} as CompositeNavigationProp< + StackNavigationProp, + NativeStackNavigationProp +> +const routeProp = { + params: { + importType: ImportType.CreateNew, + entryPoint: OnboardingEntryPoint.FreshInstallOrReplace, + }, +} as RouteProp + +describe(BackupScreen, () => { + it('renders backup options when none are completed', async () => { + const tree = render() + + await act(async () => { + // Wait for the screen to render + }) + + expect(tree.toJSON()).toMatchSnapshot() + }) + + it('renders backup options when some are completed', async () => { + const tree = renderWithProviders( + + + , + { preloadedState: preloadedSharedState({ account: ACCOUNT }) } + ) + + await act(async () => { + // Wait for the screen to render + }) + + expect(tree.toJSON()).toMatchSnapshot() + }) +}) diff --git a/apps/mobile/src/screens/Onboarding/BackupScreen.tsx b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx new file mode 100644 index 0000000..bdf1aa7 --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/BackupScreen.tsx @@ -0,0 +1,208 @@ +import { CompositeScreenProps } from '@react-navigation/native' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { StackScreenProps } from '@react-navigation/stack' +import React, { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { + AppStackParamList, + OnboardingStackParamList, + useOnboardingStackNavigation, +} from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { BackButton } from 'src/components/buttons/BackButton' +import { EducationContentType } from 'src/components/education' +import { isCloudStorageAvailable } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { OptionCard } from 'src/features/onboarding/OptionCard' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { Button, Flex, Icons, Text, TouchableArea, useIsDarkMode, useSporeColors } from 'ui/src' +import PaperIcon from 'ui/src/assets/icons/paper-stack.svg' +import { iconSizes } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { isAndroid } from 'uniswap/src/utils/platform' +import { useAsyncData } from 'utilities/src/react/hooks' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { BackupType } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { ElementName } from 'wallet/src/telemetry/constants' +import { openSettings } from 'wallet/src/utils/linking' + +type Props = CompositeScreenProps< + StackScreenProps, + NativeStackScreenProps +> + +export function BackupScreen({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + const { navigate } = useOnboardingStackNavigation() + + const { data: cloudStorageAvailable } = useAsyncData(isCloudStorageAvailable) + + const activeAccount = useActiveAccount() + const activeAccountBackups = activeAccount?.backups + + const renderHeaderLeft = useCallback( + () => ( + { + navigation.pop(2) + }} + /> + ), + [navigation] + ) + + useEffect(() => { + const shouldOverrideBackButton = params?.importType === ImportType.SeedPhrase + if (shouldOverrideBackButton) { + navigation.setOptions({ + headerLeft: renderHeaderLeft, + }) + } + }) + + const onPressNext = (): void => { + navigation.navigate({ + name: OnboardingScreens.Notifications, + params, + merge: true, + }) + } + + const onPressEducationButton = (): void => { + navigation.navigate(Screens.Education, { + type: EducationContentType.SeedPhrase, + importType: params.importType, + entryPoint: params.entryPoint, + }) + } + + const onPressCloudBackup = (): void => { + if (!cloudStorageAvailable) { + Alert.alert( + isAndroid + ? t('account.cloud.error.unavailable.title.android') + : t('account.cloud.error.unavailable.title.ios'), + isAndroid + ? t('account.cloud.error.unavailable.message.android') + : t('account.cloud.error.unavailable.message.ios'), + [ + { + text: t('account.cloud.error.unavailable.button.settings'), + onPress: openSettings, + style: 'default', + }, + { text: t('account.cloud.error.unavailable.button.cancel'), style: 'cancel' }, + ] + ) + return + } + if (!activeAccount?.address) { + return + } + navigate({ + name: OnboardingScreens.BackupCloudPasswordCreate, + params: { ...params, address: activeAccount.address }, + merge: true, + }) + } + + const onPressManualBackup = (): void => { + navigate({ name: OnboardingScreens.BackupManual, params, merge: true }) + } + + const showSkipOption = + !activeAccountBackups?.length && + (params?.importType === ImportType.SeedPhrase || params?.importType === ImportType.Restore) + + const hasCloudBackup = activeAccountBackups?.some((backup) => backup === BackupType.Cloud) + const hasManualBackup = activeAccountBackups?.some((backup) => backup === BackupType.Manual) + + const isCreatingNew = params?.importType === ImportType.CreateNew + const screenTitle = isCreatingNew + ? t('onboarding.backup.title.new') + : t('onboarding.backup.title.existing') + const options = [] + options.push( + } + title={t('onboarding.backup.option.cloud.title', { + cloudProviderName: getCloudProviderName(), + })} + onPress={onPressCloudBackup} + /> + ) + if (isCreatingNew) { + options.push( + } + title={t('onboarding.backup.option.manual.title')} + onPress={onPressManualBackup} + /> + ) + } + + return ( + + + + + {options} + + {!isCreatingNew && ( + + )} + + + + {isCreatingNew && ( + + )} + {showSkipOption && ( + + + + )} + + + + ) +} + +function RecoveryPhraseTooltip({ + onPressEducationButton, +}: { + onPressEducationButton: () => void +}): JSX.Element { + const { t } = useTranslation() + return ( + + + + {t('onboarding.tooltip.recoveryPhrase.trigger')} + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx new file mode 100644 index 0000000..cb59fcd --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordConfirmScreen.tsx @@ -0,0 +1,40 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { CloudBackupPasswordForm } from 'src/features/CloudBackup/CloudBackupPasswordForm' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' + +export type Props = NativeStackScreenProps< + OnboardingStackParamList, + OnboardingScreens.BackupCloudPasswordConfirm +> + +export function CloudBackupPasswordConfirmScreen({ + navigation, + route: { params }, +}: Props): JSX.Element { + const { t } = useTranslation() + const { password } = params + + const navigateToNextScreen = (): void => { + navigation.navigate({ + name: OnboardingScreens.BackupCloudProcessing, + params, + merge: true, + }) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx new file mode 100644 index 0000000..ae1d2aa --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/CloudBackupPasswordCreateScreen.tsx @@ -0,0 +1,38 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { CloudBackupPasswordForm } from 'src/features/CloudBackup/CloudBackupPasswordForm' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' + +export type Props = NativeStackScreenProps< + OnboardingStackParamList, + OnboardingScreens.BackupCloudPasswordCreate +> + +export function CloudBackupPasswordCreateScreen({ + navigation, + route: { params }, +}: Props): JSX.Element { + const { t } = useTranslation() + + const navigateToNextScreen = ({ password }: { password: string }): void => { + navigation.navigate({ + name: OnboardingScreens.BackupCloudPasswordConfirm, + params: { + ...params, + password, + }, + merge: true, + }) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx b/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx new file mode 100644 index 0000000..bc49407 --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/CloudBackupProcessingScreen.tsx @@ -0,0 +1,47 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { Screen } from 'src/components/layout/Screen' +import { CloudBackupProcessingAnimation } from 'src/features/CloudBackup/CloudBackupProcessingAnimation' +import { OnboardingScreens } from 'src/screens/Screens' + +type Props = NativeStackScreenProps< + OnboardingStackParamList, + OnboardingScreens.BackupCloudProcessing +> + +/** Screen to perform secure recovery phrase backup to Cloud */ +export function CloudBackupProcessingScreen({ + navigation, + route: { + params: { password, importType, entryPoint, address, unitagClaim }, + }, +}: Props): JSX.Element | null { + const onBackupComplete = (): void => { + navigation.navigate({ + name: OnboardingScreens.Notifications, + params: { importType, entryPoint, unitagClaim }, + merge: true, + }) + } + + const onErrorPress = (): void => { + navigation.navigate({ + name: OnboardingScreens.Backup, + params: { importType, entryPoint, unitagClaim }, + merge: true, + }) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx new file mode 100644 index 0000000..ceda130 --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/EditNameScreen.tsx @@ -0,0 +1,195 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { Dispatch, SetStateAction, useEffect, useRef, useState } from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { ActivityIndicator, TextInput as NativeTextInput, StyleSheet } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { SafeKeyboardOnboardingScreen } from 'src/features/onboarding/SafeKeyboardOnboardingScreen' +import { OnboardingScreens } from 'src/screens/Screens' +import { useAddBackButton } from 'src/utils/useAddBackButton' +import { AnimatePresence, Button, Flex, Icons, Text } from 'ui/src' +import { fonts } from 'ui/src/theme' +import { isAndroid } from 'uniswap/src/utils/platform' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' +import { ImportType } from 'wallet/src/features/onboarding/types' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { + PendingAccountActions, + pendingAccountActions, +} from 'wallet/src/features/wallet/create/pendingAccountsSaga' +import { usePendingAccounts } from 'wallet/src/features/wallet/hooks' +import { ElementName } from 'wallet/src/telemetry/constants' +import { shortenAddress } from 'wallet/src/utils/addresses' + +type Props = NativeStackScreenProps + +export function EditNameScreen({ navigation, route: { params } }: Props): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + + // Reference pending accounts to avoid any lag in saga import. + const pendingAccount = Object.values(usePendingAccounts())?.[0] + + useEffect(() => { + // Sets the default wallet nickname based on derivation index once the pendingAccount is set. + if (pendingAccount && pendingAccount.type !== AccountType.Readonly) { + setNewAccountName( + pendingAccount.name || + t('onboarding.wallet.defaultName', { number: pendingAccount.derivationIndex + 1 }) || + '' + ) + } + }, [pendingAccount, t]) + + const [newAccountName, setNewAccountName] = useState('') + + useAddBackButton(navigation) + + useEffect(() => { + const beforeRemoveListener = (): void => { + dispatch(pendingAccountActions.trigger(PendingAccountActions.Delete)) + } + navigation.addListener('beforeRemove', beforeRemoveListener) + + return () => navigation.removeListener('beforeRemove', beforeRemoveListener) + }, [dispatch, navigation]) + + const onPressNext = (): void => { + navigation.navigate({ + name: + params?.importType === ImportType.CreateNew + ? OnboardingScreens.WelcomeWallet + : OnboardingScreens.Notifications, + merge: true, + params, + }) + + if (pendingAccount) { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.Rename, + address: pendingAccount?.address, + newName: newAccountName.trim(), + }) + ) + } + } + + return ( + + {pendingAccount ? ( + + ) : ( + + )} + + + + + + + ) +} + +function CustomizationSection({ + address, + accountName, + setAccountName, +}: { + address: Address + accountName: string + setAccountName: Dispatch> +}): JSX.Element { + const { t } = useTranslation() + const textInputRef = useRef(null) + + // we default it to `true` to avoid flickering of a pencil icon, + // because CustomizationSection has `autoFocus=true` + const [focused, setFocused] = useState(true) + + const focusInputWithKeyboard = (): void => { + textInputRef.current?.focus() + } + const walletAddress = shortenAddress(address) + + return ( + + + + + { + setFocused(false) + setAccountName(accountName.trim()) + }} + onChangeText={setAccountName} + onFocus={(): void => setFocused(true)} + /> + + {!focused && ( + + + + => { + if (isDevBuild()) { + await selectionAsync() + dispatch(openModal({ name: ModalName.Experiments })) + } + }} + onPress={onPressImportWallet}> + + {t('onboarding.landing.button.add')} + + + + + + + + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx new file mode 100644 index 0000000..c5b710b --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/ManualBackupScreen.tsx @@ -0,0 +1,197 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import { SharedEventName } from '@uniswap/analytics-events' +import { addScreenshotListener } from 'expo-screen-capture' +import React, { useEffect, useReducer, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { HiddenMnemonicWordView } from 'src/components/mnemonic/HiddenMnemonicWordView' +import { MnemonicConfirmation } from 'src/components/mnemonic/MnemonicConfirmation' +import { MnemonicDisplay } from 'src/components/mnemonic/MnemonicDisplay' +import { useLockScreenOnBlur } from 'src/features/authentication/lockScreenContext' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { ManualPageViewScreen } from 'src/features/telemetry/constants' +import { OnboardingScreens } from 'src/screens/Screens' +import { Button, Flex, Text, useMedia, useSporeColors } from 'ui/src' +import LockIcon from 'ui/src/assets/icons/lock.svg' +import { iconSizes } from 'ui/src/theme' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { BackupType, SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { useActiveAccount } from 'wallet/src/features/wallet/hooks' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = NativeStackScreenProps + +enum View { + SeedPhrase, + SeedPhraseConfirm, +} + +export function ManualBackupScreen({ navigation, route: { params } }: Props): JSX.Element | null { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const media = useMedia() + + useLockScreenOnBlur() + + const activeAccount = useActiveAccount() + const mnemonicId = (activeAccount as SignerMnemonicAccount)?.mnemonicId + + const [showScreenShotWarningModal, setShowScreenShotWarningModal] = useState(false) + const [view, nextView] = useReducer((curView: View) => curView + 1, View.SeedPhrase) + + const [continueButtonEnabled, setContinueButtonEnabled] = useState(false) + const [continueButtonPressed, setContinueButtonPressed] = useState(false) + + // warning modal on seed phrase view + const [seedWarningAcknowledged, setSeedWarningAcknowledged] = useState(false) + + const onValidationSuccessful = (): void => { + if (activeAccount) { + setContinueButtonPressed(true) + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.AddBackupMethod, + address: activeAccount.address, + backupMethod: BackupType.Manual, + }) + ) + } + } + + useEffect(() => { + if (view !== View.SeedPhrase) { + return + } + + const listener = addScreenshotListener(() => setShowScreenShotWarningModal(true)) + return () => listener?.remove() + }, [view]) + + useEffect(() => { + if (continueButtonPressed && activeAccount?.backups?.includes(BackupType.Manual)) { + navigation.navigate({ name: OnboardingScreens.Notifications, params, merge: true }) + } + }, [continueButtonPressed, activeAccount?.backups, navigation, params]) + + // Manually log as page views as these screens are not captured in navigation events + useEffect(() => { + switch (view) { + case View.SeedPhrase: + sendMobileAnalyticsEvent(SharedEventName.PAGE_VIEWED, { + screen: ManualPageViewScreen.WriteDownRecoveryPhrase, + }) + break + case View.SeedPhraseConfirm: + sendMobileAnalyticsEvent(SharedEventName.PAGE_VIEWED, { + screen: ManualPageViewScreen.ConfirmRecoveryPhrase, + }) + } + }, [view]) + + switch (view) { + case View.SeedPhrase: + return ( + + {showScreenShotWarningModal && ( + setShowScreenShotWarningModal(false)} + /> + )} + + + {seedWarningAcknowledged ? ( + + ) : ( + + )} + + + + + + {!seedWarningAcknowledged && ( + setSeedWarningAcknowledged(true)} /> + )} + + ) + case View.SeedPhraseConfirm: + return ( + + + setContinueButtonEnabled(true)} + /> + + + + + + ) + } + + return null +} + +const SeedWarningModal = ({ onPress }: { onPress: () => void }): JSX.Element => { + const colors = useSporeColors() + const { t } = useTranslation() + return ( + + + + + + + {t('onboarding.recoveryPhrase.warning.final.title')} + + + {t('onboarding.recoveryPhrase.warning.final.message')} + + + + + ) +} diff --git a/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx new file mode 100644 index 0000000..b428929 --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/NotificationsSetupScreen.tsx @@ -0,0 +1,165 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert, Image, Platform, StyleSheet } from 'react-native' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import Trace from 'src/components/Trace/Trace' +import { BackButton } from 'src/components/buttons/BackButton' +import { useBiometricContext } from 'src/features/biometrics/context' +import { useBiometricAppSettings } from 'src/features/biometrics/hooks' +import { promptPushPermission } from 'src/features/notifications/Onesignal' +import { OnboardingScreen } from 'src/features/onboarding/OnboardingScreen' +import { useCompleteOnboardingCallback } from 'src/features/onboarding/hooks' +import { OnboardingScreens } from 'src/screens/Screens' +import { Button, Flex, Text, TouchableArea, useIsDarkMode } from 'ui/src' +import { ONBOARDING_NOTIFICATIONS_DARK, ONBOARDING_NOTIFICATIONS_LIGHT } from 'ui/src/assets' +import { isIOS } from 'uniswap/src/utils/platform' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { useNativeAccountExists } from 'wallet/src/features/wallet/hooks' +import { selectAccounts } from 'wallet/src/features/wallet/selectors' +import i18n from 'wallet/src/i18n/i18n' +import { ElementName } from 'wallet/src/telemetry/constants' +import { openSettings } from 'wallet/src/utils/linking' + +type Props = NativeStackScreenProps + +export const showNotificationSettingsAlert = (): void => { + Alert.alert( + i18n.t('onboarding.notification.permission.title'), + i18n.t('onboarding.notification.permission.message'), + [ + { text: i18n.t('common.navigation.settings'), onPress: openSettings }, + { + text: i18n.t('common.button.cancel'), + }, + ] + ) +} + +export function NotificationsSetupScreen({ navigation, route: { params } }: Props): JSX.Element { + const { t } = useTranslation() + const { requiredForTransactions: isBiometricAuthEnabled } = useBiometricAppSettings() + const accounts = useAppSelector(selectAccounts) + const dispatch = useAppDispatch() + const addresses = Object.keys(accounts) + const hasSeedPhrase = useNativeAccountExists() + const { deviceSupportsBiometrics } = useBiometricContext() + + const onCompleteOnboarding = useCompleteOnboardingCallback(params) + + const renderBackButton = useCallback( + (nav: OnboardingScreens): JSX.Element => ( + navigation.navigate({ name: nav, params, merge: true })} + /> + ), + [navigation, params] + ) + + /* For some screens, we want to override the back button to go to a different screen. + * This helps avoid re-visiting loading states or confirmation views. + */ + useEffect(() => { + const shouldOverrideBackButton = [ + ImportType.SeedPhrase, + ImportType.Restore, + ImportType.CreateNew, + ].includes(params.importType) + if (shouldOverrideBackButton) { + const nextScreen = + params.importType === ImportType.Restore + ? OnboardingScreens.RestoreCloudBackup + : OnboardingScreens.Backup + navigation.setOptions({ + headerLeft: () => renderBackButton(nextScreen), + }) + } + }, [navigation, params, renderBackButton]) + + const navigateToNextScreen = useCallback(async () => { + // Skip security setup if already enabled or already imported seed phrase + if ( + !deviceSupportsBiometrics || + isBiometricAuthEnabled || + (params.entryPoint === OnboardingEntryPoint.Sidebar && hasSeedPhrase) + ) { + await onCompleteOnboarding() + } else { + navigation.navigate({ name: OnboardingScreens.Security, params, merge: true }) + } + }, [ + deviceSupportsBiometrics, + hasSeedPhrase, + isBiometricAuthEnabled, + navigation, + onCompleteOnboarding, + params, + ]) + + const onPressEnableNotifications = useCallback(async () => { + promptPushPermission(() => { + addresses.forEach((address) => { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.TogglePushNotification, + enabled: true, + address, + }) + ) + }) + }, showNotificationSettingsAlert) + + await navigateToNextScreen() + }, [addresses, dispatch, navigateToNextScreen]) + + return ( + + + + + + + + + {t('common.button.later')} + + + + + + + + + ) +} + +const NotificationsBackgroundImage = (): JSX.Element => { + const isDarkMode = useIsDarkMode() + return ( + + ) +} + +const styles = StyleSheet.create({ + image: { + height: '100%', + resizeMode: 'contain', + width: '100%', + }, +}) diff --git a/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx new file mode 100644 index 0000000..4f9cf6e --- /dev/null +++ b/apps/mobile/src/screens/Onboarding/QRAnimation/QRAnimation.tsx @@ -0,0 +1,382 @@ +import { + Blur, + Canvas, + Group, + Oval, + RoundedRect, + SkiaValue, + useSharedValueEffect, + useValue, +} from '@shopify/react-native-skia' +import { ResizeMode, Video } from 'expo-av' +import React, { useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { StyleProp, StyleSheet, ViewStyle } from 'react-native' +import { + AnimateStyle, + Easing, + EntryExitAnimationFunction, + runOnJS, + useAnimatedStyle, + useSharedValue, + withDelay, + withTiming, +} from 'react-native-reanimated' +import { GradientBackground } from 'src/components/gradients/GradientBackground' +import { UniconThemedGradient } from 'src/components/gradients/UniconThemedGradient' +import { QRCodeDisplay } from 'src/components/QRCodeScanner/QRCode' +import Trace from 'src/components/Trace/Trace' +import { + flashWipeAnimation, + letsGoButtonFadeIn, + qrInnerBlurConfig, + qrScaleIn, + qrSlideUpAndFadeInConfig, + qrSlideUpAtEnd, + realQrFadeIn, + realQrTopGlowFadeIn, + textSlideUpAtEnd, + videoFadeOut, +} from 'src/screens/Onboarding/QRAnimation/animations' +import { + Button, + Flex, + getUniconV2Colors, + Text, + useIsDarkMode, + useMedia, + useSporeColors, + useUniconColors, +} from 'ui/src' +import { ONBOARDING_QR_ETCHING_VIDEO_DARK, ONBOARDING_QR_ETCHING_VIDEO_LIGHT } from 'ui/src/assets' +import LockIcon from 'ui/src/assets/icons/lock.svg' +import { AnimatedFlex, flexStyles } from 'ui/src/components/layout' +import { fonts, iconSizes, opacify, spacing } from 'ui/src/theme' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { Arrow } from 'wallet/src/components/icons/Arrow' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { ElementName } from 'wallet/src/telemetry/constants' + +export function QRAnimation({ + activeAddress, + isNewWallet, + onPressNext, +}: { + activeAddress: string + isNewWallet: boolean + onPressNext: () => void +}): JSX.Element { + const colors = useSporeColors() + const { t } = useTranslation() + const video = useRef + + + )} + + + ) +} diff --git a/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx b/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx new file mode 100644 index 0000000..82d575e --- /dev/null +++ b/apps/mobile/src/screens/SettingsCloudBackupProcessingScreen.tsx @@ -0,0 +1,35 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { SettingsStackParamList } from 'src/app/navigation/types' +import { Screen } from 'src/components/layout/Screen' +import { CloudBackupProcessingAnimation } from 'src/features/CloudBackup/CloudBackupProcessingAnimation' +import { Screens } from 'src/screens/Screens' + +type Props = NativeStackScreenProps + +export function SettingsCloudBackupProcessingScreen({ + navigation, + route: { + params: { address, password }, + }, +}: Props): JSX.Element | null { + const onBackupComplete = (): void => { + navigation.replace(Screens.SettingsCloudBackupStatus, { address }) + } + + const onErrorPress = (): void => { + navigation.navigate(Screens.Settings) + } + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx new file mode 100644 index 0000000..459ceb7 --- /dev/null +++ b/apps/mobile/src/screens/SettingsCloudBackupStatus.tsx @@ -0,0 +1,171 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Alert } from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { SettingsStackParamList } from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { deleteCloudStorageMnemonicBackup } from 'src/features/CloudBackup/RNCloudStorageBackupsManager' +import { useCloudBackups } from 'src/features/CloudBackup/hooks' +import { useBiometricAppSettings, useBiometricPrompt } from 'src/features/biometrics/hooks' +import { Screens } from 'src/screens/Screens' +import { Button, Flex, Text, useSporeColors } from 'ui/src' +import Checkmark from 'ui/src/assets/icons/check.svg' +import { iconSizes } from 'ui/src/theme' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { logger } from 'utilities/src/logger/logger' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { WarningModal } from 'wallet/src/components/modals/WarningModal/WarningModal' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { + AccountType, + BackupType, + SignerMnemonicAccount, +} from 'wallet/src/features/wallet/accounts/types' +import { useAccounts } from 'wallet/src/features/wallet/hooks' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = NativeStackScreenProps + +export function SettingsCloudBackupStatus({ + navigation, + route: { + params: { address }, + }, +}: Props): JSX.Element { + const { t } = useTranslation() + const colors = useSporeColors() + const dispatch = useAppDispatch() + const accounts = useAccounts() + const mnemonicId = (accounts[address] as SignerMnemonicAccount)?.mnemonicId + const backups = useCloudBackups(mnemonicId) + const associatedAccounts = Object.values(accounts).filter( + (a) => a.type === AccountType.SignerMnemonic && a.mnemonicId === mnemonicId + ) + + const [showBackupDeleteWarning, setShowBackupDeleteWarning] = useState(false) + const onConfirmDeleteBackup = async (): Promise => { + if (requiredForTransactions) { + await biometricTrigger() + } else { + await deleteBackup() + } + } + + const deleteBackup = async (): Promise => { + try { + await deleteCloudStorageMnemonicBackup(mnemonicId) + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.RemoveBackupMethod, + address, + backupMethod: BackupType.Cloud, + }) + ) + setShowBackupDeleteWarning(false) + navigation.navigate(Screens.Settings) + } catch (error) { + setShowBackupDeleteWarning(false) + logger.error(error, { tags: { file: 'SettingsCloudBackupStatus', function: 'deleteBackup' } }) + + Alert.alert( + t('settings.setting.backup.error.title', { cloudProviderName: getCloudProviderName() }), + t('settings.setting.backup.error.message.short'), + [{ text: t('common.button.ok'), style: 'default' }] + ) + } + } + + const { requiredForTransactions } = useBiometricAppSettings() + const { trigger: biometricTrigger } = useBiometricPrompt(deleteBackup) + + const onPressBack = (): void => { + navigation.navigate(Screens.Settings) + } + + const googleDriveEmail = backups[0]?.googleDriveEmail + + return ( + + + + {t('settings.setting.backup.status.title', { cloudProviderName: getCloudProviderName() })} + + + + + + + {t('settings.setting.backup.status.description', { + cloudProviderName: getCloudProviderName(), + })} + + + + {t('settings.setting.backup.recoveryPhrase.label')} + + + + + {t('settings.setting.backup.status.recoveryPhrase.backed')} + + + {/* @TODO: [MOB-249] Add non-backed up state once we have more options on this page */} + + + {googleDriveEmail && ( + + {googleDriveEmail} + + )} + + + + + + + {showBackupDeleteWarning && ( + { + setShowBackupDeleteWarning(false) + }} + onConfirm={onConfirmDeleteBackup}> + {associatedAccounts.length > 1 && ( + + + {t('settings.setting.backup.delete.confirm.message')} + + + {associatedAccounts.map((account) => ( + + ))} + + + )} + + )} + + ) +} diff --git a/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx new file mode 100644 index 0000000..ffb3de6 --- /dev/null +++ b/apps/mobile/src/screens/SettingsFiatCurrencyModal.tsx @@ -0,0 +1,87 @@ +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { Action } from 'redux' +import { useAppDispatch } from 'src/app/hooks' +import { VirtualizedList } from 'src/components/layout/VirtualizedList' +import { closeModal } from 'src/features/modals/modalSlice' +import { Flex, Icons, Text, TouchableArea, useSporeColors } from 'ui/src' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { FiatCurrency, ORDERED_CURRENCIES } from 'wallet/src/features/fiatCurrency/constants' +import { useAppFiatCurrency, useFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { setCurrentFiatCurrency } from 'wallet/src/features/fiatCurrency/slice' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function SettingsFiatCurrencyModal(): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + + return ( + dispatch(closeModal({ name: ModalName.FiatCurrencySelector }))}> + + {t('settings.setting.currency.title')} + + + { + dispatch(closeModal({ name: ModalName.FiatCurrencySelector })) + }} + /> + + + ) +} + +function FiatCurrencySelection({ onClose }: { onClose: () => void }): JSX.Element { + const selectedCurrency = useAppFiatCurrency() + + return ( + + {ORDERED_CURRENCIES.map((currency) => ( + + ))} + + ) +} + +interface FiatCurrencyOptionProps { + active?: boolean + currency: FiatCurrency + onPress: () => void +} + +function FiatCurrencyOption({ active, currency, onPress }: FiatCurrencyOptionProps): JSX.Element { + const dispatch = useAppDispatch() + const colors = useSporeColors() + const { name, code } = useFiatCurrencyInfo(currency) + + const changeCurrency = useCallback(() => { + dispatch(setCurrentFiatCurrency(currency)) + onPress() + }, [dispatch, onPress, currency]) + + return ( + + + + {name} + + {code} + + + {active && } + + + ) +} diff --git a/apps/mobile/src/screens/SettingsLanguageModal.tsx b/apps/mobile/src/screens/SettingsLanguageModal.tsx new file mode 100644 index 0000000..f704588 --- /dev/null +++ b/apps/mobile/src/screens/SettingsLanguageModal.tsx @@ -0,0 +1,57 @@ +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { Linking } from 'react-native' +import { Action } from 'redux' +import { useAppDispatch } from 'src/app/hooks' +import { closeModal } from 'src/features/modals/modalSlice' +import { Button, Flex, Icons, Text } from 'ui/src' +import { isAndroid } from 'uniswap/src/utils/platform' +import { BottomSheetModal } from 'wallet/src/components/modals/BottomSheetModal' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +// TODO(MOB-1190): this is DEP_blue_300 at 10% opacity, remove when we have a named color for this +const LIGHT_BLUE = '#4C82FB1A' + +const openLanguageSettings = async (): Promise => { + if (isAndroid) { + await Linking.openSettings() + } else { + await Linking.openURL('app-settings:') + } +} + +export function SettingsLanguageModal(): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + + const onClose = useCallback( + (): Action => dispatch(closeModal({ name: ModalName.LanguageSelector })), + [dispatch] + ) + + return ( + + + + + + + + + + {t('settings.setting.language.title')} + + + {t('settings.setting.language.description')} + + + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsPrivacyScreen.tsx b/apps/mobile/src/screens/SettingsPrivacyScreen.tsx new file mode 100644 index 0000000..c58a155 --- /dev/null +++ b/apps/mobile/src/screens/SettingsPrivacyScreen.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { selectAllowAnalytics } from 'src/features/telemetry/selectors' +import { setAllowAnalytics } from 'src/features/telemetry/slice' +import { Flex, Text } from 'ui/src' +import { Switch } from 'wallet/src/components/buttons/Switch' + +export function SettingsPrivacyScreen(): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const analyticsAllowed = useAppSelector(selectAllowAnalytics) + + const onChangeAllowAnalytics = (enabled: boolean): void => { + dispatch(setAllowAnalytics({ enabled })) + } + + return ( + + + {t('settings.setting.privacy.title')} + + + + {t('settings.setting.privacy.analytics.title')} + + {t('settings.setting.privacy.analytics.description')} + + + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsScreen.tsx b/apps/mobile/src/screens/SettingsScreen.tsx new file mode 100644 index 0000000..54b32e7 --- /dev/null +++ b/apps/mobile/src/screens/SettingsScreen.tsx @@ -0,0 +1,526 @@ +/* eslint-disable max-lines */ +import { useNavigation } from '@react-navigation/core' +import { default as React, useCallback, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Image, ListRenderItemInfo, SectionList, StyleSheet } from 'react-native' +import { FadeInDown, FadeOutUp } from 'react-native-reanimated' +import { SvgProps } from 'react-native-svg' +import { useDispatch } from 'react-redux' +import { useAppDispatch } from 'src/app/hooks' +import { + OnboardingStackNavigationProp, + SettingsStackNavigationProp, + useSettingsStackNavigation, +} from 'src/app/navigation/types' +import { + SettingsRow, + SettingsSection, + SettingsSectionItem, + SettingsSectionItemComponent, +} from 'src/components/Settings/SettingsRow' +import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' +import { APP_FEEDBACK_LINK } from 'src/constants/urls' +import { useBiometricContext } from 'src/features/biometrics/context' +import { useBiometricName, useDeviceSupportsBiometricAuth } from 'src/features/biometrics/hooks' +import { useWalletRestore } from 'src/features/wallet/hooks' +import { OnboardingScreens, Screens } from 'src/screens/Screens' +import { getFullAppVersion } from 'src/utils/version' +import { + AnimatedFlex, + Button, + Flex, + IconProps, + Icons, + Text, + TouchableArea, + useDeviceInsets, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import { AVATARS_DARK, AVATARS_LIGHT } from 'ui/src/assets' +import BookOpenIcon from 'ui/src/assets/icons/book-open.svg' +import ContrastIcon from 'ui/src/assets/icons/contrast.svg' +import FaceIdIcon from 'ui/src/assets/icons/faceid.svg' +import FingerprintIcon from 'ui/src/assets/icons/fingerprint.svg' +import LikeSquare from 'ui/src/assets/icons/like-square.svg' +import LockIcon from 'ui/src/assets/icons/lock.svg' +import MessageQuestion from 'ui/src/assets/icons/message-question.svg' +import UniswapIcon from 'ui/src/assets/icons/uniswap-logo.svg' +import { iconSizes, spacing } from 'ui/src/theme' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { getCloudProviderName } from 'uniswap/src/utils/cloud-backup/getCloudProviderName' +import { isAndroid } from 'uniswap/src/utils/platform' +import { ONE_SECOND_MS } from 'utilities/src/time/time' +import { useTimeout } from 'utilities/src/time/timing' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { useCurrentAppearanceSetting } from 'wallet/src/features/appearance/hooks' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useAppFiatCurrencyInfo } from 'wallet/src/features/fiatCurrency/hooks' +import { useCurrentLanguageInfo } from 'wallet/src/features/language/hooks' +import { ImportType, OnboardingEntryPoint } from 'wallet/src/features/onboarding/types' +import { + AccountType, + BackupType, + SignerMnemonicAccount, +} from 'wallet/src/features/wallet/accounts/types' +import { + useAccounts, + useHideSmallBalancesSetting, + useHideSpamTokensSetting, + useSignerAccounts, +} from 'wallet/src/features/wallet/hooks' +import { + resetWallet, + setFinishedOnboarding, + setHideSmallBalances, + setHideSpamTokens, +} from 'wallet/src/features/wallet/slice' +import { ModalName } from 'wallet/src/telemetry/constants' + +export function SettingsScreen(): JSX.Element { + const navigation = useNavigation() + const dispatch = useAppDispatch() + const colors = useSporeColors() + const insets = useDeviceInsets() + const { deviceSupportsBiometrics } = useBiometricContext() + const { t } = useTranslation() + + const currencyConversionEnabled = useFeatureFlag(FEATURE_FLAGS.CurrencyConversion) + + // check if device supports biometric authentication, if not, hide option + const { touchId: isTouchIdSupported, faceId: isFaceIdSupported } = + useDeviceSupportsBiometricAuth() + + const biometricsMethod = useBiometricName(isTouchIdSupported) + const currentAppearanceSetting = useCurrentAppearanceSetting() + const currentFiatCurrencyInfo = useAppFiatCurrencyInfo() + const { originName: currentLanguage } = useCurrentLanguageInfo() + + const hideSmallBalances = useHideSmallBalancesSetting() + const onToggleHideSmallBalances = useCallback(() => { + dispatch(setHideSmallBalances(!hideSmallBalances)) + }, [dispatch, hideSmallBalances]) + + const hideSpamTokens = useHideSpamTokensSetting() + const onToggleHideSpamTokens = useCallback(() => { + dispatch(setHideSpamTokens(!hideSpamTokens)) + }, [dispatch, hideSpamTokens]) + + // Signer account info + const signerAccount = useSignerAccounts()[0] + // We sync backup state across all accounts under the same mnemonic, so can check status with any account. + const hasCloudBackup = signerAccount?.backups?.includes(BackupType.Cloud) + const noSignerAccountImported = !signerAccount + const { walletNeedsRestore } = useWalletRestore() + + const sections: SettingsSection[] = useMemo((): SettingsSection[] => { + const svgProps: SvgProps = { + color: colors.neutral2.get(), + height: iconSizes.icon24, + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '2', + width: iconSizes.icon24, + } + const iconProps: IconProps = { + color: '$neutral2', + size: '$icon.24', + strokeLinecap: 'round', + strokeLinejoin: 'round', + } + + // Defining them inline instead of outside component b.c. they need t() + return [ + { + subTitle: t('settings.section.preferences'), + data: [ + { + screen: Screens.SettingsAppearance, + text: t('settings.setting.appearance.title'), + currentSetting: + currentAppearanceSetting === 'system' + ? t('settings.setting.appearance.option.device.title') + : currentAppearanceSetting === 'dark' + ? t('settings.setting.appearance.option.dark.title') + : t('settings.setting.appearance.option.light.title'), + icon: , + }, + + ...(currencyConversionEnabled + ? ([ + { + modal: ModalName.FiatCurrencySelector, + text: t('settings.setting.currency.title'), + currentSetting: currentFiatCurrencyInfo.code, + icon: , + }, + ] as SettingsSectionItem[]) + : []), + { + modal: ModalName.LanguageSelector, + text: t('settings.setting.language.title'), + currentSetting: currentLanguage, + icon: , + }, + { + screen: Screens.SettingsPrivacy, + text: t('settings.setting.privacy.title'), + icon: , + }, + { + text: t('settings.setting.smallBalances.title'), + icon: , + isToggleEnabled: hideSmallBalances, + onToggle: onToggleHideSmallBalances, + }, + { + text: t('settings.setting.unknownTokens.title'), + icon: , + isToggleEnabled: hideSpamTokens, + onToggle: onToggleHideSpamTokens, + }, + // @TODO: [MOB-250] add back testnet toggle once we support testnets + ], + }, + { + subTitle: t('settings.section.security'), + isHidden: noSignerAccountImported, + data: [ + ...(deviceSupportsBiometrics + ? [ + { + screen: Screens.SettingsBiometricAuth as Screens.SettingsBiometricAuth, + isHidden: !isTouchIdSupported && !isFaceIdSupported, + text: isAndroid ? t('settings.setting.biometrics.title') : biometricsMethod, + icon: isTouchIdSupported ? ( + + ) : ( + + ), + }, + ] + : []), + { + screen: Screens.SettingsViewSeedPhrase, + text: t('settings.setting.recoveryPhrase.title'), + icon: , + screenProps: { address: signerAccount?.address ?? '', walletNeedsRestore }, + isHidden: noSignerAccountImported, + }, + { + screen: walletNeedsRestore + ? Screens.OnboardingStack + : hasCloudBackup + ? Screens.SettingsCloudBackupStatus + : Screens.SettingsCloudBackupPasswordCreate, + screenProps: walletNeedsRestore + ? { + screen: OnboardingScreens.RestoreCloudBackupLoading, + params: { + entryPoint: OnboardingEntryPoint.Sidebar, + importType: ImportType.Restore, + }, + } + : { address: signerAccount?.address ?? '' }, + text: t('settings.setting.backup.selected', { + cloudProviderName: getCloudProviderName(), + }), + icon: , + isHidden: noSignerAccountImported, + }, + ], + }, + { + subTitle: t('settings.section.support'), + data: [ + { + screen: Screens.WebView, + screenProps: { + uriLink: APP_FEEDBACK_LINK, + headerTitle: t('settings.action.feedback'), + }, + text: t('settings.action.feedback'), + icon: , + }, + { + screen: Screens.WebView, + screenProps: { + uriLink: uniswapUrls.helpUrl, + headerTitle: t('settings.action.help'), + }, + text: t('settings.action.help'), + icon: , + }, + ], + }, + { + subTitle: t('settings.section.about'), + data: [ + { + screen: Screens.WebView, + screenProps: { + uriLink: uniswapUrls.privacyPolicyUrl, + headerTitle: t('settings.action.privacy'), + }, + text: t('settings.action.privacy'), + icon: , + }, + { + screen: Screens.WebView, + screenProps: { + uriLink: uniswapUrls.termsOfServiceUrl, + headerTitle: t('settings.action.terms'), + }, + text: t('settings.action.terms'), + icon: , + }, + ], + }, + { + subTitle: 'Developer settings', + isHidden: !__DEV__, + data: [ + { + screen: Screens.Dev, + text: 'Dev options', + icon: , + }, + { component: }, + ], + }, + ] + }, [ + colors.neutral2, + t, + currentAppearanceSetting, + currencyConversionEnabled, + currentFiatCurrencyInfo.code, + currentLanguage, + hideSmallBalances, + onToggleHideSmallBalances, + hideSpamTokens, + onToggleHideSpamTokens, + noSignerAccountImported, + deviceSupportsBiometrics, + isTouchIdSupported, + isFaceIdSupported, + biometricsMethod, + signerAccount?.address, + walletNeedsRestore, + hasCloudBackup, + ]) + + const renderItem = ({ + item, + }: ListRenderItemInfo< + SettingsSectionItem | SettingsSectionItemComponent + >): JSX.Element | null => { + if (item.isHidden) { + return null + } + if ('component' in item) { + return item.component + } + return + } + + return ( + {t('settings.title')}}> + + } + ListHeaderComponent={} + initialNumToRender={20} + keyExtractor={(_item, index): string => 'settings' + index} + renderItem={renderItem} + renderSectionFooter={(): JSX.Element => } + renderSectionHeader={({ section: { subTitle } }): JSX.Element => ( + + + {subTitle} + + + )} + sections={sections.filter((p) => !p.isHidden)} + showsVerticalScrollIndicator={false} + /> + + + ) +} + +const renderItemSeparator = (): JSX.Element => + +function OnboardingRow({ iconProps }: { iconProps: SvgProps }): JSX.Element { + const dispatch = useDispatch() + const navigation = useSettingsStackNavigation() + + return ( + { + navigation.goBack() + dispatch(resetWallet()) + dispatch(setFinishedOnboarding({ finishedOnboarding: false })) + }}> + + + + + + + Onboarding + + + + + + ) +} + +const DEFAULT_ACCOUNTS_TO_DISPLAY = 6 + +function WalletSettings(): JSX.Element { + const { t } = useTranslation() + const navigation = useSettingsStackNavigation() + const addressToAccount = useAccounts() + const [showAll, setShowAll] = useState(false) + + const allAccounts = useMemo(() => { + const accounts = Object.values(addressToAccount) + const _mnemonicWallets = accounts + .filter((a): a is SignerMnemonicAccount => a.type === AccountType.SignerMnemonic) + .sort((a, b) => { + return a.derivationIndex - b.derivationIndex + }) + const _viewOnlyWallets = accounts + .filter((a) => a.type === AccountType.Readonly) + .sort((a, b) => { + return a.timeImportedMs - b.timeImportedMs + }) + return [..._mnemonicWallets, ..._viewOnlyWallets] + }, [addressToAccount]) + + const toggleViewAll = (): void => { + setShowAll(!showAll) + } + + const handleNavigation = (address: string): void => { + navigation.navigate(Screens.SettingsWallet, { address }) + } + + return ( + + + + {t('settings.section.wallet.title')} + + + {allAccounts + .slice(0, showAll ? allAccounts.length : DEFAULT_ACCOUNTS_TO_DISPLAY) + .map((account) => ( + handleNavigation(account.address)}> + + + + + + + + ))} + {allAccounts.length > DEFAULT_ACCOUNTS_TO_DISPLAY && ( + + )} + + ) +} + +function FooterSettings(): JSX.Element { + const { t } = useTranslation() + const [showSignature, setShowSignature] = useState(false) + const isDarkMode = useIsDarkMode() + + // Fade out signature after duration + useTimeout( + showSignature + ? (): void => { + setShowSignature(false) + } + : (): void => undefined, + SIGNATURE_VISIBLE_DURATION + ) + + return ( + + {showSignature ? ( + + + + {t('settings.footer')} + + + {isDarkMode ? ( + + ) : ( + + )} + + ) : null} + { + setShowSignature(true) + }}> + {t('settings.version', { appVersion: getFullAppVersion() })} + + + ) +} + +const ImageStyles = StyleSheet.create({ + responsiveImage: { + aspectRatio: 135 / 76, + height: undefined, + width: '100%', + }, +}) + +const SIGNATURE_VISIBLE_DURATION = ONE_SECOND_MS * 10 diff --git a/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx new file mode 100644 index 0000000..15a6747 --- /dev/null +++ b/apps/mobile/src/screens/SettingsViewSeedPhraseScreen.tsx @@ -0,0 +1,43 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { SettingsStackParamList } from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { SeedPhraseDisplay } from 'src/components/mnemonic/SeedPhraseDisplay' +import { Screens } from 'src/screens/Screens' +import { Text } from 'ui/src' +import { SignerMnemonicAccount } from 'wallet/src/features/wallet/accounts/types' +import { useAccounts } from 'wallet/src/features/wallet/hooks' + +type Props = NativeStackScreenProps + +export function SettingsViewSeedPhraseScreen({ + navigation, + route: { + params: { address, walletNeedsRestore }, + }, +}: Props): JSX.Element { + const { t } = useTranslation() + + const accounts = useAccounts() + const account = accounts[address] + const mnemonicId = (account as SignerMnemonicAccount)?.mnemonicId + + const navigateBack = (): void => { + navigation.goBack() + } + + return ( + + + {t('settings.setting.recoveryPhrase.title')} + + + + ) +} diff --git a/apps/mobile/src/screens/SettingsWallet.tsx b/apps/mobile/src/screens/SettingsWallet.tsx new file mode 100644 index 0000000..0ac57ab --- /dev/null +++ b/apps/mobile/src/screens/SettingsWallet.tsx @@ -0,0 +1,285 @@ +import { useFocusEffect, useNavigation } from '@react-navigation/core' +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useCallback, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { ListRenderItemInfo, SectionList } from 'react-native' +import { SvgProps } from 'react-native-svg' +import { useAppDispatch } from 'src/app/hooks' +import { navigate } from 'src/app/navigation/rootNavigation' +import { + OnboardingStackNavigationProp, + SettingsStackNavigationProp, + SettingsStackParamList, +} from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { + SettingsRow, + SettingsSection, + SettingsSectionItem, + SettingsSectionItemComponent, +} from 'src/components/Settings/SettingsRow' +import { openModal } from 'src/features/modals/modalSlice' +import { + NotificationPermission, + useNotificationOSPermissionsEnabled, +} from 'src/features/notifications/hooks/useNotificationOSPermissionsEnabled' +import { promptPushPermission } from 'src/features/notifications/Onesignal' +import { sendMobileAnalyticsEvent } from 'src/features/telemetry' +import { MobileEventName } from 'src/features/telemetry/constants' +import { showNotificationSettingsAlert } from 'src/screens/Onboarding/NotificationsSetupScreen' +import { Screens, UnitagScreens } from 'src/screens/Screens' +import { Button, Flex, Text, useSporeColors } from 'ui/src' +import NotificationIcon from 'ui/src/assets/icons/bell.svg' +import GlobalIcon from 'ui/src/assets/icons/global.svg' +import TextEditIcon from 'ui/src/assets/icons/textEdit.svg' +import { iconSizes, spacing } from 'ui/src/theme' +import { AddressDisplay } from 'wallet/src/components/accounts/AddressDisplay' +import { Switch } from 'wallet/src/components/buttons/Switch' +import { ChainId } from 'wallet/src/constants/chains' +import { useENS } from 'wallet/src/features/ens/useENS' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useUnitagByAddress } from 'wallet/src/features/unitags/hooks' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useAccounts, useSelectAccountNotificationSetting } from 'wallet/src/features/wallet/hooks' +import { ElementName, ModalName } from 'wallet/src/telemetry/constants' + +type Props = NativeStackScreenProps + +// Specific design request not in standard sizing type +const UNICON_ICON_SIZE = 56 + +export function SettingsWallet({ + route: { + params: { address }, + }, +}: Props): JSX.Element { + const dispatch = useAppDispatch() + const { t } = useTranslation() + const colors = useSporeColors() + const addressToAccount = useAccounts() + const currentAccount = addressToAccount[address] + const ensName = useENS(ChainId.Mainnet, address)?.name + const { unitag } = useUnitagByAddress(address) + const readonly = currentAccount?.type === AccountType.Readonly + const navigation = useNavigation() + + const notificationOSPermission = useNotificationOSPermissionsEnabled() + const notificationsEnabledOnFirebase = useSelectAccountNotificationSetting(address) + const [notificationSwitchEnabled, setNotificationSwitchEnabled] = useState( + notificationsEnabledOnFirebase + ) + const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) + + const showEditProfile = unitagsFeatureFlagEnabled && !readonly + + useEffect(() => { + // If the user deletes the account while on this screen, go back + if (!currentAccount) { + navigation.goBack() + } + }, [currentAccount, navigation]) + + // Need to trigger a state update when the user backgrounds the app to enable notifications and then returns to this screen + useFocusEffect( + useCallback( + () => + setNotificationSwitchEnabled( + notificationsEnabledOnFirebase && + notificationOSPermission === NotificationPermission.Enabled + ), + [notificationOSPermission, notificationsEnabledOnFirebase] + ) + ) + + const onChangeNotificationSettings = (enabled: boolean): void => { + sendMobileAnalyticsEvent(MobileEventName.NotificationsToggled, { enabled }) + if (notificationOSPermission === NotificationPermission.Enabled) { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.TogglePushNotification, + enabled, + address, + }) + ) + setNotificationSwitchEnabled(enabled) + } else { + promptPushPermission(() => { + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.TogglePushNotification, + enabled: true, + address, + }) + ) + setNotificationSwitchEnabled(enabled) + }, showNotificationSettingsAlert) + } + } + + const iconProps: SvgProps = { + color: colors.neutral2.get(), + height: iconSizes.icon24, + strokeLinecap: 'round', + strokeLinejoin: 'round', + strokeWidth: '2', + width: iconSizes.icon24, + } + + const editNicknameSectionOption: SettingsSectionItem = { + screen: Screens.SettingsWalletEdit, + text: t('settings.setting.wallet.label'), + icon: , + screenProps: { address }, + isHidden: !!ensName || !!unitag?.username, + } + + const sections: SettingsSection[] = [ + { + subTitle: t('settings.setting.wallet.preferences.title'), + data: [ + ...(showEditProfile ? [] : [editNicknameSectionOption]), + { + action: ( + + ), + text: t('settings.setting.wallet.notifications.title'), + icon: , + }, + { + screen: Screens.SettingsWalletManageConnection, + text: t('settings.setting.wallet.connections.title'), + icon: , + screenProps: { address }, + isHidden: readonly, + }, + ], + }, + ] + + const renderItem = ({ + item, + }: ListRenderItemInfo< + SettingsSectionItem | SettingsSectionItemComponent + >): JSX.Element | null => { + if ('component' in item) { + return item.component + } + if (item.isHidden) { + return null + } + return + } + + const onRemoveWallet = (): void => { + dispatch( + openModal({ + name: ModalName.RemoveWallet, + initialState: { address }, + }) + ) + } + + return ( + + + + + + + + + + : undefined + } + keyExtractor={(_item, index): string => 'wallet_settings' + index} + renderItem={renderItem} + renderSectionFooter={(): JSX.Element => } + renderSectionHeader={({ section: { subTitle } }): JSX.Element => ( + + + {subTitle} + + + )} + sections={sections.filter((p) => !p.isHidden)} + showsVerticalScrollIndicator={false} + stickySectionHeadersEnabled={false} + /> + + + + + ) +} + +const renderItemSeparator = (): JSX.Element => + +function AddressDisplayHeader({ address }: { address: Address }): JSX.Element { + const { t } = useTranslation() + const ensName = useENS(ChainId.Mainnet, address)?.name + const { unitag } = useUnitagByAddress(address) + + const onPressEditProfile = (): void => { + if (unitag?.username) { + navigate(Screens.UnitagStack, { + screen: UnitagScreens.EditProfile, + params: { + address, + unitag: unitag.username, + entryPoint: Screens.SettingsWallet, + }, + }) + } else { + navigate(Screens.SettingsWalletEdit, { + address, + }) + } + } + + return ( + + + + + {(!ensName || !!unitag) && ( + + )} + + ) +} diff --git a/apps/mobile/src/screens/SettingsWalletEdit.tsx b/apps/mobile/src/screens/SettingsWalletEdit.tsx new file mode 100644 index 0000000..c923bd6 --- /dev/null +++ b/apps/mobile/src/screens/SettingsWalletEdit.tsx @@ -0,0 +1,169 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React, { useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + Keyboard, + KeyboardAvoidingView, + TextInput as NativeTextInput, + StyleSheet, +} from 'react-native' +import { useAppDispatch } from 'src/app/hooks' +import { SettingsStackParamList } from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { UnitagBanner } from 'src/components/unitags/UnitagBanner' +import { Button, Flex, Icons, Text } from 'ui/src' +import { fonts } from 'ui/src/theme' +import { isIOS } from 'uniswap/src/utils/platform' +import { TextInput } from 'wallet/src/components/input/TextInput' +import { NICKNAME_MAX_LENGTH } from 'wallet/src/constants/accounts' +import { FEATURE_FLAGS } from 'wallet/src/features/experiments/constants' +import { useFeatureFlag } from 'wallet/src/features/experiments/hooks' +import { useCanAddressClaimUnitag } from 'wallet/src/features/unitags/hooks' +import { + EditAccountAction, + editAccountActions, +} from 'wallet/src/features/wallet/accounts/editAccountSaga' +import { AccountType } from 'wallet/src/features/wallet/accounts/types' +import { useAccounts, useDisplayName } from 'wallet/src/features/wallet/hooks' +import { DisplayNameType } from 'wallet/src/features/wallet/types' +import { Screens } from './Screens' + +type Props = NativeStackScreenProps + +export function SettingsWalletEdit({ + route: { + params: { address }, + }, +}: Props): JSX.Element { + const { t } = useTranslation() + const dispatch = useAppDispatch() + const activeAccount = useAccounts()[address] + const displayName = useDisplayName(address) + const [nickname, setNickname] = useState(displayName?.name) + const [showEditButton, setShowEditButton] = useState(true) + const unitagsFeatureFlagEnabled = useFeatureFlag(FEATURE_FLAGS.Unitags) + const { canClaimUnitag } = useCanAddressClaimUnitag(address) + const showUnitagBanner = + unitagsFeatureFlagEnabled && + activeAccount?.type === AccountType.SignerMnemonic && + canClaimUnitag + + const accountNameIsEditable = + displayName?.type === DisplayNameType.Local || displayName?.type === DisplayNameType.Address + + const inputRef = useRef(null) + + const onEditButtonPress = (): void => { + inputRef.current?.focus() + setShowEditButton(false) + } + + const onFinishEditing = (): void => { + Keyboard.dismiss() + setShowEditButton(true) + setNickname(nickname?.trim()) + } + + const onPressSaveChanges = (): void => { + onFinishEditing() + dispatch( + editAccountActions.trigger({ + type: EditAccountAction.Rename, + address, + newName: nickname?.trim() ?? '', + }) + ) + } + + return ( + + + + {t('settings.setting.wallet.action.editLabel')} + + + + + + {showEditButton && accountNameIsEditable && ( + + + + + ) +} + +const styles = StyleSheet.create({ + base: { + flex: 1, + justifyContent: 'flex-end', + }, + expand: { + flexGrow: 1, + }, +}) diff --git a/apps/mobile/src/screens/SettingsWalletManageConnection.tsx b/apps/mobile/src/screens/SettingsWalletManageConnection.tsx new file mode 100644 index 0000000..9b79e40 --- /dev/null +++ b/apps/mobile/src/screens/SettingsWalletManageConnection.tsx @@ -0,0 +1,23 @@ +import { NativeStackScreenProps } from '@react-navigation/native-stack' +import React from 'react' +import { SettingsStackParamList } from 'src/app/navigation/types' +import { ConnectedDappsList } from 'src/components/WalletConnect/ConnectedDapps/ConnectedDappsList' +import { Screen } from 'src/components/layout/Screen' +import { useWalletConnect } from 'src/features/walletConnect/useWalletConnect' +import { Screens } from './Screens' + +type Props = NativeStackScreenProps + +export function SettingsWalletManageConnection({ + route: { + params: { address }, + }, +}: Props): JSX.Element { + const { sessions } = useWalletConnect(address) + + return ( + + + + ) +} diff --git a/apps/mobile/src/screens/TokenDetailsScreen.tsx b/apps/mobile/src/screens/TokenDetailsScreen.tsx new file mode 100644 index 0000000..7dc4869 --- /dev/null +++ b/apps/mobile/src/screens/TokenDetailsScreen.tsx @@ -0,0 +1,424 @@ +import { ReactNavigationPerformanceView } from '@shopify/react-native-performance-navigation' +import React, { useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import ContextMenu from 'react-native-context-menu-view' +import { FadeIn, FadeInDown, FadeOutDown } from 'react-native-reanimated' +import { useAppSelector } from 'src/app/hooks' +import { AppStackScreenProp } from 'src/app/navigation/types' +import { PriceExplorer } from 'src/components/PriceExplorer/PriceExplorer' +import { useTokenPriceHistory } from 'src/components/PriceExplorer/usePriceHistory' +import { TokenBalances } from 'src/components/TokenDetails/TokenBalances' +import { TokenDetailsActionButtons } from 'src/components/TokenDetails/TokenDetailsActionButtons' +import { TokenDetailsFavoriteButton } from 'src/components/TokenDetails/TokenDetailsFavoriteButton' +import { TokenDetailsHeader } from 'src/components/TokenDetails/TokenDetailsHeader' +import { TokenDetailsLinks } from 'src/components/TokenDetails/TokenDetailsLinks' +import { TokenDetailsStats } from 'src/components/TokenDetails/TokenDetailsStats' +import { useCrossChainBalances } from 'src/components/TokenDetails/hooks' +import Trace from 'src/components/Trace/Trace' +import { HeaderScrollScreen } from 'src/components/layout/screens/HeaderScrollScreen' +import { Loader } from 'src/components/loading' +import { useTokenContextMenu } from 'src/features/balances/hooks' +import { selectModalState } from 'src/features/modals/selectModalState' +import { useNavigateToSend } from 'src/features/send/hooks' +import { useNavigateToSwap } from 'src/features/swap/hooks' +import { Screens } from 'src/screens/Screens' +import { disableOnPress } from 'src/utils/disableOnPress' +import { useSkeletonLoading } from 'src/utils/useSkeletonLoading' +import { + AnimatedFlex, + Flex, + Separator, + Text, + TouchableArea, + useDeviceInsets, + useIsDarkMode, + useSporeColors, +} from 'ui/src' +import EllipsisIcon from 'ui/src/assets/icons/ellipsis.svg' +import { fonts, iconSizes, spacing } from 'ui/src/theme' +import { + SafetyLevel, + TokenDetailsScreenQuery, + useTokenDetailsScreenQuery, +} from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { NumberType } from 'utilities/src/format/types' +import { BaseCard } from 'wallet/src/components/BaseCard/BaseCard' +import { TokenLogo } from 'wallet/src/components/CurrencyLogo/TokenLogo' +import { ChainId } from 'wallet/src/constants/chains' +import { PollingInterval } from 'wallet/src/constants/misc' +import { isError, isNonPollingRequestInFlight } from 'wallet/src/data/utils' +import { fromGraphQLChain } from 'wallet/src/features/chains/utils' +import { PortfolioBalance } from 'wallet/src/features/dataApi/types' +import { currencyIdToContractInput } from 'wallet/src/features/dataApi/utils' +import { useLocalizationContext } from 'wallet/src/features/language/LocalizationContext' +import { Language } from 'wallet/src/features/language/constants' +import { useCurrentLanguage } from 'wallet/src/features/language/hooks' +import TokenWarningModal from 'wallet/src/features/tokens/TokenWarningModal' +import { useTokenWarningDismissed } from 'wallet/src/features/tokens/safetyHooks' +import { CurrencyField } from 'wallet/src/features/transactions/transactionState/types' +import { ModalName } from 'wallet/src/telemetry/constants' +import { useExtractedTokenColor } from 'wallet/src/utils/colors' +import { currencyIdToAddress, currencyIdToChain } from 'wallet/src/utils/currencyId' + +function HeaderTitleElement({ + data, + ellipsisMenuVisible, +}: { + data: TokenDetailsScreenQuery | undefined + ellipsisMenuVisible?: boolean +}): JSX.Element { + const { t } = useTranslation() + const { convertFiatAmountFormatted } = useLocalizationContext() + + const onChainData = data?.token + const offChainData = onChainData?.project + + const price = offChainData?.markets?.[0]?.price?.value ?? onChainData?.market?.price?.value + const logo = offChainData?.logoUrl ?? undefined + const symbol = onChainData?.symbol + const name = onChainData?.name + const chain = onChainData?.chain + + return ( + + + {convertFiatAmountFormatted(price, NumberType.FiatTokenPrice)} + + + + + {symbol ?? t('token.error.unknown')} + + + + ) +} + +export function TokenDetailsScreen({ + route, +}: AppStackScreenProp): JSX.Element { + const { currencyId: _currencyId } = route.params + // Potentially delays loading of perf-heavy content to speed up navigation + const showSkeleton = useSkeletonLoading() + const language = useCurrentLanguage() + + // Token details screen query + const { data, refetch, networkStatus } = useTokenDetailsScreenQuery({ + variables: { + ...currencyIdToContractInput(_currencyId), + includeSpanish: + language === Language.SpanishSpain || + language === Language.SpanishLatam || + language === Language.SpanishUnitedStates, + includeFrench: language === Language.French, + includeJapanese: language === Language.Japanese, + includePortuguese: language === Language.Portuguese, + includeChineseSimplified: language === Language.ChineseSimplified, + includeChineseTraditional: language === Language.ChineseTraditional, + }, + pollInterval: PollingInterval.Normal, + notifyOnNetworkStatusChange: true, + returnPartialData: true, + }) + + const retry = useCallback(async () => { + await refetch(currencyIdToContractInput(_currencyId)) + }, [_currencyId, refetch]) + + const isLoading = !data && isNonPollingRequestInFlight(networkStatus) + + // Preload token price graphs + const { error: tokenPriceHistoryError } = useTokenPriceHistory(_currencyId) + + const traceProperties = useMemo( + () => ({ + address: currencyIdToAddress(_currencyId), + chain: currencyIdToChain(_currencyId), + currencyName: data?.token?.project?.name, + }), + [_currencyId, data?.token?.project?.name] + ) + + return ( + + + + + + ) +} + +function TokenDetails({ + _currencyId, + data, + error, + retry, + loading, + showSkeleton, +}: { + _currencyId: string + data: TokenDetailsScreenQuery | undefined + error: boolean + retry: () => void + loading: boolean + showSkeleton: boolean +}): JSX.Element { + const colors = useSporeColors() + const insets = useDeviceInsets() + const isDarkMode = useIsDarkMode() + + const currencyChainId = currencyIdToChain(_currencyId) ?? ChainId.Mainnet + const currencyAddress = currencyIdToAddress(_currencyId) + + const token = data?.token + const tokenLogoUrl = token?.project?.logoUrl + const tokenSymbol = token?.project?.name + + const crossChainTokens = token?.project?.tokens + const { currentChainBalance, otherChainBalances } = useCrossChainBalances( + _currencyId, + crossChainTokens + ) + + const { tokenColor, tokenColorLoading } = useExtractedTokenColor( + tokenLogoUrl, + tokenSymbol, + /*background=*/ colors.surface1.val, + /*default=*/ colors.neutral3.val + ) + + const onPriceChartRetry = useCallback((): void => { + if (!error) { + return + } + retry() + }, [error, retry]) + + const navigateToSwap = useNavigateToSwap() + const navigateToSend = useNavigateToSend() + + // set if attempting buy or sell, use for warning modal + const [activeTransactionType, setActiveTransactionType] = useState( + undefined + ) + + const [showWarningModal, setShowWarningModal] = useState(false) + const { tokenWarningDismissed, dismissWarningCallback } = useTokenWarningDismissed(_currencyId) + + const safetyLevel = token?.project?.safetyLevel + + const onPressSwap = useCallback( + (currencyField: CurrencyField) => { + if (safetyLevel === SafetyLevel.Blocked) { + setShowWarningModal(true) + // show warning modal speed bump if token has a warning level and user has not dismissed + } else if (safetyLevel !== SafetyLevel.Verified && !tokenWarningDismissed) { + setActiveTransactionType(currencyField) + setShowWarningModal(true) + } else { + setActiveTransactionType(undefined) + navigateToSwap(currencyField, currencyAddress, currencyChainId) + } + }, + [currencyAddress, currencyChainId, navigateToSwap, safetyLevel, tokenWarningDismissed] + ) + + const onPressSend = useCallback(() => { + // Do not show warning modal speedbump if user is trying to send tokens they own + navigateToSend(currencyAddress, currencyChainId) + }, [currencyAddress, currencyChainId, navigateToSend]) + + const onAcceptWarning = useCallback(() => { + dismissWarningCallback() + setShowWarningModal(false) + if (activeTransactionType !== undefined) { + navigateToSwap(activeTransactionType, currencyAddress, currencyChainId) + } + }, [ + activeTransactionType, + currencyAddress, + currencyChainId, + dismissWarningCallback, + navigateToSwap, + ]) + + const inModal = useAppSelector(selectModalState(ModalName.Explore)).isOpen + + const loadingColor = isDarkMode ? colors.neutral3.val : colors.surface3.val + + const [ellipsisMenuVisible, setEllipsisMenuVisible] = useState(false) + + return ( + + } + renderedInModal={inModal} + rightElement={ + showSkeleton ? undefined : ( + + ) + } + showHandleBar={inModal}> + + + setShowWarningModal(true)} + /> + + + {error ? ( + + + + ) : null} + + + + {showSkeleton ? ( + + ) : ( + <> + + + + + + )} + + + + + {!loading && !tokenColorLoading ? ( + + onPressSwap(CurrencyField.OUTPUT)} + onPressSell={(): void => onPressSwap(CurrencyField.INPUT)} + /> + + ) : null} + + { + setActiveTransactionType(undefined) + setShowWarningModal(false) + }} + /> + + ) +} + +function TokenDetailsTextPlaceholders(): JSX.Element { + return ( + <> + + + + + + + + + + + + + ) +} + +function HeaderRightElement({ + currencyId, + currentChainBalance, + data, + setEllipsisMenuVisible, +}: { + currencyId: string + currentChainBalance: PortfolioBalance | null + data?: TokenDetailsScreenQuery + setEllipsisMenuVisible: (visible: boolean) => void +}): JSX.Element { + const colors = useSporeColors() + const isDarkMode = useIsDarkMode() + + const { menuActions, onContextMenuPress } = useTokenContextMenu({ + currencyId, + tokenSymbolForNotification: data?.token?.symbol, + portfolioBalance: currentChainBalance, + }) + + // Should be the same color as heart icon in not favorited state next to it + const ellipsisColor = isDarkMode ? colors.neutral2.get() : colors.neutral2.get() + + const ellipsisMenuVisible = menuActions.length > 0 + + useEffect(() => { + setEllipsisMenuVisible(ellipsisMenuVisible) + }, [ellipsisMenuVisible, setEllipsisMenuVisible]) + + return ( + + {ellipsisMenuVisible && ( + + + + + + )} + + + ) +} diff --git a/apps/mobile/src/screens/WebViewScreen.tsx b/apps/mobile/src/screens/WebViewScreen.tsx new file mode 100644 index 0000000..7e04178 --- /dev/null +++ b/apps/mobile/src/screens/WebViewScreen.tsx @@ -0,0 +1,72 @@ +import React, { useCallback, useRef } from 'react' +import WebView from 'react-native-webview' +import { AppStackScreenProp, SettingsStackScreenProp } from 'src/app/navigation/types' +import { BackHeader } from 'src/components/layout/BackHeader' +import { Screen } from 'src/components/layout/Screen' +import { Screens } from 'src/screens/Screens' +import { Separator, Text } from 'ui/src' +import { uniswapUrls } from 'uniswap/src/constants/urls' +import { useActiveAccountAddress } from 'wallet/src/features/wallet/hooks' + +export function WebViewScreen({ + route, +}: SettingsStackScreenProp | AppStackScreenProp): JSX.Element { + const { headerTitle, uriLink } = route.params + + return ( + + + {headerTitle} + + + + + {uriLink === uniswapUrls.helpUrl ? ( + + ) : ( + + )} + + ) +} + +function ZendeskWebView({ uriLink }: { uriLink: string }): JSX.Element { + const webviewRef = useRef(null) + + const zendeskInjectJs = usePrefillAddressInZendeskTicketJs() + + const onNavigationStateChange = useCallback( + ({ url }: { url: string }): void => { + // Ok to ignore because `uniswapUrls.helpUrl` is hardcoded into our code. + // eslint-disable-next-line security/detect-non-literal-regexp + if (zendeskInjectJs && new RegExp(`${uniswapUrls.helpUrl}.+/requests/new`).test(url)) { + webviewRef.current?.injectJavaScript(zendeskInjectJs) + } + }, + [zendeskInjectJs] + ) + + return ( + + ) +} + +const WALLET_ADDRESS_QUERY_SELECTOR = '#request_custom_fields_11041337007757' + +function usePrefillAddressInZendeskTicketJs(): string | null { + const address = useActiveAccountAddress() + + if (!address) { + return null + } + + return `(function() { + var input = document.querySelector('${WALLET_ADDRESS_QUERY_SELECTOR}'); + if (!input) { return; } + input.value = "${address}"; + })();` +} diff --git a/apps/mobile/src/stories/Introduction.stories.mdx b/apps/mobile/src/stories/Introduction.stories.mdx new file mode 100644 index 0000000..8fc137a --- /dev/null +++ b/apps/mobile/src/stories/Introduction.stories.mdx @@ -0,0 +1,21 @@ +import { Meta } from '@storybook/addon-docs' + + + + + +# `@uniswap/mobile` + +# 🎠 + +{/* TODO : Write about our design philosophy and how to use the design system. */} + +## Philosophy + +## Colors + +## Layout + +## Components diff --git a/apps/mobile/src/stories/README.md b/apps/mobile/src/stories/README.md new file mode 100644 index 0000000..0b2cdcd --- /dev/null +++ b/apps/mobile/src/stories/README.md @@ -0,0 +1,76 @@ +# Storybook + +[Storybook](https://storybook.js.org/) helps build UI components in isolation. Testing and component documentation are built into the development workflow, leading to better tested and better documented component libraries. + +## Useful resources + +- [Official Getting Started](https://storybook.js.org/docs/react/get-started/introduction) by Storybook +- Our [Chromatic app](https://www.chromatic.com/builds?appId=61d89aa649fc7d003ae21c76) +- Our published [Storybook app](https://61d89aa649fc7d003ae21c76-gyrkwmtvsx.chromatic.com/) + +## What components should have stories? + +Literally any component can have a story, either for visual testing or documentation. I (judo) recommend we treat Storybook as a component library for now—that is, low-level presentational components that have high reusability and minimal dependencies (think spacing components, buttons, pills, etc.) + +### Heuristic + +If you expect your component to be imported by more than 3 other components, consider writing a story to document it (and get visual testing for free!) + +### Recommendation + +- Presentational components with short dependency list +- Components with hard to reach use cases (e.g. graph with mocked historical data) +- Design system philosophy (Storybook doesn't _require_ a component to be rendered, pages can also just be documentation) +- Design tokens + +As our Storybook grows, this recommendation may extend to "compound" components, components that compose other components that have their own stories. The major benefit here is visual testing and documentation for new engineers. + +## How to write stories? + +Refer to [Storybook stories documentation](https://storybook.js.org/docs/react/writing-stories/introduction) + +Storybook supports various `stories` formats. + +- `.mdx`: Markdown with support for `jsx`. **No Typescript support** +- `.tsx`: Draw on canvas with Typescript + +We're currently leaning on writing stories `.tsx` because of Typescript support. + +### Run Storybook locally +To run Storybook locally and view stories: +``` +yarn storybook +``` + +## Chromatic + +Chromatic is a cloud service build for Storybook. It allows running visual tests with zero-config. + +Chromatic is set up to build and publish our Storybook for each PR. ([example build](https://www.chromatic.com/build?appId=61d89aa649fc7d003ae21c76&number=25)). + +In a PR, you should see 4 checks by Chromatic: + +- chromatic-deployment workflow +- Storybook Publish: published Storybook to Chromatic +- [UI Review](https://www.chromatic.com/docs/review): invite reviewers to review the changeset introduced by your PR (code review, but for your UI) +- [UI Test](https://www.chromatic.com/docs/test): capture visual snapshot of every story and compares against baseline + +## Going Forward + +Storybook inside `@uniswap/mobile` is still experimental. Feel free to modify the config as you think would help! + +### To research + +- Accessability tests +- Interaction tests + +### Addons + +Storybook has a ton of [addons](https://storybook.js.org/addons/) that we could leverage / write our own. + +- + +### Open questions + +- Figma integration? Can Figma re-use our low-level components and design tokens? +- Possibility to share Storybook with `react` (interface/widgets) diff --git a/apps/mobile/src/test/fixtures/index.ts b/apps/mobile/src/test/fixtures/index.ts new file mode 100644 index 0000000..c7c7b90 --- /dev/null +++ b/apps/mobile/src/test/fixtures/index.ts @@ -0,0 +1 @@ +export * from './redux' diff --git a/apps/mobile/src/test/fixtures/redux.ts b/apps/mobile/src/test/fixtures/redux.ts new file mode 100644 index 0000000..9584c19 --- /dev/null +++ b/apps/mobile/src/test/fixtures/redux.ts @@ -0,0 +1,25 @@ +import { PreloadedState } from 'redux' +import { MobileState } from 'src/app/reducer' +import { ModalsState } from 'src/features/modals/ModalsState' +import { initialModalsState } from 'src/features/modals/modalSlice' +import { Account } from 'wallet/src/features/wallet/accounts/types' +import { SharedState } from 'wallet/src/state/reducer' +import { preloadedSharedState } from 'wallet/src/test/fixtures' +import { createFixture } from 'wallet/src/test/utils' + +export const preloadedModalsState = createFixture()(() => ({ + ...initialModalsState, +})) + +type PreloadedMobileStateOptions = { + account: Account | undefined +} + +export const preloadedMobileState = createFixture< + PreloadedState, + PreloadedMobileStateOptions +>({ + account: undefined, +})(({ account }) => ({ + ...(preloadedSharedState({ account }) as PreloadedState), +})) diff --git a/apps/mobile/src/test/render.tsx b/apps/mobile/src/test/render.tsx new file mode 100644 index 0000000..557af31 --- /dev/null +++ b/apps/mobile/src/test/render.tsx @@ -0,0 +1,146 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { NavigationContainer } from '@react-navigation/native' +import type { EnhancedStore, PreloadedState } from '@reduxjs/toolkit' +import { configureStore } from '@reduxjs/toolkit' +import { + render as RNRender, + renderHook as RNRenderHook, + RenderHookOptions, + RenderHookResult, + RenderOptions, + RenderResult, +} from '@testing-library/react-native' +import React, { PropsWithChildren } from 'react' +import { navigationRef } from 'src/app/navigation/NavigationContainer' +import type { MobileState } from 'src/app/reducer' +import type { AppStore } from 'src/app/store' +import { persistedReducer } from 'src/app/store' +import { Resolvers } from 'uniswap/src/data/graphql/uniswap-data-api/__generated__/types-and-hooks' +import { UnitagUpdaterContextProvider } from 'uniswap/src/features/unitags/context' +import { SharedProvider } from 'wallet/src/provider' +import { AutoMockedApolloProvider } from 'wallet/src/test/mocks/gql/provider' + +// This type extends the default options for render from RTL, as well +// as allows the user to specify other things such as initialState, store. +type ExtendedRenderOptions = RenderOptions & { + resolvers?: Resolvers + preloadedState?: PreloadedState + store?: AppStore +} + +/** + * + * @param ui Component to render + * @param resolvers Custom resolvers that override the default ones + * @param preloadedState and store + * @returns `ui` wrapped with providers + */ +export function renderWithProviders( + ui: React.ReactElement, + { + resolvers, + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = configureStore({ + reducer: persistedReducer, + preloadedState, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), + }), + ...renderOptions + }: ExtendedRenderOptions = {} +): RenderResult & { + store: EnhancedStore +} { + function Wrapper({ children }: PropsWithChildren): JSX.Element { + return ( + + + + {children} + + + + ) + } + + // Return an object with the store and all of RTL's query functions + return { store, ...RNRender(ui, { wrapper: Wrapper, ...renderOptions }) } +} + +// This type extends the default options for render from RTL, as well +// as allows the user to specify other things such as initialState, store. +type ExtendedRenderHookOptions

= RenderHookOptions

& { + resolvers?: Resolvers + preloadedState?: PreloadedState + store?: AppStore +} + +type RenderHookWithProvidersResult = Omit< + RenderHookResult, + 'rerender' +> & { + store: EnhancedStore + rerender: P extends any[] ? (args: P) => void : () => void +} + +// Don't require hookOptions if hook doesn't take any arguments +export function renderHookWithProviders( + hook: () => R, + hookOptions?: ExtendedRenderHookOptions +): RenderHookWithProvidersResult + +// Require hookOptions if hook takes arguments +export function renderHookWithProviders( + hook: (...args: P) => R, + hookOptions: ExtendedRenderHookOptions

+): RenderHookWithProvidersResult + +/** + * + * @param hook Hook to render + * @param resolvers Custom resolvers that override the default ones + * @param preloadedState and store + * @returns `hook` wrapped with providers + */ +export function renderHookWithProviders

( + hook: (...args: P) => R, + hookOptions?: ExtendedRenderHookOptions

+): RenderHookWithProvidersResult { + const { + resolvers, + preloadedState = {}, + // Automatically create a store instance if no store was passed in + store = configureStore({ + reducer: persistedReducer, + preloadedState, + middleware: (getDefaultMiddleware) => getDefaultMiddleware(), + }), + ...renderOptions + } = (hookOptions ?? {}) as ExtendedRenderHookOptions

+ + function Wrapper({ children }: PropsWithChildren): JSX.Element { + return ( + + + + {children} + + + + ) + } + + const options: RenderHookOptions

= { + wrapper: Wrapper, + ...(renderOptions as RenderHookOptions

), + } + + const { rerender, ...rest } = RNRenderHook((args: P) => hook(...(args ?? [])), options) + + // Return an object with the store and all of RTL's query functions + return { + store, + rerender: rerender as P extends any[] ? (args: P) => void : () => void, + ...rest, + } +} diff --git a/apps/mobile/src/test/test-utils.ts b/apps/mobile/src/test/test-utils.ts new file mode 100644 index 0000000..bbc1870 --- /dev/null +++ b/apps/mobile/src/test/test-utils.ts @@ -0,0 +1,6 @@ +import { renderHookWithProviders, renderWithProviders } from './render' + +// re-export everything +export * from '@testing-library/react-native' +// override render method +export { renderWithProviders as render, renderHookWithProviders as renderHook } diff --git a/apps/mobile/src/utils/disableOnPress.ts b/apps/mobile/src/utils/disableOnPress.ts new file mode 100644 index 0000000..7083762 --- /dev/null +++ b/apps/mobile/src/utils/disableOnPress.ts @@ -0,0 +1,3 @@ +export const disableOnPress = (): void => { + // Empty function used to work around nested touchable issues and longpress to press fallbacks. +} diff --git a/apps/mobile/src/utils/haptic.ts b/apps/mobile/src/utils/haptic.ts new file mode 100644 index 0000000..d23c56d --- /dev/null +++ b/apps/mobile/src/utils/haptic.ts @@ -0,0 +1,14 @@ +import { impactAsync, ImpactFeedbackStyle } from 'expo-haptics' + +/** Simple wrapper around impactAsync to avoid anonymous functions which can hurt performance */ +export const invokeImpact = { + [ImpactFeedbackStyle.Light]: async (): Promise => { + await impactAsync(ImpactFeedbackStyle.Light) + }, + [ImpactFeedbackStyle.Medium]: async (): Promise => { + await impactAsync(ImpactFeedbackStyle.Medium) + }, + [ImpactFeedbackStyle.Heavy]: async (): Promise => { + await impactAsync(ImpactFeedbackStyle.Heavy) + }, +} as const diff --git a/apps/mobile/src/utils/haptics.test.ts b/apps/mobile/src/utils/haptics.test.ts new file mode 100644 index 0000000..c2a6503 --- /dev/null +++ b/apps/mobile/src/utils/haptics.test.ts @@ -0,0 +1,23 @@ +import * as haptics from 'expo-haptics' +import { invokeImpact } from 'src/utils/haptic' + +const ImpactFeedbackStyle = haptics.ImpactFeedbackStyle +const mockedImpactAsync = jest.spyOn(haptics, 'impactAsync') + +describe('impact', () => { + it('triggers impactAsync', async () => { + const impactLight = invokeImpact[ImpactFeedbackStyle.Light] + const impactMedium = invokeImpact[ImpactFeedbackStyle.Medium] + const impactHeavy = invokeImpact[ImpactFeedbackStyle.Heavy] + + await impactLight() + await impactMedium() + await impactHeavy() + await impactLight() + + expect(mockedImpactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Light) + expect(mockedImpactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Medium) + expect(mockedImpactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Heavy) + expect(mockedImpactAsync).toHaveBeenCalledWith(ImpactFeedbackStyle.Light) + }) +}) diff --git a/apps/mobile/src/utils/hooks.ts b/apps/mobile/src/utils/hooks.ts new file mode 100644 index 0000000..6737080 --- /dev/null +++ b/apps/mobile/src/utils/hooks.ts @@ -0,0 +1,26 @@ +import { useFocusEffect, useIsFocused } from '@react-navigation/core' +import { useCallback, useRef } from 'react' +import { PollingInterval } from 'wallet/src/constants/misc' + +export function usePollOnFocusOnly( + startPolling: (interval: PollingInterval) => void, + stopPolling: () => void, + pollingInterval: PollingInterval +): void { + useFocusEffect( + useCallback(() => { + startPolling(pollingInterval) + return () => { + stopPolling() + } + }, [startPolling, stopPolling, pollingInterval]) + ) +} + +export function useSuspendUpdatesWhenBlured(data: T): T { + const ref = useRef(data) + if (useIsFocused()) { + ref.current = data + } + return ref.current +} diff --git a/apps/mobile/src/utils/reanimated.test.ts b/apps/mobile/src/utils/reanimated.test.ts new file mode 100644 index 0000000..efa7e8b --- /dev/null +++ b/apps/mobile/src/utils/reanimated.test.ts @@ -0,0 +1,195 @@ +import { Language, numberToLocaleStringWorklet, numberToPercentWorklet } from 'src/utils/reanimated' + +describe('reanimated numberToLocaleStringWorklet', function () { + 'use strict' + + it('needs to be overridden for phantomjs', function () { + const num = 123456 + const locale = 'en-GB' + + function testLocale(): string { + return numberToLocaleStringWorklet(num, locale) + } + + expect(testLocale).not.toThrow() + }) + + it('returns a string', function () { + const num = 123456 + const locale = 'en-GB' + + expect(typeof numberToLocaleStringWorklet(num, locale)).toBe('string') + }) + + it('returns <$0.00000001 if the value is below that amount', function () { + const num = 0.000000001 + + expect( + numberToLocaleStringWorklet(num, 'en-US', { + style: 'currency', + currency: 'USD', + }) + ).toBe('<$0.00000001') + }) + + it('returns a string with 3 sig figs if it is between 0.00000001 and 1,', function () { + const num = 0.0000000123 + + expect( + numberToLocaleStringWorklet(num, 'en-US', { + style: 'currency', + currency: 'USD', + }) + ).toBe('$0.0000000123') + }) + + it('returns a string formatted in FR style (1\u00A0234.5) when passed FR', function () { + const num = 1234.5 + const locale = 'fr' + + expect(numberToLocaleStringWorklet(num, locale)).toEqual('1\u00A0234,50') + }) + + it('returns a string formatted in US style (1,234.5) when passed US', function () { + const num = 1234.5 + const locale = 'en-US' + + expect(numberToLocaleStringWorklet(num, locale)).toBe('1,234.50') + }) + + it('returns a string formatted in IT style (1.234,5) when passed IT', function () { + const num = 1234.5 + const locale = 'it' + + expect(numberToLocaleStringWorklet(num, locale)).toBe('1.234,50') + }) + + it("returns a string formatted in de-CH style (1'234.5) when passed de-CH", function () { + const num = 1234.5 + const locale = 'de-CH' + + expect(numberToLocaleStringWorklet(num, locale)).toBe("1'234.50") + }) + + it('returns a string formatted in DK style (1.234,5) when passed da-DK', function () { + const num = 1234.5 + const locale = 'da-DK' + + expect(numberToLocaleStringWorklet(num, locale)).toBe('1.234,50') + }) + + it('returns a string formatted in NO style (1 234,5) when passed nb-NO', function () { + const num = 1234.5 + const locale = 'nb-NO' + + expect(numberToLocaleStringWorklet(num, locale)).toBe('1\u00A0234,50') + }) + + it('throws when the language tag does not conform to the standard', function () { + const num = 1234.5 + const locale = 'i' + + function testLocale(): string { + return numberToLocaleStringWorklet(num, locale as Language) + } + + expect(testLocale).toThrow(new RangeError('Invalid language tag: ' + locale)) + }) + + it('returns a string formatted in US style (1,234.5) by default', function () { + const num = 1234.5 + + expect(numberToLocaleStringWorklet(num)).toBe('1,234.50') + expect(numberToLocaleStringWorklet(num, 'es' as Language)).toBe('1,234.50') + expect(numberToLocaleStringWorklet(num, 'AU' as Language)).toBe('1,234.50') + }) + + it('returns a string formatted in Hungarian style (1 234,56) by default', function () { + const num = 1234.56 + + expect(numberToLocaleStringWorklet(num, 'hu')).toBe('1\u00A0234,56') + expect(numberToLocaleStringWorklet(num, 'hu-HU')).toBe('1\u00A0234,56') + }) + + it('returns currency properly formatted for the locale specified', function () { + const num = 1234.56 + const negative_num = -1234.56 + const style = 'currency' + const currency = 'USD' + + expect( + numberToLocaleStringWorklet(num, 'en-US', { + style, + currency, + }) + ).toBe('$1,234.56') + + expect( + numberToLocaleStringWorklet(negative_num, 'en-US', { + style, + currency, + }) + ).toBe('-$1,234.56') + + expect( + numberToLocaleStringWorklet(num, 'de-DE', { + style, + currency, + }) + ).toBe('1.234,56 $') + + expect( + numberToLocaleStringWorklet(num, 'hu', { + style, + currency: 'huf', + }) + ).toBe('1\u00A0234,56 Ft') + + expect( + numberToLocaleStringWorklet(num, 'hu-HU', { + style, + currency: 'huf', + }) + ).toBe('1\u00A0234,56 Ft') + + expect( + numberToLocaleStringWorklet(num, 'da-DK', { + style, + currency: 'DKK', + }) + ).toBe('1.234,56 kr') + + expect( + numberToLocaleStringWorklet(num, 'nb-NO', { + style, + currency: 'NOK', + }) + ).toBe('1\u00A0234,56 kr') + }) + + it('format percentages with rounding and zero padding', function () { + const num = -1234.56 + + expect(numberToPercentWorklet(num, { precision: 0, absolute: true })).toBe('1235%') + expect(numberToPercentWorklet(num, { precision: 1, absolute: true })).toBe('1234.6%') + expect(numberToPercentWorklet(num, { precision: 2, absolute: true })).toBe('1234.56%') + expect(numberToPercentWorklet(num, { precision: 3, absolute: true })).toBe('1234.560%') + expect(numberToPercentWorklet(num, { precision: 4, absolute: true })).toBe('1234.5600%') + expect(numberToPercentWorklet(num, { precision: 4, absolute: false })).toBe('-1234.5600%') + }) + + it('format zero value percentages with zero padding', function () { + expect(numberToPercentWorklet(0, { precision: 0, absolute: true })).toBe('0%') + expect(numberToPercentWorklet(0, { precision: 1, absolute: true })).toBe('0.0%') + expect(numberToPercentWorklet(0, { precision: 2, absolute: true })).toBe('0.00%') + expect(numberToPercentWorklet(0.001, { precision: 2, absolute: true })).toBe('0.00%') + }) + + it('format integer values with zero padding', function () { + const intNum = -10 + + expect(numberToPercentWorklet(intNum, { precision: 0, absolute: true })).toBe('10%') + expect(numberToPercentWorklet(intNum, { precision: 1, absolute: true })).toBe('10.0%') + expect(numberToPercentWorklet(intNum, { precision: 2, absolute: true })).toBe('10.00%') + }) +}) diff --git a/apps/mobile/src/utils/reanimated.ts b/apps/mobile/src/utils/reanimated.ts new file mode 100644 index 0000000..3235279 --- /dev/null +++ b/apps/mobile/src/utils/reanimated.ts @@ -0,0 +1,441 @@ +/** + * Util to format numbers inside reanimated worklets. + * + * This is nowhere near complete, but it has all the locales + * we're likely to use. + * + * Code adapted from: + * https://github.com/willsp/polyfill-Number.toLocaleString-with-Locales/blob/master/polyfill.number.toLocaleString.js + */ + +function replaceSeparators( + sNum: string, + separators: { decimal: string; thousands: string } +): string { + 'worklet' + const sNumParts = sNum.split('.') + if (separators && separators.thousands && sNumParts[0]) { + // every three digits, replace it with the digits + the thousands separator + // $1 indicates that the matched substring is to be replaced by the first captured group + sNumParts[0] = sNumParts[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1' + separators.thousands) + } + sNum = sNumParts.join(separators.decimal) + + return sNum +} + +function renderFormat( + template: string, + options: Record & { num?: string; code?: string } +): string { + 'worklet' + for (const [option, value] of Object.entries(options)) { + let updatedValue = value + if (value.indexOf('-') !== -1) { + updatedValue = updatedValue.replace('-', '') + template = '-' + template + } + + if (value.indexOf('<') !== -1) { + updatedValue = updatedValue.replace('<', '') + template = '<' + template + } + + template = template.replace('{{' + option + '}}', updatedValue) + } + + return template +} + +/** + * @summary Computes animation node rounded to precision. + * from https://github.com/wcandillon/react-native-redash/blob/master/src/Math.ts + * @worklet + */ +const round = (value: number, precision = 0): number => { + 'worklet' + const p = Math.pow(10, precision) + return Math.round(value * p) / p +} + +function mapMatch( + map: { [key in Language]: string | ((key: string, options?: OptionsType) => string) }, + locale: Language +): string | ((key: string, options?: OptionsType) => string) { + 'worklet' + let match = locale + + if (!Object.prototype.hasOwnProperty.call(map, locale)) { + match = 'en' + } + + return map[match] +} + +function dotThousCommaDec(sNum: string): string { + 'worklet' + const separators = { + decimal: ',', + thousands: '.', + } + + return replaceSeparators(sNum, separators) +} + +function commaThousDotDec(sNum: string): string { + 'worklet' + const separators = { + decimal: '.', + thousands: ',', + } + + return replaceSeparators(sNum, separators) +} + +function spaceThousCommaDec(sNum: string): string { + 'worklet' + const separators = { + decimal: ',', + thousands: '\u00A0', + } + + return replaceSeparators(sNum, separators) +} + +function apostropheThousDotDec(sNum: string): string { + 'worklet' + const separators = { + decimal: '.', + thousands: '\u0027', + } + + return replaceSeparators(sNum, separators) +} + +const transformForLocale = { + en: commaThousDotDec, + 'en-GB': commaThousDotDec, + 'en-US': commaThousDotDec, + it: dotThousCommaDec, + fr: spaceThousCommaDec, + 'fr-FR': spaceThousCommaDec, + de: dotThousCommaDec, + 'de-DE': dotThousCommaDec, + 'de-AT': dotThousCommaDec, + 'de-CH': apostropheThousDotDec, + 'de-LI': apostropheThousDotDec, + 'de-BE': dotThousCommaDec, + 'es-ES': dotThousCommaDec, + 'es-US': commaThousDotDec, + 'es-419': commaThousDotDec, + 'hi-IN': dotThousCommaDec, + 'id-ID': commaThousDotDec, + 'ja-JP': commaThousDotDec, + 'ms-MY': dotThousCommaDec, + nl: dotThousCommaDec, + 'nl-BE': dotThousCommaDec, + 'nl-NL': dotThousCommaDec, + 'pt-PT': spaceThousCommaDec, + ro: dotThousCommaDec, + 'ro-RO': dotThousCommaDec, + ru: spaceThousCommaDec, + 'ru-RU': spaceThousCommaDec, + 'th-TH': commaThousDotDec, + 'tr-TR': dotThousCommaDec, + 'uk-UA': spaceThousCommaDec, + 'ur-PK': commaThousDotDec, + 'vi-VN': dotThousCommaDec, + 'zh-Hans': commaThousDotDec, + 'zh-Hant': commaThousDotDec, + hu: spaceThousCommaDec, + 'hu-HU': spaceThousCommaDec, + 'da-DK': dotThousCommaDec, + 'nb-NO': spaceThousCommaDec, +} + +const currencyFormatMap = { + en: 'pre', + 'en-GB': 'pre', + 'en-US': 'pre', + it: 'post', + fr: 'post', + 'fr-FR': 'post', + de: 'post', + 'de-DE': 'post', + 'de-AT': 'prespace', + 'de-CH': 'prespace', + 'de-LI': 'post', + 'de-BE': 'post', + 'es-ES': 'post', + 'es-US': 'pre', + 'es-419': 'pre', + 'hi-IN': 'pre', + 'id-ID': 'pre', + 'ja-JP': 'pre', + 'ms-MY': 'pre', + nl: 'post', + 'nl-BE': 'post', + 'nl-NL': 'post', + 'pt-PT': 'post', + ro: 'post', + 'ro-RO': 'post', + ru: 'post', + 'ru-RU': 'post', + 'th-TH': 'pre', + 'tr-TR': 'pre', + 'uk-UA': 'post', + 'ur-PK': 'pre', + 'vi-VN': 'post', + 'zh-Hans': 'pre', + 'zh-Hant': 'pre', + hu: 'post', + 'hu-HU': 'post', + 'da-DK': 'post', + 'nb-NO': 'post', +} + +export type Language = keyof typeof currencyFormatMap | keyof typeof transformForLocale + +const currencySymbols: { [key: string]: string } = { + afn: '؋', + ars: '$', + awg: 'ƒ', + aud: '$', + azn: '₼', + bsd: '$', + bbd: '$', + byr: 'p.', + bzd: 'BZ$', + bmd: '$', + bob: 'Bs.', + bam: 'KM', + bwp: 'P', + bgn: 'лв', + brl: 'R$', + bnd: '$', + khr: '៛', + cad: '$', + kyd: '$', + clp: '$', + cny: '¥', + cop: '$', + crc: '₡', + hrk: 'kn', + cup: '₱', + czk: 'Kč', + dkk: 'kr', + dop: 'RD$', + xcd: '$', + egp: '£', + svc: '$', + eek: 'kr', + eur: '€', + fkp: '£', + fjd: '$', + ghc: '¢', + gip: '£', + gtq: 'Q', + ggp: '£', + gyd: '$', + hnl: 'L', + hkd: '$', + huf: 'Ft', + isk: 'kr', + inr: '₹', + idr: 'Rp', + irr: '﷼', + imp: '£', + ils: '₪', + jmd: 'J$', + jpy: '¥', + jep: '£', + kes: 'KSh', + kzt: 'лв', + kpw: '₩', + krw: '₩', + kgs: 'лв', + lak: '₭', + lvl: 'Ls', + lbp: '£', + lrd: '$', + ltl: 'Lt', + mkd: 'ден', + myr: 'RM', + mur: '₨', + mxn: '$', + mnt: '₮', + mzn: 'MT', + nad: '$', + npr: '₨', + ang: 'ƒ', + nzd: '$', + nio: 'C$', + ngn: '₦', + nok: 'kr', + omr: '﷼', + pkr: '₨', + pab: 'B/.', + pyg: 'Gs', + pen: 'S/.', + php: '₱', + pln: 'zł', + qar: '﷼', + ron: 'lei', + rub: '₽', + shp: '£', + sar: '﷼', + rsd: 'Дин.', + scr: '₨', + sgd: '$', + sbd: '$', + sos: 'S', + zar: 'R', + lkr: '₨', + sek: 'kr', + chf: 'CHF', + srd: '$', + syp: '£', + tzs: 'TSh', + twd: 'NT$', + thb: '฿', + ttd: 'TT$', + try: '', + trl: '₤', + tvd: '$', + ugx: 'USh', + uah: '₴', + gbp: '£', + usd: '$', + uyu: '$U', + uzs: 'лв', + vef: 'Bs', + vnd: '₫', + yer: '﷼', + zwd: 'Z$', +} + +const currencyFormats: { [key: string]: string } = { + pre: '{{code}}{{num}}', + post: '{{num}} {{code}}', + prespace: '{{code}} {{num}}', +} + +interface OptionsType { + maximumFractionDigits?: number + currency?: string + style?: string + currencyDisplay?: string +} + +// need this function because JS will auto convert very small numbers to scientific notation +function convertSmallSciNotationToDecimal(value: number): string { + 'worklet' + const num = value.toPrecision(3) + if (!num.includes('e-')) { + return num + } + + const [base, exponent] = num.split('e-') + if (!base || !exponent) { + return '-' + } + + const decimal = base.replace('.', '') + return '0.'.concat('0'.repeat(Number(exponent) - 1).concat(decimal)) +} + +export function numberToLocaleStringWorklet( + value: number, + locale: Language = 'en-US', + options: OptionsType = {}, + symbol?: string +): string { + 'worklet' + if (locale && locale.length < 2) { + throw new RangeError('Invalid language tag: ' + locale) + } + + if (!value) { + return '-' + } + + let sNum: string + + // if we encounter any coins with a unit price over $1M then add a shorthand case + if (value < 0) { + sNum = value.toString() + } else if (value < 0.00000001) { + sNum = '<0.00000001' + } else if (value < 1) { + sNum = convertSmallSciNotationToDecimal(value) + } else { + sNum = value.toFixed(2) + } + + sNum = (<(key: string, options?: OptionsType) => string>mapMatch(transformForLocale, locale))( + sNum, + options + ) + + if (options && options.currency && options.style === 'currency') { + const format = currencyFormats[mapMatch(currencyFormatMap, locale)] + const targetSymbol = symbol ?? currencySymbols[options.currency.toLowerCase()] + if (format) { + sNum = renderFormat(format, { + num: sNum, + code: options.currencyDisplay === 'code' || !targetSymbol ? options.currency : targetSymbol, + }) + } + } + + return sNum +} + +const DEFAULT_PRECISION = 2 +const DEFAULT_ABSOLUTE = false + +export function numberToPercentWorklet( + value?: number, + options: { + precision: number + absolute: boolean + } = { precision: DEFAULT_PRECISION, absolute: DEFAULT_ABSOLUTE } +): string { + 'worklet' + + const { precision, absolute } = options + + if (precision < 0) { + throw new Error('numberToPercentWorklet does not handle negative precision values') + } + + if (value === undefined || isNaN(value)) { + return '-' + } + + // Round and absolute value as needed + let shapedValue = round(value, precision) + if (absolute) { + shapedValue = Math.abs(shapedValue) + } + + // return raw value when precision is zero + if (precision === 0) { + return `${shapedValue}%` + } + + // Add trailing zeros when value has no decimal + if (Number.isInteger(shapedValue)) { + return `${shapedValue}.${'0'.repeat(precision)}%` + } + + let endingZeros = '' + const shiftedValue = shapedValue * Math.pow(10, precision) + for (let d = 0; d < precision; d++) { + if (shiftedValue % Math.pow(10, d + 1) === 0) { + endingZeros += '0' + } + } + + return `${shapedValue}${endingZeros}%` +} diff --git a/apps/mobile/src/utils/splashScreen.ts b/apps/mobile/src/utils/splashScreen.ts new file mode 100644 index 0000000..fb21bd5 --- /dev/null +++ b/apps/mobile/src/utils/splashScreen.ts @@ -0,0 +1,9 @@ +import SplashScreen from 'react-native-splash-screen' + +/** + * Custom wrapped function to hide the splash screen. + * We need this so that we can hide any errors that may occur (e.g. unhandled promise rejection when FaceID is unlocking) + */ +export function hideSplashScreen(): void { + SplashScreen.hide() +} diff --git a/apps/mobile/src/utils/useAddBackButton.test.ts b/apps/mobile/src/utils/useAddBackButton.test.ts new file mode 100644 index 0000000..4dbb1b5 --- /dev/null +++ b/apps/mobile/src/utils/useAddBackButton.test.ts @@ -0,0 +1,34 @@ +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { OnboardingStackParamList } from 'src/app/navigation/types' +import { renderHook } from 'src/test/test-utils' +import { useAddBackButton } from 'src/utils/useAddBackButton' + +const setOptionsSpy = jest.fn() + +describe(useAddBackButton, () => { + it('sets navigation options when item is first in stack', () => { + renderHook(() => + useAddBackButton({ + getState: () => ({ + index: 0, + }), + setOptions: setOptionsSpy, + } as unknown as NativeStackNavigationProp) + ) + + expect(setOptionsSpy).toHaveBeenCalled() + }) + + it("doesn't set navigation options for subsequent stack levels", () => { + renderHook(() => + useAddBackButton({ + getState: () => ({ + index: 1, + }), + setOptions: setOptionsSpy, + } as unknown as NativeStackNavigationProp) + ) + + expect(setOptionsSpy).not.toHaveBeenCalled() + }) +}) diff --git a/apps/mobile/src/utils/useAddBackButton.tsx b/apps/mobile/src/utils/useAddBackButton.tsx new file mode 100644 index 0000000..97993f8 --- /dev/null +++ b/apps/mobile/src/utils/useAddBackButton.tsx @@ -0,0 +1,24 @@ +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import React, { useEffect } from 'react' +import { OnboardingStackParamList, UnitagStackParamList } from 'src/app/navigation/types' +import { BackButton } from 'src/components/buttons/BackButton' +import { iconSizes } from 'ui/src/theme' + +/** + * Adds a back button to the navigation header if the screen is the first in the stack. + * By default react-navigation will only show the back button if the screen is not the first one in the stack. + */ +export function useAddBackButton( + navigation: + | NativeStackNavigationProp + | NativeStackNavigationProp +): void { + useEffect((): void => { + const shouldRenderBackButton = navigation.getState().index === 0 + if (shouldRenderBackButton) { + navigation.setOptions({ + headerLeft: () => , + }) + } + }, [navigation]) +} diff --git a/apps/mobile/src/utils/useAppStateTrigger.ts b/apps/mobile/src/utils/useAppStateTrigger.ts new file mode 100644 index 0000000..21334fa --- /dev/null +++ b/apps/mobile/src/utils/useAppStateTrigger.ts @@ -0,0 +1,25 @@ +import { useEffect, useRef } from 'react' +import { AppState, AppStateStatus } from 'react-native' + +/** Invokes `callback` when app state goes from `from` to `to`. */ +export function useAppStateTrigger( + from: AppStateStatus, + to: AppStateStatus, + callback: () => void +): void { + const appState = useRef(AppState.currentState) + + useEffect(() => { + const subscription = AppState.addEventListener('change', (nextAppState) => { + if (appState.current === from && nextAppState === to) { + callback() + } + + appState.current = nextAppState + }) + + return () => { + subscription.remove() + } + }, [from, callback, to]) +} diff --git a/apps/mobile/src/utils/useNoYoloParser.ts b/apps/mobile/src/utils/useNoYoloParser.ts new file mode 100644 index 0000000..6241563 --- /dev/null +++ b/apps/mobile/src/utils/useNoYoloParser.ts @@ -0,0 +1,18 @@ +import { getAbiFetchersForChainId, Parser } from 'no-yolo-signatures' +import { useMemo } from 'react' +import { config } from 'uniswap/src/config' +import { ChainId } from 'wallet/src/constants/chains' + +export function useNoYoloParser(chainId: ChainId): Parser { + const parser = useMemo(() => { + // TODO: [MOB-1] use better ABI Fetchers and/or our own Infura nodes for all chains. + const abiFetchers = getAbiFetchersForChainId(chainId, { + rpcUrls: { + [ChainId.Mainnet]: `https://mainnet.infura.io/v3/${config.infuraProjectId}`, + }, + }) + return new Parser({ abiFetchers }) + }, [chainId]) + + return parser +} diff --git a/apps/mobile/src/utils/useSagaStatus.ts b/apps/mobile/src/utils/useSagaStatus.ts new file mode 100644 index 0000000..3a5060c --- /dev/null +++ b/apps/mobile/src/utils/useSagaStatus.ts @@ -0,0 +1,43 @@ +import { useEffect } from 'react' +import { useAppDispatch, useAppSelector } from 'src/app/hooks' +import { monitoredSagas } from 'src/app/saga' +import { SagaState, SagaStatus } from 'wallet/src/utils/saga' + +// Convenience hook to get the status + error of an active saga +export function useSagaStatus( + sagaName: string, + onSuccess?: () => void, + resetSagaOnSuccess = true +): SagaState { + const dispatch = useAppDispatch() + const sagaState = useAppSelector((s): SagaState | undefined => s.saga[sagaName]) + if (!sagaState) { + throw new Error(`No saga state found, is sagaName valid? Name: ${sagaName}`) + } + + const saga = monitoredSagas[sagaName] + if (!saga) { + throw new Error(`No saga found, is sagaName valid? Name: ${sagaName}`) + } + + const { status, error } = sagaState + + useEffect(() => { + if (status === SagaStatus.Success) { + if (resetSagaOnSuccess) { + dispatch(saga.actions.reset()) + } + onSuccess?.() + } + }, [saga, status, error, onSuccess, resetSagaOnSuccess, dispatch]) + + useEffect(() => { + return () => { + if (resetSagaOnSuccess) { + dispatch(saga.actions.reset()) + } + } + }, [saga, resetSagaOnSuccess, dispatch]) + + return sagaState +} diff --git a/apps/mobile/src/utils/useSkeletonLoading.ts b/apps/mobile/src/utils/useSkeletonLoading.ts new file mode 100644 index 0000000..8403588 --- /dev/null +++ b/apps/mobile/src/utils/useSkeletonLoading.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react' + +/** + * Utility hook used to delay rendering initially so that the screen render a skeleton of placeholders + * to allow navigation to progress before rendering heavier components that may appear as lag + */ +export function useSkeletonLoading(): boolean { + const [enabled, setEnabled] = useState(true) + + useEffect(() => { + setTimeout(() => setEnabled(false), 0) + }) + + return enabled +} diff --git a/apps/mobile/src/utils/version.ts b/apps/mobile/src/utils/version.ts new file mode 100644 index 0000000..4cd1a9f --- /dev/null +++ b/apps/mobile/src/utils/version.ts @@ -0,0 +1,67 @@ +import DeviceInfo from 'react-native-device-info' +import { StatsigEnvironmentTier } from 'wallet/src/version' + +/** + * Returns a string with the app version and build number in the format: + * + * DEV: AppSemVer.BuildNumber: e.g. 1.2.3.233 + * PROD: AppSemVer: eg. 1 + */ +export function getFullAppVersion(): string { + const version = DeviceInfo.getVersion() + const buildVersion = DeviceInfo.getBuildNumber() + + if (__DEV__) { + return `${version}.${buildVersion}` + } + return version +} + +export enum BuildVariant { + Production = 'prod', + Beta = 'beta', + Development = 'dev', +} + +export function getBuildVariant(): BuildVariant { + if (isDevBuild()) { + return BuildVariant.Development + } else if (isBetaBuild()) { + return BuildVariant.Beta + } else { + return BuildVariant.Production + } +} + +export function isDevBuild(): boolean { + return DeviceInfo.getBundleId().endsWith('.dev') +} + +export function isBetaBuild(): boolean { + return DeviceInfo.getBundleId().endsWith('.beta') +} +export function getStatsigEnvironmentTier(): StatsigEnvironmentTier { + if (isDevBuild()) { + return StatsigEnvironmentTier.DEV + } + if (isBetaBuild()) { + return StatsigEnvironmentTier.BETA + } + return StatsigEnvironmentTier.PROD +} + +export function getSentryEnvironment(): SentryEnvironment { + if (isDevBuild()) { + return SentryEnvironment.DEV + } + if (isBetaBuild()) { + return SentryEnvironment.BETA + } + return SentryEnvironment.PROD +} + +enum SentryEnvironment { + DEV = 'development', + BETA = 'beta', + PROD = 'production', +} diff --git a/apps/mobile/tsconfig.json b/apps/mobile/tsconfig.json new file mode 100644 index 0000000..6234ab1 --- /dev/null +++ b/apps/mobile/tsconfig.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Mobile App", + "extends": "tsconfig/expo.json", + "references": [ + { + "path": "../../packages/ui" + }, + { + "path": "../../packages/utilities" + }, + { + "path": "../../packages/wallet" + }, + { + "path": "../../packages/uniswap" + } + ], + "compilerOptions": { + "baseUrl": "./", + } +} diff --git a/apps/mobile/wdyr.ts b/apps/mobile/wdyr.ts new file mode 100644 index 0000000..2ae0bd8 --- /dev/null +++ b/apps/mobile/wdyr.ts @@ -0,0 +1,13 @@ +/// +import React from 'react' + +if (process.env.NODE_ENV === 'development') { + // The library should NEVER be used in production because it slows down React + const whyDidYouRender = require('@welldone-software/why-did-you-render') + // Default to not tracking all components, add whyDidYouRender = true to component functions you want to track + whyDidYouRender(React, { + trackAllPureComponents: false, + // trackHooks: true, + // trackExtraHooks: [[ReactRedux, 'useSelector']], + }) +} diff --git a/apps/web/.depcheckrc b/apps/web/.depcheckrc new file mode 100644 index 0000000..6e1458b --- /dev/null +++ b/apps/web/.depcheckrc @@ -0,0 +1,68 @@ +ignores: [ + # Dependencies that depcheck thinks are unused but are actually used + "@web3-react/eip1193", + "@web3-react/empty", + "workbox-navigation-preload", + "@lingui/cli", + "@lingui/swc-plugin", + "@swc/core", + "@swc/jest", + "@swc/plugin-styled-components", + "@typechain/ethers-v5", + "@vanilla-extract/jest-transform", + "buffer", + "swc-loader", + "postinstall-postinstall", + "process", + # Dependencies that depcheck thinks are missing but are actually present or never used + ## GraphQL + "@graphql-codegen/*", + "get-graphql-schema", + "graphql-tag", + ## React Scripts and subpackages used from within it + "case-sensitive-paths-webpack-plugin", + "react-dev-utils", + "workbox-expiration", + "workbox-strategies", + ## Packages used in github actions + "start-server-and-test", + ## Polyfills + "resize-observer-polyfill", + ## Linting and Babel + "@babel/preset-env", + "eslint-plugin-import", + "prettier", + "terser-webpack-plugin", + ## Testing + "@types/testing-library__cypress", + ## sub-packages + "@ethersproject/*", + "@walletconnect/ethereum-provider", + "@testing-library/dom", + ## Internal packages / workspaces + "utilities", + "ui", + ## Top level local file paths + "abis", + "analytics", + "assets", + "components", + "connection", + "constants", + "dev", + "featureFlags", + "hooks", + "lib", + "locales", + "nft", + "pages", + "polyfills", + "rpc", + "serviceWorker", + "state", + "test-utils", + "theme", + "tracing", + "types", + "utils", + ] diff --git a/apps/web/.env b/apps/web/.env new file mode 100644 index 0000000..a59607a --- /dev/null +++ b/apps/web/.env @@ -0,0 +1,22 @@ +# These API keys are intentionally public. Please do not report them - thank you for your concern. +ESLINT_NO_DEV_ERRORS=true +REACT_APP_AMPLITUDE_PROXY_URL="https://interface.gateway.uniswap.org/v1/amplitude-proxy" +REACT_APP_AWS_API_ENDPOINT="https://beta.gateway.uniswap.org/v1/graphql" +REACT_APP_AWS_REALTIME_ENDPOINT="wss://beta.realtime.gateway.uniswap.org/graphql" +REACT_APP_AWS_REALTIME_TOKEN="realtime-3DDE0DA0-3408-4FD0-9DC3-5E285D70D29C" +REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf" +REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847" +REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://magical-alien-tab.quiknode.pro/669e87e569a8277d3fbd9e202f9df93189f19f4c" +REACT_APP_QUICKNODE_ARBITRUM_RPC_URL="https://black-ultra-valley.arbitrum-mainnet.quiknode.pro/96d7122781cfdcbccf5377cf0c68187332891e79" +REACT_APP_MOONPAY_API="https://api.moonpay.com" +REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging" +REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_test_DycfESRid31UaSxhI5yWKe1r5E5kKSz" +REACT_APP_SENTRY_DSN="https://a3c62e400b8748b5a8d007150e2f38b7@o1037921.ingest.sentry.io/4504255148851200" +REACT_APP_STATSIG_PROXY_URL="https://interface.gateway.uniswap.org/v1/statsig-proxy" +REACT_APP_TEMP_API_URL="https://temp.gateway.uniswap.org/v1" +REACT_APP_UNISWAP_API_URL="https://interface.gateway.uniswap.org/v2" +REACT_APP_UNISWAP_BASE_API_URL="https://interface.gateway.uniswap.org" +REACT_APP_UNISWAP_GATEWAY_DNS="https://interface.gateway.uniswap.org/v2" +REACT_APP_UNITAGS_API_URL="https://gateway.uniswap.org/v2/unitags" +REACT_APP_WALLET_CONNECT_PROJECT_ID="c6c9bacd35afa3eb9e6cccf6d8464395" +REACT_APP_IS_UNISWAP_INTERFACE=true diff --git a/apps/web/.env.production b/apps/web/.env.production new file mode 100644 index 0000000..90d1b9a --- /dev/null +++ b/apps/web/.env.production @@ -0,0 +1,18 @@ +# These API keys are intentionally public. Please do not report them - thank you for your concern. +REACT_APP_AMPLITUDE_PROXY_URL="https://interface.gateway.uniswap.org/v1/amplitude-proxy" +REACT_APP_AWS_API_ENDPOINT="https://interface.gateway.uniswap.org/v1/graphql" +REACT_APP_AWS_REALTIME_ENDPOINT="wss://realtime.gateway.uniswap.org/graphql" +REACT_APP_BNB_RPC_URL="https://old-wispy-arrow.bsc.quiknode.pro/f5c060177236065c1058531a0615ab4f7a34a2fd" +REACT_APP_FIREBASE_KEY="AIzaSyBcZWwTcTJHj_R6ipZcrJkXdq05PuX0Rs0" +REACT_APP_FORTMATIC_KEY="pk_live_F937DF033A1666BF" +REACT_APP_GOOGLE_ANALYTICS_ID="G-KDP9B6W4H8" +REACT_APP_INFURA_KEY="099fc58e0de9451d80b18d7c74caa7c1" +REACT_APP_MOONPAY_API="https://api.moonpay.com" +REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=production" +REACT_APP_MOONPAY_PUBLISHABLE_KEY="pk_live_uQG4BJC4w3cxnqpcSqAfohdBFDTsY6E" +REACT_APP_SENTRY_ENABLED=true +REACT_APP_SENTRY_TRACES_SAMPLE_RATE=0.003 +REACT_APP_STATSIG_PROXY_URL="https://interface.gateway.uniswap.org/v1/statsig-proxy" +REACT_APP_QUICKNODE_MAINNET_RPC_URL="https://ultra-blue-flower.quiknode.pro/770b22d5f362c537bc8fe19b034c45b22958f880" +REACT_APP_QUICKNODE_ARBITRUM_RPC_URL="https://tiniest-stylish-arrow.arbitrum-mainnet.quiknode.pro/d06833352b8de605914d9e24a390d8b4d3aff7ba" +THE_GRAPH_SCHEMA_ENDPOINT="https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap" diff --git a/apps/web/.eslintignore b/apps/web/.eslintignore new file mode 100644 index 0000000..4f71464 --- /dev/null +++ b/apps/web/.eslintignore @@ -0,0 +1,5 @@ +.eslintrc.js +babel.config.js +jest.config.js +metro.config.js +node_modules diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js new file mode 100644 index 0000000..c059602 --- /dev/null +++ b/apps/web/.eslintrc.js @@ -0,0 +1,111 @@ +/* eslint-env node */ + +const { node: restrictedImports } = require('@uniswap/eslint-config/restrictedImports') +require('@uniswap/eslint-config/load') + +const rulesDirPlugin = require('eslint-plugin-rulesdir') +rulesDirPlugin.RULES_DIR = 'eslint_rules' + +module.exports = { + root: true, + extends: ['@uniswap/eslint-config/react'], + plugins: ['rulesdir'], + + rules: { + // TODO: had to add this rule to avoid errors on monorepo migration that didnt happen in interface + 'cypress/unsafe-to-chain-command': 'off', + }, + + overrides: [ + { + files: ['**/*'], + rules: { + 'multiline-comment-style': ['error', 'separate-lines'], + 'rulesdir/no-undefined-or': 'error', + }, + }, + { + // Configuration/typings typically export objects/definitions that are used outside of the transpiled package + // (eg not captured by the tsconfig). Because it's typical and not exceptional, this is turned off entirely. + files: ['**/*.config.*', '**/*.d.ts'], + rules: { + 'import/no-unused-modules': 'off', + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + '@typescript-eslint/no-restricted-imports': [ + 'error', + { + ...restrictedImports, + paths: [ + ...restrictedImports.paths, + { + name: '@uniswap/smart-order-router', + message: 'Only import types, unless you are in the client-side SOR, to preserve lazy-loading.', + allowTypeImports: true, + }, + { + name: 'moment', + // tree-shaking for moment is not configured because it degrades performance - see craco.config.cjs. + message: 'moment is not configured for tree-shaking. If you use it, update the Webpack configuration.', + }, + { + name: 'react-helmet-async', + // default package's esm export is broken, but the explicit cjs export works. + message: `Import from 'react-helment-async/lib/index' instead.`, + }, + { + name: 'zustand', + importNames: ['default'], + message: 'Default import from zustand is deprecated. Import `{ create }` instead.', + }, + ], + }, + ], + 'import/no-restricted-paths': [ + 'error', + { + zones: [ + { + target: ['src/**/*[!.test].ts', 'src/**/*[!.test].tsx'], + from: 'src/test-utils', + }, + ], + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: ':matches(ExportAllDeclaration)', + message: 'Barrel exports bloat the bundle size by preventing tree-shaking.', + }, + { + selector: `:matches(Literal[value='NATIVE'])`, + message: + "Don't use the string 'NATIVE' directly. Use the NATIVE_CHAIN_ID variable from constants/tokens instead.", + }, + ], + }, + }, + { + files: ['**/*.ts', '**/*.tsx'], + excludedFiles: ['src/analytics/*'], + rules: { + // Uses 'no-restricted-imports' to avoid overriding the above rules in '@typescript-eslint/no-restricted-imports' + 'no-restricted-imports': [ + 'error', + { + paths: [ + { + name: '@uniswap/analytics', + message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`, + }, + ], + }, + ], + }, + }, + ], +} diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..a296578 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,55 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + + +# generated contract types +/src/locales/**/*.js +/src/locales/**/*.po + +# generated files +/src/**/__generated__ + +# dependencies +/node_modules + +# testing +/coverage +/cache +/functions/coverage +/.swc + +# builds +/build +/dts + +# misc +.DS_Store + +!.env + +.env.local +.env.development.local +.env.test.local +.env.production.local + +instrumented +.nyc_output +.nyc_output/**/* + +/.netlify + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +notes.txt +.idea/ + +package-lock.json + +cypress/downloads +cypress/videos +cypress/screenshots + +.vercel + +.wrangler diff --git a/apps/web/.npmrc b/apps/web/.npmrc new file mode 100644 index 0000000..c42da84 --- /dev/null +++ b/apps/web/.npmrc @@ -0,0 +1 @@ +engine-strict = true diff --git a/apps/web/.nvmrc b/apps/web/.nvmrc new file mode 100644 index 0000000..8ddbc0c --- /dev/null +++ b/apps/web/.nvmrc @@ -0,0 +1 @@ +v18.16.0 diff --git a/apps/web/.swcrc b/apps/web/.swcrc new file mode 100644 index 0000000..4f12082 --- /dev/null +++ b/apps/web/.swcrc @@ -0,0 +1,30 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "keepClassNames": true, + "experimental": { + "plugins": [ + ["@lingui/swc-plugin", {}], + [ + "@swc/plugin-styled-components", + { + "displayName": true + } + ] + ] + }, + "parser": { + "syntax": "typescript", + "tsx": true + }, + "transform": { + "react": { + "runtime": "automatic" + } + } + }, + "env": { + "targets": { "chrome": "80" }, + "include": ["transform-async-to-generator"] + } +} diff --git a/apps/web/CONTRIBUTING.md b/apps/web/CONTRIBUTING.md new file mode 100644 index 0000000..5f08fce --- /dev/null +++ b/apps/web/CONTRIBUTING.md @@ -0,0 +1,133 @@ +# Contributing + +Thank you for your interest in contributing to the Uniswap interface! 🦄 + +# Development + +Before running anything, you'll need to install the dependencies: + +``` +yarn install +``` + +## Running the web app locally + +``` +yarn web start +``` + +The interface should automatically open. If it does not, navigate to [http://localhost:3000]. + +## Creating a production build + +``` +yarn web build:production +``` + +To serve the production build: + +``` +yarn web serve +``` + +Then, navigate to [http://localhost:3000] to see it. + +## Running unit tests + +``` +yarn web test +``` + +By default, this runs only unit tests that have been affected since the last commit. To run _all_ unit tests: + +``` +yarn web test --watchAll +``` + +## Running integration tests (cypress) + +Integration tests require a server to be running. In order to see your changes quickly, run `start` in its own tab/window: + +``` +yarn web start +``` + +Integration tests are run using `cypress`. When developing locally, use `cypress:open` for an interactive UI, and to inspect the rendered page: + +``` +yarn web cypress:open +``` + +To run _all_ cypress integration tests _from the command line_: + +``` +yarn web cypress:run +``` + +## Adding a new dependency + +Adding many new dependencies would cause bloat, so we have a test to guard against this: `scripts/test-size.js`. This will run as part of CI with every PR. + +If you *need* to add a new dependency, and it causes the generated build to exceed its size quota, you'll need to increase the quota. Do so in `scripts/test-size.js`. + +You can also run the test on your last build using `yarn build && yarn test:size`. If you exceed the size quota, it will let you know what to do :). + +## Engineering standards + +Code merged into the `main` branch of this repository should adhere to high standards of correctness and maintainability. +Use your best judgment when applying these standards. If code is in the critical path, will be frequently visited, or +makes large architectural changes, consider following all the standards. + +- Have at least one engineer approve of large code refactorings +- At least manually test small code changes, prefer automated tests +- Thoroughly unit test when code is not obviously correct +- If something breaks, add automated tests so it doesn't break again +- Add integration tests for new pages or flows +- Verify that all CI checks pass before merging +- Have at least one product manager or designer approve of any significant UX changes + +## Guidelines + +The following points should help guide your development: + +- Security: the interface is safe to use + - Avoid adding unnecessary dependencies due to [supply chain risk](https://github.com/LavaMoat/lavamoat#further-reading-on-software-supplychain-security) +- Reproducibility: anyone can build the interface + - Avoid adding steps to the development/build processes + - The build must be deterministic, i.e. a particular commit hash always produces the same build +- Decentralization: anyone can run the interface + - An Ethereum node should be the only critical dependency + - All other external dependencies should only enhance the UX ([graceful degradation](https://developer.mozilla.org/en-US/docs/Glossary/Graceful_degradation)) +- Accessibility: anyone can use the interface + - The interface should be responsive, small and also run well on low performance devices (majority of swaps on mobile!) + +## Release process + +Releases are cut automatically from the `main` branch Monday-Thursday in the morning according to the [release workflow](./.github/workflows/release.yaml). + +Fix pull requests should be merged whenever ready and tested. +If a fix is urgently needed in production, releases can be manually triggered on [GitHub](https://github.com/Uniswap/uniswap-interface/actions/workflows/release.yaml) +after the fix is merged into `main`. + +Features should not be merged into `main` until they are ready for users. +When building larger features or collaborating with other developers, create a new branch from `main` to track its development. +Use the automatic Vercel preview for sharing the feature to collect feedback. +When the feature is ready for review, create a new pull request from the feature branch into `main` and request reviews from +the appropriate UX reviewers (PMs or designers). + +## Finding a first issue + +Start with issues with the label +[`good first issue`](https://github.com/Uniswap/uniswap-interface/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). + +# Translations + +Uniswap uses [Crowdin](https://crowdin.com/project/uniswap-interface) for managing translations. +[This workflow](./.github/workflows/crowdin.yaml) uploads new strings for translation to the Crowdin project whenever code using the [lingui translation macros](https://lingui.js.org/ref/macro.html) is merged into `main`. + +Every hour, translations are synced back down from Crowdin to the repository in [this other workflow](./.github/workflows/crowdin-sync.yaml). +We sync to the repository on a schedule, rather than download translations at build time, so that builds are always reproducible. + +You can contribute by joining Crowdin to proofread existing translations [here](https://crowdin.com/project/uniswap-interface/invite?d=93i5n413q403t4g473p443o4c3t2g3s21343u2c3n403l4b3v2735353i4g4k4l4g453j4g4o4j4e4k4b323l4a3h463s4g453q443m4e3t2b303s2a35353l403o443v293e303k4g4n4r4g483i4g4r4j4e4o473i5n4a3t463t4o4) + +Or, ask to join us as a translator in the Discord!! diff --git a/apps/web/LICENSE b/apps/web/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/apps/web/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..5a4e08e --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,34 @@ +# Uniswap Labs Web Interface + +## Accessing the Uniswap Interface + +To access the Uniswap Interface, use an IPFS gateway link from the +[latest release](https://github.com/Uniswap/uniswap-interface/releases/latest), +or visit [app.uniswap.org](https://app.uniswap.org). + +## Running the interface locally + +```bash +yarn +yarn web start +``` + +## Unsupported tokens + +Check out `useUnsupportedTokenList()` in [src/state/lists/hooks.ts](./src/state/lists/hooks.ts) for blocking tokens in your instance of the interface. + +You can block an entire list of tokens by passing in a tokenlist like [here](./src/constants/lists.ts) + +## Accessing Uniswap V2 + +The Uniswap Interface supports swapping, adding liquidity, removing liquidity and migrating liquidity for Uniswap protocol V2. + +- Swap on Uniswap V2: +- View V2 liquidity: +- Add V2 liquidity: +- Migrate V2 liquidity to V3: + +## Accessing Uniswap V1 + +The Uniswap V1 interface for mainnet and testnets is accessible via IPFS gateways +linked from the [v1.0.0 release](https://github.com/Uniswap/uniswap-interface/releases/tag/v1.0.0). diff --git a/apps/web/codegen.yml b/apps/web/codegen.yml new file mode 100644 index 0000000..cf6dced --- /dev/null +++ b/apps/web/codegen.yml @@ -0,0 +1,6 @@ +overrideExisting: true +schema: 'https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3?source=uniswap' +generates: + ./src/graphql/thegraph/schema/schema.graphql: + plugins: + - schema-ast diff --git a/apps/web/craco.config.cjs b/apps/web/craco.config.cjs new file mode 100644 index 0000000..7c2b1b8 --- /dev/null +++ b/apps/web/craco.config.cjs @@ -0,0 +1,231 @@ +/* eslint-env node */ +const { VanillaExtractPlugin } = require('@vanilla-extract/webpack-plugin') +const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin') +const { execSync } = require('child_process') +const { readFileSync } = require('fs') +const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const path = require('path') +const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin') +const { IgnorePlugin, ProvidePlugin } = require('webpack') +const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin') + +const commitHash = execSync('git rev-parse HEAD').toString().trim() +const isProduction = process.env.NODE_ENV === 'production' + +process.env.REACT_APP_GIT_COMMIT_HASH = commitHash + +// Linting and type checking are only necessary as part of development and testing. +// Omit them from production builds, as they slow down the feedback loop. +const shouldLintOrTypeCheck = !isProduction + +// Our .swcrc wasn't being picked up in the monorepo, so we load it directly. +const swcrc = JSON.parse(readFileSync('./.swcrc', 'utf-8')) + +function getCacheDirectory(cacheName) { + // Include the trailing slash to denote that this is a directory. + return `${path.join(__dirname, 'node_modules/.cache/', cacheName)}/` +} + +module.exports = { + eslint: { + enable: shouldLintOrTypeCheck, + pluginOptions(eslintConfig) { + return Object.assign(eslintConfig, { + cache: true, + cacheLocation: getCacheDirectory('eslint'), + ignorePath: '.gitignore', + // Use our own eslint/plugins/config, as overrides interfere with caching. + // This ensures that `yarn start` and `yarn lint` share one cache. + eslintPath: require.resolve('eslint'), + resolvePluginsRelativeTo: null, + baseConfig: null, + }) + }, + }, + typescript: { + enableTypeChecking: shouldLintOrTypeCheck, + }, + jest: { + configure(jestConfig) { + return Object.assign(jestConfig, { + globals: { + __DEV__: true, + }, + cacheDirectory: getCacheDirectory('jest'), + transform: { + ...Object.entries(jestConfig.transform).reduce((transform, [key, value]) => { + if (value.match(/babel/)) return transform + return { ...transform, [key]: value } + }, {}), + // Transform vanilla-extract using its own transformer. + // See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform. + '\\.css\\.ts$': '@vanilla-extract/jest-transform', + '\\.(t|j)sx?$': ['@swc/jest', swcrc], + }, + // Use d3-arrays's build directly, as jest does not support its exports. + transformIgnorePatterns: ['d3-array'], + moduleNameMapper: { + 'd3-array': 'd3-array/dist/d3-array.min.js', + '^react-native$': 'react-native-web', + 'react-native-gesture-handler': require.resolve('react-native-gesture-handler'), + }, + }) + }, + }, + webpack: { + plugins: [ + // Webpack 5 does not polyfill node globals, so we do so for those necessary: + new ProvidePlugin({ + // - react-markdown requires process.cwd + process: 'process/browser.js', + }), + new VanillaExtractPlugin(), + new RetryChunkLoadPlugin({ + cacheBust: `function() { + return 'cache-bust=' + Date.now(); + }`, + // Retries with exponential backoff (500ms, 1000ms, 2000ms). + retryDelay: `function(retryAttempt) { + return 2 ** (retryAttempt - 1) * 500; + }`, + maxRetries: 3, + }), + ], + configure: (webpackConfig) => { + // Configure webpack plugins: + webpackConfig.plugins = webpackConfig.plugins + .map((plugin) => { + // CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings. + // See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings. + if (plugin instanceof MiniCssExtractPlugin) { + plugin.options.ignoreOrder = true + } + + // Disable TypeScript's config overwrite, as it interferes with incremental build caching. + // This ensures that `yarn start` and `yarn typecheck` share one cache. + if (plugin.constructor.name == 'ForkTsCheckerWebpackPlugin') { + delete plugin.options.typescript.configOverwrite + } + + return plugin + }) + .filter((plugin) => { + // Case sensitive paths are already enforced by TypeScript. + // See https://www.typescriptlang.org/tsconfig#forceConsistentCasingInFileNames. + if (plugin instanceof CaseSensitivePathsPlugin) return false + + // IgnorePlugin is used to tree-shake moment locales, but we do not use moment in this project. + if (plugin instanceof IgnorePlugin) return false + + return true + }) + + // Configure webpack resolution: + webpackConfig.resolve = Object.assign(webpackConfig.resolve, { + alias: { + ...webpackConfig.resolve.alias, + 'react-native-gesture-handler$': require.resolve('react-native-gesture-handler'), + }, + plugins: webpackConfig.resolve.plugins.map((plugin) => { + // Allow vanilla-extract in production builds. + // This is necessary because create-react-app guards against external imports. + // See https://sandroroth.com/blog/vanilla-extract-cra#production-build. + if (plugin instanceof ModuleScopePlugin) { + plugin.allowedPaths.push(path.join(__dirname, '..', '..', 'node_modules/@vanilla-extract/webpack-plugin')) + } + + return plugin + }), + // Webpack 5 does not resolve node modules, so we do so for those necessary: + fallback: { + // - react-markdown requires path + path: require.resolve('path-browserify'), + }, + }) + + // Retain source maps for node_modules packages: + webpackConfig.module.rules[0] = { + ...webpackConfig.module.rules[0], + exclude: /node_modules/, + } + + // Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf): + webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => { + if (rule.loader && rule.loader.match(/babel-loader/)) { + rule.loader = 'swc-loader' + rule.options = swcrc + + rule.include = (inPath) => { + // if not a node_module we parse with SWC (so other packages in monorepo are importable) + return inPath.indexOf('node_modules') === -1 + } + } + return rule + }) + + // since wallet package uses react-native-dotenv and that needs a babel plugin + // adding this before the swc loader + webpackConfig.module.rules[1].oneOf.unshift({ + loader: 'babel-loader', + include: (path) => /uniswap\/src.*\.(js|ts)x?$/.test(path), + options: { + presets: ['module:metro-react-native-babel-preset'], + plugins: [ + [ + 'module:react-native-dotenv', + { + // ideally use envName here to add a mobile namespace but this doesn't work when sharing with dotenv-webpack + moduleName: 'react-native-dotenv', + path: '../../.env.defaults', // must use this path so this file can be shared with web since dotenv-webpack is less flexible + safe: true, + allowUndefined: false, + }, + ], + ], + }, + }) + + // TODO(WEB-3632): Tamagui linear gradient isn't fully-specified, temporary + webpackConfig.module.rules.unshift({ + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }) + + // Run terser compression on node_modules before tree-shaking, so that tree-shaking is more effective. + // This works by eliminating dead code, so that webpack can identify unused imports and tree-shake them; + // it is only necessary for node_modules - it is done through linting for our own source code - + // see https://medium.com/engineering-housing/dead-code-elimination-and-tree-shaking-at-housing-part-1-307a94b30f23#7e03: + webpackConfig.module.rules.push({ + enforce: 'post', + test: /node_modules.*\.(js)$/, + loader: path.join(__dirname, 'scripts/terser-loader.js'), + options: { compress: true, mangle: false }, + }) + + // Configure webpack optimization: + webpackConfig.optimization = Object.assign( + webpackConfig.optimization, + isProduction + ? { + splitChunks: { + // Cap the chunk size to 5MB. + // react-scripts suggests a chunk size under 1MB after gzip, but we can only measure maxSize before gzip. + // react-scripts also caps cacheable chunks at 5MB, which gzips to below 1MB, so we cap chunk size there. + // See https://github.com/facebook/create-react-app/blob/d960b9e/packages/react-scripts/config/webpack.config.js#L713-L716. + maxSize: 5 * 1024 * 1024, + // Optimize over all chunks, instead of async chunks (the default), so that initial chunks are also optimized. + chunks: 'all', + }, + } + : {} + ) + + // Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache: + webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true }) + + return webpackConfig + }, + }, +} diff --git a/apps/web/cypress.config.ts b/apps/web/cypress.config.ts new file mode 100644 index 0000000..75fc1ba --- /dev/null +++ b/apps/web/cypress.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'cypress' +import { setupHardhatEvents } from 'cypress-hardhat' + +export default defineConfig({ + projectId: 'fabfoi', + defaultCommandTimeout: 24000, // 2x average block time + chromeWebSecurity: false, + experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462 + retries: { runMode: process.env.CYPRESS_RETRIES ? +process.env.CYPRESS_RETRIES : 1 }, + video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025 + e2e: { + async setupNodeEvents(on, config) { + await setupHardhatEvents(on, config) + return config + }, + baseUrl: 'http://localhost:3000', + specPattern: 'cypress/{e2e,staging}/**/*.test.ts', + }, +}) diff --git a/apps/web/cypress/README.md b/apps/web/cypress/README.md new file mode 100644 index 0000000..c1db9f2 --- /dev/null +++ b/apps/web/cypress/README.md @@ -0,0 +1,201 @@ +# e2e testing with Cypress + +End-to-end tests are run through [Cypress](https://docs.cypress.io/api/table-of-contents/), which runs tests in a real browser. Cypress is a little different than other testing frameworks, and e2e tests are a little different than unit tests, so this directory has its own set of patterns, idioms, and best practices. Not only that, but we're testing against a forked blockchain, not just against typical Web APIs, so we have unique flows that you may not have seen elsewhere. + +## Running your first e2e tests + +Cypress tests run against a local server, so you'll need to run the application locally at the same time. The fastest way to run e2e tests is to use your dev server: `yarn start`. + +Open cypress at the same time with `yarn cypress:open`. You should do this from another window or tab, so that you can continue to see any typechecking/linting warnings from `yarn start`. + +Cypress opens its own instance of Chrome, with a list of "E2E specs" for you to select. When you're developing locally, you usually only want to run one spec file at a time. Select your spec by clicking on the filename and it will run. + +## Glossary + +#### spec +Cypress considers each file a separate spec, or collection of tests. +Specs are always run as a whole through `yarn cypress:open` or on the same machine through CI. + +#### Thenable +Cypress queues commands to run in the browser using [Thenables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise#thenables), not Promises. +For this reason, you should not use `async/await` syntax in Cypress unless it is wholly-contained in a `cy.then` function argument. + +## Writing your first e2e test + +_For an excellent treatment on tests, check out the [Cypress Fundamentals](https://learn.cypress.io/cypress-fundamentals/how-to-write-a-test) course._ +_While some of that will be paraphrased here, this should be sufficient to get you started:_ + +### What is a test? + +Cypress tests are just like any other test: you should set up an initial state, execute an action, and verify the action's consequence. This is codified in the AAA (Arrange-Act-Assert) pattern, and you'll see this in most of our tests. In _our_ case, it plays out as: + +1. Arrange: Visit a page, eg `cy.visit('/swap')`, and set up the state, on the blockchain and the page. +2. Act: Initiate your action under test, eg `initiateSwap()` +3. Assert: Verify that the action has occured, eg `// Verify swap has occured` + +You'll usually see the setup, followed by a newline, followed by assertions with comments stating what they are asserting. +Because Cypress tests are translated into user actions, it may be hard to follow the action being described. You should use comments liberally to describe what you are doing and what you intend to test, to make tests easier to read and maintain in the future. + +### Thinking about tests: queuing up a sequence of commands + +Cypress uses `Thenable`s to achieve "command chaining". A test is described as a series of commands, which are only executed once the previous command in the chain has executed. + +``` +cy.visit('/swap') +cy.contains('Select token').click() +cy.contains('DAI').click() +``` + +In this example, `cy.contains('Select token').click()` is queued up right away (all the code is synchronous), but it will not execute until `/swap` has loaded (all the commands are chained); and `click()` will not execute until `Select token` has been found. + +This becomes more relevant as you work with data on the blockchain, as you'll need to load it at the correct time, _after_ it's been modified by the application: + +``` +cy.hardhat().then(async (hardhat) => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + // wait for the transaction to be executed + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + + // BAD: This will get the balance _before_ the other queued actions have executed. + const balance = await hardhat.getBalance(hardhat.wallet, USDC_MAINNET) + cy.wrap(balance).should('deep.equal', expectedBalance) +}) +``` + +``` + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + // wait for the transaction to be executed + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + + // GOOD: cy.then chains the command so that it runs _after_ executing the swap + cy.hardhat() + .then((hardhat) => hardhat.getBalance(hardhat.wallet, USDC_MAINNET)) + .should('deep.equal', expectedBalance) +}) +``` + +### Working with the blockchain (ie hardhat) + +Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`. + +By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`. + +The hardhat integration has built-in utilities to let you modify and assert on balances, approvals, and permits, and should be fully typed. Check it out at [Uniswap/cypress-hardhat](https://github.com/Uniswap/cypress-hardhat). + +### Asserting on wallet methods + +Wallet methods to hardhat are all aliased. If you'd like to assert that a method was sent to the wallet, you can do so using the method name, prefixed with `@`: + +``` +// Asserts that `eth_sendRawTransaction` was sent to the wallet. +cy.wait('@eth_sendRawTransaction') +``` + +Sometimes, you may want a method to _fail_. In this case, you can stub it, but you should disable logging to avoid spamming the test: + +``` +// Stub calls to eth_signTypedData_v4 and fail them +cy.hardhat().then((hardhat) => { + // Note the closure to keep signTypedDataStub in scope. Using closures instead of variables (eg let) helps prevent misuse of chaining. + const signTypedDataStub = cy.stub(hardhat.provider, 'send').log(false) + signTypedDataStub.withArgs('eth_signTypedData_v4).rejects(USER_REJECTION) + signTypedDataStub.callThrough() // allws other methods to call through to hardhat + + cy.contains('Confirm swap').click() + + // Verify the call occured + // Note the call to cy.wrap to correctly queue the chained command. Without this, the test would occur before the stub is called. + cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4') + + // Restore the stub + // note the call to cy.then to correctly queue the chained command. Without this, the stub would be restored immediately. + cy.then(() => permitApprovalStub.restore()) +}) +``` + +## Best practices + + + + +### Spec / test grouping + +Each spec should be specific to one route, _not_ one functional behavior. +For example, `token-details.test.ts` is separated from `swap.test.ts`. + +If a route has different functional behaviors, that route should become a directory name, and its spec should be split. +For example, `swap.test.ts` may be split into `swap/swap.test.ts`, `swap/wrap.test.ts`, `swap/permit2.test.ts`. + +_This prevents specs from growing too large, which is important because they are always run as a whole locally and on the same machine through CI. If a spec grows too large, it will have a longer local feedback loop, and it will become the bottleneck for CI test runtime._ + +_Similarly, avoid actions outside the scope of your spec, as it will cause total testing time to increase._ + +### Use closures instead of variables + +Avoid usage of `let`, instead assigning a constant. In practice, this means using closures for your variables: + +```javascript +let badVariable + +cy.hardhat({ automine: false }) + .then((hardhat) => cy.then(() => hardhat.provider.getBalance(hardhat.wallet.address))) + .then((initialBalance) => { + // Do not assign to a variable outside of your closure! + badVariable = initialBalance // <-- bad! + + // Use initial balance here, within the closure. + }) + +cy.get('.class-name').then((el) => { + // Do not use badVariable here! It may have changed value due to the queued async nature of Cypress. + expect(el).should('contain', badVariable) // <-- bad! +}) +``` + +_This prevents misuse of a not-yet-initialized variable, or a variable that has changed as the test progresses._ + +### Prefer selecting elements using on-screen text over data-testid attributes + +When selecting components (eg with `cy.get`), prefer defining your selector with visible UI. Sometimes this is not possible (eg if the text is duplicated on-screen), and you'll need to add a `data-testid` property. + +_Defining tests using visual fields helps ensure that we don't break them. `data-testid` may select an element that is only selectable programmatically, and should be used only when necessary, as its use may cover up UI breakages._ + +_You'll still want to use `data-testid` in cases where the text is rendered in multiple containers and you need to select the correct one, or where the component doesn't render predictable text output._ + +### Avoid branching logic + +Do not write tests that rely on if-statements or conditionals. Do not create helper methods which do more than one thing, and rely on branching logic to apply to different but similar situations. + +_Tests should be readable and simple. Branching logic makes it harder to reason about tests, and may hide otherwise flaky or ill-defined behaviors._ + +_Similarly, you should avoid complicated for-loops. Sometimes, for simple repetition, for-loops are ok._ + +### Avoid spamming the console + +It is ok to include logging while you are developing a test, but that logging should be removed if it is not needed to debug (potential) errors. + +For example, stubbing a wallet method will result in dumping a hex string (the calldata) to the log. Instead, suppress logging from methods which you know will flood the log. + +```javascript +cy.stub(hardhat.wallet, 'sendTransaction') + .log(false) // <-- suppresses logs from this stub + .rejects(new Error('user cancelled')) +``` + +_Unnecessary logs it makes it harder to reason about a test overall._ + +### Name helper methods using transitive verbs + +Name helper methods using "action verbs": `expectsThisToHappen`, not `expectThisToHappen`; `selectsToken(token: string)`, not `selectAToken(token: string)`. + +_This makes your tests read more naturally, and makes it easier to follow given existing `should` syntax._ diff --git a/apps/web/cypress/e2e/add-liquidity.test.ts b/apps/web/cypress/e2e/add-liquidity.test.ts new file mode 100644 index 0000000..4d0d340 --- /dev/null +++ b/apps/web/cypress/e2e/add-liquidity.test.ts @@ -0,0 +1,116 @@ +import { CyHttpMessages } from 'cypress/types/net-stubbing' + +import { aliasQuery, hasQuery } from '../utils/graphql-test-utils' + +describe('Add Liquidity', () => { + beforeEach(() => { + cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3?source=uniswap', (req) => { + aliasQuery(req, 'feeTierDistribution') + }) + }) + + it('loads the token pair', () => { + cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI') + cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH') + cy.contains('0.05% fee tier') + }) + + it('clears the token selection when chain changes', () => { + cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI') + cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH') + cy.get('[data-testid="chain-selector"]').last().click() + cy.contains('Polygon').click() + cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('not.contain.text', 'UNI') + }) + + it('does not crash if token is duplicated', () => { + cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI') + cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI') + }) + + it('single token can be selected', () => { + cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984') + cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI') + }) + + it('loads fee tier distribution', () => { + cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => { + cy.intercept( + 'POST', + '/subgraphs/name/uniswap/uniswap-v3?source=uniswap', + (req: CyHttpMessages.IncomingHttpRequest) => { + if (hasQuery(req, 'FeeTierDistribution')) { + req.alias = 'FeeTierDistribution' + + req.reply({ + body: { + data: { + ...feeTierDistribution, + }, + }, + headers: { + 'access-control-allow-origin': '*', + }, + }) + } + } + ) + + cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH') + cy.wait('@FeeTierDistribution') + cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.30% fee tier') + cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select') + }) + }) + + it('disables increment and decrement until initial prices are inputted', () => { + // ETH / BITCOIN pool (0.05% tier not created) + cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500') + // Set starting price in order to enable price range step counters + cy.get('.start-price-input').type('1000') + + // Min Price increment / decrement buttons should be disabled + cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled') + // Enter min price, which should enable the buttons + cy.get('.rate-input-0').eq(0).type('900').blur() + cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled') + + // Repeat for Max Price step counter + cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled') + // Enter max price, which should enable the buttons + cy.get('.rate-input-0').eq(1).type('1100').blur() + cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled') + }) + + it('allows full range selection on new pool creation', () => { + // ETH / BITCOIN pool (0.05% tier not created) + cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500') + // Set starting price in order to enable price range step counters + cy.get('.start-price-input').type('1000') + cy.get('[data-testid="set-full-range"]').click() + // Check that the min price is 0 and the max price is infinity + cy.get('.rate-input-0').eq(0).should('have.value', '0') + cy.get('.rate-input-0').eq(1).should('have.value', '∞') + // Increment and decrement buttons are disabled when full range is selected + cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled') + cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled') + cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled') + // Check that url params were added + cy.url().then((url) => { + const params = new URLSearchParams(url) + const minPrice = params.get('minPrice') + const maxPrice = params.get('maxPrice') + // Note: although 0 and ∞ displayed, actual values in query are ticks at limit + return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice) + }) + }) +}) diff --git a/apps/web/cypress/e2e/buy-crypto-modal.test.ts b/apps/web/cypress/e2e/buy-crypto-modal.test.ts new file mode 100644 index 0000000..9357cc3 --- /dev/null +++ b/apps/web/cypress/e2e/buy-crypto-modal.test.ts @@ -0,0 +1,28 @@ +import { getTestSelector } from '../utils' + +describe('Buy Crypto Modal', () => { + it('should open and close', () => { + cy.visit('/') + + // Open the fiat onramp modal + cy.get(getTestSelector('buy-fiat-button')).click() + cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible') + + // Click on a location that should be outside the modal, which should close it + cy.get('body').click(0, 100) + cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist') + }) + + it('should open and close, mobile viewport', () => { + cy.viewport('iphone-6') + cy.visit('/') + + // Open the fiat onramp modal + cy.get(getTestSelector('buy-fiat-button')).click() + cy.get(getTestSelector('fiat-onramp-modal')).should('be.visible') + + // Click on a location that should be outside the modal, which should close it + cy.get('body').click(10, 10) + cy.get(getTestSelector('fiat-onramp-modal')).should('not.exist') + }) +}) diff --git a/apps/web/cypress/e2e/landing.test.ts b/apps/web/cypress/e2e/landing.test.ts new file mode 100644 index 0000000..5f1dd9f --- /dev/null +++ b/apps/web/cypress/e2e/landing.test.ts @@ -0,0 +1,139 @@ +import { getTestSelector } from '../utils' +import { CONNECTED_WALLET_USER_STATE, DISCONNECTED_WALLET_USER_STATE } from '../utils/user-state' + +describe('Landing Page', () => { + it('shows landing page when no user state exists', () => { + cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE }) + cy.get(getTestSelector('landing-page')) + cy.screenshot() + }) + + it('redirects to swap page when a user has already connected a wallet', () => { + cy.visit('/', { userState: CONNECTED_WALLET_USER_STATE }) + cy.get('#swap-page') + cy.url().should('include', '/swap') + cy.screenshot() + }) + + it('shows landing page when a user has already connected a wallet but ?intro=true is in query', () => { + cy.visit('/?intro=true', { userState: CONNECTED_WALLET_USER_STATE }) + cy.get(getTestSelector('landing-page')) + }) + + it('remains on landing page when account drawer is opened and only redirects after user becomes connected', () => { + // Visit landing page with no connection or recent connection, and open account drawer + cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE }) + cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click() + cy.url().should('not.include', '/swap') + + // Connect and verify redirect + cy.contains('MetaMask').click() + cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6))) + cy.url().should('include', '/swap') + }) + + it('allows navigation to pool', () => { + cy.viewport(2000, 1600) + cy.visit('/swap') + cy.get(getTestSelector('pool-nav-link')).first().click() + cy.url().should('include', '/pool') + }) + + it('allows navigation to pool on mobile', () => { + cy.viewport('iphone-6') + cy.visit('/swap') + cy.get(getTestSelector('pool-nav-link')).last().click() + cy.url().should('include', '/pool') + }) + + it('does not render landing page when / path is blocked', () => { + cy.intercept('/', (req) => { + req.reply((res) => { + const parser = new DOMParser() + const doc = parser.parseFromString(res.body, 'text/html') + const meta = document.createElement('meta') + meta.setAttribute('property', 'x:blocked-paths') + meta.setAttribute('content', '/,/buy') + doc.head.appendChild(meta) + + res.body = doc.documentElement.outerHTML + }) + }) + cy.visit('/', { userState: DISCONNECTED_WALLET_USER_STATE }) + + cy.get(getTestSelector('landing-page')).should('not.exist') + cy.get(getTestSelector('buy-fiat-button')).should('not.exist') + cy.url().should('include', '/swap') + }) + + it('does not render uk compliance banner in US', () => { + cy.visit('/swap') + cy.contains('UK disclaimer').should('not.exist') + }) + + it('renders uk compliance banner in uk', () => { + cy.intercept('https://interface.gateway.uniswap.org/v1/amplitude-proxy', (req) => { + const requestBody = JSON.stringify(req.body) + const byteSize = new Blob([requestBody]).size + req.alias = 'amplitude' + req.reply( + JSON.stringify({ + code: 200, + server_upload_time: Date.now(), + payload_size_bytes: byteSize, + events_ingested: req.body.events.length, + }), + { + 'origin-country': 'GB', + } + ) + }) + cy.visit('/swap') + cy.contains('UK disclaimer') + }) + + it('shows a nav button to download the app when feature is enabled', () => { + cy.visit('/?intro=true') + cy.get('nav').within(() => { + cy.contains('Get the app').should('be.visible') + }) + cy.visit('/swap') + cy.get('nav').within(() => { + cy.contains('Get the app').should('not.exist') + }) + }) + + it('hides call to action text on small screen sizes', () => { + cy.viewport('iphone-8') + cy.visit('/?intro=true') + cy.get(getTestSelector('get-the-app-cta')).should('not.be.visible') + }) + + it('opens modal when Get-the-App button is selected', () => { + cy.visit('/?intro=true') + cy.get('nav').within(() => { + cy.contains('Get the app').should('exist').click() + }) + cy.contains('Download the Uniswap app').should('exist') + }) + + it('closes modal when close button is selected', () => { + cy.visit('/?intro=true') + cy.get('nav').within(() => { + cy.contains('Get the app').should('exist').click() + }) + cy.contains('Download the Uniswap app').should('exist') + cy.get(getTestSelector('get-the-app-close-button')).click() + cy.contains('Download the Uniswap app').should('not.exist') + }) + + it('closes modal when user selects area outside of modal', () => { + cy.visit('/?intro=true') + cy.get('nav').within(() => { + cy.contains('Get the app').should('exist').click() + }) + cy.contains('Download the Uniswap app').should('exist') + cy.get('nav').click({ force: true }) + cy.contains('Download the Uniswap app').should('not.exist') + }) +}) diff --git a/apps/web/cypress/e2e/link.test.ts b/apps/web/cypress/e2e/link.test.ts new file mode 100644 index 0000000..0d25610 --- /dev/null +++ b/apps/web/cypress/e2e/link.test.ts @@ -0,0 +1,9 @@ +// see https://github.com/Uniswap/interface/pull/4115 +describe('Link', () => { + it('should update route', () => { + cy.viewport(2000, 1600) + cy.visit('/swap') + cy.contains('Pool').click() + cy.get('[data-cy="join-pool-button"]').should('exist') + }) +}) diff --git a/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts b/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts new file mode 100644 index 0000000..34fcfaa --- /dev/null +++ b/apps/web/cypress/e2e/mini-portfolio/accounts.test.ts @@ -0,0 +1,131 @@ +import { getTestSelector } from '../../utils' + +describe('Mini Portfolio account drawer', () => { + beforeEach(() => { + const portfolioSpy = cy.spy().as('portfolioSpy') + cy.intercept(/interface.gateway.uniswap.org\/v1\/graphql/, (req) => { + if (req.body.operationName === 'PortfolioBalances') { + portfolioSpy(req) + } + }) + cy.visit('/swap') + }) + + it('fetches balances when account button is first hovered', () => { + // The balances should not be fetched before the account button is hovered + cy.get('@portfolioSpy').should('not.have.been.called') + + // Balances should have been fetched once after hover + cy.get(getTestSelector('web3-status-connected')).trigger('mouseover') + cy.get('@portfolioSpy').should('have.been.calledOnce') + }) + + it('should not re-fetch balances on second hover', () => { + // The balances should not be fetched before the account button is hovered + cy.get('@portfolioSpy').should('not.have.been.called') + + // Balances should have been fetched once after hover + cy.get(getTestSelector('web3-status-connected')).trigger('mouseover') + cy.get('@portfolioSpy').should('have.been.calledOnce') + + // Balances should not be refetched upon second hover + cy.get(getTestSelector('web3-status-connected')).trigger('mouseover') + cy.get('@portfolioSpy').should('have.been.calledOnce') + }) + + it('should not re-fetch balances when the account drawer is opened', () => { + // The balances should not be fetched before the account button is hovered + cy.get('@portfolioSpy').should('not.have.been.called') + + // Balances should have been fetched once after hover + cy.get(getTestSelector('web3-status-connected')).trigger('mouseover') + cy.get('@portfolioSpy').should('have.been.calledOnce') + + // Balances should not be refetched upon opening drawer + cy.get(getTestSelector('web3-status-connected')).click() + cy.get('@portfolioSpy').should('have.been.calledOnce') + }) + + it('fetches account information', () => { + // Open the mini portfolio + cy.intercept(/graphql/, { fixture: 'mini-portfolio/tokens.json' }) + cy.get(getTestSelector('web3-status-connected')).click() + + // Verify that wallet state loads correctly + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Tokens') + cy.get(getTestSelector('mini-portfolio-page')).contains('Hidden (197)') + + cy.intercept(/graphql/, { fixture: 'mini-portfolio/nfts.json' }) + cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click() + cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty') + + // Skip this for now, someone sent test account an NFT on block 17445713 that causes this test to fail + // cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' }) + // cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click() + // cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet') + + cy.intercept(/graphql/, { fixture: 'mini-portfolio/full_activity.json' }) + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + cy.get(getTestSelector('mini-portfolio-page')).contains('Contract Interaction') + }) + + it('refetches balances when account changes', () => { + cy.hardhat().then((hardhat) => { + const accountA = hardhat.wallets[0].address + const accountB = hardhat.wallets[1].address + + // Opens the account drawer + cy.get(getTestSelector('web3-status-connected')).click() + + // A shortened version of the first account's address should be shown + cy.contains(accountA.slice(0, 6)).should('exist') + + // Stores the current portfolio balance to later compare to next account's balance + cy.get(getTestSelector('portfolio-total-balance')) + .invoke('text') + .then((originalBalance) => { + // TODO(INFRA-3) Replace window.ethereum access below with cypress-hardhat utility + // Simulates the wallet changing accounts via eip-1193 event + cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountB])) + + // The second account's address should now be shown + cy.contains(accountB.slice(0, 6)).should('exist') + + // The second account's portfolio balance should differ from the original balance + cy.get(getTestSelector('portfolio-total-balance')).should('not.have.text', originalBalance) + }) + }) + }) + + it('fetches ENS name', () => { + cy.hardhat().then(() => { + const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' + const haydenENS = 'hayden.eth' + + // Opens the account drawer + cy.get(getTestSelector('web3-status-connected')).click() + + // Simulate wallet changing to Hayden's account + cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + + // Hayden's ENS name should be shown + cy.contains(haydenENS).should('exist') + + // Close account drawer + cy.get(getTestSelector('close-account-drawer')).click() + + // Switch chain to Polygon + cy.get(getTestSelector('chain-selector')).eq(1).click() + cy.contains('Polygon').click() + + //Reopen account drawer + cy.get(getTestSelector('web3-status-connected')).click() + + // Simulate wallet changing to Hayden's account + cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + + // Hayden's ENS name should be shown + cy.contains(haydenENS).should('exist') + }) + }) +}) diff --git a/apps/web/cypress/e2e/mini-portfolio/activity-history.test.ts b/apps/web/cypress/e2e/mini-portfolio/activity-history.test.ts new file mode 100644 index 0000000..d298ba2 --- /dev/null +++ b/apps/web/cypress/e2e/mini-portfolio/activity-history.test.ts @@ -0,0 +1,114 @@ +import { USDC_MAINNET } from '../../../src/constants/tokens' +import { getTestSelector } from '../../utils' + +describe('mini-portfolio activity history', () => { + beforeEach(() => { + cy.hardhat() + .then((hardhat) => hardhat.wallet.getTransactionCount()) + .then((nonce) => { + // Mock graphql response to include specific nonces. + cy.intercept( + { + method: 'POST', + url: 'https://beta.gateway.uniswap.org/v1/graphql', + }, + { + body: { + data: { + portfolios: [ + { + id: 'UG9ydGZvbGlvOjB4NUNlYUI3NGU0NDZkQmQzYkY2OUUyNzcyMDBGMTI5ZDJiQzdBMzdhMQ==', + assetActivities: [ + { + id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnME5tUm1PVGs0T0RrNVl6UmtNR1kzWTJNNE9HRTVNVFEzTURBME9EWmtOVGhrTURnNFpqbG1NelkxTnpRM1l6WXdZek15WVRFNE4yWXlaRFEwWVdVNFh6QjRZV1EyWXpCa05XTmlOVEZsWWpjMU5qUTFaRGszT1RneE4yRTJZVEkxTmpreU1UbG1ZbVE1Wmw4d2VEQXpOR0UwTURjMk5EUTROV1kzWlRBNFkyRXhOak0yTm1VMU1ETTBPVEZoTm1GbU56ZzFNR1E9', + timestamp: 1681150079, + type: 'UNKNOWN', + chain: 'ETHEREUM', + transaction: { + id: 'VHJhbnNhY3Rpb246MHg0NmRmOTk4ODk5YzRkMGY3Y2M4OGE5MTQ3MDA0ODZkNThkMDg4ZjlmMzY1NzQ3YzYwYzMyYTE4N2YyZDQ0YWU4XzB4YWQ2YzBkNWNiNTFlYjc1NjQ1ZDk3OTgxN2E2YTI1NjkyMTlmYmQ5Zl8weDAzNGE0MDc2NDQ4NWY3ZTA4Y2ExNjM2NmU1MDM0OTFhNmFmNzg1MGQ=', + blockNumber: 17019453, + hash: '0x46df998899c4d0f7cc88a914700486d58d088f9f365747c60c32a187f2d44ae8', + status: 'CONFIRMED', + to: '0x034a40764485f7e08ca16366e503491a6af7850d', + from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + nonce, + __typename: 'Transaction', + }, + assetChanges: [], + __typename: 'AssetActivity', + }, + { + id: 'QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE16UXpaR1ppTlROaE9XRmpNR00yWW1aaVpqUTNNRFEyWWpObFkyRXhORGN3TUdZd00yWXhOMkV3WWpnM1pqWXpPRFpsWVRnNU16QTRNVFZtWmpoaFh6QjRZMkUzTXpOalkySm1OelZoTXpnME1ERXhPR1ZpT1RjNU9EVTJOemRpTkdRMk56TTBZemMwWmw4d2VERmlOVEUxTkdGaE5HSTRaakF5TjJJNVptUXhPVE0wTVRFek1tWmpPV1JoWlRFd1pqY3pOVGs9', + timestamp: 1681149995, + type: 'SEND', + chain: 'ETHEREUM', + transaction: { + id: 'VHJhbnNhY3Rpb246MHgxMzQzZGZiNTNhOWFjMGM2YmZiZjQ3MDQ2YjNlY2ExNDcwMGYwM2YxN2EwYjg3ZjYzODZlYTg5MzA4MTVmZjhhXzB4Y2E3MzNjY2JmNzVhMzg0MDExOGViOTc5ODU2NzdiNGQ2NzM0Yzc0Zl8weDFiNTE1NGFhNGI4ZjAyN2I5ZmQxOTM0MTEzMmZjOWRhZTEwZjczNTk=', + blockNumber: 17019446, + hash: '0x1343dfb53a9ac0c6bfbf47046b3eca14700f03f17a0b87f6386ea8930815ff8a', + status: 'CONFIRMED', + to: '0x1b5154aa4b8f027b9fd19341132fc9dae10f7359', + from: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + nonce: nonce + 1, + __typename: 'Transaction', + }, + assetChanges: [ + { + __typename: 'TokenTransfer', + id: 'VG9rZW5UcmFuc2ZlcjoweDVjZWFiNzRlNDQ2ZGJkM2JmNjllMjc3MjAwZjEyOWQyYmM3YTM3YTFfMHhiMWRjNDlmMDY1N2FkNTA1YjUzNzUyN2RkOWE1MDk0YTM2NTkzMWMxXzB4MTM0M2RmYjUzYTlhYzBjNmJmYmY0NzA0NmIzZWNhMTQ3MDBmMDNmMTdhMGI4N2Y2Mzg2ZWE4OTMwODE1ZmY4YQ==', + asset: { + id: 'VG9rZW46RVRIRVJFVU1fMHgxY2MyYjA3MGNhZjAxNmE3ZGRjMzA0N2Y5MzI3MmU4Yzc3YzlkZGU5', + name: 'USD Coin (USDC)', + symbol: 'USDC', + address: '0x1cc2b070caf016a7ddc3047f93272e8c77c9dde9', + decimals: 6, + chain: 'ETHEREUM', + standard: null, + project: { + id: 'VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MWNjMmIwNzBjYWYwMTZhN2RkYzMwNDdmOTMyNzJlOGM3N2M5ZGRlOQ==', + isSpam: true, + logo: null, + __typename: 'TokenProject', + }, + __typename: 'Token', + }, + tokenStandard: 'ERC20', + quantity: '18011.212084', + sender: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + recipient: '0xb1dc49f0657ad505b537527dd9a5094a365931c1', + direction: 'OUT', + transactedValue: null, + }, + ], + __typename: 'AssetActivity', + }, + ], + __typename: 'Portfolio', + }, + ], + }, + }, + } + ).as('graphql') + }) + }) + + it('should deduplicate activity history by nonce', () => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false }) + + // Input swap info. + cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1') + cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '') + + cy.get('#swap-button').click() + cy.get('#confirm-swap-or-send').click() + cy.get(getTestSelector('confirmation-close-icon')).click() + + // Check activity history tab. + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + + // Assert that the local pending transaction is replaced by a remote transaction with the same nonce. + cy.contains('Swapping').should('not.exist') + }) +}) diff --git a/apps/web/cypress/e2e/mini-portfolio/uniTags.test.ts b/apps/web/cypress/e2e/mini-portfolio/uniTags.test.ts new file mode 100644 index 0000000..341dcef --- /dev/null +++ b/apps/web/cypress/e2e/mini-portfolio/uniTags.test.ts @@ -0,0 +1,70 @@ +import { getTestSelector } from '../../utils' + +describe('Uni tags support', () => { + beforeEach(() => { + const unitagSpy = cy.spy().as('unitagSpy') + cy.intercept(/gateway.uniswap.org\/v2\/address/, (req) => { + unitagSpy(req) + }) + cy.visit('/swap') + }) + + it('shows address if no Unitag or ENS exists', () => { + cy.hardhat().then(() => { + const unusedAccount = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb' + cy.get(getTestSelector('web3-status-connected')).click() + cy.window().then((win) => win.ethereum.emit('accountsChanged', [unusedAccount])) + cy.get(getTestSelector('account-drawer-status')).within(() => { + cy.contains('0xF030...69Fb').should('be.visible') + }) + }) + }) + + it('shows Unitag, followed by address, if Unitag exists but not ENS', () => { + cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' }) + cy.hardhat().then(() => { + const accountWithUnitag = '0xF030EaA01aFf57A23483dC8A1c3550d153be69Fb' + cy.get(getTestSelector('web3-status-connected')).click() + cy.window().then((win) => win.ethereum.emit('accountsChanged', [accountWithUnitag])) + cy.get(getTestSelector('account-drawer-status')).within(() => { + cy.contains('hayden').should('be.visible') + cy.contains('0xF030...69Fb').should('be.visible') + }) + }) + }) + + it('shows ENS, followed by address, if ENS exists but not Unitag', () => { + cy.hardhat().then(() => { + const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' + const haydenENS = 'hayden.eth' + cy.get(getTestSelector('web3-status-connected')).click() + cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + cy.get(getTestSelector('account-drawer-status')).within(() => { + cy.contains(haydenENS).should('be.visible') + cy.contains('0x50EC...79C3').should('be.visible') + }) + }) + }) + + it('shows Unitag and more option if user has both Unitag and ENS', () => { + cy.intercept(/address/, { fixture: 'mini-portfolio/unitag.json' }) + cy.hardhat().then(() => { + const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' + const haydenUnitag = 'hayden' + const haydenENS = 'hayden.eth' + cy.get(getTestSelector('web3-status-connected')).click() + cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount])) + cy.get(getTestSelector('account-drawer-status')).within(() => { + cy.contains(haydenUnitag).should('be.visible') + cy.contains('0x50EC...79C3').should('be.visible') + }) + cy.get(getTestSelector('secondary-identifiers')) + .trigger('mouseover') + .click() + .within(() => { + cy.contains(haydenENS).should('be.visible') + cy.contains('0x50EC...79C3').should('be.visible') + }) + }) + }) +}) diff --git a/apps/web/cypress/e2e/navigation.test.ts b/apps/web/cypress/e2e/navigation.test.ts new file mode 100644 index 0000000..4344219 --- /dev/null +++ b/apps/web/cypress/e2e/navigation.test.ts @@ -0,0 +1,69 @@ +import { getTestSelector } from '../utils' + +describe('Navigation', () => { + beforeEach(() => { + cy.viewport(1400, 900) + cy.visit('/?intro=true') + }) + it('displays Swap tab', () => { + cy.get('nav').within(() => { + cy.contains('Swap').should('be.visible').click() + }) + cy.url().should('include', '/swap') + }) + + it('displays Explore tab', () => { + cy.get('nav').within(() => { + cy.contains('Explore').should('be.visible').click() + }) + cy.url().should('include', '/explore') + }) + + it('displays NFTs tab', () => { + cy.get('nav').within(() => { + cy.contains('NFTs').should('be.visible').click() + }) + cy.url().should('include', '/nfts') + }) + + it('displays Pool tab', () => { + cy.get('nav').within(() => { + cy.contains('Pool').should('be.visible').click() + }) + cy.url().should('include', '/pool') + }) + + describe('More Menu', () => { + it('displays more menu for additional pages and resources', () => { + cy.get('nav').within(() => { + cy.get(getTestSelector('nav-more-button')).should('be.visible').click() + }) + }) + + it('moves pools tab to more menu on smaller screen sizes', () => { + cy.viewport(1200, 900) + cy.visit('/?intro=true') + cy.get('nav').within(() => { + cy.contains('Pool').should('not.be.visible') + cy.get(getTestSelector('nav-more-button')).should('be.visible').click() + cy.get(getTestSelector('nav-more-menu')).within(() => { + cy.contains('Pool').should('be.visible').click() + cy.url().should('include', '/pool') + }) + }) + }) + + it('lets user open app download modal', () => { + cy.get('nav') + .within(() => { + cy.get(getTestSelector('nav-more-button')).should('be.visible').click() + cy.get(getTestSelector('nav-more-menu')).within(() => { + cy.contains('Download Uniswap').should('be.visible').click() + }) + }) + .then(() => { + cy.contains('Download the Uniswap app').should('be.visible') + }) + }) + }) +}) diff --git a/apps/web/cypress/e2e/nfts.test.ts b/apps/web/cypress/e2e/nfts.test.ts new file mode 100644 index 0000000..cd882b5 --- /dev/null +++ b/apps/web/cypress/e2e/nfts.test.ts @@ -0,0 +1,58 @@ +import { getTestSelector } from '../utils' + +const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8' + +describe('Testing nfts', () => { + it('should load nft leaderboard', () => { + cy.visit('/') + cy.get(getTestSelector('nft-nav')).first().click() + cy.get(getTestSelector('nft-nav')).first().should('exist') + cy.get(getTestSelector('nft-nav')).first().click() + cy.get(getTestSelector('nft-trending-collection')).its('length').should('be.gte', 25) + }) + + it('should load pudgy penguin collection page', () => { + cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`) + cy.get(getTestSelector('nft-collection-asset')).should('exist') + cy.get(getTestSelector('nft-collection-filter-buy-now')).should('not.exist') + cy.get(getTestSelector('nft-filter')).first().click() + cy.get(getTestSelector('nft-collection-filter-buy-now')).should('exist') + }) + + it('should be able to navigate to activity', () => { + cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`) + cy.get(getTestSelector('nft-activity')).first().click() + cy.get(getTestSelector('nft-activity-row')).should('exist') + }) + + it('should go to the details page', () => { + cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`) + cy.get(getTestSelector('nft-filter')).first().click() + cy.get(getTestSelector('nft-collection-filter-buy-now')).click() + cy.get(getTestSelector('nft-collection-asset')).first().click() + cy.get(getTestSelector('nft-details-traits')).should('exist') + cy.get(getTestSelector('nft-details-activity')).should('exist') + cy.get(getTestSelector('nft-details-description')).should('exist') + cy.get(getTestSelector('nft-details-asset-details')).should('exist') + }) + + it('should toggle buy now on details page', () => { + cy.visit(`/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`) + cy.get(getTestSelector('nft-filter')).first().click() + cy.get(getTestSelector('nft-collection-filter-buy-now')).click() + cy.get(getTestSelector('nft-collection-asset')).first().click() + cy.get(getTestSelector('nft-details-description-text')).should('exist') + cy.get(getTestSelector('nft-details-description')).click() + cy.get(getTestSelector('nft-details-description-text')).should('not.exist') + cy.get(getTestSelector('nft-details-toggle-bag')).eq(1).click() + cy.get(getTestSelector('nft-bag')).should('exist') + }) + + it('should navigate to and from the owned nfts page', () => { + cy.visit('/') + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click() + cy.get(getTestSelector('mini-portfolio-nft')).first().click() + cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible') + }) +}) diff --git a/apps/web/cypress/e2e/permit2.test.ts b/apps/web/cypress/e2e/permit2.test.ts new file mode 100644 index 0000000..3ffea2e --- /dev/null +++ b/apps/web/cypress/e2e/permit2.test.ts @@ -0,0 +1,270 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { MaxUint160, MaxUint256 } from '@uniswap/permit2-sdk' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' + +import { DAI, USDC_MAINNET, USDT } from '../../src/constants/tokens' + +/** Initiates a swap. */ +function initiateSwap(swapButtonText?: string) { + // The swap button is re-rendered once enabled, so we must wait until the original button is not disabled to re-select the appropriate button. + cy.get('#swap-button').should('not.be.disabled') + // Completes the swap. + cy.get('#swap-button').click() + cy.contains(swapButtonText ?? 'Confirm swap').click() +} + +describe('Permit2', () => { + function setupInputs(inputToken: Token, outputToken: Token) { + // Sets up a swap between inputToken and outputToken. + cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`) + cy.get('#swap-currency-input .token-amount-input').type('0.01') + } + + /** Asserts permit2 has a max approval for spend of the input token on-chain. */ + function expectTokenAllowanceForPermit2ToBeMax(inputToken: Token) { + // check token approval + cy.hardhat() + .then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken })) + .then((allowance) => { + Cypress.log({ name: `Token allowance: ${allowance.toString()}` }) + cy.wrap(allowance).should('deep.equal', MaxUint256) + }) + } + + /** Asserts the universal router has a max permit2 approval for spend of the input token on-chain. */ + function expectPermit2AllowanceForUniversalRouterToBeMax(inputToken: Token) { + cy.hardhat() + .then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken })) + .then((allowance) => { + Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` }) + cy.wrap(allowance.amount).should('deep.equal', MaxUint160) + // Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds. + const THIRTY_DAYS_SECONDS = 2_592_000 + const expected = Math.floor(Date.now() / 1000 + THIRTY_DAYS_SECONDS) + cy.wrap(allowance.expiration).should('be.closeTo', expected, 40) + }) + } + + beforeEach(() => + cy.hardhat().then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(DAI, 1e18)) + await hardhat.mine() + }) + ) + + describe('approval process (with intermediate screens)', () => { + // Turn off automine so that intermediate screens are available to assert on. + beforeEach(() => cy.hardhat({ automine: false })) + + it('swaps after completing full permit2 approval process', () => { + setupInputs(DAI, USDC_MAINNET) + initiateSwap('Approve and swap') + + // verify that the modal retains its state when the window loses focus + cy.window().trigger('blur') + + // Verify token approval + cy.contains('Approval pending...') + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + expectTokenAllowanceForPermit2ToBeMax(DAI) + + // Verify permit2 approval + cy.wait('@eth_signTypedData_v4') + cy.wait('@eth_sendRawTransaction') + cy.contains('Swap pending...') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.contains('Swap success!') + expectPermit2AllowanceForUniversalRouterToBeMax(DAI) + }) + + it('swaps with existing permit approval and missing token approval', () => { + setupInputs(DAI, USDC_MAINNET) + cy.hardhat().then(async (hardhat) => { + await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: DAI }) + await hardhat.mine() + }) + initiateSwap('Approve and swap') + + // Verify token approval + cy.contains('Approval pending...') + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + expectTokenAllowanceForPermit2ToBeMax(DAI) + + // Verify transaction + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.contains('Swap success!') + }) + + /** + * On mainnet, you have to revoke USDT approval before increasing it. + * From the token contract: + * To change the approve amount you first have to reduce the addresses` + * allowance to zero by calling `approve(_spender, 0)` if it is not + * already 0 to mitigate the race condition described here: + * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + */ + it('swaps USDT with existing permit, and existing but insufficient token approval', () => { + cy.hardhat().then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6)) + await hardhat.mine() + await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6) + await hardhat.mine() + await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT }) + await hardhat.mine() + }) + setupInputs(USDT, USDC_MAINNET) + cy.get('#swap-currency-input .token-amount-input').clear().type('2') + initiateSwap('Approve and swap') + + // Verify allowance revocation + cy.contains('Resetting USDT limit...') + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.hardhat() + .then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: USDT })) + .should('deep.equal', BigNumber.from(0)) + + // Verify token approval + cy.contains('Approval pending...') + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + expectTokenAllowanceForPermit2ToBeMax(USDT) + + // Verify transaction + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.contains('Swap success!') + }) + + it('swaps USDT with existing permit, and existing and sufficient token approval', () => { + cy.hardhat().then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDT, 2e6)) + await hardhat.mine() + await hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDT }, 1e6) + await hardhat.mine() + await hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDT }) + await hardhat.mine() + }) + setupInputs(USDT, USDC_MAINNET) + cy.get('#swap-currency-input .token-amount-input').clear().type('1') + initiateSwap() + + // Verify transaction + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.contains('Swap success!') + }) + }) + + it('swaps when user has already approved token and permit2', () => { + cy.hardhat().then(({ approval, wallet }) => + Promise.all([ + approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }), + approval.setPermit2Allowance({ owner: wallet, token: DAI }), + ]) + ) + setupInputs(DAI, USDC_MAINNET) + initiateSwap() + + // Verify transaction + cy.contains('Swap success!') + }) + + it('swaps after handling user rejection of both approval and signature', () => { + setupInputs(DAI, USDC_MAINNET) + const USER_REJECTION = { code: 4001 } + cy.hardhat().then((hardhat) => { + // Reject token approval + const tokenApprovalStub = cy.stub(hardhat.wallet, 'sendTransaction').log(true) + tokenApprovalStub.rejects(USER_REJECTION) // rejects token approval + initiateSwap('Approve and swap') + + // Verify token approval rejection + cy.wrap(tokenApprovalStub).should('be.calledOnce') + cy.contains('Approve and swap') + + // Allow token approval + cy.then(() => tokenApprovalStub.restore()) + + // Reject permit2 approval + const permitApprovalStub = cy.stub(hardhat.provider, 'send').log(false) + permitApprovalStub.withArgs('eth_signTypedData_v4').rejects(USER_REJECTION) // rejects permit approval + permitApprovalStub.callThrough() // allows non-eth_signTypedData_v4 send calls to return non-stubbed values + cy.contains('Approve and swap').click() + + // Verify token approval + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + expectTokenAllowanceForPermit2ToBeMax(DAI) + + // Verify permit2 approval rejection + cy.wrap(permitApprovalStub).should('be.calledWith', 'eth_signTypedData_v4') + cy.contains('Sign and swap') + + // Allow permit2 approval + cy.then(() => permitApprovalStub.restore()) + cy.contains('Sign and swap').click() + + // Verify permit2 approval + cy.contains('Swap success!') + expectPermit2AllowanceForUniversalRouterToBeMax(DAI) + }) + }) + + it('prompts token approval when existing approval amount is too low', () => { + setupInputs(DAI, USDC_MAINNET) + cy.hardhat().then(({ approval, wallet }) => + Promise.all([ + approval.setPermit2Allowance({ owner: wallet, token: DAI }), + approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }, 1), + ]) + ) + initiateSwap('Approve and swap') + + // Verify token approval + expectPermit2AllowanceForUniversalRouterToBeMax(DAI) + }) + + it('prompts signature when existing permit approval is expired', () => { + const expiredAllowance = { expiration: Math.floor((Date.now() - 1) / 1000) } + cy.hardhat() + .then(({ approval, wallet }) => + Promise.all([ + approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }), + approval.setPermit2Allowance({ owner: wallet, token: DAI }, expiredAllowance), + ]) + ) + .then(() => { + setupInputs(DAI, USDC_MAINNET) + initiateSwap('Sign and swap') + }) + + // Verify permit2 approval + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap success!') + expectPermit2AllowanceForUniversalRouterToBeMax(DAI) + }) + + it('prompts signature when existing permit approval amount is too low', () => { + const smallAllowance = { amount: 1 } + cy.hardhat() + .then(({ approval, wallet }) => + Promise.all([ + approval.setTokenAllowanceForPermit2({ owner: wallet, token: DAI }), + approval.setPermit2Allowance({ owner: wallet, token: DAI }, smallAllowance), + ]) + ) + .then(() => { + setupInputs(DAI, USDC_MAINNET) + initiateSwap('Sign and swap') + }) + + // Verify permit2 approval + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap success!') + expectPermit2AllowanceForUniversalRouterToBeMax(DAI) + }) +}) diff --git a/apps/web/cypress/e2e/pool.test.ts b/apps/web/cypress/e2e/pool.test.ts new file mode 100644 index 0000000..f2d1f35 --- /dev/null +++ b/apps/web/cypress/e2e/pool.test.ts @@ -0,0 +1,17 @@ +describe('Pool', () => { + beforeEach(() => { + cy.visit('/pool').then(() => { + cy.wait('@eth_blockNumber') + }) + }) + + it('add liquidity links to /add/ETH', () => { + cy.get('body').then(() => { + cy.get('#join-pool-button') + .click() + .then(() => { + cy.url().should('contain', '/add/ETH') + }) + }) + }) +}) diff --git a/apps/web/cypress/e2e/position.test.ts b/apps/web/cypress/e2e/position.test.ts new file mode 100644 index 0000000..d0af324 --- /dev/null +++ b/apps/web/cypress/e2e/position.test.ts @@ -0,0 +1,11 @@ +describe('Position', () => { + it('shows an valid state on a supported network', () => { + cy.visit('/pools/1') + cy.contains('UNI / ETH') + }) + + it('shows an invalid state on a supported network', () => { + cy.visit('/pools/788893') + cy.contains('To view a position, you must be connected to the network it belongs to.') + }) +}) diff --git a/apps/web/cypress/e2e/redirects.test.ts b/apps/web/cypress/e2e/redirects.test.ts new file mode 100644 index 0000000..534c863 --- /dev/null +++ b/apps/web/cypress/e2e/redirects.test.ts @@ -0,0 +1,23 @@ +describe('Redirect', () => { + it('should redirect to /vote/create-proposal when visiting /create-proposal', () => { + cy.visit('/create-proposal') + cy.url().should('match', /\/vote\/create-proposal/) + }) + it('should redirect to /not-found when visiting nonexist url', () => { + cy.visit('/none-exist-url') + cy.url().should('match', /\/not-found/) + }) +}) + +describe('RedirectExplore', () => { + it('should redirect from /tokens/ to /explore', () => { + cy.visit('/tokens') + cy.url().should('match', /\/explore/) + + cy.visit('/tokens/ethereum') + cy.url().should('match', /\/explore\/tokens\/ethereum/) + + cy.visit('/tokens/optimism/NATIVE') + cy.url().should('match', /\/explore\/tokens\/optimism\/NATIVE/) + }) +}) diff --git a/apps/web/cypress/e2e/remove-liquidity.test.ts b/apps/web/cypress/e2e/remove-liquidity.test.ts new file mode 100644 index 0000000..b09f021 --- /dev/null +++ b/apps/web/cypress/e2e/remove-liquidity.test.ts @@ -0,0 +1,33 @@ +import { ChainId, MaxUint256, UNI_ADDRESSES } from '@uniswap/sdk-core' + +const UNI_MAINNET = UNI_ADDRESSES[ChainId.MAINNET] + +describe('Remove Liquidity', () => { + it('loads the token pair in v2', () => { + cy.visit(`/remove/v2/ETH/${UNI_MAINNET}`) + cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH') + cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI') + }) + + it('loads the token pair in v3', () => { + cy.visit(`/remove/1`) + cy.get('#remove-liquidity-tokens').should('contain.text', 'UNI/ETH') + + cy.get('#remove-pooled-tokena-symbol').should('contain.text', 'Pooled UNI') + cy.get('#remove-pooled-tokenb-symbol').should('contain.text', 'Pooled ETH') + }) + + it('should redirect to error pages if pool does not exist', () => { + // Duplicate-token v2 pools redirect to position unavailable + cy.visit(`/remove/v2/ETH/ETH`) + cy.contains('Position unavailable') + + // Single-token pools don't exist + cy.visit('/remove/v2/ETH') + cy.url().should('match', /\/not-found/) + + // Nonexistent v3 pool + cy.visit(`/remove/${MaxUint256}`) + cy.contains('Position unavailable') + }) +}) diff --git a/apps/web/cypress/e2e/service-worker.test.ts b/apps/web/cypress/e2e/service-worker.test.ts new file mode 100644 index 0000000..3b4ef3d --- /dev/null +++ b/apps/web/cypress/e2e/service-worker.test.ts @@ -0,0 +1,100 @@ +import assert from 'assert' + +describe('Service Worker', () => { + before(() => { + // Fail fast if there is no Service Worker on this build. + cy.request({ url: '/service-worker.js', headers: { 'Service-Worker': 'script' } }).then((response) => { + const isValid = isValidServiceWorker(response) + if (!isValid) { + throw new Error( + '\n' + + 'Service Worker tests must be run on a production-like build\n' + + 'To test, build with `yarn build` and serve with `yarn serve`' + ) + } + }) + + function isValidServiceWorker(response: Cypress.Response) { + const contentType = response.headers['content-type'] + return !(response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) + } + }) + + function unregisterServiceWorker() { + return cy.log('unregisters service worker').then(async () => { + const sw = await window.navigator.serviceWorker.getRegistration(Cypress.config().baseUrl ?? undefined) + await sw?.unregister() + }) + } + before(unregisterServiceWorker) + after(unregisterServiceWorker) + + beforeEach(() => { + cy.intercept('https://interface.gateway.uniswap.org/v1/amplitude-proxy', (req) => { + const body = JSON.stringify(req.body) + const serviceWorkerStatus = body.match(/"service_worker":"(\w+)"/)?.[1] + if (serviceWorkerStatus) { + req.alias = `ServiceWorker:${serviceWorkerStatus}` + } + }) + }) + + it('installs a ServiceWorker and reports the uninstalled status to analytics', () => { + cy.visit('/', { serviceWorker: true }) + cy.wait('@ServiceWorker:uninstalled') + cy.window().should( + 'have.nested.property', + // The parent is checked instead of the AUT because it is on the same origin, + // and the AUT will not be considered "activated" until the parent is idle. + 'parent.navigator.serviceWorker.controller.state', + 'activated' + ) + }) + + describe('cache hit', () => { + // TODO re-enable web test + it.skip('reports the hit to analytics', () => { + cy.visit('/', { serviceWorker: true }) + cy.wait('@ServiceWorker:hit') + }) + }) + + describe('cache miss', () => { + let cache: Cache | undefined + let request: Request | undefined + let response: Response | undefined + before(() => { + // Mocks the index.html in the cache to force a cache miss. + cy.visit('/', { serviceWorker: true }).then(async () => { + const cacheKeys = await window.caches.keys() + const cacheKey = cacheKeys.find((key) => key.match(/precache/)) + assert(cacheKey) + + cache = await window.caches.open(cacheKey) + const keys = await cache.keys() + request = keys.find((key) => key.url.match(/index/)) + assert(request) + + response = await cache.match(request) + assert(response) + + await cache.put(request, new Response()) + }) + }) + after(() => { + // Restores the index.html in the cache so that re-runs behave as expected. + // This is necessary because the Service Worker will not re-populate the cache. + cy.then(async () => { + if (cache && request && response) { + await cache.put(request, response) + } + }) + }) + + // TODO re-enable web test + it.skip('reports the miss to analytics', () => { + cy.visit('/', { serviceWorker: true }) + cy.wait('@ServiceWorker:miss') + }) + }) +}) diff --git a/apps/web/cypress/e2e/swap/errors.test.ts b/apps/web/cypress/e2e/swap/errors.test.ts new file mode 100644 index 0000000..750a444 --- /dev/null +++ b/apps/web/cypress/e2e/swap/errors.test.ts @@ -0,0 +1,129 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { InterfaceSectionName } from '@uniswap/analytics-events' +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc' +import { DAI, USDC_MAINNET } from '../../../src/constants/tokens' +import { getBalance, getTestSelector } from '../../utils' + +describe('Swap errors', () => { + it('wallet rejection', () => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) + cy.hardhat().then((hardhat) => { + // Stub the wallet to reject any transaction. + cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled')) + + // Enter amount to swap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') + + // Submit transaction + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_estimateGas') + + // Verify rejection + cy.contains('Review swap') + cy.contains('Confirm swap') + }) + }) + + it('transaction past deadline', () => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) + cy.hardhat({ automine: false }) + getBalance(USDC_MAINNET).then((initialBalance) => { + // Enter amount to swap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') + + // Submit transaction + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') + cy.contains('Swap submitted') + cy.get(getTestSelector('confirmation-close-icon')).click() + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + + // Mine transaction + cy.hardhat().then(async (hardhat) => { + // Remove the transaction from the mempool, so that it doesn't fail but it is past the deadline. + // This should result in it being removed from pending transactions, without a failure notificiation. + const transactions = await hardhat.send('eth_pendingTransactions', []) + await hardhat.send('hardhat_dropTransaction', [transactions[0].hash]) + // Mine past the deadline + await hardhat.mine(1, DEFAULT_DEADLINE_FROM_NOW + 1) + }) + cy.wait('@eth_getTransactionReceipt') + + // Verify transaction did not occur + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).should('not.contain', 'Swap failed') + cy.get('#swap-currency-output').contains(`Balance: ${initialBalance}`) + getBalance(USDC_MAINNET).should('eq', initialBalance) + }) + }) + + it('slippage failure', () => { + cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) + cy.hardhat({ automine: false }).then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6)) + await hardhat.mine() + await Promise.all([ + hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }), + hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }), + ]) + await hardhat.mine() + }) + + getBalance(DAI).then((initialBalance) => { + // Gas estimation fails for this transaction (that would normally fail), so we stub it. + cy.hardhat().then((hardhat) => { + const send = cy.stub(hardhat.provider, 'send').log(false) + send.withArgs('eth_estimateGas').resolves(BigNumber.from(2_000_000)) + send.callThrough() + }) + // Set slippage to a very low value. + cy.get(getTestSelector('open-settings-dialog-button')).click() + cy.get(getTestSelector('max-slippage-settings')).click() + cy.get(getTestSelector('slippage-input')).clear().type('0.01') + cy.get(getTestSelector('toggle-uniswap-x-button')).click() // turn off uniswapx + cy.get('body').click('topRight') // close modal + cy.get(getTestSelector('slippage-input')).should('not.exist') + // Submit 2 transactions + for (let i = 0; i < 2; i++) { + cy.get('#swap-currency-input .token-amount-input').type('200').should('have.value', '200') + cy.get('#swap-currency-output .token-amount-input').should('not.have.value', '') + cy.get('#swap-button').click() + cy.contains(i === 0 ? 'Sign and swap' : 'Confirm swap').click() + cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') + cy.contains(i === 0 ? 'Swap pending...' : 'Swap submitted') + if (i === 0) { + cy.get(getTestSelector('confirmation-close-icon')).click() + } + } + cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending') + + // Mine transactions + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.wait('@eth_getTransactionReceipt') + + // Verify only 1 transaction occurred + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + getBalance(DAI).should('be.closeTo', initialBalance + 200, 1) + }) + }) + + it('insufficient liquidity', () => { + // The API response is too variable so stubbing a 404. + cy.intercept('POST', 'https://interface.gateway.uniswap.org/v2/quote', { + statusCode: 404, + fixture: 'insufficientLiquidity.json', + }) + + cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) + cy.get('#swap-currency-output .token-amount-input').type('100000000000000').should('have.value', '100000000000000') // 100 trillion + cy.contains('Insufficient liquidity for this trade.') + cy.get('#swap-button').should('not.exist') + cy.get(getTestSelector(`fiat-value-${InterfaceSectionName.CURRENCY_OUTPUT_PANEL}`)).contains('-') + }) +}) diff --git a/apps/web/cypress/e2e/swap/fees.test.ts b/apps/web/cypress/e2e/swap/fees.test.ts new file mode 100644 index 0000000..c8922de --- /dev/null +++ b/apps/web/cypress/e2e/swap/fees.test.ts @@ -0,0 +1,143 @@ +import { CurrencyAmount } from '@uniswap/sdk-core' + +import { USDC_MAINNET } from '../../../src/constants/tokens' +import { getBalance, getTestSelector } from '../../utils' + +describe('Swap with fees', () => { + describe('Classic swaps', () => { + beforeEach(() => { + cy.visit('/swap') + + // Store trade quote into alias + cy.intercept({ url: 'https://interface.gateway.uniswap.org/v2/quote' }, (req) => { + // Avoid tracking stablecoin pricing fetches + if (JSON.parse(req.body).intent !== 'pricing') req.alias = 'quoteFetch' + }) + }) + + it('displays $0 fee on swaps without fees', () => { + // Set up a stablecoin <> stablecoin swap (no fees) + cy.get('#swap-currency-input .open-currency-select-button').click() + cy.contains('DAI').click() + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.contains('USDC').click() + cy.get('#swap-currency-output .token-amount-input').type('1') + + // Verify 0 fee UI is displayed + cy.get(getTestSelector('swap-details-header-row')).click() + cy.contains('Fee') + cy.contains('$0') + }) + + it('swaps ETH for USDC exact-out with swap fee', () => { + cy.hardhat().then((hardhat) => { + getBalance(USDC_MAINNET).then((initialBalance) => { + // Set up swap + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.contains('USDC').click() + cy.get('#swap-currency-output .token-amount-input').type('1') + + cy.wait('@quoteFetch') + .its('response.body') + .then(({ quote: { portionBips, portionRecipient, portionAmount } }) => { + // Fees are generally expected to always be enabled for ETH -> USDC swaps + // If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars + if (portionRecipient) return + + cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => { + const feeCurrencyAmount = CurrencyAmount.fromRawAmount(USDC_MAINNET, portionAmount) + + // Initiate transaction + cy.get('#swap-button').click() + cy.contains('Review swap') + + // Verify fee percentage and amount is displayed + cy.contains(`Fee (${portionBips / 100}%)`) + + // Confirm transaction + cy.contains('Confirm swap').click() + + // Verify transaction + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).contains('Swapped') + + // Verify the post-fee output is the expected exact-out amount + const finalBalance = initialBalance + 1 + cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`) + getBalance(USDC_MAINNET).should('eq', finalBalance) + + // Verify fee recipient received fee + cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => { + const expectedFinalRecipientBalance = initialRecipientBalance.add(feeCurrencyAmount) + cy.then(() => finalRecipientBalance.equalTo(expectedFinalRecipientBalance)).should('be.true') + }) + }) + }) + }) + }) + }) + + it('swaps ETH for USDC exact-in with swap fee', () => { + cy.hardhat().then((hardhat) => { + // Set up swap + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.contains('USDC').click() + cy.get('#swap-currency-input .token-amount-input').type('.01') + + cy.wait('@quoteFetch') + .its('response.body') + .then(({ quote: { portionBips, portionRecipient } }) => { + // Fees are generally expected to always be enabled for ETH -> USDC swaps + // If the routing api does not include a fee, end the test early rather than manually update routes and hardcode fee vars + if (portionRecipient) return + + cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((initialRecipientBalance) => { + // Initiate transaction + cy.get('#swap-button').click() + cy.contains('Review swap') + + // Verify fee percentage and amount is displayed + cy.contains(`Fee (${portionBips / 100}%)`) + + // Confirm transaction + cy.contains('Confirm swap').click() + + // Verify transaction + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).contains('Swapped') + + // Verify fee recipient received fee + cy.then(() => hardhat.getBalance(portionRecipient, USDC_MAINNET)).then((finalRecipientBalance) => { + cy.then(() => finalRecipientBalance.greaterThan(initialRecipientBalance)).should('be.true') + }) + }) + }) + }) + }) + }) + + describe('UniswapX swaps', () => { + it('displays UniswapX fee in UI', () => { + cy.visit('/swap') + + // Intercept the trade quote + cy.intercept({ url: 'https://interface.gateway.uniswap.org/v2/quote' }, (req) => { + // Avoid intercepting stablecoin pricing fetches + if (JSON.parse(req.body).intent !== 'pricing') { + req.reply({ fixture: 'uniswapx/feeQuote.json' }) + } + }) + + // Setup swap + cy.get('#swap-currency-input .open-currency-select-button').click() + cy.contains('USDC').click() + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.contains('ETH').click() + cy.get('#swap-currency-input .token-amount-input').type('200') + + // Verify fee UI is displayed + cy.get(getTestSelector('swap-details-header-row')).click() + cy.contains('Fee (0.15%)') + }) + }) +}) diff --git a/apps/web/cypress/e2e/swap/logging.test.ts b/apps/web/cypress/e2e/swap/logging.test.ts new file mode 100644 index 0000000..be3e1bd --- /dev/null +++ b/apps/web/cypress/e2e/swap/logging.test.ts @@ -0,0 +1,74 @@ +import { SwapEventName } from '@uniswap/analytics-events' + +import { USDC_MAINNET } from '../../../src/constants/tokens' +import { getTestSelector } from '../../utils' + +describe('swap flow logging', () => { + it('completes two swaps and verifies the TTS logging for the first, plus all intermediate steps along the way', () => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`) + cy.hardhat() + + // First swap in the session: + // Enter amount to swap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') + + // Verify first swap action + cy.waitForAmplitudeEvent(SwapEventName.SWAP_FIRST_ACTION).then((event: any) => { + cy.wrap(event.event_properties).should('have.property', 'time_to_first_swap_action') + cy.wrap(event.event_properties.time_to_first_swap_action).should('be.a', 'number') + cy.wrap(event.event_properties.time_to_first_swap_action).should('be.gte', 0) + }) + + // Verify Swap Quote + cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH, ['time_to_first_quote_request']).then((event: any) => { + // Price quotes don't include these values, so we only verify the types if they exist + cy.wrap(event.event_properties.time_to_first_quote_request).should('be.a', 'number') + cy.wrap(event.event_properties.time_to_first_quote_request).should('be.gte', 0) + cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.a', 'number') + cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.gte', 0) + }) + + // Submit transaction + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.get(getTestSelector('confirmation-close-icon')).click() + + cy.get(getTestSelector('popups')).contains('Swapped') + + // Verify logging + cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => { + cy.wrap(event.event_properties).should('have.property', 'time_to_swap') + cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number') + cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0) + cy.wrap(event.event_properties).should('have.property', 'time_to_swap_since_first_input') + cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.a', 'number') + cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.gte', 0) + }) + + // Second swap in the session: + // Enter amount to swap (different from first trade, to trigger a new quote request) + cy.get('#swap-currency-output .token-amount-input').clear().type('10').should('have.value', '10') + cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') + + // Verify second Swap Quote + cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH).then((event: any) => { + // Price quotes don't include these values, so we only verify the types if they exist + if (event.event_properties.time_to_first_quote_request) { + cy.wrap(event.event_properties.time_to_first_quote_request).should('be.undefined') + cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.undefined') + } + }) + + // Submit transaction + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.get(getTestSelector('confirmation-close-icon')).click() + + cy.get(getTestSelector('popups')).contains('Swapped') + cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => { + cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap') + cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap_since_first_input') + }) + }) +}) diff --git a/apps/web/cypress/e2e/swap/settings.test.ts b/apps/web/cypress/e2e/swap/settings.test.ts new file mode 100644 index 0000000..0dbbe74 --- /dev/null +++ b/apps/web/cypress/e2e/swap/settings.test.ts @@ -0,0 +1,34 @@ +import { getTestSelector } from '../../utils' + +describe('Swap settings', () => { + it('Opens and closes the settings menu', () => { + cy.visit('/swap') + cy.contains('Settings').should('not.exist') + cy.get(getTestSelector('open-settings-dialog-button')).click() + cy.get(getTestSelector('mobile-settings-menu')).should('not.exist') + cy.contains('Max. slippage').should('exist') + cy.contains('Transaction deadline').should('exist') + cy.contains('UniswapX').should('exist') + cy.get(getTestSelector('open-settings-dialog-button')).click() + cy.contains('Settings').should('not.exist') + }) + + it('should open the mobile settings menu', () => { + // Set viewport to iPhone 6 + cy.viewport('iphone-6') + cy.visit('/swap') + + // Click the button to open the settings dialog + cy.get(getTestSelector('open-settings-dialog-button')).click({ waitForAnimations: true }) + + // Verify the mobile settings menu and its contents + cy.get(getTestSelector('mobile-settings-menu')) + .should('exist') + .within(() => { + cy.contains('Max. slippage').should('exist') + cy.contains('UniswapX').should('exist') + cy.contains('Transaction deadline').should('exist') + cy.get(getTestSelector('mobile-settings-close')).click() + }) + }) +}) diff --git a/apps/web/cypress/e2e/swap/swap.test.ts b/apps/web/cypress/e2e/swap/swap.test.ts new file mode 100644 index 0000000..51dbeb1 --- /dev/null +++ b/apps/web/cypress/e2e/swap/swap.test.ts @@ -0,0 +1,99 @@ +import { SwapEventName } from '@uniswap/analytics-events' +import { ChainId } from '@uniswap/sdk-core' + +import { UNI, USDC_MAINNET } from '../../../src/constants/tokens' +import { getBalance, getTestSelector } from '../../utils' + +const UNI_MAINNET = UNI[ChainId.MAINNET] + +describe('Swap', () => { + describe('Swap on main page', () => { + it('starts with ETH selected by default', () => { + cy.visit('/swap') + cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '') + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH') + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token') + }) + + it('should default inputs from URL params ', () => { + cy.visit(`/swap?inputCurrency=${UNI_MAINNET.address}`) + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'UNI') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token') + + cy.visit(`/swap?outputCurrency=${UNI_MAINNET.address}`) + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI') + + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`) + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'ETH') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI') + }) + + it('inputs reset when navigating between pages', () => { + cy.visit('/swap') + cy.get('#swap-currency-input .token-amount-input').should('have.value', '') + cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01') + cy.visit('/pool').visit('/swap') + cy.get('#swap-currency-input .token-amount-input').should('have.value', '') + }) + + it('resets the dependent input when the independent input is cleared', () => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`) + cy.get('#swap-currency-input .token-amount-input').should('have.value', '') + cy.get(`#swap-currency-output .token-amount-input`).should('have.value', '') + + cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01') + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value', '') + cy.get('#swap-currency-input .token-amount-input').clear() + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value') + + cy.window().trigger('blur') + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value') + }) + + it('swaps ETH for USDC', () => { + cy.interceptGraphqlOperation('Activity', 'mini-portfolio/empty_activity.json') + cy.visit('/swap') + cy.hardhat({ automine: false }) + getBalance(USDC_MAINNET).then((initialBalance) => { + // Select USDC + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address) + cy.get(getTestSelector('common-base-USDC')).click() + + // Enter amount to swap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1') + cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '') + + // Verify logging + cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => { + cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds') + cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number') + cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0) + }) + + // Submit transaction + cy.get('#swap-button').click() + cy.contains('Review swap') + cy.contains('Confirm swap').click() + cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') + cy.contains('Swap submitted') + cy.get(getTestSelector('confirmation-close-icon')).click() + cy.contains('Swap submitted').should('not.exist') + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + + // Mine transaction + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.wait('@eth_getTransactionReceipt') + + // Verify transaction + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).contains('Swapped') + const finalBalance = initialBalance + 1 + cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`) + getBalance(USDC_MAINNET).should('eq', finalBalance) + }) + }) + }) +}) diff --git a/apps/web/cypress/e2e/swap/uniswapx.test.ts b/apps/web/cypress/e2e/swap/uniswapx.test.ts new file mode 100644 index 0000000..baa9054 --- /dev/null +++ b/apps/web/cypress/e2e/swap/uniswapx.test.ts @@ -0,0 +1,412 @@ +import { ChainId, CurrencyAmount } from '@uniswap/sdk-core' +import { CyHttpMessages } from 'cypress/types/net-stubbing' + +import { FeatureFlag } from 'featureFlags' +import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens' +import { getTestSelector } from '../../utils' + +const QuoteWhereUniswapXIsBetter = 'uniswapx/quote1.json' +const QuoteWithEthInput = 'uniswapx/quote2.json' +const PricingQuoteUSDC = 'uniswapx/pricingQuoteUSDC.json' +const PricingQuoteDAI = 'uniswapx/pricingQuoteDAI.json' + +const QuoteEndpoint = 'https://interface.gateway.uniswap.org/v2/quote' +const OrderSubmissionEndpoint = 'https://interface.gateway.uniswap.org/v2/order' +const OrderStatusEndpoint = + 'https://interface.gateway.uniswap.org/v2/orders?swapper=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&orderHashes=0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19' + +/** + * Stubs quote to return a quote for non-price requests + */ +function stubNonPriceQuoteWith(fixture: string) { + cy.intercept(QuoteEndpoint, (req: CyHttpMessages.IncomingHttpRequest) => { + let body = req.body + if (typeof body === 'string') { + body = JSON.parse(body) + } + if (body.intent === 'pricing') { + const pricingFixture = body.tokenIn === USDC_MAINNET.address ? PricingQuoteUSDC : PricingQuoteDAI + req.reply({ fixture: pricingFixture }) + } else { + req.reply({ fixture }) + } + }).as('quote') +} + +/** Stubs the provider to return a tx receipt corresponding to the mock filled uniswapx order's txHash */ +function stubSwapTxReceipt() { + cy.hardhat().then((hardhat) => { + cy.fixture('uniswapx/fillTransactionReceipt.json').then((mockTxReceipt) => { + const getTransactionReceiptStub = cy.stub(hardhat.provider, 'getTransactionReceipt').log(false) + getTransactionReceiptStub.withArgs(mockTxReceipt.transactionHash).resolves(mockTxReceipt) + getTransactionReceiptStub.callThrough() + }) + }) +} + +describe('UniswapX Toggle', () => { + beforeEach(() => { + stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter) + cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`) + }) + + it('displays uniswapx ui when setting is on', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + cy.wait('@quote') + + // UniswapX UI should be visible + cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist') + }) +}) + +describe('UniswapX Orders', () => { + beforeEach(() => { + stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter) + cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' }) + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' }) + + stubSwapTxReceipt() + + cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))) + cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, { + featureFlags: [{ name: FeatureFlag.gatewayDNSUpdate, value: false }], + }) + }) + + it('can swap exact-in trades using uniswapX', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + cy.wait('@quote') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap submitted') + cy.contains('Learn more about swapping with UniswapX') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + // Verify swap success + cy.contains('Swapped') + }) + + it('can swap exact-out trades using uniswapX', () => { + // Setup a swap + cy.get('#swap-currency-output .token-amount-input').type('300') + cy.wait('@quote') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap submitted') + cy.contains('Learn more about swapping with UniswapX') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + // Verify swap success + cy.contains('Swapped') + }) + + it('renders proper view if uniswapx order expires', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + cy.wait('@quote') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + // Return expired order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' }) + + // Verify swap failure message + cy.contains('Swap expired') + }) + + it('renders proper view if uniswapx order has insufficient funds', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + cy.wait('@quote') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + // Return insufficient_funds order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/insufficientFundsStatusResponse.json' }) + + // Verify swap failure message + cy.contains('Insufficient funds') + }) + + it('cancels a pending uniswapx order', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + cy.wait('@quote') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + cy.wait('@eth_signTypedData_v4') + cy.get(getTestSelector('confirmation-close-icon')).click() + + // Open mini portfolio and navigate to activity history + cy.get(getTestSelector('web3-status-connected')).click() + cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' }) + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + + // Open pending order modal + cy.contains('Swapping').click() + cy.get(getTestSelector('offchain-activity-modal')).contains('Transaction details') + + // Cancel order + cy.get(getTestSelector('offchain-activity-modal')).contains('Cancel').click() + cy.contains('Proceed').click() + + // Return cancelled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/cancelledStatusResponse.json' }) + + // Verify swap failure message + cy.contains('Swap cancelled') + }) +}) + +describe('UniswapX Eth Input', () => { + beforeEach(() => { + stubNonPriceQuoteWith(QuoteWithEthInput) + cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' }) + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' }) + + // Turn off automine so that intermediate screens are available to assert on. + cy.hardhat({ automine: false }).then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(nativeOnChain(ChainId.MAINNET), 2e18)) + await hardhat.mine() + }) + + stubSwapTxReceipt() + + cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`) + }) + + it('can swap using uniswapX with ETH as input', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('1') + + cy.wait('@quote') + + // Prompt ETH wrap to use for order + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.contains('Wrap ETH') + + // Wrap ETH + cy.wait('@eth_sendRawTransaction') + cy.contains('Pending...') + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.contains('Wrapped') + + // Approve WETH spend + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + + // Verify signed order submission + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap submitted') + cy.contains('Learn more about swapping with UniswapX') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + // Verify swap success + cy.contains('Swapped') + }) + + it('keeps ETH as the input currency before wrap completes', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('1') + cy.wait('@quote') + + // Prompt ETH wrap and confirm + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_sendRawTransaction') + + // Close review modal before wrap is confirmed on chain + cy.get(getTestSelector('confirmation-close-icon')).click() + // Confirm ETH is still the input token before wrap succeeds + cy.contains('ETH') + }) + + it('switches swap input to WETH after wrap', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('1') + cy.wait('@quote') + + // Prompt ETH wrap and confirm + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + + // Confirm wrap is successful and WETH is now input token + cy.contains('Wrapped') + cy.contains('WETH') + + // Approve WETH spend + cy.wait('@eth_sendRawTransaction') + cy.hardhat().then((hardhat) => hardhat.mine()) + + // Submit uniswapx order signature + cy.wait('@eth_signTypedData_v4') + cy.contains('Swap submitted') + cy.contains('Learn more about swapping with UniswapX') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + // Verify swap success + cy.contains('Swapped') + + // Close modal + cy.get(getTestSelector('confirmation-close-icon')).click() + // The input currency should now be WETH + cy.contains('WETH') + }) +}) + +describe('UniswapX activity history', () => { + beforeEach(() => { + stubNonPriceQuoteWith(QuoteWhereUniswapXIsBetter) + cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' }) + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' }) + + stubSwapTxReceipt() + + cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))) + cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`, { + featureFlags: [{ name: FeatureFlag.gatewayDNSUpdate, value: false }], + }) + }) + + it('can view UniswapX order status progress in activity', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_signTypedData_v4') + cy.get(getTestSelector('confirmation-close-icon')).click() + + // Open mini portfolio and navigate to activity history + cy.get(getTestSelector('web3-status-connected')).click() + cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' }) + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + + // Open pending order modal + cy.contains('Swapping').click() + cy.get(getTestSelector('offchain-activity-modal')).contains('Transaction details') + cy.get(getTestSelector('offchain-activity-modal')).contains('Order pending') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + cy.get(getTestSelector('offchain-activity-modal')).contains('Order executed') + cy.get(getTestSelector('offchain-activity-modal')).contains('Transaction ID') + }) + + it('can view UniswapX order status progress in activity upon expiry', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_signTypedData_v4') + cy.get(getTestSelector('confirmation-close-icon')).click() + + // Open mini portfolio and navigate to activity history + cy.get(getTestSelector('web3-status-connected')).click() + cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' }) + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + + // Open pending order modal + cy.contains('Swapping').click() + cy.get(getTestSelector('offchain-activity-modal')).contains('Transaction details') + cy.get(getTestSelector('offchain-activity-modal')).contains('Order pending') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' }) + + cy.get(getTestSelector('offchain-activity-modal')).contains('Order expired') + }) + + it('deduplicates remote vs local uniswapx orders', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + cy.wait('@eth_signTypedData_v4') + cy.get(getTestSelector('confirmation-close-icon')).click() + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + cy.contains('Swapped') + + // Open mini portfolio + cy.get(getTestSelector('web3-status-connected')).click() + + cy.fixture('mini-portfolio/uniswapx_activity.json').then((uniswapXActivity) => { + // Replace fixture's timestamp with current time + uniswapXActivity.data.portfolios[0].assetActivities[0].timestamp = Date.now() / 1000 + cy.intercept(/graphql/, uniswapXActivity) + }) + + // Open activity history + cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click() + + // Ensure gql and local order have been deduped, such that there is one swap activity listed + cy.get(getTestSelector('activity-content')).contains('Swapped').should('have.length', 1) + }) + + it('balances should refetch after uniswapx swap', () => { + // Setup a swap + cy.get('#swap-currency-input .token-amount-input').type('300') + + const gqlSpy = cy.spy().as('gqlSpy') + cy.intercept(/graphql/, (req) => { + // Spy on request frequency + req.on('response', gqlSpy) + // Reply with a fixture to speed up test + req.reply({ + fixture: 'mini-portfolio/tokens.json', + }) + }) + + // Expect balances to fetch upon opening mini portfolio + cy.get(getTestSelector('web3-status-connected')).click() + cy.get('@gqlSpy').should('have.been.calledOnce') + + // Submit uniswapx order signature + cy.get('#swap-button').click() + cy.contains('Confirm swap').click() + + // Expect balances to refetch after approval + cy.get('@gqlSpy').should('have.been.calledTwice') + + // Return filled order status from uniswapx api + cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' }) + + // Expect balances to refetch after swap + cy.get('@gqlSpy').should('have.been.calledThrice') + }) +}) diff --git a/apps/web/cypress/e2e/swap/wrap.test.ts b/apps/web/cypress/e2e/swap/wrap.test.ts new file mode 100644 index 0000000..d4a5f42 --- /dev/null +++ b/apps/web/cypress/e2e/swap/wrap.test.ts @@ -0,0 +1,78 @@ +import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core' + +import { getBalance, getTestSelector } from '../../utils' + +const WETH = WETH9[ChainId.MAINNET] + +describe('Swap wrap', () => { + beforeEach(() => { + cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false }) + }) + + it('ETH to wETH is same value (wrapped swaps have no price impact)', () => { + cy.get('#swap-currency-input .token-amount-input').type('0.01').should('have.value', '0.01') + cy.get('#swap-currency-output .token-amount-input').should('have.value', '0.01') + + cy.get('#swap-currency-output .token-amount-input').clear().type('0.02').should('have.value', '0.02') + cy.get('#swap-currency-input .token-amount-input').should('have.value', '0.02') + }) + + it('should be able to wrap ETH', () => { + getBalance(WETH).then((initialBalance) => { + cy.contains('Enter ETH amount') + + // Enter amount to wrap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1) + cy.get('#swap-currency-input .token-amount-input').should('have.value', 1) + + // Submit transaction + cy.contains('Wrap').click() + cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + + // Mine transaction + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.wait('@eth_getTransactionReceipt') + + // Verify transaction + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).contains('Wrapped') + const finalBalance = initialBalance + 1 + cy.get('#swap-currency-output').contains(`Balance: ${finalBalance}`) + getBalance(WETH).should('equal', finalBalance) + }) + }) + + it('should be able to unwrap WETH', () => { + cy.hardhat().then(async (hardhat) => { + await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(WETH, 1e18)) + await hardhat.mine() + }) + + getBalance(WETH).then((initialBalance) => { + // Swap input/output to unwrap WETH + cy.get(getTestSelector('swap-currency-button')).click() + cy.contains('Enter WETH amount') + + // Enter the amount to unwrap + cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', 1) + cy.get('#swap-currency-input .token-amount-input').should('have.value', 1) + + // Submit transaction + cy.contains('Unwrap').click() + cy.wait('@eth_estimateGas').wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt') + cy.get(getTestSelector('web3-status-connected')).should('contain', '1 Pending') + + // Mine transaction + cy.hardhat().then((hardhat) => hardhat.mine()) + cy.wait('@eth_getTransactionReceipt') + + // Verify transaction + cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending') + cy.get(getTestSelector('popups')).contains('Unwrapped') + const finalBalance = initialBalance - 1 + cy.get('#swap-currency-input').contains(`Balance: ${finalBalance}`) + getBalance(WETH).should('equal', finalBalance) + }) + }) +}) diff --git a/apps/web/cypress/e2e/token-details.test.ts b/apps/web/cypress/e2e/token-details.test.ts new file mode 100644 index 0000000..e439660 --- /dev/null +++ b/apps/web/cypress/e2e/token-details.test.ts @@ -0,0 +1,147 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { shortenAddress } from 'utilities/src/addresses' +import { ARB, UNI } from '../../src/constants/tokens' +import { getTestSelector } from '../utils' + +const UNI_MAINNET = UNI[ChainId.MAINNET] + +const UNI_ADDRESS = '0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984' + +describe('Token details', () => { + beforeEach(() => { + cy.viewport(1440, 900) + }) + + it('should have a single h1 tag on smaller screen size', () => { + cy.viewport(800, 600) + cy.visit(`/explore/tokens/ethereum/${UNI_ADDRESS}`) + cy.get('h1').should('have.length', 1) + }) + + it('UNI token should have all information populated', () => { + // $UNI token + cy.visit(`/explore/tokens/ethereum/${UNI_ADDRESS}`) + // There should be a single h1 tag on large screen sizes + cy.get('h1').should('have.length', 1) + + // Price chart should be filled in + cy.get('[data-cy="chart-header"]').should('include.text', '$') + cy.get('[data-cy="tdp-Price-chart-container"]').should('exist') + + // Stats should have: TVL, FDV, market cap, 24H volume + cy.get(getTestSelector('token-details-stats')).should('exist') + cy.get(getTestSelector('token-details-stats')).within(() => { + cy.get('[data-cy="tvl"]').should('include.text', '$') + cy.get('[data-cy="fdv"]').should('include.text', '$') + cy.get('[data-cy="market-cap"]').should('include.text', '$') + cy.get('[data-cy="volume-24h"]').should('include.text', '$') + }) + + // Info section should have description of token & relevant links + cy.get(getTestSelector('token-details-info-section')).should('exist') + cy.contains('UNI is the governance token for Uniswap').should('exist') + cy.get(getTestSelector('token-details-info-links')).within(() => { + cy.contains('Etherscan').should('have.attr', 'href').and('include', `etherscan.io/token/${UNI_ADDRESS}`) + cy.contains('Website').should('have.attr', 'href').and('include', 'uniswap.org') + cy.contains('Twitter').should('have.attr', 'href').and('include', 'x.com/Uniswap') + }) + + // Contract address should be displayed + cy.contains(shortenAddress(UNI_ADDRESS)).should('exist') + }) + + it('token with warning and low trading volume should have all information populated', () => { + // Null token created for this test, 0 trading volume and has warning modal + cy.visit('/explore/tokens/ethereum/0x1eFBB78C8b917f67986BcE54cE575069c0143681') + + // Should have missing price view when price unavailable (expected for this token) + cy.get('[data-cy="chart-error-view"]').should('exist') + + // Stats should not exist + cy.get('[data-cy="token-details-no-stats-data"]').should('exist') + + // Info section should have description of token + cy.get(getTestSelector('token-details-info-section')).should('exist') + cy.contains('No token information available').should('exist') + + // Links section should link out to Etherscan + cy.get(getTestSelector('token-details-info-links')).within(() => { + cy.contains('Etherscan') + .should('have.attr', 'href') + .and('include', 'etherscan.io/token/0x1eFBB78C8b917f67986BcE54cE575069c0143681') + }) + + // Contract address should be displayed + cy.contains(shortenAddress('0x1eFBB78C8b917f67986BcE54cE575069c0143681')).should('exist') + + // Warning label should show if relevant ([spec](https://www.notion.so/3f7fce6f93694be08a94a6984d50298e)) + cy.get('[data-cy="token-safety-message"]') + .should('include.text', 'Warning') + .and('include.text', "This token isn't traded on leading U.S. centralized exchanges") + }) + + describe('swapping', () => { + beforeEach(() => { + // On mobile widths, we just link back to /swap instead of rendering the swap component. + cy.viewport(1200, 800) + cy.visit(`/explore/tokens/ethereum/${UNI_MAINNET.address}`).then(() => { + cy.wait('@eth_blockNumber') + cy.scrollTo('top') + }) + }) + + it('should have the expected output for a tokens detail page', () => { + cy.get(`#swap-currency-input .token-amount-input`).should('have.value', '') + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'Select token') + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI') + }) + + it('should automatically navigate to the new TDP', () => { + cy.get(`#swap-currency-output .open-currency-select-button`).click() + cy.get('[data-reach-dialog-content]').contains('WETH').click() + cy.url().should('include', `${WETH9[1].address}`) + cy.url().should('not.include', `${UNI_MAINNET.address}`) + }) + + it('should not share swap state with the main swap page', () => { + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'UNI') + cy.get(`#swap-currency-input .open-currency-select-button`).click() + cy.contains('WETH').click() + cy.visit('/swap') + cy.contains('UNI').should('not.exist') + cy.contains('WETH').should('not.exist') + }) + + it('can enter an amount into input', () => { + cy.get('#swap-currency-input .token-amount-input').clear().type('0.001').should('have.value', '0.001') + }) + + it('zero swap amount', () => { + cy.get('#swap-currency-input .token-amount-input').clear().type('0.0').should('have.value', '0.0') + }) + + it('invalid swap amount', () => { + cy.get('#swap-currency-input .token-amount-input').clear().type('\\').should('have.value', '') + }) + + it('can enter an amount into output', () => { + cy.get('#swap-currency-output .token-amount-input').clear().type('0.001').should('have.value', '0.001') + }) + + it('zero output amount', () => { + cy.get('#swap-currency-output .token-amount-input').clear().type('0.0').should('have.value', '0.0') + }) + + it('should show a L2 token even if the user is connected to a different network', () => { + cy.visit('/explore/tokens') + cy.get(getTestSelector('tokens-network-filter-selected')).click() + cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click() + cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum') + cy.get(getTestSelector(`token-table-row-${ARB.address.toLowerCase()}`)).click() + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'ARB') + cy.get(getTestSelector('open-settings-dialog-button')).should('be.disabled') + cy.contains('Connect to Arbitrum').should('exist') + }) + }) +}) diff --git a/apps/web/cypress/e2e/token-explore-filter.test.ts b/apps/web/cypress/e2e/token-explore-filter.test.ts new file mode 100644 index 0000000..88e57e5 --- /dev/null +++ b/apps/web/cypress/e2e/token-explore-filter.test.ts @@ -0,0 +1,40 @@ +import { getTestSelector } from '../utils' + +describe('Token explore filter', () => { + beforeEach(() => { + cy.visit('/tokens') + }) + + function aliasFilteredTokens(filter: string) { + cy.get(getTestSelector('token-name')).then((tokens) => { + cy.wrap(Array.from(tokens).filter((token) => token.innerText.toLowerCase().includes(filter))).as('filteredTokens') + }) + } + + function searchFor(filter: string) { + cy.get(getTestSelector('explore-tokens-search-input')).clear().type(filter).type('{enter}') + // wait for it to finish the filtered render + cy.get(getTestSelector('token-name')).first().contains(filter, { + matchCase: false, + }) + } + + it('should filter correctly by dao search term', () => { + aliasFilteredTokens('dao') + searchFor('dao') + + cy.get('@filteredTokens').then((filteredTokens) => { + const filteredTokenTexts = Cypress.$(filteredTokens) + .map((i, token) => token.innerText) + .get() + + cy.get(getTestSelector('token-name')).then((tokens) => { + const tokenTexts = Cypress.$(tokens) + .map((i, token) => token.innerText) + .get() + + cy.wrap(tokenTexts).should('deep.equal', filteredTokenTexts) + }) + }) + }) +}) diff --git a/apps/web/cypress/e2e/token-explore.test.ts b/apps/web/cypress/e2e/token-explore.test.ts new file mode 100644 index 0000000..9e673b0 --- /dev/null +++ b/apps/web/cypress/e2e/token-explore.test.ts @@ -0,0 +1,65 @@ +import { getTestSelector, getTestSelectorStartsWith } from '../utils' + +describe('Token explore', () => { + before(() => { + cy.visit('/') + }) + + it('should load token leaderboard', () => { + cy.visit('/explore/tokens/ethereum') + cy.get(getTestSelectorStartsWith('token-table')).its('length').should('be.greaterThan', 0) + + cy.get(getTestSelector('token-table-row-NATIVE')) + .find(getTestSelector('token-name')) + .should('include.text', 'Ether') + cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('volume-cell')).should('include.text', '$') + cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('price-cell')).should('include.text', '$') + cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('fdv-cell')).should('include.text', '$') + + // TODO(WEB-3844): test the default sorting by checking the column headers + }) + + it('should update when time window toggled', () => { + cy.visit('/explore/tokens/ethereum') + cy.get(getTestSelector('time-selector')).should('contain', '1D') + cy.get(getTestSelector('token-table-row-NATIVE')) + .find(getTestSelector('volume-cell')) + .then(function ($elem) { + cy.wrap($elem.text()).as('dailyEthVol') + }) + cy.get(getTestSelector('time-selector')).click() + cy.get(getTestSelector('1Y')).click() + cy.get(getTestSelector('token-table-row-NATIVE')) + .find(getTestSelector('volume-cell')) + .then(function ($elem) { + cy.wrap($elem.text()).as('yearlyEthVol') + }) + cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol')) + }) + + it('should navigate to token detail page when row clicked', () => { + cy.visit('/explore/tokens/ethereum') + cy.get(getTestSelector('token-table-row-NATIVE')).click() + cy.url().should('match', /\/explore\/tokens\/ethereum\/NATIVE/) + }) + + it('should update when global network changed', () => { + cy.visit('/explore/tokens/ethereum') + cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Ethereum') + cy.get(getTestSelector('token-table-row-NATIVE')).should('exist') + + // note: cannot switch global chain via UI because we cannot approve the network switch + // in metamask modal using plain cypress. this is a workaround. + cy.visit('/explore/tokens/polygon') + cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Polygon') + cy.get(getTestSelector('token-table-row-NATIVE')).find(getTestSelector('name-cell')).should('include.text', 'Matic') + }) + + it('should update when token explore table network changed', () => { + cy.visit('/explore/tokens/ethereum') + cy.get(getTestSelector('tokens-network-filter-selected')).click() + cy.get(getTestSelector('tokens-network-filter-option-optimism')).click() + cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Optimism') + cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', 'Ethereum logo') + }) +}) diff --git a/apps/web/cypress/e2e/universal-search.test.ts b/apps/web/cypress/e2e/universal-search.test.ts new file mode 100644 index 0000000..4208b96 --- /dev/null +++ b/apps/web/cypress/e2e/universal-search.test.ts @@ -0,0 +1,72 @@ +import { ChainId } from '@uniswap/sdk-core' +import { UNI } from 'constants/tokens' + +import { getTestSelector } from '../utils' + +const UNI_ADDRESS = UNI[ChainId.MAINNET].address.toLowerCase() + +describe('Universal search bar', () => { + function openSearch() { + // can't just type "/" because on mobile it doesn't respond to that + cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click() + } + + beforeEach(() => { + cy.visit('/') + }) + + function getSearchBar() { + return cy.get('[data-cy="search-bar-input"]').last() + } + + it('should yield clickable result that is then added to recent searches', () => { + // Search for UNI token by name. + openSearch() + getSearchBar().clear().type('uni') + + cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`)) + .should('contain.text', 'Uniswap') + .and('contain.text', 'UNI') + .and('contain.text', '$') + .and('contain.text', '%') + .click() + cy.location('pathname').should('equal', '/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984') + + openSearch() + cy.get(getTestSelector('searchbar-dropdown')) + .contains(getTestSelector('searchbar-dropdown'), 'Recent searches') + .find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`)) + .should('exist') + }) + + it( + 'should go to the selected result when recent results are shown', + // this test is experiencing flake despite being correct, i can see the right value in DOM + // but for some reason cypress doesn't find it, so adding retries for now :/ + { + // @ts-ignore see https://uniswapteam.slack.com/archives/C047U65H422/p1691455547556309 + // basically cypress has bad types due to overlap with jest and you just have to deal with it + // i tried removing jest types but still happens + retries: { + runMode: 3, + openMode: 3, + }, + }, + () => { + // Seed recent results with UNI. + openSearch() + getSearchBar().type('uni') + cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`)) + getSearchBar().clear().type('{esc}') + + // Search a different token by name. + openSearch() + getSearchBar().type('eth') + cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')) + + // Validate that we go to the searched/selected result. + cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE')).click() + cy.url().should('contain', '/explore/tokens/ethereum/NATIVE') + } + ) +}) diff --git a/apps/web/cypress/e2e/wallet-connection/connect.test.ts b/apps/web/cypress/e2e/wallet-connection/connect.test.ts new file mode 100644 index 0000000..f46dcd3 --- /dev/null +++ b/apps/web/cypress/e2e/wallet-connection/connect.test.ts @@ -0,0 +1,41 @@ +import { getTestSelector } from '../../utils' +import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state' + +describe('disconnect wallet', () => { + it('should clear state', () => { + cy.visit('/swap') + cy.get('#swap-currency-input .token-amount-input').clear().type('1') + + // Verify wallet is connected + cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6))) + cy.contains('Balance:') + + // Disconnect the wallet + cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6)).click()) + cy.get(getTestSelector('wallet-disconnect')).click() + cy.get(getTestSelector('wallet-disconnect')).contains('Disconnect') + cy.get(getTestSelector('wallet-disconnect')).click() + + // Verify wallet has disconnected + cy.contains('Connect a wallet').should('exist') + cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect') + cy.contains('Connect wallet') + + // Verify swap input is cleared + cy.get('#swap-currency-input .token-amount-input').should('have.value', '1') + }) +}) + +describe('connect wallet', () => { + it('should load state', () => { + cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE }) + + // Connect the wallet + cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click() + cy.contains('MetaMask').click() + + // Verify wallet is connected + cy.hardhat().then((hardhat) => cy.contains(hardhat.wallet.address.substring(0, 6))) + cy.contains('Balance:') + }) +}) diff --git a/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts b/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts new file mode 100644 index 0000000..917a719 --- /dev/null +++ b/apps/web/cypress/e2e/wallet-connection/switch-network.test.ts @@ -0,0 +1,144 @@ +import { createDeferredPromise } from '../../../src/test-utils/promise' +import { getTestSelector } from '../../utils' + +function waitsForActiveChain(chain: string) { + cy.get(getTestSelector('chain-selector-logo')).find('title').should('include.text', `${chain} logo`) +} + +function switchChain(chain: string) { + cy.get(getTestSelector('chain-selector')).eq(1).click() + cy.contains(chain).click() +} + +describe('network switching', () => { + beforeEach(() => { + cy.visit('/swap') + cy.get(getTestSelector('web3-status-connected')) + }) + + function rejectsNetworkSwitchWith(rejection: unknown) { + cy.hardhat().then((hardhat) => { + // Reject network switch + const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch') + sendStub.withArgs('wallet_switchEthereumChain').rejects(rejection) + sendStub.callThrough() // allows other calls to return non-stubbed values + }) + + switchChain('Polygon') + + // Verify rejected network switch + cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain') + waitsForActiveChain('Ethereum') + cy.get(getTestSelector('web3-status-connected')) + } + + it('should not display message on user rejection', () => { + const USER_REJECTION = { code: 4001 } + rejectsNetworkSwitchWith(USER_REJECTION) + cy.get(getTestSelector('popups')).should('not.contain', 'Failed to switch networks') + }) + + it('should display message on unknown error', () => { + rejectsNetworkSwitchWith(new Error('Unknown error')) + cy.get(getTestSelector('popups')).contains('Failed to switch networks') + }) + + it('should add missing chain', () => { + cy.hardhat().then((hardhat) => { + // https://docs.metamask.io/guide/rpc-api.html#unrestricted-methods + const CHAIN_NOT_ADDED = { code: 4902 } // missing message in useSelectChain + + // Reject network switch with CHAIN_NOT_ADDED + const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch') + let added = false + sendStub + .withArgs('wallet_switchEthereumChain') + .callsFake(() => (added ? Promise.resolve(null) : Promise.reject(CHAIN_NOT_ADDED))) + sendStub.withArgs('wallet_addEthereumChain').callsFake(() => { + added = true + return Promise.resolve(null) + }) + sendStub.callThrough() // allows other calls to return non-stubbed values + }) + + switchChain('Polygon') + + // Verify the network was added + cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain') + cy.get('@switch').should('have.been.calledWith', 'wallet_addEthereumChain', [ + { + blockExplorerUrls: ['https://polygonscan.com/'], + chainId: '0x89', + chainName: 'Polygon', + nativeCurrency: { name: 'Polygon Matic', symbol: 'MATIC', decimals: 18 }, + rpcUrls: ['https://polygon-rpc.com/'], + }, + ]) + }) + + it('should not disconnect while switching', () => { + const promise = createDeferredPromise() + + cy.hardhat().then((hardhat) => { + // Reject network switch with CHAIN_NOT_ADDED + const sendStub = cy.stub(hardhat.provider, 'send').log(false).as('switch') + sendStub.withArgs('wallet_switchEthereumChain').returns(promise) + sendStub.callThrough() // allows other calls to return non-stubbed values + }) + + switchChain('Polygon') + + // Verify there is no disconnection + cy.get('@switch').should('have.been.calledWith', 'wallet_switchEthereumChain') + cy.contains('Connecting to Polygon') + cy.get(getTestSelector('web3-status-connected')).should('be.disabled') + promise.resolve() + }) + + it('should switch networks', () => { + // Select an output currency + cy.get('#swap-currency-output .open-currency-select-button').click() + cy.contains('USDC').click() + + // Populate input/output fields + cy.get('#swap-currency-input .token-amount-input').clear().type('1') + cy.get('#swap-currency-output .token-amount-input').should('not.equal', '') + + // Switch network + switchChain('Polygon') + + // Verify network switch + cy.wait('@wallet_switchEthereumChain') + waitsForActiveChain('Polygon') + cy.get(getTestSelector('web3-status-connected')) + cy.url().should('contain', 'chain=polygon') + + // Verify that the input/output fields were reset + cy.get('#swap-currency-input .token-amount-input').should('have.value', '') + cy.get(`#swap-currency-input .token-symbol-container`).should('contain.text', 'MATIC') + cy.get(`#swap-currency-output .token-amount-input`).should('not.have.value') + cy.get(`#swap-currency-output .token-symbol-container`).should('contain.text', 'Select token') + }) +}) + +describe('network switching from URL param', () => { + it('should switch network from URL param', () => { + cy.visit('/swap?chain=polygon') + cy.get(getTestSelector('web3-status-connected')) + cy.wait('@wallet_switchEthereumChain') + waitsForActiveChain('Polygon') + }) + + it('should be able to switch network after loading from URL param', () => { + cy.visit('/swap?chain=polygon') + cy.get(getTestSelector('web3-status-connected')) + cy.wait('@wallet_switchEthereumChain') + waitsForActiveChain('Polygon') + + // switching to another chain clears query param + switchChain('Ethereum') + cy.wait('@wallet_switchEthereumChain') + waitsForActiveChain('Ethereum') + cy.url().should('not.contain', 'chain=polygon') + }) +}) diff --git a/apps/web/cypress/e2e/wallet-dropdown.test.ts b/apps/web/cypress/e2e/wallet-dropdown.test.ts new file mode 100644 index 0000000..c79c59e --- /dev/null +++ b/apps/web/cypress/e2e/wallet-dropdown.test.ts @@ -0,0 +1,205 @@ +import { FeatureFlag } from 'featureFlags' + +import { getTestSelector } from '../utils' + +describe('Wallet Dropdown', () => { + function itChangesTheme() { + it('should change theme', () => { + cy.get(getTestSelector('theme-lightmode')).click() + + cy.get(getTestSelector('theme-lightmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + + cy.get(getTestSelector('theme-darkmode')).click() + cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-darkmode')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-auto')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + + cy.get(getTestSelector('theme-auto')).click() + cy.get(getTestSelector('theme-lightmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-darkmode')).should('have.css', 'background-color', 'rgba(0, 0, 0, 0)') + cy.get(getTestSelector('theme-auto')).should('not.have.css', 'background-color', 'rgba(0, 0, 0, 0)') + }) + } + + function itChangesLocale({ featureFlag = false }: { featureFlag?: boolean } = {}) { + it('should change locale', () => { + cy.contains('Uniswap available in: English').should('not.exist') + + if (featureFlag) { + cy.get(getTestSelector('language-settings-button')).click() + } + + cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true }) + cy.location('search').should('match', /\?lng=af-ZA$/) + cy.contains('Uniswap available in: English') + + cy.get(getTestSelector('wallet-language-item')).contains('English').click({ force: true }) + cy.location('search').should('match', /\?lng=en-US$/) + cy.contains('Uniswap available in: English').should('not.exist') + }) + } + + describe('connected', () => { + beforeEach(() => { + cy.visit('/') + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + }) + itChangesTheme() + itChangesLocale() + + it('should not show buy crypto button in uk', () => { + cy.document().then((doc) => { + const meta = document.createElement('meta') + meta.setAttribute('property', 'x:blocked-paths') + meta.setAttribute('content', '/,/nfts,/buy') + doc.head.appendChild(meta) + }) + cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist') + }) + }) + + describe('do not render buy button when /buy is blocked', () => { + beforeEach(() => { + cy.document().then((doc) => { + const meta = document.createElement('meta') + meta.setAttribute('property', 'x:blocked-paths') + meta.setAttribute('content', '/buy') + doc.head.appendChild(meta) + }) + cy.visit('/') + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + }) + + it('should not render buy button', () => { + cy.get(getTestSelector('wallet-buy-crypto')).should('not.exist') + }) + }) + + describe('should change locale with feature flag', () => { + beforeEach(() => { + cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + }) + itChangesLocale({ featureFlag: true }) + }) + + describe('testnet toggle', () => { + beforeEach(() => { + cy.visit('/swap') + }) + it('should toggle testnet visibility', () => { + cy.get(getTestSelector('chain-selector')).last().click() + cy.get(getTestSelector('chain-selector-options')).should('not.contain.text', 'Sepolia') + + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.get('#testnets-toggle').click() + cy.get(getTestSelector('close-account-drawer')).click() + cy.get(getTestSelector('chain-selector')).last().click() + cy.get(getTestSelector('chain-selector-options')).should('contain.text', 'Sepolia') + }) + }) + + describe('disconnected', () => { + beforeEach(() => { + cy.visit('/') + cy.get(getTestSelector('web3-status-connected')).click() + // click twice, first time to show confirmation, second to confirm + cy.get(getTestSelector('wallet-disconnect')).click() + cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect') + cy.get(getTestSelector('wallet-disconnect')).click() + cy.get(getTestSelector('wallet-settings')).click() + }) + itChangesTheme() + itChangesLocale() + }) + + describe('with color theme', () => { + function visitSwapWithColorTheme({ dark }: { dark: boolean }) { + cy.visit('/swap', { + onBeforeLoad(win) { + cy.stub(win, 'matchMedia') + .withArgs('(prefers-color-scheme: dark)') + .returns({ + matches: dark, + addEventListener() { + /* noop */ + }, + removeEventListener() { + /* noop */ + }, + }) + }, + }) + } + + it('should properly use dark system theme when auto theme setting is selected', () => { + visitSwapWithColorTheme({ dark: true }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.get(getTestSelector('theme-auto')).click() + cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(155, 155, 155)') + }) + + it('should properly use light system theme when auto theme setting is selected', () => { + visitSwapWithColorTheme({ dark: false }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.get(getTestSelector('theme-auto')).click() + cy.get(getTestSelector('wallet-header')).should('have.css', 'color', 'rgb(125, 125, 125)') + }) + }) + + describe('mobile', () => { + beforeEach(() => { + cy.viewport('iphone-6').visit('/') + }) + + it('should dismiss the wallet bottom sheet when clicking buy crypto', () => { + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-buy-crypto')).click() + cy.get(getTestSelector('wallet-settings')).should('not.be.visible') + }) + + it('should use a bottom sheet and dismiss when on a mobile screen size', () => { + cy.get(getTestSelector('web3-status-connected')).click() + cy.root().click(15, 40) + cy.get(getTestSelector('wallet-settings')).should('not.be.visible') + }) + }) + + describe('local currency', () => { + it('loads local currency from the query param', () => { + cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.contains('USD') + + cy.visit('/?cur=AUD', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.contains('AUD') + }) + + it('loads local currency from menu', () => { + cy.visit('/', { featureFlags: [{ name: FeatureFlag.currencyConversion, value: true }] }) + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.contains('USD') + + cy.get(getTestSelector('local-currency-settings-button')).click() + cy.get(getTestSelector('wallet-local-currency-item')).contains('AUD').click({ force: true }) + cy.location('search').should('match', /\?cur=AUD$/) + cy.contains('AUD') + + cy.get(getTestSelector('wallet-local-currency-item')).contains('USD').click({ force: true }) + cy.location('search').should('match', /\?cur=USD$/) + cy.contains('USD') + }) + }) +}) diff --git a/apps/web/cypress/fixtures/feeTierDistribution.json b/apps/web/cypress/fixtures/feeTierDistribution.json new file mode 100644 index 0000000..4a01d75 --- /dev/null +++ b/apps/web/cypress/fixtures/feeTierDistribution.json @@ -0,0 +1,30 @@ +{ + "_meta": { + "block": { + "number": 99999999 + } + }, + "asToken0": [ + { + "feeTier": "100", + "totalValueLockedToken0": "0", + "totalValueLockedToken1": "3" + }, + { + "feeTier": "500", + "totalValueLockedToken0": "0", + "totalValueLockedToken1": "1" + }, + { + "feeTier": "3000", + "totalValueLockedToken0": "0", + "totalValueLockedToken1": "4" + }, + { + "feeTier": "10000", + "totalValueLockedToken0": "0", + "totalValueLockedToken1": "2" + } + ], + "asToken1": [] +} diff --git a/apps/web/cypress/fixtures/insufficientLiquidity.json b/apps/web/cypress/fixtures/insufficientLiquidity.json new file mode 100644 index 0000000..ff36da9 --- /dev/null +++ b/apps/web/cypress/fixtures/insufficientLiquidity.json @@ -0,0 +1,5 @@ +{ + "errorCode": "QUOTE_ERROR", + "detail": "No quotes available", + "id": "63363cc1-d474-4584-b386-7c356814b79f" +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/mini-portfolio/empty_activity.json b/apps/web/cypress/fixtures/mini-portfolio/empty_activity.json new file mode 100644 index 0000000..e3e94b3 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/empty_activity.json @@ -0,0 +1,12 @@ +{ + "data": { + "portfolios": [ + { + "id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==", + "assetActivities": [], + "__typename": "Portfolio" + } + ] + }, + "errors": [] +} diff --git a/apps/web/cypress/fixtures/mini-portfolio/full_activity.json b/apps/web/cypress/fixtures/mini-portfolio/full_activity.json new file mode 100644 index 0000000..a007bb0 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/full_activity.json @@ -0,0 +1,580 @@ +{ + "data": { + "portfolios": [ + { + "id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==", + "assetActivities": [ + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM09EQm1NamcwTURSak1qRXpPRGd6TVRVM00yRXdOakJtTVRaaE1UQTNaV0ZtTW1Jd01qazFZbUZqTmpjNU5tUm1ZamN5TW1WbVl6VmpPVE5tTmpRM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9", + "timestamp": 1684364195, + "chain": "ETHEREUM", + "details": { + "__typename": "TransactionDetails", + "id": "VHJhbnNhY3Rpb246MHg3ODBmMjg0MDRjMjEzODgzMTU3M2EwNjBmMTZhMTA3ZWFmMmIwMjk1YmFjNjc5NmRmYjcyMmVmYzVjOTNmNjQ3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=", + "type": "UNKNOWN", + "blockNumber": 17282434, + "hash": "0x780f28404c2138831573a060f16a107eaf2b0295bac6796dfb722efc5c93f647", + "status": "CONFIRMED", + "to": "0x000000030f49bf2e002e60c6ed1661fb2346d882", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 465, + "assetChanges": [] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFl6QTROMkpoTjJJMk4yUTFPVEEwTVdNMU5XTXdObU16TkdNNVlXVmhPVEUxTkRreVpUYzRNRFl4WldRd016TTBNMlprWmprMU1qa3dPR1U0WTJSa1h6QjRaR0psWmpNM05HWmtaamhrTnpNMVpUYzFPRGxoT1dFNVpUSmpOV0V3T1RGbFlqSmtZbVUxTjE4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9", + "timestamp": 1684364135, + "chain": "ETHEREUM", + "details": { + "__typename": "TransactionDetails", + "id": "VHJhbnNhY3Rpb246MHhlYzA4N2JhN2I2N2Q1OTA0MWM1NWMwNmMzNGM5YWVhOTE1NDkyZTc4MDYxZWQwMzM0M2ZkZjk1MjkwOGU4Y2RkXzB4ZGJlZjM3NGZkZjhkNzM1ZTc1ODlhOWE5ZTJjNWEwOTFlYjJkYmU1N18weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=", + "type": "RECEIVE", + "blockNumber": 17282429, + "hash": "0xec087ba7b67d59041c55c06c34c9aea915492e78061ed03343fdf952908e8cdd", + "status": "CONFIRMED", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "from": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57", + "nonce": 66, + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGRiZWYzNzRmZGY4ZDczNWU3NTg5YTlhOWUyYzVhMDkxZWIyZGJlNTdfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWMwODdiYTdiNjdkNTkwNDFjNTVjMDZjMzRjOWFlYTkxNTQ5MmU3ODA2MWVkMDMzNDNmZGY5NTI5MDhlOGNkZA==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.001", + "sender": "0xdbef374fdf8d735e7589a9a9e2c5a091eb2dbe57", + "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjEuODI5NjcwMDAwMDAwMDAwMV9VU0Q=", + "currency": "USD", + "value": 1.8296700000000001, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhoaE9URXdPVFEwT1Rka01UVmpNelpsWWprd1pXUXpZVEkwWW1Wa09ESTBOalpqWmpKaU9URXpNV1l4WkRVMk1EUmlNelppWW1aallqRTBOMkUzTURnNFh6QjRaV1JoTldVeE9ERXhORFppTVdZNVlUZG1OREJtT0RWak1HUmhNek0wT1RNNE5ESXdaRFV4TkY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9", + "timestamp": 1684319903, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHhhOTEwOTQ0OTdkMTVjMzZlYjkwZWQzYTI0YmVkODI0NjZjZjJiOTEzMWYxZDU2MDRiMzZiYmZjYjE0N2E3MDg4XzB4ZWRhNWUxODExNDZiMWY5YTdmNDBmODVjMGRhMzM0OTM4NDIwZDUxNF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=", + "type": "RECEIVE", + "blockNumber": 17278819, + "hash": "0xa91094497d15c36eb90ed3a24bed82466cf2b9131f1d5604b36bbfcb147a7088", + "status": "CONFIRMED", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "from": "0xeda5e181146b1f9a7f40f85c0da334938420d514", + "nonce": 5, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGVkYTVlMTgxMTQ2YjFmOWE3ZjQwZjg1YzBkYTMzNDkzODQyMGQ1MTRfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4YTkxMDk0NDk3ZDE1YzM2ZWI5MGVkM2EyNGJlZDgyNDY2Y2YyYjkxMzFmMWQ1NjA0YjM2YmJmY2IxNDdhNzA4OA==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.15", + "sender": "0xeda5e181146b1f9a7f40f85c0da334938420d514", + "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjI3NC40NTA1X1VTRA==", + "currency": "USD", + "value": 274.4505, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMFkyUm1Nell6T0dRME1ERXdOV1U1WkRZMVlUZGxObUV6WVdFMlpHTXpNREZpWVRNNVpHTXlNV1ppT0dGaE5USTBNVFppT1ROaE5tWXhOVEUwTWpReVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUmxOR1F6WVRJME5XUXlZall4WW1WaE1tTmlaREl4TmpVNE1XVXlaR1ZrTmpWbFl6azFNRFE9", + "timestamp": 1684319903, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg0Y2RmMzYzOGQ0MDEwNWU5ZDY1YTdlNmEzYWE2ZGMzMDFiYTM5ZGMyMWZiOGFhNTI0MTZiOTNhNmYxNTE0MjQyXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGRlNGQzYTI0NWQyYjYxYmVhMmNiZDIxNjU4MWUyZGVkNjVlYzk1MDQ=", + "type": "SEND", + "blockNumber": 17278819, + "hash": "0x4cdf3638d40105e9d65a7e6a3aa6dc301ba39dc21fb8aa52416b93a6f1514242", + "status": "CONFIRMED", + "to": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 464, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhkZTRkM2EyNDVkMmI2MWJlYTJjYmQyMTY1ODFlMmRlZDY1ZWM5NTA0XzB4NGNkZjM2MzhkNDAxMDVlOWQ2NWE3ZTZhM2FhNmRjMzAxYmEzOWRjMjFmYjhhYTUyNDE2YjkzYTZmMTUxNDI0Mg==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.00134999999999999", + "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "recipient": "0xde4d3a245d2b61bea2cbd216581e2ded65ec9504", + "direction": "OUT", + "transactedValue": { + "id": "QW1vdW50OjIuNDcwMDU0NDk5OTk5OTgyX1VTRA==", + "currency": "USD", + "value": 2.470054499999982, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM04yRXhPVGRoWmpjek9EUXpNRFk0WVRCaVlqUmlaV1V6WWpabFptWmxaakpsTkdZMFptTXlNR1UxWVRGbVltSTBOak14WXpoak1UQTROMk15WWpjM1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHWTFZekZoTnpCbU5qY3pPV0k1TW1ZNU4yTmtOVE5qTXpFMk1ETTJNbU14TXpBMVpUa3hZVGc9", + "timestamp": 1684202579, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg3N2ExOTdhZjczODQzMDY4YTBiYjRiZWUzYjZlZmZlZjJlNGY0ZmMyMGU1YTFmYmI0NjMxYzhjMTA4N2MyYjc3XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGY1YzFhNzBmNjczOWI5MmY5N2NkNTNjMzE2MDM2MmMxMzA1ZTkxYTg=", + "type": "SEND", + "blockNumber": 17269191, + "hash": "0x77a197af73843068a0bb4bee3b6effef2e4f4fc20e5a1fbb4631c8c1087c2b77", + "status": "CONFIRMED", + "to": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 463, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhmNWMxYTcwZjY3MzliOTJmOTdjZDUzYzMxNjAzNjJjMTMwNWU5MWE4XzB4NzdhMTk3YWY3Mzg0MzA2OGEwYmI0YmVlM2I2ZWZmZWYyZTRmNGZjMjBlNWExZmJiNDYzMWM4YzEwODdjMmI3Nw==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.001216034894406018", + "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "recipient": "0xf5c1a70f6739b92f97cd53c3160362c1305e91a8", + "direction": "OUT", + "transactedValue": { + "id": "QW1vdW50OjIuMjI0OTQyNTY1MjQ3ODU5X1VTRA==", + "currency": "USD", + "value": 2.224942565247859, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMlpXSmtZbVJrTVRZMk0yVmxNV1ZrT0RVeE16TXlZelUyWmpkall6YzJaV1ZqTVROaE5qTm1PVEkxTldOa1ltWXlZVEUxWWpReFl6azBPVGhrWW1Wa1h6QjROREZpTXpBNU1qTTJZemczWWpGaVl6Wm1ZVGhsWWpnMk5UZ3pNMlUwTkRFMU9HWmhPVGt4WVY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9", + "timestamp": 1684202579, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg2ZWJkYmRkMTY2M2VlMWVkODUxMzMyYzU2ZjdjYzc2ZWVjMTNhNjNmOTI1NWNkYmYyYTE1YjQxYzk0OThkYmVkXzB4NDFiMzA5MjM2Yzg3YjFiYzZmYThlYjg2NTgzM2U0NDE1OGZhOTkxYV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=", + "type": "RECEIVE", + "blockNumber": 17269191, + "hash": "0x6ebdbdd1663ee1ed851332c56f7cc76eec13a63f9255cdbf2a15b41c9498dbed", + "status": "CONFIRMED", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "from": "0x41b309236c87b1bc6fa8eb865833e44158fa991a", + "nonce": 111266, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweDQxYjMwOTIzNmM4N2IxYmM2ZmE4ZWI4NjU4MzNlNDQxNThmYTk5MWFfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4NmViZGJkZDE2NjNlZTFlZDg1MTMzMmM1NmY3Y2M3NmVlYzEzYTYzZjkyNTVjZGJmMmExNWI0MWM5NDk4ZGJlZA==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.00275365", + "sender": "0x41b309236c87b1bc6fa8eb865833e44158fa991a", + "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjUuMDM4MjcwNzk1NV9VU0Q=", + "currency": "USD", + "value": 5.0382707955, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNU5EUmlNR00wTVROa1l6QmpNekU0TUdFelkyTTNZakUyT1RCbVlqZzBNRFExWm1FME9UTXpObUV5WmprNE16VmpORFpqTURsak1UY3lObUUzTm1aalh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9", + "timestamp": 1684171943, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg5NDRiMGM0MTNkYzBjMzE4MGEzY2M3YjE2OTBmYjg0MDQ1ZmE0OTMzNmEyZjk4MzVjNDZjMDljMTcyNmE3NmZjXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=", + "type": "SEND", + "blockNumber": 17266680, + "hash": "0x944b0c413dc0c3180a3cc7b1690fb84045fa49336a2f9835c46c09c1726a76fc", + "status": "CONFIRMED", + "to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 462, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4OTQ0YjBjNDEzZGMwYzMxODBhM2NjN2IxNjkwZmI4NDA0NWZhNDkzMzZhMmY5ODM1YzQ2YzA5YzE3MjZhNzZmYw==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.003476850926189204", + "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e", + "direction": "OUT", + "transactedValue": { + "id": "QW1vdW50OjYuMzYxNDg5ODM0MTIwNjAxX1VTRA==", + "currency": "USD", + "value": 6.361489834120601, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhneE0yRTRNRGxsT1RZd05USmhOVGxrWlRjNU56WXhObVZrTlRjME1qTTVNakV3WkRJMVpUY3hNRGhqTkRjek9EbG1NbVJoTnpjeU5qTXhZbVZpTUdZMlh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEWXlNakJsTURoak9XUTJNMkZpTjJKaE1tVTFOalk0TXpsbU5ESTVaV1ZsWm1VeE9UbGlOMlU9", + "timestamp": 1684171943, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHgxM2E4MDllOTYwNTJhNTlkZTc5NzYxNmVkNTc0MjM5MjEwZDI1ZTcxMDhjNDczODlmMmRhNzcyNjMxYmViMGY2XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDYyMjBlMDhjOWQ2M2FiN2JhMmU1NjY4MzlmNDI5ZWVlZmUxOTliN2U=", + "type": "SEND", + "blockNumber": 17266680, + "hash": "0x13a809e96052a59de797616ed574239210d25e7108c47389f2da772631beb0f6", + "status": "CONFIRMED", + "to": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 461, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHg2MjIwZTA4YzlkNjNhYjdiYTJlNTY2ODM5ZjQyOWVlZWZlMTk5YjdlXzB4MTNhODA5ZTk2MDUyYTU5ZGU3OTc2MTZlZDU3NDIzOTIxMGQyNWU3MTA4YzQ3Mzg5ZjJkYTc3MjYzMWJlYjBmNg==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.000900000000000318", + "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "recipient": "0x6220e08c9d63ab7ba2e566839f429eeefe199b7e", + "direction": "OUT", + "transactedValue": { + "id": "QW1vdW50OjEuNjQ2NzAzMDAwMDAwNTgxOF9VU0Q=", + "currency": "USD", + "value": 1.6467030000005818, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhobFkyRTJNVEZrTlRVME1EZGxPVGt6WlRFM1lqWmtaVGhpWVRJMFlqWXlOREpqWVRSbFlXWTBORGN3TkRKbFpHRmtNRFE0TTJNNFptSTJabUU0WkRJNVh6QjROekU0WVRVeE5ESXhNR0kwTnpWaU9USXhOVGd6WldGaU5ERXlaV0ptTUdaaVlXUm1NMkl6T1Y4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9", + "timestamp": 1684171931, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHhlY2E2MTFkNTU0MDdlOTkzZTE3YjZkZThiYTI0YjYyNDJjYTRlYWY0NDcwNDJlZGFkMDQ4M2M4ZmI2ZmE4ZDI5XzB4NzE4YTUxNDIxMGI0NzViOTIxNTgzZWFiNDEyZWJmMGZiYWRmM2IzOV8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=", + "type": "RECEIVE", + "blockNumber": 17266679, + "hash": "0xeca611d55407e993e17b6de8ba24b6242ca4eaf447042edad0483c8fb6fa8d29", + "status": "CONFIRMED", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "from": "0x718a514210b475b921583eab412ebf0fbadf3b39", + "nonce": 92, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweDcxOGE1MTQyMTBiNDc1YjkyMTU4M2VhYjQxMmViZjBmYmFkZjNiMzlfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4ZWNhNjExZDU1NDA3ZTk5M2UxN2I2ZGU4YmEyNGI2MjQyY2E0ZWFmNDQ3MDQyZWRhZDA0ODNjOGZiNmZhOGQyOQ==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.01", + "sender": "0x718a514210b475b921583eab412ebf0fbadf3b39", + "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjE4LjI5NjdfVVNE", + "currency": "USD", + "value": 18.2967, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnMllqTTJNelEwT1daaU1HWTROems0TkRnM1pqWmlOREkwTkRjMFkySXdNbVF5WlRVNE1EZ3dPVEpoWVRneE1EVm1ObUU0T1dOalpHTTBORGRsTURSa1h6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VEQXdNREF3TURBek1HWTBPV0ptTW1Vd01ESmxOakJqTm1Wa01UWTJNV1ppTWpNME5tUTRPREk9", + "timestamp": 1684085063, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg2YjM2MzQ0OWZiMGY4Nzk4NDg3ZjZiNDI0NDc0Y2IwMmQyZTU4MDgwOTJhYTgxMDVmNmE4OWNjZGM0NDdlMDRkXzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weDAwMDAwMDAzMGY0OWJmMmUwMDJlNjBjNmVkMTY2MWZiMjM0NmQ4ODI=", + "type": "UNKNOWN", + "blockNumber": 17259555, + "hash": "0x6b363449fb0f8798487f6b424474cb02d2e5808092aa8105f6a89ccdc447e04d", + "status": "CONFIRMED", + "to": "0x000000030f49bf2e002e60c6ed1661fb2346d882", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 460, + "__typename": "TransactionDetails", + "assetChanges": [] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNFlXRTVNVFJqTkRjeU5qWTNNVGxqWkRFeE1EYzNOMkprTnpZek0yVTFOV1kyWkdWbVpXRmpPVEV4TlRjd09EZzNZVEEyWXpNNE5UTmxaV0kyTldZeVh6QjRaR0V4TTJRMk5HVmpPVFZqWkRZM056VXlPVEZpTVdNek1qRXdNamN4TWpGaVpUSXdPV1JtTUY4d2VHWXpPV1prTm1VMU1XRmhaRGc0WmpabU5HTmxObUZpT0RneU56STNPV05tWm1aaU9USXlOalk9", + "timestamp": 1684085051, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg4YWE5MTRjNDcyNjY3MTljZDExMDc3N2JkNzYzM2U1NWY2ZGVmZWFjOTExNTcwODg3YTA2YzM4NTNlZWI2NWYyXzB4ZGExM2Q2NGVjOTVjZDY3NzUyOTFiMWMzMjEwMjcxMjFiZTIwOWRmMF8weGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjY=", + "type": "RECEIVE", + "blockNumber": 17259554, + "hash": "0x8aa914c47266719cd110777bd7633e55f6defeac911570887a06c3853eeb65f2", + "status": "CONFIRMED", + "to": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "from": "0xda13d64ec95cd6775291b1c321027121be209df0", + "nonce": 832, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGRhMTNkNjRlYzk1Y2Q2Nzc1MjkxYjFjMzIxMDI3MTIxYmUyMDlkZjBfMHhmMzlmZDZlNTFhYWQ4OGY2ZjRjZTZhYjg4MjcyNzljZmZmYjkyMjY2XzB4OGFhOTE0YzQ3MjY2NzE5Y2QxMTA3NzdiZDc2MzNlNTVmNmRlZmVhYzkxMTU3MDg4N2EwNmMzODUzZWViNjVmMg==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fbnVsbA==", + "name": "Ether", + "symbol": "ETH", + "address": null, + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly90b2tlbi1pY29ucy5zMy5hbWF6b25hd3MuY29tL2V0aC5wbmc=", + "url": "https://token-icons.s3.amazonaws.com/eth.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "NATIVE", + "quantity": "0.00129866", + "sender": "0xda13d64ec95cd6775291b1c321027121be209df0", + "recipient": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjIuMzc2MTE5MjQyMl9VU0Q=", + "currency": "USD", + "value": 2.3761192422, + "__typename": "Amount" + } + } + ] + }, + "__typename": "AssetActivity" + }, + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnM00yTXdZMlJpTnpReU9UVTJZVFUxWXpZd016YzBOemd6TkRRNVpUSmpNbVZtTURnM1lqUTVPRFl4TVdGak5EZ3dZalJrTVRFMU1UbGhZemRpTXpZNVh6QjRaak01Wm1RMlpUVXhZV0ZrT0RobU5tWTBZMlUyWVdJNE9ESTNNamM1WTJabVptSTVNakkyTmw4d2VHUXpaR1UwTkRneE5qTXlNakl5TURVME9UazJZVE0yTlRsaE5UTXlNR0k1TWpWbU5qUXhNR1k9", + "timestamp": 1684006019, + "chain": "ETHEREUM", + "details": { + "id": "VHJhbnNhY3Rpb246MHg3M2MwY2RiNzQyOTU2YTU1YzYwMzc0NzgzNDQ5ZTJjMmVmMDg3YjQ5ODYxMWFjNDgwYjRkMTE1MTlhYzdiMzY5XzB4ZjM5ZmQ2ZTUxYWFkODhmNmY0Y2U2YWI4ODI3Mjc5Y2ZmZmI5MjI2Nl8weGQzZGU0NDgxNjMyMjIyMDU0OTk2YTM2NTlhNTMyMGI5MjVmNjQxMGY=", + "type": "SEND", + "blockNumber": 17253116, + "hash": "0x73c0cdb742956a55c60374783449e2c2ef087b498611ac480b4d11519ac7b369", + "status": "CONFIRMED", + "to": "0xd3de4481632222054996a3659a5320b925f6410f", + "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "nonce": 459, + "__typename": "TransactionDetails", + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweGYzOWZkNmU1MWFhZDg4ZjZmNGNlNmFiODgyNzI3OWNmZmZiOTIyNjZfMHhiZTgyODI1NjRlYzJiNzAwMDlmMmQ2ODk1NDAxMmViMDlmNDhiYzhkXzB4NzNjMGNkYjc0Mjk1NmE1NWM2MDM3NDc4MzQ0OWUyYzJlZjA4N2I0OTg2MTFhYzQ4MGI0ZDExNTE5YWM3YjM2OQ==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fMHhkM2RlNDQ4MTYzMjIyMjA1NDk5NmEzNjU5YTUzMjBiOTI1ZjY0MTBm", + "name": "EL CHAPO", + "symbol": "CHAPO", + "address": "0xd3de4481632222054996a3659a5320b925f6410f", + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ZDNkZTQ0ODE2MzIyMjIwNTQ5OTZhMzY1OWE1MzIwYjkyNWY2NDEwZg==", + "isSpam": true, + "logo": null, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "ERC20", + "quantity": "50000000000000.002683081102196736", + "sender": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "recipient": "0xbe8282564ec2b70009f2d68954012eb09f48bc8d", + "direction": "OUT", + "transactedValue": null + } + ] + }, + "__typename": "AssetActivity" + } + ], + "__typename": "Portfolio" + } + ] + }, + "errors": [] +} diff --git a/apps/web/cypress/fixtures/mini-portfolio/nfts.json b/apps/web/cypress/fixtures/mini-portfolio/nfts.json new file mode 100644 index 0000000..46f4964 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/nfts.json @@ -0,0 +1 @@ +{"data":{"nftBalances":{"edges":[{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHgzQzkwNTAyZjBDQjBhZDBBNDhjNTEzNTdFNjVGZjE1MjQ3QTFEODhFXzIx","animationUrl":"https://c.neevacdn.net/video/upload/xyz/IFfA-7mHig754Y1IWwIJrwKY7PTwYlCSrTjFouvG2iM.wav","collection":{"id":"TmZ0Q29sbGVjdGlvbjppLWdvdC1wbGVudHktai1tb2Utai16LXotZV8weDNDOTA1MDJmMENCMGFkMEE0OGM1MTM1N0U2NUZmMTUyNDdBMUQ4OEU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy82ZDU4MmVmZWVmNWQ0MjI2YzU3ZDU1OTZkNjMzN2ZlMi5qcGc=","url":"https://i.seadn.io/gcs/files/6d582efeef5d4226c57d5596d6337fe2.jpg","__typename":"Image"},"name":"I Got Plenty - J Moe & J@z-Z-E","twitterName":null,"nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHgzQzkwNTAyZjBDQjBhZDBBNDhjNTEzNTdFNjVGZjE1MjQ3QTFEODhF","address":"0x3C90502f0CB0ad0A48c51357E65Ff15247A1D88E","chain":"ETHEREUM","name":"I Got Plenty - J Moe & J@z-Z-E","standard":"ERC721","symbol":"GOT","totalSupply":null,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDNjOTA1MDJmMGNiMGFkMGE0OGM1MTM1N2U2NWZmMTUyNDdhMWQ4OGU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDA0LjEyMl9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"Walking into a new society filled with many options can be exciting & exhausting at the same time. This song is about the experience of the lives of 2 artist growing up in Houston, and accumulating a multitude of memories and accomplishments with new obstacles to conquer.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9JTFdXWlNCNDZvc2tzajF1cHZpc3NlVlBwQUtCbkxMTnkyakFNYkpkOW1FLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC92aWRlby91cGxvYWQveHl6L0lGZkEtN21IaWc3NTRZMUlXd0lKcndLWTdQVHdZbENTclRqRm91dkcyaU0ud2F2","url":"https://c.neevacdn.net/video/upload/xyz/IFfA-7mHig754Y1IWwIJrwKY7PTwYlCSrTjFouvG2iM.wav","__typename":"Image"},"name":"I Got Plenty - J Moe & J@z-Z-E","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9JTFdXWlNCNDZvc2tzajF1cHZpc3NlVlBwQUtCbkxMTnkyakFNYkpkOW1FLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"21","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovSUxXV1pTQjQ2b3Nrc2oxdXB2aXNzZVZQcEFLQm5MTE55MmpBTWJKZDltRS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHgzQzkwNTAyZjBDQjBhZDBBNDhjNTEzNTdFNjVGZjE1MjQ3QTFEODhFXzIw","animationUrl":"https://c.neevacdn.net/video/upload/xyz/IFfA-7mHig754Y1IWwIJrwKY7PTwYlCSrTjFouvG2iM.wav","collection":{"id":"TmZ0Q29sbGVjdGlvbjppLWdvdC1wbGVudHktai1tb2Utai16LXotZV8weDNDOTA1MDJmMENCMGFkMEE0OGM1MTM1N0U2NUZmMTUyNDdBMUQ4OEU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy82ZDU4MmVmZWVmNWQ0MjI2YzU3ZDU1OTZkNjMzN2ZlMi5qcGc=","url":"https://i.seadn.io/gcs/files/6d582efeef5d4226c57d5596d6337fe2.jpg","__typename":"Image"},"name":"I Got Plenty - J Moe & J@z-Z-E","twitterName":null,"nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHgzQzkwNTAyZjBDQjBhZDBBNDhjNTEzNTdFNjVGZjE1MjQ3QTFEODhF","address":"0x3C90502f0CB0ad0A48c51357E65Ff15247A1D88E","chain":"ETHEREUM","name":"I Got Plenty - J Moe & J@z-Z-E","standard":"ERC721","symbol":"GOT","totalSupply":null,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDNjOTA1MDJmMGNiMGFkMGE0OGM1MTM1N2U2NWZmMTUyNDdhMWQ4OGU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDA0LjEyMl9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"Walking into a new society filled with many options can be exciting & exhausting at the same time. This song is about the experience of the lives of 2 artist growing up in Houston, and accumulating a multitude of memories and accomplishments with new obstacles to conquer.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9JTFdXWlNCNDZvc2tzajF1cHZpc3NlVlBwQUtCbkxMTnkyakFNYkpkOW1FLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC92aWRlby91cGxvYWQveHl6L0lGZkEtN21IaWc3NTRZMUlXd0lKcndLWTdQVHdZbENTclRqRm91dkcyaU0ud2F2","url":"https://c.neevacdn.net/video/upload/xyz/IFfA-7mHig754Y1IWwIJrwKY7PTwYlCSrTjFouvG2iM.wav","__typename":"Image"},"name":"I Got Plenty - J Moe & J@z-Z-E","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9JTFdXWlNCNDZvc2tzajF1cHZpc3NlVlBwQUtCbkxMTnkyakFNYkpkOW1FLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"20","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovSUxXV1pTQjQ2b3Nrc2oxdXB2aXNzZVZQcEFLQm5MTE55MmpBTWJKZDltRS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/ILWWZSB46osksj1upvisseVPpAKBnLLNy2jAMbJd9mE.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzQ5NTczOTEwOTQ4OTI5NjQ0MDU3MzIwMDMyOTMwMzc0NDgzNzkzNjQ4NTU4MTUyNDQ5Mjg2NDQ1NTY3NjQ2MzMzNTcxNjI5NTgwMjk0","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"660 HP","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L0p6M2xaU3NfOHAwLXcwb2JZUFNHNUt2M2ZVTVVkUi1yT21qUjBDSmNhbnMuanBn","url":"https://c.neevacdn.net/image/upload/xyz/Jz3lZSs_8p0-w0obYPSG5Kv3fUMUdR-rOmjR0CJcans.jpg","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L0p6M2xaU3NfOHAwLXcwb2JZUFNHNUt2M2ZVTVVkUi1yT21qUjBDSmNhbnMuanBn","url":"https://c.neevacdn.net/image/upload/xyz/Jz3lZSs_8p0-w0obYPSG5Kv3fUMUdR-rOmjR0CJcans.jpg","__typename":"Image"},"name":"MI","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovSnozbFpTc184cDAtdzBvYllQU0c1S3YzZlVNVWRSLXJPbWpSMENKY2Fucy53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/Jz3lZSs_8p0-w0obYPSG5Kv3fUMUdR-rOmjR0CJcans.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"49573910948929644057320032930374483793648558152449286445567646333571629580294","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovSnozbFpTc184cDAtdzBvYllQU0c1S3YzZlVNVWRSLXJPbWpSMENKY2Fucy53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/Jz3lZSs_8p0-w0obYPSG5Kv3fUMUdR-rOmjR0CJcans.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzQ5NTczOTEwOTQ4OTI5NjQ0MDU3MzIwMDMyOTMwMzc0NDgzNzkzNjQ4NTU4MTUyNDQ5Mjg2NDQ1NTY3NjQ2MzMyNDcyMTE3OTUyNTE2","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"975m2","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"name":"Kleefeld_TR","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"49573910948929644057320032930374483793648558152449286445567646332472117952516","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzQ5NTczOTEwOTQ4OTI5NjQ0MDU3MzIwMDMyOTMwMzc0NDgzNzkzNjQ4NTU4MTUyNDQ5Mjg2NDQ1NTY3NjQ2MzMxMzcyNjA2MzI0NzQw","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"975m2","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"name":"Kleefeld_CH","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"49573910948929644057320032930374483793648558152449286445567646331372606324740","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzQ5NTczOTEwOTQ4OTI5NjQ0MDU3MzIwMDMyOTMwMzc0NDgzNzkzNjQ4NTU4MTUyNDQ5Mjg2NDQ1NTY3NjQ2MzMwMjczMDk0Njk2OTY0","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"975 m2","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3Mta252NHRxTldtU2ZndHU5SkNBVFZZTE9TNHdYaTRMcW1uSm5LRHRDcEkuanBn","url":"https://c.neevacdn.net/image/upload/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.jpg","__typename":"Image"},"name":"Kleefeld","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"49573910948929644057320032930374483793648558152449286445567646330273094696964","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovcy1rbnY0dHFOV21TZmd0dTlKQ0FUVllMT1M0d1hpNExxbW5KbktEdENwSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/s-knv4tqNWmSfgtu9JCATVYLOS4wXi4LqmnJnKDtCpI.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg1YTZBMjhFNjc1MmI1NDI0QmUxNjhmNGQ3QzdmQzhjMjk2MjkxZDE0Xzc=","animationUrl":"https://c.neevacdn.net/video/upload/xyz/b-yQfrhae5mYYfCaJSe322Av0d4yCW1tTK24F58kg-A.mp3","collection":{"id":"TmZ0Q29sbGVjdGlvbjpqLXotei1lLWxvc2luZy1teS1taW5kLWZlYXQtZmFtZS1ibGFja2VyYS1mbGlnaHQtZ18weDVhNkEyOEU2NzUyYjU0MjRCZTE2OGY0ZDdDN2ZDOGMyOTYyOTFkMTQ=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy9mNTYxZjAyOWZiYzI5ZjE4NjdhZTljOWI2YTU0MGY3Ni5qcGc=","url":"https://i.seadn.io/gcs/files/f561f029fbc29f1867ae9c9b6a540f76.jpg","__typename":"Image"},"name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","twitterName":null,"nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg1YTZBMjhFNjc1MmI1NDI0QmUxNjhmNGQ3QzdmQzhjMjk2MjkxZDE0","address":"0x5a6A28E6752b5424Be168f4d7C7fC8c296291d14","chain":"ETHEREUM","name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","standard":"ERC721","symbol":"LMM","totalSupply":null,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDVhNmEyOGU2NzUyYjU0MjRiZTE2OGY0ZDdjN2ZjOGMyOTYyOTFkMTQ=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDA0LjEyMl9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"The song is about the struggles and pressures of trying to make ends meet and keep a roof over our heads. It's a reflection on the stress and anxiety that comes with trying to provide for loved ones while juggling bills and expenses. The NFT release is a way for me to share my music and experiences with a wider audience while also providing a unique and valuable collectible for fans and collectors. It's a reminder that we're all going through similar struggles and that we're not alone in our struggles to make it through.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC92aWRlby91cGxvYWQveHl6L2IteVFmcmhhZTVtWVlmQ2FKU2UzMjJBdjBkNHlDVzF0VEsyNEY1OGtnLUEubXAz","url":"https://c.neevacdn.net/video/upload/xyz/b-yQfrhae5mYYfCaJSe322Av0d4yCW1tTK24F58kg-A.mp3","__typename":"Image"},"name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"7","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg1YTZBMjhFNjc1MmI1NDI0QmUxNjhmNGQ3QzdmQzhjMjk2MjkxZDE0XzA=","animationUrl":"https://c.neevacdn.net/video/upload/xyz/b-yQfrhae5mYYfCaJSe322Av0d4yCW1tTK24F58kg-A.mp3","collection":{"id":"TmZ0Q29sbGVjdGlvbjpqLXotei1lLWxvc2luZy1teS1taW5kLWZlYXQtZmFtZS1ibGFja2VyYS1mbGlnaHQtZ18weDVhNkEyOEU2NzUyYjU0MjRCZTE2OGY0ZDdDN2ZDOGMyOTYyOTFkMTQ=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy9mNTYxZjAyOWZiYzI5ZjE4NjdhZTljOWI2YTU0MGY3Ni5qcGc=","url":"https://i.seadn.io/gcs/files/f561f029fbc29f1867ae9c9b6a540f76.jpg","__typename":"Image"},"name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","twitterName":null,"nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg1YTZBMjhFNjc1MmI1NDI0QmUxNjhmNGQ3QzdmQzhjMjk2MjkxZDE0","address":"0x5a6A28E6752b5424Be168f4d7C7fC8c296291d14","chain":"ETHEREUM","name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","standard":"ERC721","symbol":"LMM","totalSupply":null,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDVhNmEyOGU2NzUyYjU0MjRiZTE2OGY0ZDdjN2ZjOGMyOTYyOTFkMTQ=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDA0LjEyM19FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"The song is about the struggles and pressures of trying to make ends meet and keep a roof over our heads. It's a reflection on the stress and anxiety that comes with trying to provide for loved ones while juggling bills and expenses. The NFT release is a way for me to share my music and experiences with a wider audience while also providing a unique and valuable collectible for fans and collectors. It's a reminder that we're all going through similar struggles and that we're not alone in our struggles to make it through.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC92aWRlby91cGxvYWQveHl6L2IteVFmcmhhZTVtWVlmQ2FKU2UzMjJBdjBkNHlDVzF0VEsyNEY1OGtnLUEubXAz","url":"https://c.neevacdn.net/video/upload/xyz/b-yQfrhae5mYYfCaJSe322Av0d4yCW1tTK24F58kg-A.mp3","__typename":"Image"},"name":"J@z-Z-E - Losing My Mind (Feat. Fame Blackera & Flight G)","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"0","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovUVVrejNhNzFsZkFQMDgxTXoxTndYSXFrcXVlM2pXQmJDeG1TckN3U09qSS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/QUkz3a71lfAP081Mz1NwXIqkque3jWBbCxmSrCwSOjI.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHhkZjQ0NjRhMjM3OWVFMUJENDhhOTVDZkViRUI0RTA3YzUyZjc5ZEEyXzEz","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweGRmNDQ2NGEyMzc5ZWUxYmQ0OGE5NWNmZWJlYjRlMDdjNTJmNzlkYTI=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy9hZTc3MzU5NWFmN2FkMGYyYzNkYzhhYTBlZGE4NDJhNi5wbmc/dz01MDAmYXV0bz1mb3JtYXQ=","url":"https://i.seadn.io/gcs/files/ae773595af7ad0f2c3dc8aa0eda842a6.png?w=500&auto=format","__typename":"Image"},"name":"Simple Gravestone","twitterName":"metaGraveGo","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHhkZjQ0NjRhMjM3OWVlMWJkNDhhOTVjZmViZWI0ZTA3YzUyZjc5ZGEy","address":"0xdf4464a2379ee1bd48a95cfebeb4e07c52f79da2","chain":"ETHEREUM","name":"Simple Gravestone","standard":"ERC721","symbol":"SimpleGraveStone","totalSupply":381,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweGRmNDQ2NGEyMzc5ZWUxYmQ0OGE5NWNmZWJlYjRlMDdjNTJmNzlkYTI=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MC4wNV8xNjg0NDE5NDE5LjkzOV9FVEg=","value":0.05,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"Simple Gravestone","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1kwRWtyR3Zad2NKR3FiY1lTODdkRTFLTXNjS0wxc2RjWjNzbUxxRHBFZkUucG5n","url":"https://c.neevacdn.net/image/upload/xyz/Y0EkrGvZwcJGqbcYS87dE1KMscKL1sdcZ3smLqDpEfE.png","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1kwRWtyR3Zad2NKR3FiY1lTODdkRTFLTXNjS0wxc2RjWjNzbUxxRHBFZkUucG5n","url":"https://c.neevacdn.net/image/upload/xyz/Y0EkrGvZwcJGqbcYS87dE1KMscKL1sdcZ3smLqDpEfE.png","__typename":"Image"},"name":"SimpleGraveStone","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9ZMEVrckd2WndjSkdxYmNZUzg3ZEUxS01zY0tMMXNkY1ozc21McURwRWZFLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/Y0EkrGvZwcJGqbcYS87dE1KMscKL1sdcZ3smLqDpEfE.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"13","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovWTBFa3JHdlp3Y0pHcWJjWVM4N2RFMUtNc2NLTDFzZGNaM3NtTHFEcEVmRS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/Y0EkrGvZwcJGqbcYS87dE1KMscKL1sdcZ3smLqDpEfE.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OEEwNTAxRDY3ZWIwRGNjY0M3MjVlMzNGQTQzZTA4RGUwNDU4MTZGXzIyMjM=","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ4YTA1MDFkNjdlYjBkY2NjYzcyNWUzM2ZhNDNlMDhkZTA0NTgxNmY=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2djcy9maWxlcy81YjRjOTk3NWQyMWM1ZDM4YTcyMmJiOTdkNDllYTFhYi5wbmc/dz01MDAmYXV0bz1mb3JtYXQ=","url":"https://i.seadn.io/gcs/files/5b4c9975d21c5d38a722bb97d49ea1ab.png?w=500&auto=format","__typename":"Image"},"name":"Bear Market Buds","twitterName":"bearmarketbuds","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OGEwNTAxZDY3ZWIwZGNjY2M3MjVlMzNmYTQzZTA4ZGUwNDU4MTZm","address":"0x48a0501d67eb0dcccc725e33fa43e08de045816f","chain":"ETHEREUM","name":"Bear Market Buds","standard":"ERC721","symbol":"BMBUDS","totalSupply":6960,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ4YTA1MDFkNjdlYjBkY2NjYzcyNWUzM2ZhNDNlMDhkZTA0NTgxNmY=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MC4wMDA4MDAwMDAwMDAwMDAwMDFfMTY4NDc1MzQ1MS40NzZfRVRI","value":8.00000000000001E-4,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"6969 Buds to hold your hand while you cry yourself to sleep at night after spending all your money on shitty jpegs. Free Mint.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3gzWHZpd2FVYmltaXpHeTdlakxkVFpVVE9nT3d6QVdVU2V1WGRzSTZQT28ucG5n","url":"https://c.neevacdn.net/image/upload/xyz/x3XviwaUbimizGy7ejLdTZUTOgOwzAWUSeuXdsI6POo.png","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3gzWHZpd2FVYmltaXpHeTdlakxkVFpVVE9nT3d6QVdVU2V1WGRzSTZQT28ucG5n","url":"https://c.neevacdn.net/image/upload/xyz/x3XviwaUbimizGy7ejLdTZUTOgOwzAWUSeuXdsI6POo.png","__typename":"Image"},"name":"Bear Market Buds #2223","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXoveDNYdml3YVViaW1pekd5N2VqTGRUWlVUT2dPd3pBV1VTZXVYZHNJNlBPby53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/x3XviwaUbimizGy7ejLdTZUTOgOwzAWUSeuXdsI6POo.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"2223","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXoveDNYdml3YVViaW1pekd5N2VqTGRUWlVUT2dPd3pBV1VTZXVYZHNJNlBPby53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/x3XviwaUbimizGy7ejLdTZUTOgOwzAWUSeuXdsI6POo.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzc0NzA5NTMzODUxMTk1MDYwMDQ1MDA1MTQ5NzY2NDU1MzgzMjQ5NTk0MjIyMTc4NzgxNTM1Njg4ODg5MDc2MDA1NjcyNTQxMjI1MDYw","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":null,"flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3pXcWN4bFdFMm85TUpJNlp5UEw0c1pkZG41YmxscmJYdW1laGlkUVAtYlUucG5n","url":"https://c.neevacdn.net/image/upload/xyz/zWqcxlWE2o9MJI6ZyPL4sZddn5bllrbXumehidQP-bU.png","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L3pXcWN4bFdFMm85TUpJNlp5UEw0c1pkZG41YmxscmJYdW1laGlkUVAtYlUucG5n","url":"https://c.neevacdn.net/image/upload/xyz/zWqcxlWE2o9MJI6ZyPL4sZddn5bllrbXumehidQP-bU.png","__typename":"Image"},"name":"Crypto Monkz #2","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXoveldxY3hsV0UybzlNSkk2WnlQTDRzWmRkbjVibGxyYlh1bWVoaWRRUC1iVS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/zWqcxlWE2o9MJI6ZyPL4sZddn5bllrbXumehidQP-bU.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"74709533851195060045005149766455383249594222178781535688889076005672541225060","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXoveldxY3hsV0UybzlNSkk2WnlQTDRzWmRkbjVibGxyYlh1bWVoaWRRUC1iVS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/zWqcxlWE2o9MJI6ZyPL4sZddn5bllrbXumehidQP-bU.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzc0NzA5NTMzODUxMTk1MDYwMDQ1MDA1MTQ5NzY2NDU1MzgzMjQ5NTk0MjIyMTc4NzgxNTM1Njg4ODg5MDc2MDA3ODcxNTY0NDgwNjEy","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":null,"flaggedBy":null,"image":null,"originalImage":null,"name":null,"ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":null,"suspiciousFlag":null,"tokenId":"74709533851195060045005149766455383249594222178781535688889076007871564480612","thumbnail":null,"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Q2U2NDZmNjhBQzhjMjQ4NDIwMDQ1Y2I3YjVlXzc0NzA5NTMzODUxMTk1MDYwMDQ1MDA1MTQ5NzY2NDU1MzgzMjQ5NTk0MjIyMTc4NzgxNTM1Njg4ODg5MDc2MDA4OTcxMDc2MTA4Mzg4","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9vcGVuc2VhdXNlcmRhdGEuY29tL2ZpbGVzLzg2MGI5NGJlZTA3OWUzODY0ZDA0ODQ5MzgzZDJiNGQxLmJpbg==","url":"https://openseauserdata.com/files/860b94bee079e3864d04849383d2b4d1.bin","__typename":"Image"},"name":"OS Shared Storefront Collection","twitterName":"ApeGangNFT","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg0OTVmOTQ3Mjc2NzQ5Y2U2NDZmNjhhYzhjMjQ4NDIwMDQ1Y2I3YjVl","address":"0x495f947276749ce646f68ac8c248420045cb7b5e","chain":"ETHEREUM","name":"OS Shared Storefront Collection","standard":"ERC1155","symbol":"OPENSTORE","totalSupply":62,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDQ5NWY5NDcyNzY3NDljZTY0NmY2OGFjOGMyNDg0MjAwNDVjYjdiNWU=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjY5MTM5MDMxLjA2OF9FVEg=","value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":null,"flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1c1Q2xkU3AxYlRacVVwQkFBZXBrc2YxWjNGLXcxcWFmZDE5Z21jOUduVTAucG5n","url":"https://c.neevacdn.net/image/upload/xyz/W5CldSp1bTZqUpBAAepksf1Z3F-w1qafd19gmc9GnU0.png","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1c1Q2xkU3AxYlRacVVwQkFBZXBrc2YxWjNGLXcxcWFmZDE5Z21jOUduVTAucG5n","url":"https://c.neevacdn.net/image/upload/xyz/W5CldSp1bTZqUpBAAepksf1Z3F-w1qafd19gmc9GnU0.png","__typename":"Image"},"name":"Crypto Monkz #209","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovVzVDbGRTcDFiVFpxVXBCQUFlcGtzZjFaM0YtdzFxYWZkMTlnbWM5R25VMC53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/W5CldSp1bTZqUpBAAepksf1Z3F-w1qafd19gmc9GnU0.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"74709533851195060045005149766455383249594222178781535688889076008971076108388","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMjAwLHdfMjAwL2Zfd2VicC94eXovVzVDbGRTcDFiVFpxVXBCQUFlcGtzZjFaM0YtdzFxYWZkMTlnbWM5R25VMC53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_200,w_200/f_webp/xyz/W5CldSp1bTZqUpBAAepksf1Z3F-w1qafd19gmc9GnU0.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"},{"node":{"ownedAsset":{"id":"TmZ0QXNzZXQ6RVRIRVJFVU1fMHg5QTk1ZUNFZTUxNjFiODg4ZkZFOUFiZDNEOTIwYzVEMzhlODUzOWRBXzM4Njk=","animationUrl":null,"collection":{"id":"TmZ0Q29sbGVjdGlvbjoweDlhOTVlY2VlNTE2MWI4ODhmZmU5YWJkM2Q5MjBjNWQzOGU4NTM5ZGE=","isVerified":false,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9pLnNlYWRuLmlvL2dhZS9UYnVXVnpydm5EMTdnWjBiN3d2R2xKVFlual9KZU9KMDNMdlJtTUdWSklSNlRNTjNhQTlPNEtCVzhlLUZETnFNbUJKZXd3ZUlXNHg4ZkxvaDI5eVJSLThBelZhWVlBd2pCSzFHUWc/dz01MDAmYXV0bz1mb3JtYXQ=","url":"https://i.seadn.io/gae/TbuWVzrvnD17gZ0b7wvGlJTYnj_JeOJ03LvRmMGVJIR6TMN3aA9O4KBW8e-FDNqMmBJewweIW4x8fLoh29yRR-8AzVaYYAwjBK1GQg?w=500&auto=format","__typename":"Image"},"name":"Basement Dwellers","twitterName":"basementnft","nftContracts":[{"id":"TmZ0Q29udHJhY3Q6RVRIRVJFVU1fMHg5YTk1ZWNlZTUxNjFiODg4ZmZlOWFiZDNkOTIwYzVkMzhlODUzOWRh","address":"0x9a95ecee5161b888ffe9abd3d920c5d38e8539da","chain":"ETHEREUM","name":"Basement Dwellers","standard":"ERC721","symbol":"DWEL","totalSupply":9834,"__typename":"NftContract"}],"markets":[{"id":"TmZ0Q29sbGVjdGlvbk1hcmtldDoweDlhOTVlY2VlNTE2MWI4ODhmZmU5YWJkM2Q5MjBjNWQzOGU4NTM5ZGE=","floorPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MC4wMDRfMTY4NDgwNDEzOC41NDVfRVRI","value":0.004,"__typename":"TimestampedAmount"},"__typename":"NftCollectionMarket"}],"__typename":"NftCollection"},"description":"Basement Dwellers.","flaggedBy":null,"image":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1BvNVRDUlpUNXNiU3h6d0l2ZDRIZUI4bDJNVFh2dTUycVZWQ1huekZsQ00uanBn","url":"https://c.neevacdn.net/image/upload/xyz/Po5TCRZT5sbSxzwIvd4HeB8l2MTXvu52qVVCXnzFlCM.jpg","__typename":"Image"},"originalImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQveHl6L1BvNVRDUlpUNXNiU3h6d0l2ZDRIZUI4bDJNVFh2dTUycVZWQ1huekZsQ00uanBn","url":"https://c.neevacdn.net/image/upload/xyz/Po5TCRZT5sbSxzwIvd4HeB8l2MTXvu52qVVCXnzFlCM.jpg","__typename":"Image"},"name":"#3869","ownerAddress":"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266","smallImage":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfMTIwMCx3XzEyMDAvZl93ZWJwL3h5ei9QbzVUQ1JaVDVzYlN4endJdmQ0SGVCOGwyTVRYdnU1MnFWVkNYbnpGbENNLndlYnA=","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_1200,w_1200/f_webp/xyz/Po5TCRZT5sbSxzwIvd4HeB8l2MTXvu52qVVCXnzFlCM.webp","__typename":"Image"},"suspiciousFlag":null,"tokenId":"3869","thumbnail":{"id":"SW1hZ2U6aHR0cHM6Ly9jLm5lZXZhY2RuLm5ldC9pbWFnZS91cGxvYWQvY19saW1pdCxwZ18xLGhfNjAwLHdfNjAwL2Zfd2VicC94eXovUG81VENSWlQ1c2JTeHp3SXZkNEhlQjhsMk1UWHZ1NTJxVlZDWG56RmxDTS53ZWJw","url":"https://c.neevacdn.net/image/upload/c_limit,pg_1,h_600,w_600/f_webp/xyz/Po5TCRZT5sbSxzwIvd4HeB8l2MTXvu52qVVCXnzFlCM.webp","__typename":"Image"},"listings":{"edges":[],"__typename":"NftOrderConnection"},"__typename":"NftAsset"},"listedMarketplaces":[],"listingFees":null,"lastPrice":{"id":"VGltZXN0YW1wZWRBbW91bnQ6MF8xNjg1MTUyMDAzX0VUSA==","currency":"ETH","timestamp":1685152003,"value":0.0,"__typename":"TimestampedAmount"},"__typename":"NftBalance"},"__typename":"NftBalanceEdge"}],"pageInfo":{"endCursor":"","hasNextPage":false,"hasPreviousPage":false,"startCursor":"","__typename":"PageInfo"},"__typename":"NftBalanceConnection"}}} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/mini-portfolio/pools.json b/apps/web/cypress/fixtures/mini-portfolio/pools.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/pools.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/mini-portfolio/tokens.json b/apps/web/cypress/fixtures/mini-portfolio/tokens.json new file mode 100644 index 0000000..db3ed50 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/tokens.json @@ -0,0 +1 @@ +{"data":{"portfolios":[{"id":"UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==","tokensTotalDenominatedValue":{"id":"QW1vdW50OjEuMjA3OTE0NzY1MzE3NDY0N19VU0Q=","value":1.2079147653174647,"__typename":"Amount"},"tokensTotalDenominatedValueChange":{"absolute":{"id":"QW1vdW50OjAuMDA1MDQ0NzA4NjQzOTkxNzU4X1VTRA==","value":0.005044708643991758,"__typename":"Amount"},"percentage":{"id":"QW1vdW50OjAuNDE5Mzg5MzI3NzE2OTgxOV9VU0Q=","value":0.4193893277169819,"__typename":"Amount"},"__typename":"AmountChange"},"tokenBalances":[{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IZzRZamt6TjJGbU56RTBZV00zWlRJeE1qbGlaRE16WkRrek5qUXhaalV5WWpZMk5XTmhNelV5X1VTRA==","quantity":201225.0,"denominatedValue":{"id":"QW1vdW50OjAuNjExNzI0X1VTRA==","currency":"USD","value":0.611724,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0T0dJNU16ZGhaamN4TkdGak4yVXlNVEk1WW1Rek0yUTVNelkwTVdZMU1tSTJOalZqWVRNMU1nPT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjIuMDMxODM5MDg1OTQxOTFfVVNE","value":2.03183908594191,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4OGI5MzdhZjcxNGFjN2UyMTI5YmQzM2Q5MzY0MWY1MmI2NjVjYTM1Mg==","logoUrl":"https://assets.coingecko.com/coins/images/28985/large/bLQvXuh_%281%29.png?1675818176","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHg4YjkzN2FmNzE0YWM3ZTIxMjliZDMzZDkzNjQxZjUyYjY2NWNhMzUy","chain":"ETHEREUM","address":"0x8b937af714ac7e2129bd33d93641f52b665ca352","name":"JizzRocket","symbol":"JIZZ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlQxQlVTVTFKVTAxZmJuVnNiQT09X1VTRA==","quantity":9.7394477591879E-5,"denominatedValue":{"id":"QW1vdW50OjAuMTc3OTU1MjkzNjc2Nzc3NjVfVVNE","currency":"USD","value":0.17795529367677765,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YMjUxYkd3PV9VU0Q=","pricePercentChange":{"id":"QW1vdW50OjEuMzYxOTQyMTY3NjkxNTg2Nl9VU0Q=","value":1.3619421676915866,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46T1BUSU1JU01fbnVsbA==","chain":"OPTIMISM","address":null,"name":"Ether","symbol":"ETH","standard":"NATIVE","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IZzJZakUzTlRRM05HVTRPVEE1TkdNME5HUmhPVGhpT1RVMFpXVmtaV0ZqTkRrMU1qY3haREJtX1VTRA==","quantity":0.1478516211722401,"denominatedValue":{"id":"QW1vdW50OjAuMTQ3Nzg3NjAxNDIwMjcyNTNfVVNE","currency":"USD","value":0.14778760142027253,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0Tm1JeE56VTBOelJsT0Rrd09UUmpORFJrWVRrNFlqazFOR1ZsWkdWaFl6UTVOVEkzTVdRd1pnPT1fVVNE","pricePercentChange":{"id":"QW1vdW50Oi0wLjA5MTgyNTA5NzMxNzkxNjA0X1VTRA==","value":-0.09182509731791604,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NmIxNzU0NzRlODkwOTRjNDRkYTk4Yjk1NGVlZGVhYzQ5NTI3MWQwZg==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHg2YjE3NTQ3NGU4OTA5NGM0NGRhOThiOTU0ZWVkZWFjNDk1MjcxZDBm","chain":"ETHEREUM","address":"0x6b175474e89094c44da98b954eedeac495271d0f","name":"Dai Stablecoin","symbol":"DAI","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaGtNamczTnpjd01qWTNOV1UyWTJWaU9UYzFZalJoTVdSbVpqbG1ZamRpWVdZMFl6a3haV0U1X1VTRA==","quantity":1554.8517132200002,"denominatedValue":{"id":"QW1vdW50OjAuMTMzODI2MDg2OTU2ODQ1NDJfVVNE","currency":"USD","value":0.13382608695684542,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WkRJNE56YzNNREkyTnpWbE5tTmxZamszTldJMFlURmtabVk1Wm1JM1ltRm1OR001TVdWaE9RPT1fVVNE","pricePercentChange":{"id":"QW1vdW50Oi03LjU4OTI0ODkzMTA5NjcwOTVfVVNE","value":-7.5892489310967095,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ZDI4Nzc3MDI2NzVlNmNlYjk3NWI0YTFkZmY5ZmI3YmFmNGM5MWVhOQ==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xd2877702675e6cEb975b4A1dFf9fb7BAF4C91ea9/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhkMjg3NzcwMjY3NWU2Y2ViOTc1YjRhMWRmZjlmYjdiYWY0YzkxZWE5","chain":"ETHEREUM","address":"0xd2877702675e6ceb975b4a1dff9fb7baf4c91ea9","name":"Wrapped LUNA Token","symbol":"LUNA","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TXpFME56RmxNRGM1TVdaalpHSmxPREptWW1ZMFl6UTBPVFF6TWpVMVpUa3lNMll4WWpjNU5BPT1fVVNE","quantity":5.0,"denominatedValue":{"id":"QW1vdW50OjAuMDYxNTg3Ml9VU0Q=","currency":"USD","value":0.0615872,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VETXhORGN4WlRBM09URm1ZMlJpWlRneVptSm1OR00wTkRrME16STFOV1U1TWpObU1XSTNPVFE9X1VTRA==","pricePercentChange":{"id":"QW1vdW50OjEuNTcwNTc1NDc1ODc4NzIwM19VU0Q=","value":1.5705754758787203,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDMxNDcxZTA3OTFmY2RiZTgyZmJmNGM0NDk0MzI1NWU5MjNmMWI3OTQ=","logoUrl":"https://assets.coingecko.com/coins/images/17461/thumb/token-200x200.png?1627883446","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MzE0NzFlMDc5MWZjZGJlODJmYmY0YzQ0OTQzMjU1ZTkyM2YxYjc5NA==","chain":"BNB","address":"0x31471e0791fcdbe82fbf4c44943255e923f1b794","name":"Plant vs Undead Token","symbol":"PVU","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlQxQlVTVTFKVTAxZk1IaGpOVEV3TW1abE9UTTFPV1prT1dFeU9HWTROemRoTmpkbE16WmlNR1l3TlRCa09ERmhNMk5qX1VTRA==","quantity":0.4210036471409005,"denominatedValue":{"id":"QW1vdW50OjAuMDM1MTQ5NTk0NDk5NzkzNzhfVVNE","currency":"USD","value":0.03514959449979378,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WXpVeE1ESm1aVGt6TlRsbVpEbGhNamhtT0RjM1lUWTNaVE0yWWpCbU1EVXdaRGd4WVROall3PT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjAuNjkwOTIzNTEwNzEyMDczNl9VU0Q=","value":0.6909235107120736,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzUxMDJmZTkzNTlmZDlhMjhmODc3YTY3ZTM2YjBmMDUwZDgxYTNjYw==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xc5102fE9359FD9a28f877a67E36B0F050d81a3CC/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46T1BUSU1JU01fMHhjNTEwMmZlOTM1OWZkOWEyOGY4NzdhNjdlMzZiMGYwNTBkODFhM2Nj","chain":"OPTIMISM","address":"0xc5102fe9359fd9a28f877a67e36b0f050d81a3cc","name":"Hop","symbol":"HOP","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TVdReVpqQmtZVEUyT1dObFlqbG1ZemRpTXpFME5EWXlPR1JpTVRVMlpqTm1ObU0yTUdSaVpRPT1fVVNE","quantity":0.047754171300298506,"denominatedValue":{"id":"QW1vdW50OjAuMDIyMzE2ODYxMzY1NDI1OV9VU0Q=","currency":"USD","value":0.0223168613654259,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VERmtNbVl3WkdFeE5qbGpaV0k1Wm1NM1lqTXhORFEyTWpoa1lqRTFObVl6Wmpaak5qQmtZbVU9X1VTRA==","pricePercentChange":{"id":"QW1vdW50OjAuOTU0NDc1MjU4MTI3NTQxMl9VU0Q=","value":0.9544752581275412,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDFkMmYwZGExNjljZWI5ZmM3YjMxNDQ2MjhkYjE1NmYzZjZjNjBkYmU=","logoUrl":"https://assets.coingecko.com/coins/images/15458/thumb/ryyrCapt_400x400.jpg?1620895978","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MWQyZjBkYTE2OWNlYjlmYzdiMzE0NDYyOGRiMTU2ZjNmNmM2MGRiZQ==","chain":"BNB","address":"0x1d2f0da169ceb9fc7b3144628db156f3f6c60dbe","name":"XRP Token","symbol":"XRP","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFWSkNTVlJTVlUxZmJuVnNiQT09X1VTRA==","quantity":8.9428E-6,"denominatedValue":{"id":"QW1vdW50OjAuMDE2MzM5OTI2NDQ4X1VTRA==","currency":"USD","value":0.016339926448,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YMjUxYkd3PV9VU0Q=","pricePercentChange":{"id":"QW1vdW50OjEuMzYxOTQyMTY3NjkxNTg2Nl9VU0Q=","value":1.3619421676915866,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46QVJCSVRSVU1fbnVsbA==","chain":"ARBITRUM","address":null,"name":"Ether","symbol":"ETH","standard":"NATIVE","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IZzRZV1kxWm1Wa1l6Qm1Nall6T0RReFl6RTRaak14WkRsa1ltTmpPVGRoTkRkbE1XRmlORFl5X1VTRA==","quantity":1500.0,"denominatedValue":{"id":"QW1vdW50OjAuMDAxMjI3OTczNV9VU0Q=","currency":"USD","value":0.0012279735,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0T0dGbU5XWmxaR013WmpJMk16ZzBNV014T0dZek1XUTVaR0pqWXprM1lUUTNaVEZoWWpRMk1nPT1fVVNE","pricePercentChange":{"id":"QW1vdW50Oi0yLjA2MTQxNTU1NTMyOTkzX1VTRA==","value":-2.06141555532993,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4OGFmNWZlZGMwZjI2Mzg0MWMxOGYzMWQ5ZGJjYzk3YTQ3ZTFhYjQ2Mg==","logoUrl":"https://assets.coingecko.com/coins/images/25957/large/MESSIERlogonew_%281%29.png?1666773848","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHg4YWY1ZmVkYzBmMjYzODQxYzE4ZjMxZDlkYmNjOTdhNDdlMWFiNDYy","chain":"ETHEREUM","address":"0x8af5fedc0f263841c18f31d9dbcc97a47e1ab462","name":"MESSIER","symbol":"M87","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHVXdNek01WXpnd1ptWmtaVGt4WmpObE1qQTBPVFJrWmpnNFpEUXlNRFprT0RZd01qUmpaR1k9X1VTRA==","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjIuMDM0MzFlLTdfVVNE","currency":"USD","value":2.03431E-7,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0TnpZeFpETTRaVFZrWkdZMlkyTm1ObU5tTjJNMU5UYzFPV1ExTWpFd056VXdZalZrTmpCbU13PT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjMuMDc4Mzk0ODU1ODI0ODQzN19VU0Q=","value":3.0783948558248437,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NzYxZDM4ZTVkZGY2Y2NmNmNmN2M1NTc1OWQ1MjEwNzUwYjVkNjBmMw==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x761D38e5ddf6ccf6Cf7c55759d5210750B5D60F3/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGUwMzM5YzgwZmZkZTkxZjNlMjA0OTRkZjg4ZDQyMDZkODYwMjRjZGY=","chain":"POLYGON","address":"0xe0339c80ffde91f3e20494df88d4206d86024cdf","name":"Dogelon (PoS)","symbol":"ELON","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFWSkNTVlJTVlUxZk1IZ3hOVFZtTUdSa01EUTBNalE1TXprek5qZzVOekptTkdVeE9ETTROamczWkRaaE9ETXhNVFV4X1VTRA==","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjIuMjYyOWUtOF9VU0Q=","currency":"USD","value":2.2629E-8,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0ZTUWtsVVVsVk5YekI0TVRVMVpqQmtaREEwTkRJME9UTTVNelk0T1RjeVpqUmxNVGd6T0RZNE4yUTJZVGd6TVRFMU1RPT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjQuODg5OTc2Mzk2ODA1MjVfVVNE","value":4.88997639680525,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkFSQklUUlVNXzB4MTU1ZjBkZDA0NDI0OTM5MzY4OTcyZjRlMTgzODY4N2Q2YTgzMTE1MQ==","logoUrl":"https://assets.coingecko.com/coins/images/18333/large/Screen-Shot-2021-09-04-at-11-59-16-AM.png?1631584395","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46QVJCSVRSVU1fMHgxNTVmMGRkMDQ0MjQ5MzkzNjg5NzJmNGUxODM4Njg3ZDZhODMxMTUx","chain":"ARBITRUM","address":"0x155f0dd04424939368972f4e1838687d6a831151","name":"ArbiDoge","symbol":"ADoge","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WVRVNE9UVXdaakExWm1WaE1qSTNOMlF5TmpBNE56UTROREV5WW1ZNVpqZ3dNbVZoTkRrd01RPT1fVVNE","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjEuMzcyZS05X1VTRA==","currency":"USD","value":1.372E-9,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6WXpGaVlqTTVZbUkyT1RaaU5EUXpZVEZrT0RCaVlqSmlNMkV6WkRrMU1HSmhPV1JsWlRnM19VU0Q=","pricePercentChange":{"id":"QW1vdW50OjIuMzYyMzgzNzA0Nzc3NzQ0X1VTRA==","value":2.362383704777744,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzYzFiYjM5YmI2OTZiNDQzYTFkODBiYjJiM2EzZDk1MGJhOWRlZTg3","logoUrl":"https://assets.coingecko.com/coins/images/15872/large/X3Awe42.png?1622181358","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YTU4OTUwZjA1ZmVhMjI3N2QyNjA4NzQ4NDEyYmY5ZjgwMmVhNDkwMQ==","chain":"BNB","address":"0xa58950f05fea2277d2608748412bf9f802ea4901","name":"Wall Street Games","symbol":"WSG","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZmJuVnNiQT09X1VTRA==","quantity":1.0011E-14,"denominatedValue":{"id":"QW1vdW50OjEuODI5MTY5ODc2ZS0xMV9VU0Q=","currency":"USD","value":1.829169876E-11,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YMjUxYkd3PV9VU0Q=","pricePercentChange":{"id":"QW1vdW50OjEuMzYxOTQyMTY3NjkxNTg2Nl9VU0Q=","value":1.3619421676915866,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNX251bGw=","logoUrl":"https://token-icons.s3.amazonaws.com/eth.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fbnVsbA==","chain":"ETHEREUM","address":null,"name":"Ether","symbol":"ETH","standard":"NATIVE","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01EQXdNREF3TURBd01ERXdNVEE9X1VTRA==","quantity":6.345E-14,"denominatedValue":{"id":"QW1vdW50OjUuNzgyNDcxMzM0OTk5OTk5ZS0xNF9VU0Q=","currency":"USD","value":5.782471334999999E-14,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0TjJReFlXWmhOMkkzTVRobVlqZzVNMlJpTXpCaE0yRmlZekJqWm1NMk1EaGhZV05tWldKaU1BPT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjEuOTQ2Nzg3NTIwNjMyOTE5NV9VU0Q=","value":1.9467875206329195,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4N2QxYWZhN2I3MThmYjg5M2RiMzBhM2FiYzBjZmM2MDhhYWNmZWJiMA==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDEwMTA=","chain":"POLYGON","address":"0x0000000000000000000000000000000000001010","name":"MATIC","symbol":"MATIC","standard":"NATIVE","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TUdVd09XWmhZbUkzTTJKa00yRmtaVEJoTVRkbFkyTXpNakZtWkRFellURTVaVGd4WTJVNE1nPT1fVVNE","quantity":2.0E-18,"denominatedValue":{"id":"QW1vdW50OjMuMTAwMDAwMDAwMDAwMDAwNWUtMThfVVNE","currency":"USD","value":3.1000000000000005E-18,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0TVRVeU5qUTVaV0UzTTJKbFlXSXlPR00xWWpRNVlqSTJaV0kwT0dZM1pXRmtObVEwWXpnNU9BPT1fVVNE","pricePercentChange":{"id":"QW1vdW50OjEuMzg2ODI3Mzk5NTA2MjM1Ml9VU0Q=","value":1.3868273995062352,"__typename":"Amount"},"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MTUyNjQ5ZWE3M2JlYWIyOGM1YjQ5YjI2ZWI0OGY3ZWFkNmQ0Yzg5OA==","logoUrl":"https://assets.coingecko.com/coins/images/12632/large/pancakeswap-cake-logo_%281%29.png?1629359065","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MGUwOWZhYmI3M2JkM2FkZTBhMTdlY2MzMjFmZDEzYTE5ZTgxY2U4Mg==","chain":"BNB","address":"0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82","name":"PancakeSwap Token","symbol":"Cake","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRSa04yRm1ZVGRqTnpZMFpURmpaalptWW1WbVpUbG1aamd5TXpsak1HSXhNVGRtWkRjd1pBPT1fVVNE","quantity":1.0E-17,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVTBaRGRoWm1FM1l6YzJOR1V4WTJZMlptSmxabVU1Wm1ZNE1qTTVZekJpTVRFM1ptUTNNR1E9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDU0ZDdhZmE3Yzc2NGUxY2Y2ZmJlZmU5ZmY4MjM5YzBiMTE3ZmQ3MGQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTRkN2FmYTdjNzY0ZTFjZjZmYmVmZTlmZjgyMzljMGIxMTdmZDcwZA==","chain":"BNB","address":"0x54d7afa7c764e1cf6fbefe9ff8239c0b117fd70d","name":"FAST PROFIT","symbol":"FP","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TURNNE1EZ3lOV1prTWpZMVpXUXlabVE1TmpJeFlUYzJOemd3TnpZM016WTRPRFJpWkdVd013PT1fVVNE","quantity":490.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEQXpPREE0TWpWbVpESTJOV1ZrTW1aa09UWXlNV0UzTmpjNE1EYzJOek0yT0RnMFltUmxNRE09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDAzODA4MjVmZDI2NWVkMmZkOTYyMWE3Njc4MDc2NzM2ODg0YmRlMDM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MDM4MDgyNWZkMjY1ZWQyZmQ5NjIxYTc2NzgwNzY3MzY4ODRiZGUwMw==","chain":"BNB","address":"0x0380825fd265ed2fd9621a7678076736884bde03","name":"AP","symbol":"AP","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TURRMlpqVXpOelF4T0RWaE1EQmpOMk16WldGa1pEQm1PVFpqTmpBek9HSTNaRFZtTURRM1pBPT1fVVNE","quantity":3.1613242271396022E7,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEQTBObVkxTXpjME1UZzFZVEF3WXpkak0yVmhaR1F3WmprMll6WXdNemhpTjJRMVpqQTBOMlE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDA0NmY1Mzc0MTg1YTAwYzdjM2VhZGQwZjk2YzYwMzhiN2Q1ZjA0N2Q=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MDQ2ZjUzNzQxODVhMDBjN2MzZWFkZDBmOTZjNjAzOGI3ZDVmMDQ3ZA==","chain":"BNB","address":"0x046f5374185a00c7c3eadd0f96c6038b7d5f047d","name":"ChibaShiba","symbol":"CSHIBA","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TURVeE5EVmtaV0ptTXpkaFlURTRaV0ZrWkdJMU5qUTFORFUzWTJNMVlUZzRPV1l3TWpsbFpRPT1fVVNE","quantity":0.1,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEQTFNVFExWkdWaVpqTTNZV0V4T0dWaFpHUmlOVFkwTlRRMU4yTmpOV0U0T0RsbU1ESTVaV1U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDA1MTQ1ZGViZjM3YWExOGVhZGRiNTY0NTQ1N2NjNWE4ODlmMDI5ZWU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MDUxNDVkZWJmMzdhYTE4ZWFkZGI1NjQ1NDU3Y2M1YTg4OWYwMjllZQ==","chain":"BNB","address":"0x05145debf37aa18eaddb5645457cc5a889f029ee","name":"TestToken8","symbol":"TT8","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TUdNeU9XWmpOemczWlRRNU9UVm1PR1l4WmpFMFptWTBZelUyTVdaaU9USTVOR1kxT0dNMFlRPT1fVVNE","quantity":50.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEQmpNamxtWXpjNE4yVTBPVGsxWmpobU1XWXhOR1ptTkdNMU5qRm1Zamt5T1RSbU5UaGpOR0U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDBjMjlmYzc4N2U0OTk1ZjhmMWYxNGZmNGM1NjFmYjkyOTRmNThjNGE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MGMyOWZjNzg3ZTQ5OTVmOGYxZjE0ZmY0YzU2MWZiOTI5NGY1OGM0YQ==","chain":"BNB","address":"0x0c29fc787e4995f8f1f14ff4c561fb9294f58c4a","name":"COT","symbol":"COT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaG1PV1V5TURJNU5HTTNNMkl6TWpRM1kyUTRaREUxTURJM01UVm1NREUxWm1VMVltTTJPR0UwX1VTRA==","quantity":5.0E-18,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WmpsbE1qQXlPVFJqTnpOaU16STBOMk5rT0dReE5UQXlOekUxWmpBeE5XWmxOV0pqTmpoaE5BPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ZjllMjAyOTRjNzNiMzI0N2NkOGQxNTAyNzE1ZjAxNWZlNWJjNjhhNA==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhmOWUyMDI5NGM3M2IzMjQ3Y2Q4ZDE1MDI3MTVmMDE1ZmU1YmM2OGE0","chain":"ETHEREUM","address":"0xf9e20294c73b3247cd8d1502715f015fe5bc68a4","name":"Dimitra Token","symbol":"DMTR","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TVRneFkyVmhaREF3TVdVME9EZG1OMkUyTVdWa05qYzFNMk5oTURkaE0ySmhNVGhpTVdOaU13PT1fVVNE","quantity":5000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VERTRNV05sWVdRd01ERmxORGczWmpkaE5qRmxaRFkzTlROallUQTNZVE5pWVRFNFlqRmpZak09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDE4MWNlYWQwMDFlNDg3ZjdhNjFlZDY3NTNjYTA3YTNiYTE4YjFjYjM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MTgxY2VhZDAwMWU0ODdmN2E2MWVkNjc1M2NhMDdhM2JhMThiMWNiMw==","chain":"BNB","address":"0x181cead001e487f7a61ed6753ca07a3ba18b1cb3","name":"SteveWillDoIt Coin","symbol":"STEVE","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TVdFd09HUTJZMk5pTkRBMFpXWmhPRFUyWlRFeU9XUXdNemhoTVdWbE1USmpOelV6TlRBMU1BPT1fVVNE","quantity":9.4E10,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VERmhNRGhrTm1OallqUXdOR1ZtWVRnMU5tVXhNamxrTURNNFlURmxaVEV5WXpjMU16VXdOVEE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDFhMDhkNmNjYjQwNGVmYTg1NmUxMjlkMDM4YTFlZTEyYzc1MzUwNTA=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MWEwOGQ2Y2NiNDA0ZWZhODU2ZTEyOWQwMzhhMWVlMTJjNzUzNTA1MA==","chain":"BNB","address":"0x1a08d6ccb404efa856e129d038a1ee12c7535050","name":"LUSHI","symbol":"LUSHI","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TVdKbFlXUmlaVGMyTW1KalltWmpaVEF3TnpOak1XTmtOall6WlRabFpEWTJOREZrWXpBd01BPT1fVVNE","quantity":200.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VERmlaV0ZrWW1VM05qSmlZMkptWTJVd01EY3pZekZqWkRZMk0yVTJaV1EyTmpReFpHTXdNREE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDFiZWFkYmU3NjJiY2JmY2UwMDczYzFjZDY2M2U2ZWQ2NjQxZGMwMDA=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MWJlYWRiZTc2MmJjYmZjZTAwNzNjMWNkNjYzZTZlZDY2NDFkYzAwMA==","chain":"BNB","address":"0x1beadbe762bcbfce0073c1cd663e6ed6641dc000","name":"D Auditor","symbol":"DAD","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaGpOalE0TXpaak5qaGtPRE0zWXpnd05ETXhNRFkxWTJZM05qQTBZVGhsWkdFNU1UUXpPRGMwX1VTRA==","quantity":1.0E-18,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WXpZME9ETTJZelk0WkRnek4yTTRNRFF6TVRBMk5XTm1Oell3TkdFNFpXUmhPVEUwTXpnM05BPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzY0ODM2YzY4ZDgzN2M4MDQzMTA2NWNmNzYwNGE4ZWRhOTE0Mzg3NA==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhjNjQ4MzZjNjhkODM3YzgwNDMxMDY1Y2Y3NjA0YThlZGE5MTQzODc0","chain":"ETHEREUM","address":"0xc64836c68d837c80431065cf7604a8eda9143874","name":"Quantum Colosseum Token","symbol":"QCT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TWpJMVlqZzFZamt6WWpVeU9XVmxZekJsTXpabE5EZzVOREl4WldWalpHWTVaV1k1WkRJNFpRPT1fVVNE","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VESXlOV0k0TldJNU0ySTFNamxsWldNd1pUTTJaVFE0T1RReU1XVmxZMlJtT1dWbU9XUXlPR1U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDIyNWI4NWI5M2I1MjllZWMwZTM2ZTQ4OTQyMWVlY2RmOWVmOWQyOGU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MjI1Yjg1YjkzYjUyOWVlYzBlMzZlNDg5NDIxZWVjZGY5ZWY5ZDI4ZQ==","chain":"BNB","address":"0x225b85b93b529eec0e36e489421eecdf9ef9d28e","name":"DOGGGO","symbol":"DGGG","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TWpjMk5HSmxORGMxTm1abFl6aGtaVGt4TVdRNFpETTNaV1psTkdGbE9HRm1aakUzT0RJMU5BPT1fVVNE","quantity":7.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VESTNOalJpWlRRM05UWm1aV000WkdVNU1URmtPR1F6TjJWbVpUUmhaVGhoWm1ZeE56Z3lOVFE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDI3NjRiZTQ3NTZmZWM4ZGU5MTFkOGQzN2VmZTRhZThhZmYxNzgyNTQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4Mjc2NGJlNDc1NmZlYzhkZTkxMWQ4ZDM3ZWZlNGFlOGFmZjE3ODI1NA==","chain":"BNB","address":"0x2764be4756fec8de911d8d37efe4ae8aff178254","name":"PLASTIK Token","symbol":"PLASTIK","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TWpjMlpHWTRObUpoWkdGa016RmtObUpqWmpJeVlXVXdPRGM0WXpnME1qWmhOV1UxT1dNNFlnPT1fVVNE","quantity":3808.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VESTNObVJtT0RaaVlXUmhaRE14WkRaaVkyWXlNbUZsTURnM09HTTROREkyWVRWbE5UbGpPR0k9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDI3NmRmODZiYWRhZDMxZDZiY2YyMmFlMDg3OGM4NDI2YTVlNTljOGI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4Mjc2ZGY4NmJhZGFkMzFkNmJjZjIyYWUwODc4Yzg0MjZhNWU1OWM4Yg==","chain":"BNB","address":"0x276df86badad31d6bcf22ae0878c8426a5e59c8b","name":"JB200","symbol":"JB200","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TW1Ga04yWXhPR1JqWm1FeE16RmxNek0wTVRFM056QmhPV00yWXpSbVpUUTVZakU0TjJKak1nPT1fVVNE","quantity":5000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VESmhaRGRtTVRoa1kyWmhNVE14WlRNek5ERXhOemN3WVRsak5tTTBabVUwT1dJeE9EZGlZekk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDJhZDdmMThkY2ZhMTMxZTMzNDExNzcwYTljNmM0ZmU0OWIxODdiYzI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MmFkN2YxOGRjZmExMzFlMzM0MTE3NzBhOWM2YzRmZTQ5YjE4N2JjMg==","chain":"BNB","address":"0x2ad7f18dcfa131e33411770a9c6c4fe49b187bc2","name":"MetaUFO","symbol":"MetaUFO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TW1ZeU1EbGlORFk0WkRSbVkySXlaR0U1TTJZNU56WmhOalF4TmpsaE4yWmlNVFUzTkRZeFl3PT1fVVNE","quantity":490.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VESm1NakE1WWpRMk9HUTBabU5pTW1SaE9UTm1PVGMyWVRZME1UWTVZVGRtWWpFMU56UTJNV009X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDJmMjA5YjQ2OGQ0ZmNiMmRhOTNmOTc2YTY0MTY5YTdmYjE1NzQ2MWM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4MmYyMDliNDY4ZDRmY2IyZGE5M2Y5NzZhNjQxNjlhN2ZiMTU3NDYxYw==","chain":"BNB","address":"0x2f209b468d4fcb2da93f976a64169a7fb157461c","name":"RichHusky","symbol":"RH","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaGpOR1ExT0RabFpqZGlaVGxsWW1VNE1HSmtOV1ZsTkdaaVpESXlPR1psTW1SaU5XWXlZelJsX1VTRA==","quantity":110.042666359,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WXpSa05UZzJaV1kzWW1VNVpXSmxPREJpWkRWbFpUUm1ZbVF5TWpobVpUSmtZalZtTW1NMFpRPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YzRkNTg2ZWY3YmU5ZWJlODBiZDVlZTRmYmQyMjhmZTJkYjVmMmM0ZQ==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xc4d586ef7Be9EBe80bD5eE4FBd228fe2Db5F2C4e/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhjNGQ1ODZlZjdiZTllYmU4MGJkNWVlNGZiZDIyOGZlMmRiNWYyYzRl","chain":"ETHEREUM","address":"0xc4d586ef7be9ebe80bd5ee4fbd228fe2db5f2c4e","name":"Papa Shiba","symbol":"PHIBA","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TTJFMk5tUXpaREpsWkdJeU5URTRNalF6TUdRd01XSTVPVEZpWXpabFlXTTNNMkZrTldGaE9BPT1fVVNE","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VETmhOalprTTJReVpXUmlNalV4T0RJME16QmtNREZpT1RreFltTTJaV0ZqTnpOaFpEVmhZVGc9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDNhNjZkM2QyZWRiMjUxODI0MzBkMDFiOTkxYmM2ZWFjNzNhZDVhYTg=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4M2E2NmQzZDJlZGIyNTE4MjQzMGQwMWI5OTFiYzZlYWM3M2FkNWFhOA==","chain":"BNB","address":"0x3a66d3d2edb25182430d01b991bc6eac73ad5aa8","name":"SFMONEY","symbol":"SFM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TTJReU1qaGxaalJsTldNNFl6UmlNVGd5T1RreE5qRTBNVEE1TkRjeVlqWTJOekJqT1RGaFpBPT1fVVNE","quantity":2000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VETmtNakk0WldZMFpUVmpPR00wWWpFNE1qazVNVFl4TkRFd09UUTNNbUkyTmpjd1l6a3hZV1E9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDNkMjI4ZWY0ZTVjOGM0YjE4Mjk5MTYxNDEwOTQ3MmI2NjcwYzkxYWQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4M2QyMjhlZjRlNWM4YzRiMTgyOTkxNjE0MTA5NDcyYjY2NzBjOTFhZA==","chain":"BNB","address":"0x3d228ef4e5c8c4b182991614109472b6670c91ad","name":"A","symbol":"AA","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TTJWbVlUSmlOell3T1RGbU1EbGlZbUl4WVRRelpERm1ZbU0xTkRJeVkySXlaVGcxWlRsaVl3PT1fVVNE","quantity":1.0E-18,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VETmxabUV5WWpjMk1Ea3haakE1WW1KaU1XRTBNMlF4Wm1Kak5UUXlNbU5pTW1VNE5XVTVZbU09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDNlZmEyYjc2MDkxZjA5YmJiMWE0M2QxZmJjNTQyMmNiMmU4NWU5YmM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4M2VmYTJiNzYwOTFmMDliYmIxYTQzZDFmYmM1NDIyY2IyZTg1ZTliYw==","chain":"BNB","address":"0x3efa2b76091f09bbb1a43d1fbc5422cb2e85e9bc","name":"Chinese People Coin","symbol":"CPC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TkdJMk1tRTVORGN6WTJRelpqTmpNek01TVdaaFlqTm1NR0ZoWkRNMk5HTTBObUkwWXpWaVlnPT1fVVNE","quantity":1.011E-7,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEUmlOakpoT1RRM00yTmtNMll6WXpNek9URm1ZV0l6WmpCaFlXUXpOalJqTkRaaU5HTTFZbUk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDRiNjJhOTQ3M2NkM2YzYzMzOTFmYWIzZjBhYWQzNjRjNDZiNGM1YmI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NGI2MmE5NDczY2QzZjNjMzM5MWZhYjNmMGFhZDM2NGM0NmI0YzViYg==","chain":"BNB","address":"0x4b62a9473cd3f3c3391fab3f0aad364c46b4c5bb","name":"fatherdoge","symbol":"fatherdoge","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TkdRMFpUVTVOV1EyTkROa1l6WXhaV0UzWm1OaVpqRXlaVFJpTVdGaFlUTTVaams1TnpWaU9BPT1fVVNE","quantity":0.05934647391224829,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEUmtOR1UxT1RWa05qUXpaR00yTVdWaE4yWmpZbVl4TW1VMFlqRmhZV0V6T1dZNU9UYzFZamc9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDRkNGU1OTVkNjQzZGM2MWVhN2ZjYmYxMmU0YjFhYWEzOWY5OTc1Yjg=","logoUrl":"https://assets.coingecko.com/coins/images/14354/thumb/UVbm-Qs-JH-400x400.jpg?1615539255","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NGQ0ZTU5NWQ2NDNkYzYxZWE3ZmNiZjEyZTRiMWFhYTM5Zjk5NzViOA==","chain":"BNB","address":"0x4d4e595d643dc61ea7fcbf12e4b1aaa39f9975b8","name":"Pet Token","symbol":"PET","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRBME16RTNOamczTmpZeE5XSTFOV1kzTXpVeU9XTXpNRE16TnpkbU16WTNZMkUyT1RVek1nPT1fVVNE","quantity":31.022429983,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVXdORE14TnpZNE56WTJNVFZpTlRWbU56TTFNamxqTXpBek16YzNaak0yTjJOaE5qazFNekk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDUwNDMxNzY4NzY2MTViNTVmNzM1MjljMzAzMzc3ZjM2N2NhNjk1MzI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTA0MzE3Njg3NjYxNWI1NWY3MzUyOWMzMDMzNzdmMzY3Y2E2OTUzMg==","chain":"BNB","address":"0x5043176876615b55f73529c303377f367ca69532","name":"tsts","symbol":"tsts","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRKbE16Tm1ZamcyTTJZMk5XSmtOek5oWkRaaVpEUTVNemxoWmpjNFlXWTBNMk13TUdWbU13PT1fVVNE","quantity":1.5151515151515152E10,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVXlaVE16Wm1JNE5qTm1OalZpWkRjellXUTJZbVEwT1RNNVlXWTNPR0ZtTkROak1EQmxaak09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDUyZTMzZmI4NjNmNjViZDczYWQ2YmQ0OTM5YWY3OGFmNDNjMDBlZjM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTJlMzNmYjg2M2Y2NWJkNzNhZDZiZDQ5MzlhZjc4YWY0M2MwMGVmMw==","chain":"BNB","address":"0x52e33fb863f65bd73ad6bd4939af78af43c00ef3","name":"ROYALTY","symbol":"ROY","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRRME5EZGpabUU1TVRsak1EazJaREE1TldRMU9HTXlZalU0TVRRMk5qazRPVGRpWm1aalpBPT1fVVNE","quantity":5.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVTBORFEzWTJaaE9URTVZekE1Tm1Rd09UVmtOVGhqTW1JMU9ERTBOalk1T0RrM1ltWm1ZMlE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDU0NDQ3Y2ZhOTE5YzA5NmQwOTVkNThjMmI1ODE0NjY5ODk3YmZmY2Q=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTQ0NDdjZmE5MTljMDk2ZDA5NWQ1OGMyYjU4MTQ2Njk4OTdiZmZjZA==","chain":"BNB","address":"0x54447cfa919c096d095d58c2b5814669897bffcd","name":"Test Wrapped VENOM","symbol":"TWVENOM","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VERTFNV1poT0RoaU9UbGlNbUl4TURCaE5tTTFabU5tTXpBM1ptVTJaakJtWkRWbU1XTXhaREE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd4TlRGbVlUZzRZams1WWpKaU1UQXdZVFpqTldaalpqTXdOMlpsTm1Zd1ptUTFaakZqTVdRd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgxNTFmYTg4Yjk5YjJiMTAwYTZjNWZjZjMwN2ZlNmYwZmQ1ZjFjMWQw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDE1MWZhODhiOTliMmIxMDBhNmM1ZmNmMzA3ZmU2ZjBmZDVmMWMxZDA=","chain":"POLYGON","address":"0x151fa88b99b2b100a6c5fcf307fe6f0fd5f1c1d0","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRZeU1HVmlZVFUyTldRME1tVTRZVGRsWVdNeU5UY3lNV1E0Wm1VNU1qY3dZV000WTJFelpRPT1fVVNE","quantity":120000.576,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVTJNakJsWW1FMU5qVmtOREpsT0dFM1pXRmpNalUzTWpGa09HWmxPVEkzTUdGak9HTmhNMlU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDU2MjBlYmE1NjVkNDJlOGE3ZWFjMjU3MjFkOGZlOTI3MGFjOGNhM2U=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTYyMGViYTU2NWQ0MmU4YTdlYWMyNTcyMWQ4ZmU5MjcwYWM4Y2EzZQ==","chain":"BNB","address":"0x5620eba565d42e8a7eac25721d8fe9270ac8ca3e","name":"My Get Rich Token","symbol":"MGRT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TlRneE5UVTFaVE0yTm1GbU5ESTJNREl5WmpJMU1tTTRNamN3WXpZMVpqTmlZelJsTW1Sa01nPT1fVVNE","quantity":5.243167033085024E14,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVTRNVFUxTldVek5qWmhaalF5TmpBeU1tWXlOVEpqT0RJM01HTTJOV1l6WW1NMFpUSmtaREk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDU4MTU1NWUzNjZhZjQyNjAyMmYyNTJjODI3MGM2NWYzYmM0ZTJkZDI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NTgxNTU1ZTM2NmFmNDI2MDIyZjI1MmM4MjcwYzY1ZjNiYzRlMmRkMg==","chain":"BNB","address":"0x581555e366af426022f252c8270c65f3bc4e2dd2","name":"BATZilla","symbol":"BZL","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TldVeE5Ea3pNMlkyTWpZMVlUWXdPVGhtTVRGaVpqRTVNVFl6TURBMU5qRmlPRFF6Tnpaa01RPT1fVVNE","quantity":1.0E8,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVmxNVFE1TXpObU5qSTJOV0UyTURrNFpqRXhZbVl4T1RFMk16QXdOVFl4WWpnME16YzJaREU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDVlMTQ5MzNmNjI2NWE2MDk4ZjExYmYxOTE2MzAwNTYxYjg0Mzc2ZDE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NWUxNDkzM2Y2MjY1YTYwOThmMTFiZjE5MTYzMDA1NjFiODQzNzZkMQ==","chain":"BNB","address":"0x5e14933f6265a6098f11bf1916300561b84376d1","name":"GhostMarket","symbol":"GM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TldWak1tRTNOemczTVRkalpqRmhOVEF4T0dNMllXVXpZVGRoTWprMU56VTRNbUU1TWpBd053PT1fVVNE","quantity":10000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEVmxZekpoTnpjNE56RTNZMll4WVRVd01UaGpObUZsTTJFM1lUSTVOVGMxT0RKaE9USXdNRGM9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDVlYzJhNzc4NzE3Y2YxYTUwMThjNmFlM2E3YTI5NTc1ODJhOTIwMDc=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NWVjMmE3Nzg3MTdjZjFhNTAxOGM2YWUzYTdhMjk1NzU4MmE5MjAwNw==","chain":"BNB","address":"0x5ec2a778717cf1a5018c6ae3a7a2957582a92007","name":"Stakeable","symbol":"STAKE","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TmpZek9ERTNZbVZpTTJKak1UazFaR0ZtT0dFeVltTXlOamRrTkRaallqZzBNMlZtTXprMFl3PT1fVVNE","quantity":5.0E8,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEWTJNemd4TjJKbFlqTmlZekU1TldSaFpqaGhNbUpqTWpZM1pEUTJZMkk0TkRObFpqTTVOR009X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDY2MzgxN2JlYjNiYzE5NWRhZjhhMmJjMjY3ZDQ2Y2I4NDNlZjM5NGM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NjYzODE3YmViM2JjMTk1ZGFmOGEyYmMyNjdkNDZjYjg0M2VmMzk0Yw==","chain":"BNB","address":"0x663817beb3bc195daf8a2bc267d46cb843ef394c","name":"KIKUSHIBA INU","symbol":"KIKINU","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0Tmpnd09HSmhPVFEzWldNeE1EbGxZelUwWVRReFpqWXpPRGhqTkRKbU16QTBOR1E0TlRObE9RPT1fVVNE","quantity":504001.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEWTRNRGhpWVRrME4yVmpNVEE1WldNMU5HRTBNV1kyTXpnNFl6UXlaak13TkRSa09EVXpaVGs9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDY4MDhiYTk0N2VjMTA5ZWM1NGE0MWY2Mzg4YzQyZjMwNDRkODUzZTk=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NjgwOGJhOTQ3ZWMxMDllYzU0YTQxZjYzODhjNDJmMzA0NGQ4NTNlOQ==","chain":"BNB","address":"0x6808ba947ec109ec54a41f6388c42f3044d853e9","name":"NEKO","symbol":"NEKO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0Tm1Jek16VTJZakk0T1RBeVpHSTNaR000TldNeE5UTm1Nell4WVdNMk56TTFaREl6TVdNNE9BPT1fVVNE","quantity":210.1,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEWmlNek0xTm1JeU9Ea3dNbVJpTjJSak9EVmpNVFV6WmpNMk1XRmpOamN6TldReU16RmpPRGc9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDZiMzM1NmIyODkwMmRiN2RjODVjMTUzZjM2MWFjNjczNWQyMzFjODg=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NmIzMzU2YjI4OTAyZGI3ZGM4NWMxNTNmMzYxYWM2NzM1ZDIzMWM4OA==","chain":"BNB","address":"0x6b3356b28902db7dc85c153f361ac6735d231c88","name":"Balbol","symbol":"Balbol","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0Tm1JNU5UVmxZMlptWlRVME1XUmhZelV4TmpKa00ySmtZVFJqWmpnNFpEZ3hNREJqTURSbU5BPT1fVVNE","quantity":99999.99999999999,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEWmlPVFUxWldObVptVTFOREZrWVdNMU1UWXlaRE5pWkdFMFkyWTRPR1E0TVRBd1l6QTBaalE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDZiOTU1ZWNmZmU1NDFkYWM1MTYyZDNiZGE0Y2Y4OGQ4MTAwYzA0ZjQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NmI5NTVlY2ZmZTU0MWRhYzUxNjJkM2JkYTRjZjg4ZDgxMDBjMDRmNA==","chain":"BNB","address":"0x6b955ecffe541dac5162d3bda4cf88d8100c04f4","name":"BichonSpaceToken","symbol":"BCT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TnpaaU1UbGtOVFJrWkRKaU1USmpOakZtTmpVek5HSXlNelE0WldJMVpXSmlNVGswTlRNMU13PT1fVVNE","quantity":2.0889426158654305E8,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEYzJZakU1WkRVMFpHUXlZakV5WXpZeFpqWTFNelJpTWpNME9HVmlOV1ZpWWpFNU5EVXpOVE09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDc2YjE5ZDU0ZGQyYjEyYzYxZjY1MzRiMjM0OGViNWViYjE5NDUzNTM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NzZiMTlkNTRkZDJiMTJjNjFmNjUzNGIyMzQ4ZWI1ZWJiMTk0NTM1Mw==","chain":"BNB","address":"0x76b19d54dd2b12c61f6534b2348eb5ebb1945353","name":"Mouse Coin","symbol":"MSCN","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TnpoaE5EYzJZemN6T0RFd1lXSXpaVFUwTlRrNU16SXhNVGszWmprNE1EZ3pNRFkwWldNNE1BPT1fVVNE","quantity":10000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEYzRZVFEzTm1NM016Z3hNR0ZpTTJVMU5EVTVPVE15TVRFNU4yWTVPREE0TXpBMk5HVmpPREE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDc4YTQ3NmM3MzgxMGFiM2U1NDU5OTMyMTE5N2Y5ODA4MzA2NGVjODA=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4NzhhNDc2YzczODEwYWIzZTU0NTk5MzIxMTk3Zjk4MDgzMDY0ZWM4MA==","chain":"BNB","address":"0x78a476c73810ab3e54599321197f98083064ec80","name":"GalaxyLand","symbol":"GXN","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TjJOaE56QXlZMkUwTmpjd09EazJPV05tTmpkbE9EUm1NV1V4TkRoaU1qRmxNamxpTWpsak1RPT1fVVNE","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEZGpZVGN3TW1OaE5EWTNNRGc1TmpsalpqWTNaVGcwWmpGbE1UUTRZakl4WlRJNVlqSTVZekU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDdjYTcwMmNhNDY3MDg5NjljZjY3ZTg0ZjFlMTQ4YjIxZTI5YjI5YzE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4N2NhNzAyY2E0NjcwODk2OWNmNjdlODRmMWUxNDhiMjFlMjliMjljMQ==","chain":"BNB","address":"0x7ca702ca46708969cf67e84f1e148b21e29b29c1","name":"DOGGGO","symbol":"DOGGGO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0TjJVMk1qQXlPVEF6TWpjMU56Y3lNRFEwTVRrNFpEQTNZamhoTlRNMlkyTXdOalJtT0RRNE1BPT1fVVNE","quantity":211.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEZGxOakl3TWprd016STNOVGMzTWpBME5ERTVPR1F3TjJJNFlUVXpObU5qTURZMFpqZzBPREE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDdlNjIwMjkwMzI3NTc3MjA0NDE5OGQwN2I4YTUzNmNjMDY0Zjg0ODA=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4N2U2MjAyOTAzMjc1NzcyMDQ0MTk4ZDA3YjhhNTM2Y2MwNjRmODQ4MA==","chain":"BNB","address":"0x7e6202903275772044198d07b8a536cc064f8480","name":"Crypto Gold Box","symbol":"CGB","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T0RVM1pHUXpZalE1TTJNME9XRTRZakEyTUdaaFpqSXlOMlF5WWprd01XVTNNall5TVRJNU13PT1fVVNE","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEZzFOMlJrTTJJME9UTmpORGxoT0dJd05qQm1ZV1l5TWpka01tSTVNREZsTnpJMk1qRXlPVE09X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDg1N2RkM2I0OTNjNDlhOGIwNjBmYWYyMjdkMmI5MDFlNzI2MjEyOTM=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ODU3ZGQzYjQ5M2M0OWE4YjA2MGZhZjIyN2QyYjkwMWU3MjYyMTI5Mw==","chain":"BNB","address":"0x857dd3b493c49a8b060faf227d2b901e72621293","name":"DOGGGO","symbol":"DOGGGO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T0RVNU4ySmhNVFF6WVdNMU1Ea3hPRGxsT0RsaFlXSXpZbUV5T0dRMk5qRmhOV1JrT1Rnek1BPT1fVVNE","quantity":2.257E-15,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEZzFPVGRpWVRFME0yRmpOVEE1TVRnNVpUZzVZV0ZpTTJKaE1qaGtOall4WVRWa1pEazRNekE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDg1OTdiYTE0M2FjNTA5MTg5ZTg5YWFiM2JhMjhkNjYxYTVkZDk4MzA=","logoUrl":"https://assets.coingecko.com/coins/images/14597/thumb/1vDw61E.png?1627131791","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ODU5N2JhMTQzYWM1MDkxODllODlhYWIzYmEyOGQ2NjFhNWRkOTgzMA==","chain":"BNB","address":"0x8597ba143ac509189e89aab3ba28d661a5dd9830","name":"VANCAT Token","symbol":"VANCAT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T0dVM05tRmxZalE0TnpRM1l6QmpNREl5T0dFME4yRmtNR1UyWW1KallUWTJORGcwTlRObFpRPT1fVVNE","quantity":19557.909186021305,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEaGxOelpoWldJME9EYzBOMk13WXpBeU1qaGhORGRoWkRCbE5tSmlZMkUyTmpRNE5EVXpaV1U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDhlNzZhZWI0ODc0N2MwYzAyMjhhNDdhZDBlNmJiY2E2NjQ4NDUzZWU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4OGU3NmFlYjQ4NzQ3YzBjMDIyOGE0N2FkMGU2YmJjYTY2NDg0NTNlZQ==","chain":"BNB","address":"0x8e76aeb48747c0c0228a47ad0e6bbca6648453ee","name":"kk.f","symbol":"KTEST","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T1RRd1ptWmpZMlZoT0dRNU5XUTNNbUl4WmpFMk5tTXdaakV6WW1JNE5tRTFaVFJoTkRReVpnPT1fVVNE","quantity":11000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEazBNR1ptWTJObFlUaGtPVFZrTnpKaU1XWXhOalpqTUdZeE0ySmlPRFpoTldVMFlUUTBNbVk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDk0MGZmY2NlYThkOTVkNzJiMWYxNjZjMGYxM2JiODZhNWU0YTQ0MmY=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4OTQwZmZjY2VhOGQ5NWQ3MmIxZjE2NmMwZjEzYmI4NmE1ZTRhNDQyZg==","chain":"BNB","address":"0x940ffccea8d95d72b1f166c0f13bb86a5e4a442f","name":"RRZ DAO","symbol":"RRZ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T1RjM09UUTVPVGs0WldReVpEZzVOVEF4TmpsaFpqTXlZV05pTmpRek1qUXlZVFF5T1Rjek9RPT1fVVNE","quantity":0.142857142,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEazNOemswT1RrNU9HVmtNbVE0T1RVd01UWTVZV1l6TW1GallqWTBNekkwTW1FME1qazNNems9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDk3Nzk0OTk5OGVkMmQ4OTUwMTY5YWYzMmFjYjY0MzI0MmE0Mjk3Mzk=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4OTc3OTQ5OTk4ZWQyZDg5NTAxNjlhZjMyYWNiNjQzMjQyYTQyOTczOQ==","chain":"BNB","address":"0x977949998ed2d8950169af32acb643242a429739","name":"TestToken11","symbol":"TT11","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0T1RkaU1EZG1aVGMxT0ROaU1UZ3pNRFEzWlRBek4yVmpPRFJoWkRGaFpXVmlOamRtTldVMU5RPT1fVVNE","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VEazNZakEzWm1VM05UZ3pZakU0TXpBME4yVXdNemRsWXpnMFlXUXhZV1ZsWWpZM1pqVmxOVFU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weDk3YjA3ZmU3NTgzYjE4MzA0N2UwMzdlYzg0YWQxYWVlYjY3ZjVlNTU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4OTdiMDdmZTc1ODNiMTgzMDQ3ZTAzN2VjODRhZDFhZWViNjdmNWU1NQ==","chain":"BNB","address":"0x97b07fe7583b183047e037ec84ad1aeeb67f5e55","name":"Placeholder Vent Finance","symbol":"PVent","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WVRRM1pXSTFOekl4TVRJeU1tWTFaV1EwT1RRMVpEZGhPV1ppWm1NelpEUTRORE14TkdVeU9RPT1fVVNE","quantity":0.128621147,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHRTBOMlZpTlRjeU1URXlNakptTldWa05EazBOV1EzWVRsbVltWmpNMlEwT0RRek1UUmxNams9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGE0N2ViNTcyMTEyMjJmNWVkNDk0NWQ3YTlmYmZjM2Q0ODQzMTRlMjk=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YTQ3ZWI1NzIxMTIyMmY1ZWQ0OTQ1ZDdhOWZiZmMzZDQ4NDMxNGUyOQ==","chain":"BNB","address":"0xa47eb57211222f5ed4945d7a9fbfc3d484314e29","name":"TestToken7","symbol":"TT7","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaGlPR0V4WkdVeE1UTmxZMlpoWm1JNVpURXlZV1JqWXpjMk5qY3pZelk1TURZNE1UYzRORE00X1VTRA==","quantity":1.0E19,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WWpoaE1XUmxNVEV6WldObVlXWmlPV1V4TW1Ga1kyTTNOalkzTTJNMk9UQTJPREUzT0RRek9BPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YjhhMWRlMTEzZWNmYWZiOWUxMmFkY2M3NjY3M2M2OTA2ODE3ODQzOA==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhiOGExZGUxMTNlY2ZhZmI5ZTEyYWRjYzc2NjczYzY5MDY4MTc4NDM4","chain":"ETHEREUM","address":"0xb8a1de113ecfafb9e12adcc76673c69068178438","name":"FoolToken","symbol":"FOOL","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WVRWa01XRmtOMk0xTWpFMFl6Sm1OR1F5T0RGbE5HSTBaV00zT0RKa1lXRXpPR001T1RKaE5nPT1fVVNE","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHRTFaREZoWkRkak5USXhOR015WmpSa01qZ3haVFJpTkdWak56Z3laR0ZoTXpoak9Ua3lZVFk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGE1ZDFhZDdjNTIxNGMyZjRkMjgxZTRiNGVjNzgyZGFhMzhjOTkyYTY=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YTVkMWFkN2M1MjE0YzJmNGQyODFlNGI0ZWM3ODJkYWEzOGM5OTJhNg==","chain":"BNB","address":"0xa5d1ad7c5214c2f4d281e4b4ec782daa38c992a6","name":"Damour Token","symbol":"DMR","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WVdVNFpqUmhNV1k1TldWa056QXpaRFF4WTJWaFltWXhOMkV4TkROa04yVTNNMkk0TXpReU1RPT1fVVNE","quantity":545.3391767452313,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHRmxPR1kwWVRGbU9UVmxaRGN3TTJRME1XTmxZV0ptTVRkaE1UUXpaRGRsTnpOaU9ETTBNakU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGFlOGY0YTFmOTVlZDcwM2Q0MWNlYWJmMTdhMTQzZDdlNzNiODM0MjE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YWU4ZjRhMWY5NWVkNzAzZDQxY2VhYmYxN2ExNDNkN2U3M2I4MzQyMQ==","chain":"BNB","address":"0xae8f4a1f95ed703d41ceabf17a143d7e73b83421","name":"Gull","symbol":"GULL","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WVdaaE5tSmpaR1U1WXpBeE5EWTVPV0UwWVdGbU1XWTJaVEUzWTJFd01ETTRaalEyWm1ZeU1RPT1fVVNE","quantity":0.24,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHRm1ZVFppWTJSbE9XTXdNVFEyT1RsaE5HRmhaakZtTm1VeE4yTmhNREF6T0dZME5tWm1NakU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGFmYTZiY2RlOWMwMTQ2OTlhNGFhZjFmNmUxN2NhMDAzOGY0NmZmMjE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YWZhNmJjZGU5YzAxNDY5OWE0YWFmMWY2ZTE3Y2EwMDM4ZjQ2ZmYyMQ==","chain":"BNB","address":"0xafa6bcde9c014699a4aaf1f6e17ca0038f46ff21","name":"TestCoin","symbol":"TCT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WWpVeU5XUmxNalEwWXpBd05tSmpPV1k1TlRoa01EWmxNMkUxWkRSaU9UQTROV00xTjJFM1pBPT1fVVNE","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHSTFNalZrWlRJME5HTXdNRFppWXpsbU9UVTRaREEyWlROaE5XUTBZamt3T0RWak5UZGhOMlE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGI1MjVkZTI0NGMwMDZiYzlmOTU4ZDA2ZTNhNWQ0YjkwODVjNTdhN2Q=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YjUyNWRlMjQ0YzAwNmJjOWY5NThkMDZlM2E1ZDRiOTA4NWM1N2E3ZA==","chain":"BNB","address":"0xb525de244c006bc9f958d06e3a5d4b9085c57a7d","name":"TestToken6","symbol":"TT6","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WWprek1EVTNZVEU0TjJRME1qUTNNRGhsTldFMU9ETTBZMlEzTXpZeU1qUmhPREkwTW1JeE5nPT1fVVNE","quantity":6.0E-18,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHSTVNekExTjJFeE9EZGtOREkwTnpBNFpUVmhOVGd6TkdOa056TTJNakkwWVRneU5ESmlNVFk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGI5MzA1N2ExODdkNDI0NzA4ZTVhNTgzNGNkNzM2MjI0YTgyNDJiMTY=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YjkzMDU3YTE4N2Q0MjQ3MDhlNWE1ODM0Y2Q3MzYyMjRhODI0MmIxNg==","chain":"BNB","address":"0xb93057a187d424708e5a5834cd736224a8242b16","name":"Quantum","symbol":"QC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WXpRMFltWTFNekF3WW1VNU1UWTROemsxWkdWbE1qVmtZV0ZrTUdKalpUVmhPR1ppTnpjNVlRPT1fVVNE","quantity":1.26935E-4,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHTTBOR0ptTlRNd01HSmxPVEUyT0RjNU5XUmxaVEkxWkdGaFpEQmlZMlUxWVRobVlqYzNPV0U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGM0NGJmNTMwMGJlOTE2ODc5NWRlZTI1ZGFhZDBiY2U1YThmYjc3OWE=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YzQ0YmY1MzAwYmU5MTY4Nzk1ZGVlMjVkYWFkMGJjZTVhOGZiNzc5YQ==","chain":"BNB","address":"0xc44bf5300be9168795dee25daad0bce5a8fb779a","name":"BABYBSC","symbol":"BBSC","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WXpVeFpXWTRNamd6TVRsaU1UTXhZalU1TldJM1pXTTBZakk0TWpFd1pXTm1OR1F3TldGa01BPT1fVVNE","quantity":12.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHTTFNV1ZtT0RJNE16RTVZakV6TVdJMU9UVmlOMlZqTkdJeU9ESXhNR1ZqWmpSa01EVmhaREE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGM1MWVmODI4MzE5YjEzMWI1OTViN2VjNGIyODIxMGVjZjRkMDVhZDA=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4YzUxZWY4MjgzMTliMTMxYjU5NWI3ZWM0YjI4MjEwZWNmNGQwNWFkMA==","chain":"BNB","address":"0xc51ef828319b131b595b7ec4b28210ecf4d05ad0","name":"pTokens EFX","symbol":"EFX","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WkRBME9HSTBZekl6WVdZNE1qaGxOV0psTkRFeU5UQTFZVFV4WVRoa1pEZGlNemMzT0RKa1pBPT1fVVNE","quantity":23000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHUXdORGhpTkdNeU0yRm1PREk0WlRWaVpUUXhNalV3TldFMU1XRTRaR1EzWWpNM056Z3laR1E9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGQwNDhiNGMyM2FmODI4ZTViZTQxMjUwNWE1MWE4ZGQ3YjM3NzgyZGQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZDA0OGI0YzIzYWY4MjhlNWJlNDEyNTA1YTUxYThkZDdiMzc3ODJkZA==","chain":"BNB","address":"0xd048b4c23af828e5be412505a51a8dd7b37782dd","name":"AI Avail","symbol":"AI-A","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WkRBNVpqTTROakU1TVRNeFl6a3hOemd6WVRJeFptTmhZbUZqWVdFMk16QmlNRFpsTWpsak53PT1fVVNE","quantity":1.0E10,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHUXdPV1l6T0RZeE9URXpNV001TVRjNE0yRXlNV1pqWVdKaFkyRmhOak13WWpBMlpUSTVZemM9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGQwOWYzODYxOTEzMWM5MTc4M2EyMWZjYWJhY2FhNjMwYjA2ZTI5Yzc=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZDA5ZjM4NjE5MTMxYzkxNzgzYTIxZmNhYmFjYWE2MzBiMDZlMjljNw==","chain":"BNB","address":"0xd09f38619131c91783a21fcabacaa630b06e29c7","name":"Hippopotamus","symbol":"HPO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WkRCallXVXpZMkk1TlRGbU5qazJPVFZsTXpFNE9EVm1Oak5pTWpabU5UZGhPVFF3WVRrMVpRPT1fVVNE","quantity":1286.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHUXdZMkZsTTJOaU9UVXhaalk1TmprMVpUTXhPRGcxWmpZellqSTJaalUzWVRrME1HRTVOV1U9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGQwY2FlM2NiOTUxZjY5Njk1ZTMxODg1ZjYzYjI2ZjU3YTk0MGE5NWU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZDBjYWUzY2I5NTFmNjk2OTVlMzE4ODVmNjNiMjZmNTdhOTQwYTk1ZQ==","chain":"BNB","address":"0xd0cae3cb951f69695e31885f63b26f57a940a95e","name":"BUSDb","symbol":"BUSDb","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WkRJeU1qQXlaREl6Wm1VM1pHVTVaVE5rWW1VeE1XRXlZVGc0WmpReVpqUmpZamsxTURkalpnPT1fVVNE","quantity":150000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHUXlNakl3TW1ReU0yWmxOMlJsT1dVelpHSmxNVEZoTW1FNE9HWTBNbVkwWTJJNU5UQTNZMlk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGQyMjIwMmQyM2ZlN2RlOWUzZGJlMTFhMmE4OGY0MmY0Y2I5NTA3Y2Y=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZDIyMjAyZDIzZmU3ZGU5ZTNkYmUxMWEyYTg4ZjQyZjRjYjk1MDdjZg==","chain":"BNB","address":"0xd22202d23fe7de9e3dbe11a2a88f42f4cb9507cf","name":"Minereum BSC","symbol":"MNEB","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WkdJME9HWm1ObVptWTJVellXWmtaR1F5TnpNMllqQXlNREJrWW1JMFlURTFOekl3WWpNMU53PT1fVVNE","quantity":0.603279774,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHUmlORGhtWmpabVptTmxNMkZtWkdSa01qY3pObUl3TWpBd1pHSmlOR0V4TlRjeU1HSXpOVGM9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGRiNDhmZjZmZmNlM2FmZGRkMjczNmIwMjAwZGJiNGExNTcyMGIzNTc=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZGI0OGZmNmZmY2UzYWZkZGQyNzM2YjAyMDBkYmI0YTE1NzIwYjM1Nw==","chain":"BNB","address":"0xdb48ff6ffce3afddd2736b0200dbb4a15720b357","name":"TestToken9","symbol":"TT9","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WlRJMU0yTmhNREkyT0dJd1kyWmxZV1V6TjJGa016QTVOekpqTnpjeFlqQTNOVE5sTjJVd05RPT1fVVNE","quantity":1.0E-17,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVXlOVE5qWVRBeU5qaGlNR05tWldGbE16ZGhaRE13T1RjeVl6YzNNV0l3TnpVelpUZGxNRFU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGUyNTNjYTAyNjhiMGNmZWFlMzdhZDMwOTcyYzc3MWIwNzUzZTdlMDU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZTI1M2NhMDI2OGIwY2ZlYWUzN2FkMzA5NzJjNzcxYjA3NTNlN2UwNQ==","chain":"BNB","address":"0xe253ca0268b0cfeae37ad30972c771b0753e7e05","name":"META BNB","symbol":"MBNB","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WlRabE16YzRNalV4TURBNE0yRXlOVGhoWXpneE0yRmlaamM1T0dZM05ESTFObVV5WWpjeE1nPT1fVVNE","quantity":1.459922777,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVTJaVE0zT0RJMU1UQXdPRE5oTWpVNFlXTTRNVE5oWW1ZM09UaG1OelF5TlRabE1tSTNNVEk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGU2ZTM3ODI1MTAwODNhMjU4YWM4MTNhYmY3OThmNzQyNTZlMmI3MTI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZTZlMzc4MjUxMDA4M2EyNThhYzgxM2FiZjc5OGY3NDI1NmUyYjcxMg==","chain":"BNB","address":"0xe6e3782510083a258ac813abf798f74256e2b712","name":"TestTokenRC1","symbol":"TSTR","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WlRjM09UTXlZakV5TVRZeE1qVTRORGhsT0RKak16azJOMlUzTlRZNU9ETTJNakUyT0dZNU9RPT1fVVNE","quantity":0.002198173455,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVTNOemt6TW1JeE1qRTJNVEkxT0RRNFpUZ3lZek01TmpkbE56VTJPVGd6TmpJeE5qaG1PVGs9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGU3NzkzMmIxMjE2MTI1ODQ4ZTgyYzM5NjdlNzU2OTgzNjIxNjhmOTk=","logoUrl":"https://assets.coingecko.com/coins/images/25811/thumb/logo_%281%29.png?1653980666","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZTc3OTMyYjEyMTYxMjU4NDhlODJjMzk2N2U3NTY5ODM2MjE2OGY5OQ==","chain":"BNB","address":"0xe77932b1216125848e82c3967e75698362168f99","name":"GalaxyFinanceToken","symbol":"GFT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WldSaVpERTRZamd5TWpFMVpUZGxNemxpTnpBNE1HRmtPVFkzTmpZNVlqQXdabVl4WWpoaU5BPT1fVVNE","quantity":452.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVmtZbVF4T0dJNE1qSXhOV1UzWlRNNVlqY3dPREJoWkRrMk56WTJPV0l3TUdabU1XSTRZalE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGVkYmQxOGI4MjIxNWU3ZTM5YjcwODBhZDk2NzY2OWIwMGZmMWI4YjQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZWRiZDE4YjgyMjE1ZTdlMzliNzA4MGFkOTY3NjY5YjAwZmYxYjhiNA==","chain":"BNB","address":"0xedbd18b82215e7e39b7080ad967669b00ff1b8b4","name":"AGameGeek","symbol":"AGG","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WldVME1UTmxabU0wTURjeFpESm1ZV1l6WkRnM1pEVmxaakpqTjJZeFl6UTRNMk16WlRoaE5RPT1fVVNE","quantity":399999.99999999994,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVmxOREV6Wldaak5EQTNNV1F5Wm1GbU0yUTROMlExWldZeVl6ZG1NV00wT0ROak0yVTRZVFU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGVlNDEzZWZjNDA3MWQyZmFmM2Q4N2Q1ZWYyYzdmMWM0ODNjM2U4YTU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZWU0MTNlZmM0MDcxZDJmYWYzZDg3ZDVlZjJjN2YxYzQ4M2MzZThhNQ==","chain":"BNB","address":"0xee413efc4071d2faf3d87d5ef2c7f1c483c3e8a5","name":"DD","symbol":"DD","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WldabU5UZGlPRGszWVRoaU9ETXlOVE0wWWpRNVpUazVZMkZoWlRKall6Y3lZbVUzWkRGaE1nPT1fVVNE","quantity":3.712408389,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHVm1aalUzWWpnNU4yRTRZamd6TWpVek5HSTBPV1U1T1dOaFlXVXlZMk0zTW1KbE4yUXhZVEk9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGVmZjU3Yjg5N2E4YjgzMjUzNGI0OWU5OWNhYWUyY2M3MmJlN2QxYTI=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZWZmNTdiODk3YThiODMyNTM0YjQ5ZTk5Y2FhZTJjYzcyYmU3ZDFhMg==","chain":"BNB","address":"0xeff57b897a8b832534b49e99caae2cc72be7d1a2","name":"Jade","symbol":"JADE","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WmpCa09EVXpZekppTVRCalkyTmlaR1UwTkRObU56TTRZamt6TVdKaFpEZGlZakEyWkdZMk5BPT1fVVNE","quantity":18837.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHWXdaRGcxTTJNeVlqRXdZMk5qWW1SbE5EUXpaamN6T0dJNU16RmlZV1EzWW1Jd05tUm1OalE9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGYwZDg1M2MyYjEwY2NjYmRlNDQzZjczOGI5MzFiYWQ3YmIwNmRmNjQ=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZjBkODUzYzJiMTBjY2NiZGU0NDNmNzM4YjkzMWJhZDdiYjA2ZGY2NA==","chain":"BNB","address":"0xf0d853c2b10cccbde443f738b931bad7bb06df64","name":"RRabbidsCoin","symbol":"RRC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WmpNd01XTTRORE0xWkRSa1ptRTFNVFkwTVdZM01XSXdOakUxWVdSa056azBZalV5WXpobE9RPT1fVVNE","quantity":20.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHWXpNREZqT0RRek5XUTBaR1poTlRFMk5ERm1OekZpTURZeE5XRmtaRGM1TkdJMU1tTTRaVGs9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGYzMDFjODQzNWQ0ZGZhNTE2NDFmNzFiMDYxNWFkZDc5NGI1MmM4ZTk=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZjMwMWM4NDM1ZDRkZmE1MTY0MWY3MWIwNjE1YWRkNzk0YjUyYzhlOQ==","chain":"BNB","address":"0xf301c8435d4dfa51641f71b0615add794b52c8e9","name":"Lotto","symbol":"LOTTO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFrNUNYekI0WmpNd01XTmlNMkUxTm1Sa05EY3hOVFkyT1RRd01UaG1PVFZoWXpsa1pESmhOREE1TXpsbE5RPT1fVVNE","quantity":2.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0pPUWw4d2VHWXpNREZqWWpOaE5UWmtaRFEzTVRVMk5qazBNREU0WmprMVlXTTVaR1F5WVRRd09UTTVaVFU9X1VTRA==","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkJOQl8weGYzMDFjYjNhNTZkZDQ3MTU2Njk0MDE4Zjk1YWM5ZGQyYTQwOTM5ZTU=","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46Qk5CXzB4ZjMwMWNiM2E1NmRkNDcxNTY2OTQwMThmOTVhYzlkZDJhNDA5MzllNQ==","chain":"BNB","address":"0xf301cb3a56dd47156694018f95ac9dd2a40939e5","name":"COVER GAS FEE","symbol":"COVERGAS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IaGhOamhrWkRoallqZ3pNRGszTnpZMU1qWXpZV1JoWkRnNE1XRm1ObVZsWkRRM09XTTBZVE16X1VTRA==","quantity":4.075912307008821,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0WVRZNFpHUTRZMkk0TXpBNU56YzJOVEkyTTJGa1lXUTRPREZoWmpabFpXUTBOemxqTkdFek13PT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTY4ZGQ4Y2I4MzA5Nzc2NTI2M2FkYWQ4ODFhZjZlZWQ0NzljNGEzMw==","logoUrl":"https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA68Dd8cB83097765263AdAD881Af6eeD479c4a33/logo.png","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHhhNjhkZDhjYjgzMDk3NzY1MjYzYWRhZDg4MWFmNmVlZDQ3OWM0YTMz","chain":"ETHEREUM","address":"0xa68dd8cb83097765263adad881af6eed479c4a33","name":"fees.wtf","symbol":"WTF","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFWSkNTVlJTVlUxZk1IZ3dPR0V3WVdFNU1XVXdNbUU1TlRjek5XWTNabUZoTlRjMlpERTFOVE5rTjJGaVpqZGxZVE16X1VTRA==","quantity":300000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0ZTUWtsVVVsVk5YekI0TURoaE1HRmhPVEZsTURKaE9UVTNNelZtTjJaaFlUVTNObVF4TlRVelpEZGhZbVkzWldFek13PT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkFSQklUUlVNXzB4MDhhMGFhOTFlMDJhOTU3MzVmN2ZhYTU3NmQxNTUzZDdhYmY3ZWEzMw==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46QVJCSVRSVU1fMHgwOGEwYWE5MWUwMmE5NTczNWY3ZmFhNTc2ZDE1NTNkN2FiZjdlYTMz","chain":"ARBITRUM","address":"0x08a0aa91e02a95735f7faa576d1553d7abf7ea33","name":"Minereum ARBITRUM","symbol":"MNEAR","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IZzROV1E0TTJFNE56TTRNVGd4TjJKaVpUVXdOVEZsTm1RM1kyRTJNMkU1WmpkaE0ySXhZVGRqX1VTRA==","quantity":1.6983E8,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0T0RWa09ETmhPRGN6T0RFNE1UZGlZbVUxTURVeFpUWmtOMk5oTmpOaE9XWTNZVE5pTVdFM1l3PT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4ODVkODNhODczODE4MTdiYmU1MDUxZTZkN2NhNjNhOWY3YTNiMWE3Yw==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHg4NWQ4M2E4NzM4MTgxN2JiZTUwNTFlNmQ3Y2E2M2E5ZjdhM2IxYTdj","chain":"ETHEREUM","address":"0x85d83a87381817bbe5051e6d7ca63a9f7a3b1a7c","name":"Time","symbol":"TIME","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFWSkNTVlJTVlUxZk1IZ3lZV1V5TlRRMk1HTTBOR1ExTnpobE5tWTBNV0ZpT1RBd1lUZGhOVFF5TldJMk5Ea3lZekUyX1VTRA==","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0ZTUWtsVVVsVk5YekI0TW1GbE1qVTBOakJqTkRSa05UYzRaVFptTkRGaFlqa3dNR0UzWVRVME1qVmlOalE1TW1NeE5nPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkFSQklUUlVNXzB4MmFlMjU0NjBjNDRkNTc4ZTZmNDFhYjkwMGE3YTU0MjViNjQ5MmMxNg==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46QVJCSVRSVU1fMHgyYWUyNTQ2MGM0NGQ1NzhlNmY0MWFiOTAwYTdhNTQyNWI2NDkyYzE2","chain":"ARBITRUM","address":"0x2ae25460c44d578e6f41ab900a7a5425b6492c16","name":"Swaprum Token","symbol":"SAPR","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlFWSkNTVlJTVlUxZk1IaGtNMk15WWpabE5qTXhaRGcwTW1aaVlqTmpZMlF5TjJVek5tSm1NMkZtTnpjMVpEbGhZMkZoX1VTRA==","quantity":45.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa0ZTUWtsVVVsVk5YekI0WkROak1tSTJaVFl6TVdRNE5ESm1ZbUl6WTJOa01qZGxNelppWmpOaFpqYzNOV1E1WVdOaFlRPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkFSQklUUlVNXzB4ZDNjMmI2ZTYzMWQ4NDJmYmIzY2NkMjdlMzZiZjNhZjc3NWQ5YWNhYQ==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46QVJCSVRSVU1fMHhkM2MyYjZlNjMxZDg0MmZiYjNjY2QyN2UzNmJmM2FmNzc1ZDlhY2Fh","chain":"ARBITRUM","address":"0xd3c2b6e631d842fbb3ccd27e36bf3af775d9acaa","name":"Arbitrum","symbol":"ARB","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQXhOVGs1TmpWalltSmpaV1JtWW1Ga05qbGlaRGxpTXpJMU1UQmpOMlprTm1ZMU5tUmlOekU9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3TVRVNU9UWTFZMkppWTJWa1ptSmhaRFk1WW1RNVlqTXlOVEV3WXpkbVpEWm1OVFprWWpjeF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwMTU5OTY1Y2JiY2VkZmJhZDY5YmQ5YjMyNTEwYzdmZDZmNTZkYjcx","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDAxNTk5NjVjYmJjZWRmYmFkNjliZDliMzI1MTBjN2ZkNmY1NmRiNzE=","chain":"POLYGON","address":"0x0159965cbbcedfbad69bd9b32510c7fd6f56db71","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQXpaREU1WWpabE5XWTVZMlE1WkRoaU16Qm1aRGM1TVRjME9UTTFZemd5WTJFeU9UVTFaRGs9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3TTJReE9XSTJaVFZtT1dOa09XUTRZak13Wm1RM09URTNORGt6TldNNE1tTmhNamsxTldRNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwM2QxOWI2ZTVmOWNkOWQ4YjMwZmQ3OTE3NDkzNWM4MmNhMjk1NWQ5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDAzZDE5YjZlNWY5Y2Q5ZDhiMzBmZDc5MTc0OTM1YzgyY2EyOTU1ZDk=","chain":"POLYGON","address":"0x03d19b6e5f9cd9d8b30fd79174935c82ca2955d9","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQTBZMk00TUdNM05qWTFaVFJrTlRkbU5UZGxNVFpoTVRJeE1tRTFOMk13WXpFeE9USTNZMlU9X1VTRA==","quantity":10.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3TkdOak9EQmpOelkyTldVMFpEVTNaalUzWlRFMllURXlNVEpoTlRkak1HTXhNVGt5TjJObF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwNGNjODBjNzY2NWU0ZDU3ZjU3ZTE2YTEyMTJhNTdjMGMxMTkyN2Nl","logoUrl":"https://assets.coingecko.com/coins/images/29090/large/aniv.png?1676524404","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDA0Y2M4MGM3NjY1ZTRkNTdmNTdlMTZhMTIxMmE1N2MwYzExOTI3Y2U=","chain":"POLYGON","address":"0x04cc80c7665e4d57f57e16a1212a57c0c11927ce","name":"Aniverse","symbol":"ANIV","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQTFaR0V6T0ROaE5tTTJOekl3WkRCbFlXWTFaVFJsTmpVMFpERmpZVE14Tnprek9UQTVPVEE9X1VTRA==","quantity":999999.9999,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3TldSaE16Z3pZVFpqTmpjeU1HUXdaV0ZtTldVMFpUWTFOR1F4WTJFek1UYzVNemt3T1Rrd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwNWRhMzgzYTZjNjcyMGQwZWFmNWU0ZTY1NGQxY2EzMTc5MzkwOTkw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDA1ZGEzODNhNmM2NzIwZDBlYWY1ZTRlNjU0ZDFjYTMxNzkzOTA5OTA=","chain":"POLYGON","address":"0x05da383a6c6720d0eaf5e4e654d1ca3179390990","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQmlNV0ZsTVRnNU9HTXlZekZoTkRabFpXUXhaVFkzWlRneE5qa3dZbU01WmpVNU1HVm1Nelk9X1VTRA==","quantity":967000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3WWpGaFpURTRPVGhqTW1NeFlUUTJaV1ZrTVdVMk4yVTRNVFk1TUdKak9XWTFPVEJsWmpNMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwYjFhZTE4OThjMmMxYTQ2ZWVkMWU2N2U4MTY5MGJjOWY1OTBlZjM2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDBiMWFlMTg5OGMyYzFhNDZlZWQxZTY3ZTgxNjkwYmM5ZjU5MGVmMzY=","chain":"POLYGON","address":"0x0b1ae1898c2c1a46eed1e67e81690bc9f590ef36","name":"JOOS CreatorShare #7","symbol":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 ","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQmlPVEZpTURkaVpXSTJOek16TXpJeU5XRTFZbUV3TWpVNVpEVTFZV1ZsTVRCbE0yRTFOemc9X1VTRA==","quantity":300000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3WWpreFlqQTNZbVZpTmpjek16TXlNalZoTldKaE1ESTFPV1ExTldGbFpURXdaVE5oTlRjNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwYjkxYjA3YmViNjczMzMyMjVhNWJhMDI1OWQ1NWFlZTEwZTNhNTc4","logoUrl":"https://assets.coingecko.com/coins/images/702/large/mne.png?1587615060","__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDBiOTFiMDdiZWI2NzMzMzIyNWE1YmEwMjU5ZDU1YWVlMTBlM2E1Nzg=","chain":"POLYGON","address":"0x0b91b07beb67333225a5ba0259d55aee10e3a578","name":"Minereum Polygon","symbol":"MNEP","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESXhZVE00WVRoa04ySmhabVJpWVdabE9XUXlNemd3TTJGbE1qTXhaamswTkdFM1lUUTROR0k9X1VTRA==","quantity":100.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5TVdFek9HRTRaRGRpWVdaa1ltRm1aVGxrTWpNNE1ETmhaVEl6TVdZNU5EUmhOMkUwT0RSaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyMWEzOGE4ZDdiYWZkYmFmZTlkMjM4MDNhZTIzMWY5NDRhN2E0ODRi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDIxYTM4YThkN2JhZmRiYWZlOWQyMzgwM2FlMjMxZjk0NGE3YTQ4NGI=","chain":"POLYGON","address":"0x21a38a8d7bafdbafe9d23803ae231f944a7a484b","name":"KIUI","symbol":"KIUI","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlQxQlVTVTFKVTAxZk1IZ3dZamt4WWpBM1ltVmlOamN6TXpNeU1qVmhOV0poTURJMU9XUTFOV0ZsWlRFd1pUTmhOVGM0X1VTRA==","quantity":300000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPazlRVkVsTlNWTk5YekI0TUdJNU1XSXdOMkpsWWpZM016TXpNakkxWVRWaVlUQXlOVGxrTlRWaFpXVXhNR1V6WVRVM09BPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0Ok9QVElNSVNNXzB4MGI5MWIwN2JlYjY3MzMzMjI1YTViYTAyNTlkNTVhZWUxMGUzYTU3OA==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46T1BUSU1JU01fMHgwYjkxYjA3YmViNjczMzMyMjVhNWJhMDI1OWQ1NWFlZTEwZTNhNTc4","chain":"OPTIMISM","address":"0x0b91b07beb67333225a5ba0259d55aee10e3a578","name":"Minereum OPTIMISM","symbol":"MNEO","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUTBPRGs0TXpObU16STRNekF6WkdSaU1qQTNNak5sTXpJM09EZGtZVE5oWlRnNU1EWm1PRFE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrTkRnNU9ETXpaak15T0RNd00yUmtZakl3TnpJelpUTXlOemczWkdFellXVTRPVEEyWmpnMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkNDg5ODMzZjMyODMwM2RkYjIwNzIzZTMyNzg3ZGEzYWU4OTA2Zjg0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGQ0ODk4MzNmMzI4MzAzZGRiMjA3MjNlMzI3ODdkYTNhZTg5MDZmODQ=","chain":"POLYGON","address":"0xd489833f328303ddb20723e32787da3ae8906f84","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VERTVPREl5WkRJeE0yTmpNVE15TXpNM09UQXhOelJoWXpFeE1EUmtObVV4T0RNM016ZzVaV0U9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd4T1RneU1tUXlNVE5qWXpFek1qTXpOemt3TVRjMFlXTXhNVEEwWkRabE1UZ3pOek00T1dWaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgxOTgyMmQyMTNjYzEzMjMzNzkwMTc0YWMxMTA0ZDZlMTgzNzM4OWVh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDE5ODIyZDIxM2NjMTMyMzM3OTAxNzRhYzExMDRkNmUxODM3Mzg5ZWE=","chain":"POLYGON","address":"0x19822d213cc13233790174ac1104d6e1837389ea","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VERmpOVFpsTjJOaU1HWTFNREV3WVRVME5HRXlaakJqTXpaa1lUYzVOMkl6WkRFMVltVmlPV0U9X1VTRA==","quantity":2.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd4WXpVMlpUZGpZakJtTlRBeE1HRTFORFJoTW1Zd1l6TTJaR0UzT1RkaU0yUXhOV0psWWpsaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgxYzU2ZTdjYjBmNTAxMGE1NDRhMmYwYzM2ZGE3OTdiM2QxNWJlYjlh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDFjNTZlN2NiMGY1MDEwYTU0NGEyZjBjMzZkYTc5N2IzZDE1YmViOWE=","chain":"POLYGON","address":"0x1c56e7cb0f5010a544a2f0c36da797b3d15beb9a","name":"IronVest","symbol":"IRON","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEQmpZV0kzWTJJMk4yVTVNRGxqTXpjek16VXdaRFUxTXpKak0yTmxOVEJoTVRGaVpEWmxOMk09X1VTRA==","quantity":1.6355693547907E-5,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd3WTJGaU4yTmlOamRsT1RBNVl6TTNNek0xTUdRMU5UTXlZek5qWlRVd1lURXhZbVEyWlRkal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgwY2FiN2NiNjdlOTA5YzM3MzM1MGQ1NTMyYzNjZTUwYTExYmQ2ZTdj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDBjYWI3Y2I2N2U5MDljMzczMzUwZDU1MzJjM2NlNTBhMTFiZDZlN2M=","chain":"POLYGON","address":"0x0cab7cb67e909c373350d5532c3ce50a11bd6e7c","name":"NFTChain","symbol":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESXlOR0UzT1RVM1pXTmlOR1ppTjJSaFl6RTJObVEwTnpreFlUVTRZMlJqTldRMlpqUXhOV0k9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5TWpSaE56azFOMlZqWWpSbVlqZGtZV014Tmpaa05EYzVNV0UxT0dOa1l6VmtObVkwTVRWaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyMjRhNzk1N2VjYjRmYjdkYWMxNjZkNDc5MWE1OGNkYzVkNmY0MTVi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDIyNGE3OTU3ZWNiNGZiN2RhYzE2NmQ0NzkxYTU4Y2RjNWQ2ZjQxNWI=","chain":"POLYGON","address":"0x224a7957ecb4fb7dac166d4791a58cdc5d6f415b","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESXlOak5pTmpZd1pUTTVNekpqT0dRMVlqZzNNV1ZsWVRaaE5ERmhOamN5WVRCbE5EUTNaR1E9X1VTRA==","quantity":2999999.9999,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5TWpZellqWTJNR1V6T1RNeVl6aGtOV0k0TnpGbFpXRTJZVFF4WVRZM01tRXdaVFEwTjJSa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyMjYzYjY2MGUzOTMyYzhkNWI4NzFlZWE2YTQxYTY3MmEwZTQ0N2Rk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDIyNjNiNjYwZTM5MzJjOGQ1Yjg3MWVlYTZhNDFhNjcyYTBlNDQ3ZGQ=","chain":"POLYGON","address":"0x2263b660e3932c8d5b871eea6a41a672a0e447dd","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESTJPR1EwWVRWa01HRTRZamRsTURVelpqTXlNVFkzTm1JNE1HSmtPREE1TW1Nek56WTBPR1U9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5Tmpoa05HRTFaREJoT0dJM1pUQTFNMll6TWpFMk56WmlPREJpWkRnd09USmpNemMyTkRobF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyNjhkNGE1ZDBhOGI3ZTA1M2YzMjE2NzZiODBiZDgwOTJjMzc2NDhl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDI2OGQ0YTVkMGE4YjdlMDUzZjMyMTY3NmI4MGJkODA5MmMzNzY0OGU=","chain":"POLYGON","address":"0x268d4a5d0a8b7e053f321676b80bd8092c37648e","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESTNNVGd5T0dZMVpUSTFZemN4TldJeU5qQmlNMlU1TXpCa01ETmxNVGcwWVRVd056STJOV0k9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5TnpFNE1qaG1OV1V5TldNM01UVmlNall3WWpObE9UTXdaREF6WlRFNE5HRTFNRGN5TmpWaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyNzE4MjhmNWUyNWM3MTViMjYwYjNlOTMwZDAzZTE4NGE1MDcyNjVi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDI3MTgyOGY1ZTI1YzcxNWIyNjBiM2U5MzBkMDNlMTg0YTUwNzI2NWI=","chain":"POLYGON","address":"0x271828f5e25c715b260b3e930d03e184a507265b","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESmhObU5sTnpZM01URTNabVF5TTJZME9XTTFNRGM0WkdZd1ptUmxNV1ZqT1RnMU5XSTJaR1E9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5WVRaalpUYzJOekV4TjJaa01qTm1ORGxqTlRBM09HUm1NR1prWlRGbFl6azROVFZpTm1Sa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyYTZjZTc2NzExN2ZkMjNmNDljNTA3OGRmMGZkZTFlYzk4NTViNmRk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDJhNmNlNzY3MTE3ZmQyM2Y0OWM1MDc4ZGYwZmRlMWVjOTg1NWI2ZGQ=","chain":"POLYGON","address":"0x2a6ce767117fd23f49c5078df0fde1ec9855b6dd","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESmlOakk0TnpJMk5tUmhOelJrT1RSak56Rm1aRGhtTmpNNE5XSTFZalk1TmpCaVlUUmlNems9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5WWpZeU9EY3lOalprWVRjMFpEazBZemN4Wm1RNFpqWXpPRFZpTldJMk9UWXdZbUUwWWpNNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyYjYyODcyNjZkYTc0ZDk0YzcxZmQ4ZjYzODViNWI2OTYwYmE0YjM5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDJiNjI4NzI2NmRhNzRkOTRjNzFmZDhmNjM4NWI1YjY5NjBiYTRiMzk=","chain":"POLYGON","address":"0x2b6287266da74d94c71fd8f6385b5b6960ba4b39","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESmlZakJsTm1RM1ptUXlNRE0wWWpNMFkyUXpZelkzTnpoa04yTXdPV1E1WVRNNE9XWmlOMkk9X1VTRA==","quantity":967000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5WW1Jd1pUWmtOMlprTWpBek5HSXpOR05rTTJNMk56YzRaRGRqTURsa09XRXpPRGxtWWpkaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyYmIwZTZkN2ZkMjAzNGIzNGNkM2M2Nzc4ZDdjMDlkOWEzODlmYjdi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDJiYjBlNmQ3ZmQyMDM0YjM0Y2QzYzY3NzhkN2MwOWQ5YTM4OWZiN2I=","chain":"POLYGON","address":"0x2bb0e6d7fd2034b34cd3c6778d7c09d9a389fb7b","name":"JOOS CreatorShare #3","symbol":"JOOS#3","standard":"ERC20","decimals":8,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESmlZekkzWkdFMU16Wm1ORFZpTm1VM1lUZzFZamczT1dNME9HRXpZbUV6TkRabVpqUTFPV1U9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5WW1NeU4yUmhOVE0yWmpRMVlqWmxOMkU0TldJNE56bGpORGhoTTJKaE16UTJabVkwTlRsbF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyYmMyN2RhNTM2ZjQ1YjZlN2E4NWI4NzljNDhhM2JhMzQ2ZmY0NTll","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDJiYzI3ZGE1MzZmNDViNmU3YTg1Yjg3OWM0OGEzYmEzNDZmZjQ1OWU=","chain":"POLYGON","address":"0x2bc27da536f45b6e7a85b879c48a3ba346ff459e","name":"DOGE6","symbol":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VESmtOakZrWkRNM1ltUTJOekUyTm1Ka1pHTTRPRFExWTJNeFltSTBZbVJqTmpKbFpXSXdZV1U9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd5WkRZeFpHUXpOMkprTmpjeE5qWmlaR1JqT0RnME5XTmpNV0ppTkdKa1l6WXlaV1ZpTUdGbF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgyZDYxZGQzN2JkNjcxNjZiZGRjODg0NWNjMWJiNGJkYzYyZWViMGFl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDJkNjFkZDM3YmQ2NzE2NmJkZGM4ODQ1Y2MxYmI0YmRjNjJlZWIwYWU=","chain":"POLYGON","address":"0x2d61dd37bd67166bddc8845cc1bb4bdc62eeb0ae","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETXpObVF3WVRWaE4yVmpOR1prTTJSaU1tVTRNREZoWldZME5tWTFOamxpWW1NME1qbGpaVFE9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6TXpaa01HRTFZVGRsWXpSbVpETmtZakpsT0RBeFlXVm1ORFptTlRZNVltSmpOREk1WTJVMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzMzZkMGE1YTdlYzRmZDNkYjJlODAxYWVmNDZmNTY5YmJjNDI5Y2U0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDMzNmQwYTVhN2VjNGZkM2RiMmU4MDFhZWY0NmY1NjliYmM0MjljZTQ=","chain":"POLYGON","address":"0x336d0a5a7ec4fd3db2e801aef46f569bbc429ce4","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETTBaVFl6WkdNME1UUTBaR1EyWmpVMU1UY3hZV0ZtWlRZeVpERmlPVE5tTlRZNU9URmlOemM9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6TkdVMk0yUmpOREUwTkdSa05tWTFOVEUzTVdGaFptVTJNbVF4WWprelpqVTJPVGt4WWpjM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzNGU2M2RjNDE0NGRkNmY1NTE3MWFhZmU2MmQxYjkzZjU2OTkxYjc3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDM0ZTYzZGM0MTQ0ZGQ2ZjU1MTcxYWFmZTYyZDFiOTNmNTY5OTFiNzc=","chain":"POLYGON","address":"0x34e63dc4144dd6f55171aafe62d1b93f56991b77","name":"DOGE5","symbol":"DOGE5","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETTVZalV6WTJSbVlqVmhPRGxqWXpnNU4yTTJaREZsTUdRelptUXhNekJpWkdJd01XUTFZems9X1VTRA==","quantity":2.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6T1dJMU0yTmtabUkxWVRnNVkyTTRPVGRqTm1ReFpUQmtNMlprTVRNd1ltUmlNREZrTldNNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzOWI1M2NkZmI1YTg5Y2M4OTdjNmQxZTBkM2ZkMTMwYmRiMDFkNWM5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDM5YjUzY2RmYjVhODljYzg5N2M2ZDFlMGQzZmQxMzBiZGIwMWQ1Yzk=","chain":"POLYGON","address":"0x39b53cdfb5a89cc897c6d1e0d3fd130bdb01d5c9","name":"Сuriosity","symbol":"СURIOSITY","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETmhaakZqWVRkbVlXTmpOV0ZoWXpaalpEaGxOREUxWVRReU16ZGhPRFZoTWpFd016SmlOems9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6WVdZeFkyRTNabUZqWXpWaFlXTTJZMlE0WlRReE5XRTBNak0zWVRnMVlUSXhNRE15WWpjNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzYWYxY2E3ZmFjYzVhYWM2Y2Q4ZTQxNWE0MjM3YTg1YTIxMDMyYjc5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDNhZjFjYTdmYWNjNWFhYzZjZDhlNDE1YTQyMzdhODVhMjEwMzJiNzk=","chain":"POLYGON","address":"0x3af1ca7facc5aac6cd8e415a4237a85a21032b79","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETmpNR0prTWpFeE9HRTFaVFl4WXpReFpESmhaR1ZsWW1OaU9HSTNOVFkzWm1SbE1XTmlZV1k9X1VTRA==","quantity":3.28321703E-5,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6WXpCaVpESXhNVGhoTldVMk1XTTBNV1F5WVdSbFpXSmpZamhpTnpVMk4yWmtaVEZqWW1GbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzYzBiZDIxMThhNWU2MWM0MWQyYWRlZWJjYjhiNzU2N2ZkZTFjYmFm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDNjMGJkMjExOGE1ZTYxYzQxZDJhZGVlYmNiOGI3NTY3ZmRlMWNiYWY=","chain":"POLYGON","address":"0x3c0bd2118a5e61c41d2adeebcb8b7567fde1cbaf","name":"Cookie","symbol":"CKIE","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VETmxNRE5oTW1ZeU5XUm1OVFpsWWpJMU1XWTJNekkwWm1VMFpEUmhOMkZsTkRjeE5XTTFNV0k9X1VTRA==","quantity":500.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd6WlRBellUSm1NalZrWmpVMlpXSXlOVEZtTmpNeU5HWmxOR1EwWVRkaFpUUTNNVFZqTlRGaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgzZTAzYTJmMjVkZjU2ZWIyNTFmNjMyNGZlNGQ0YTdhZTQ3MTVjNTFi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDNlMDNhMmYyNWRmNTZlYjI1MWY2MzI0ZmU0ZDRhN2FlNDcxNWM1MWI=","chain":"POLYGON","address":"0x3e03a2f25df56eb251f6324fe4d4a7ae4715c51b","name":"A Coin","symbol":"ACOIN","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUXdZVEExT0dFek56YzROMlpqTjJZMU5ESTNZekZoWkRjNU5UUXhZV0U1TWpVMFl6WmxNamM9X1VTRA==","quantity":100.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwTUdFd05UaGhNemMzT0RkbVl6ZG1OVFF5TjJNeFlXUTNPVFUwTVdGaE9USTFOR00yWlRJM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0MGEwNThhMzc3ODdmYzdmNTQyN2MxYWQ3OTU0MWFhOTI1NGM2ZTI3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDQwYTA1OGEzNzc4N2ZjN2Y1NDI3YzFhZDc5NTQxYWE5MjU0YzZlMjc=","chain":"POLYGON","address":"0x40a058a37787fc7f5427c1ad79541aa9254c6e27","name":"TestUSDCToken","symbol":"TUSDC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUXlOVGxrTWpZeE5UUTJNakUxTXprMk5EZ3hOVGd6TmpReE1EUmlPR1UzT0RGaU9HRTNPR0U9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwTWpVNVpESTJNVFUwTmpJeE5UTTVOalE0TVRVNE16WTBNVEEwWWpobE56Z3hZamhoTnpoaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0MjU5ZDI2MTU0NjIxNTM5NjQ4MTU4MzY0MTA0YjhlNzgxYjhhNzhh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDQyNTlkMjYxNTQ2MjE1Mzk2NDgxNTgzNjQxMDRiOGU3ODFiOGE3OGE=","chain":"POLYGON","address":"0x4259d26154621539648158364104b8e781b8a78a","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUXlPVEUxT1dJNFpHUmlaVEV5TjJJelptUXlNbU00T1RsaFlUUTFZems0WWprd1ptUmlZMkU9X1VTRA==","quantity":120.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwTWpreE5UbGlPR1JrWW1VeE1qZGlNMlprTWpKak9EazVZV0UwTldNNU9HSTVNR1prWW1OaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0MjkxNTliOGRkYmUxMjdiM2ZkMjJjODk5YWE0NWM5OGI5MGZkYmNh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDQyOTE1OWI4ZGRiZTEyN2IzZmQyMmM4OTlhYTQ1Yzk4YjkwZmRiY2E=","chain":"POLYGON","address":"0x429159b8ddbe127b3fd22c899aa45c98b90fdbca","name":"Magen","symbol":"MAGEN","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUTFaR1F4T0dNMVpUQm1ZVGN3TVdGaVptWTBORGxtTmpVME1tRmhOVE5sTWpVNE56RXdZalE9X1VTRA==","quantity":572.7806608,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwTldSa01UaGpOV1V3Wm1FM01ERmhZbVptTkRRNVpqWTFOREpoWVRVelpUSTFPRGN4TUdJMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0NWRkMThjNWUwZmE3MDFhYmZmNDQ5ZjY1NDJhYTUzZTI1ODcxMGI0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDQ1ZGQxOGM1ZTBmYTcwMWFiZmY0NDlmNjU0MmFhNTNlMjU4NzEwYjQ=","chain":"POLYGON","address":"0x45dd18c5e0fa701abff449f6542aa53e258710b4","name":"SomniLife","symbol":"SO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUmpaVFEyWmpCbU1EVXpaV1JtTldKbVpqRTBZakE1TXpCaVlqZGhPRFUwTVRSak9EVmlPR1k9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwWTJVME5tWXdaakExTTJWa1pqVmlabVl4TkdJd09UTXdZbUkzWVRnMU5ERTBZemcxWWpobV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0Y2U0NmYwZjA1M2VkZjViZmYxNGIwOTMwYmI3YTg1NDE0Yzg1Yjhm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDRjZTQ2ZjBmMDUzZWRmNWJmZjE0YjA5MzBiYjdhODU0MTRjODViOGY=","chain":"POLYGON","address":"0x4ce46f0f053edf5bff14b0930bb7a85414c85b8f","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUmtNR1kxWXpNNU5UQXhOMk5rTkRVMU1qQXpOR0l4TkdRMk1UVmpNMkkzTkRJellqZ3dPV1k9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwWkRCbU5XTXpPVFV3TVRkalpEUTFOVEl3TXpSaU1UUmtOakUxWXpOaU56UXlNMkk0TURsbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0ZDBmNWMzOTUwMTdjZDQ1NTIwMzRiMTRkNjE1YzNiNzQyM2I4MDlm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDRkMGY1YzM5NTAxN2NkNDU1MjAzNGIxNGQ2MTVjM2I3NDIzYjgwOWY=","chain":"POLYGON","address":"0x4d0f5c395017cd4552034b14d615c3b7423b809f","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUmxNbVZpWWpZMFl6ZzNaakUxWkdVeVpHSmtNVE5qTnpVME4yRTROVE5oWldRd09EUXhZMlE9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwWlRKbFltSTJOR000TjJZeE5XUmxNbVJpWkRFell6YzFORGRoT0RVellXVmtNRGcwTVdOa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0ZTJlYmI2NGM4N2YxNWRlMmRiZDEzYzc1NDdhODUzYWVkMDg0MWNk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDRlMmViYjY0Yzg3ZjE1ZGUyZGJkMTNjNzU0N2E4NTNhZWQwODQxY2Q=","chain":"POLYGON","address":"0x4e2ebb64c87f15de2dbd13c7547a853aed0841cd","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEUmxNelZpT0dJMVpHVm1aamczT0RaaFlXVTRPR1ZrWkRnNE56VmxaVGhoWVRGa05XUTNZelE9X1VTRA==","quantity":1.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcwWlRNMVlqaGlOV1JsWm1ZNE56ZzJZV0ZsT0RobFpHUTRPRGMxWldVNFlXRXhaRFZrTjJNMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg0ZTM1YjhiNWRlZmY4Nzg2YWFlODhlZGQ4ODc1ZWU4YWExZDVkN2M0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDRlMzViOGI1ZGVmZjg3ODZhYWU4OGVkZDg4NzVlZThhYTFkNWQ3YzQ=","chain":"POLYGON","address":"0x4e35b8b5deff8786aae88edd8875ee8aa1d5d7c4","name":"999 MATIC","symbol":"https://wincoin.win/","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVXpNR0UyTjJJNU5UQTVOek5qTXpNMU1qY3hNMk5qWVRObU5qWTBOV1l4Tm1Oa09UWmtZMkk9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxTXpCaE5qZGlPVFV3T1Rjell6TXpOVEkzTVROalkyRXpaalkyTkRWbU1UWmpaRGsyWkdOaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1MzBhNjdiOTUwOTczYzMzNTI3MTNjY2EzZjY2NDVmMTZjZDk2ZGNi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDUzMGE2N2I5NTA5NzNjMzM1MjcxM2NjYTNmNjY0NWYxNmNkOTZkY2I=","chain":"POLYGON","address":"0x530a67b950973c3352713cca3f6645f16cd96dcb","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVTBNelkyWkRjeE9XUmxNelkwTWpRNVltVmtNakF6TVRFMVkyTXhNVFpsWmpoa05HUmtPR1k9X1VTRA==","quantity":10000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxTkRNMk5tUTNNVGxrWlRNMk5ESTBPV0psWkRJd016RXhOV05qTVRFMlpXWTRaRFJrWkRobV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1NDM2NmQ3MTlkZTM2NDI0OWJlZDIwMzExNWNjMTE2ZWY4ZDRkZDhm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDU0MzY2ZDcxOWRlMzY0MjQ5YmVkMjAzMTE1Y2MxMTZlZjhkNGRkOGY=","chain":"POLYGON","address":"0x54366d719de364249bed203115cc116ef8d4dd8f","name":"Interchained","symbol":"INT","standard":"ERC20","decimals":9,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVTFaR0k0TUdJMllUSTVabUV4WWpjME1UUTNOMkkzTkRnd05XWTFOVGhrT0Rjd056bGxZVGM9X1VTRA==","quantity":100.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxTldSaU9EQmlObUV5T1daaE1XSTNOREUwTnpkaU56UTRNRFZtTlRVNFpEZzNNRGM1WldFM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1NWRiODBiNmEyOWZhMWI3NDE0NzdiNzQ4MDVmNTU4ZDg3MDc5ZWE3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDU1ZGI4MGI2YTI5ZmExYjc0MTQ3N2I3NDgwNWY1NThkODcwNzllYTc=","chain":"POLYGON","address":"0x55db80b6a29fa1b741477b74805f558d87079ea7","name":"Dummy Test Token","symbol":"DTT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVmhaVGN6T0RSbFpUUm1NalU1TURVeVltUmlPRE0xTm1VNE9UWXdNR1kyTVdJMk9ERmpOamM9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxWVdVM016ZzBaV1UwWmpJMU9UQTFNbUprWWpnek5UWmxPRGsyTURCbU5qRmlOamd4WXpZM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1YWU3Mzg0ZWU0ZjI1OTA1MmJkYjgzNTZlODk2MDBmNjFiNjgxYzY3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDVhZTczODRlZTRmMjU5MDUyYmRiODM1NmU4OTYwMGY2MWI2ODFjNjc=","chain":"POLYGON","address":"0x5ae7384ee4f259052bdb8356e89600f61b681c67","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVmlNRFl5Tkdaak5UUTRNakJqTW1KbFpUQTJNekJoTVRnNU1qaGhNMkkwTmpnNU1UVmxOMk09X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxWWpBMk1qUm1ZelUwT0RJd1l6SmlaV1V3TmpNd1lURTRPVEk0WVROaU5EWTRPVEUxWlRkal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1YjA2MjRmYzU0ODIwYzJiZWUwNjMwYTE4OTI4YTNiNDY4OTE1ZTdj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDViMDYyNGZjNTQ4MjBjMmJlZTA2MzBhMTg5MjhhM2I0Njg5MTVlN2M=","chain":"POLYGON","address":"0x5b0624fc54820c2bee0630a18928a3b468915e7c","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEVmlaR05tTjJRNVpEWXhZV1ExWkdZelpXRTNOVGhrTVRnMVpqWmtNMk5rWW1GalpXRXdOR1k9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcxWW1SalpqZGtPV1EyTVdGa05XUm1NMlZoTnpVNFpERTROV1kyWkROalpHSmhZMlZoTURSbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg1YmRjZjdkOWQ2MWFkNWRmM2VhNzU4ZDE4NWY2ZDNjZGJhY2VhMDRm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDViZGNmN2Q5ZDYxYWQ1ZGYzZWE3NThkMTg1ZjZkM2NkYmFjZWEwNGY=","chain":"POLYGON","address":"0x5bdcf7d9d61ad5df3ea758d185f6d3cdbacea04f","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWXhNR0UzTVdWaE5EQmtNMlEzTkRZNU5qbGlNVFpsWmpBM1pERmlNRGsyTm1WbE56VmpPREk9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyTVRCaE56RmxZVFF3WkROa056UTJPVFk1WWpFMlpXWXdOMlF4WWpBNU5qWmxaVGMxWXpneV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2MTBhNzFlYTQwZDNkNzQ2OTY5YjE2ZWYwN2QxYjA5NjZlZTc1Yzgy","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDYxMGE3MWVhNDBkM2Q3NDY5NjliMTZlZjA3ZDFiMDk2NmVlNzVjODI=","chain":"POLYGON","address":"0x610a71ea40d3d746969b16ef07d1b0966ee75c82","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWXhZVGszWXpRNU1UUmpORE5tT0dKa09UaGtNREZoT1RrME1UaGxPREkyWXpnd1lXWmhaR009X1VTRA==","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyTVdFNU4yTTBPVEUwWXpRelpqaGlaRGs0WkRBeFlUazVOREU0WlRneU5tTTRNR0ZtWVdSal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2MWE5N2M0OTE0YzQzZjhiZDk4ZDAxYTk5NDE4ZTgyNmM4MGFmYWRj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDYxYTk3YzQ5MTRjNDNmOGJkOThkMDFhOTk0MThlODI2YzgwYWZhZGM=","chain":"POLYGON","address":"0x61a97c4914c43f8bd98d01a99418e826c80afadc","name":"Crypto Bonus Miles Token","symbol":"CBM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWTJaamMwTldVMU9UZ3pZakUzWVdSbE1UZ3pNbVl4T1RjMU5ESXdaV1prTmpFME9UTTNPVGc9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyTm1ZM05EVmxOVGs0TTJJeE4yRmtaVEU0TXpKbU1UazNOVFF5TUdWbVpEWXhORGt6TnprNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2NmY3NDVlNTk4M2IxN2FkZTE4MzJmMTk3NTQyMGVmZDYxNDkzNzk4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDY2Zjc0NWU1OTgzYjE3YWRlMTgzMmYxOTc1NDIwZWZkNjE0OTM3OTg=","chain":"POLYGON","address":"0x66f745e5983b17ade1832f1975420efd61493798","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWTNZbVk1TlRkaU5XTmhaVEpqWmprek5XRTBPR05tTlRNeE0yTXlOek13WkRsa1pUUXdabVE9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyTjJKbU9UVTNZalZqWVdVeVkyWTVNelZoTkRoalpqVXpNVE5qTWpjek1HUTVaR1UwTUdaa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2N2JmOTU3YjVjYWUyY2Y5MzVhNDhjZjUzMTNjMjczMGQ5ZGU0MGZk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDY3YmY5NTdiNWNhZTJjZjkzNWE0OGNmNTMxM2MyNzMwZDlkZTQwZmQ=","chain":"POLYGON","address":"0x67bf957b5cae2cf935a48cf5313c2730d9de40fd","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWTRZVE5rWXpsa05UWTRabUkyTjJRd1ptUmhNR0ppTlRVeE1qSmpaV0kyTW1ObFkyRmpOalk9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyT0dFelpHTTVaRFUyT0daaU5qZGtNR1prWVRCaVlqVTFNVEl5WTJWaU5qSmpaV05oWXpZMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2OGEzZGM5ZDU2OGZiNjdkMGZkYTBiYjU1MTIyY2ViNjJjZWNhYzY2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDY4YTNkYzlkNTY4ZmI2N2QwZmRhMGJiNTUxMjJjZWI2MmNlY2FjNjY=","chain":"POLYGON","address":"0x68a3dc9d568fb67d0fda0bb55122ceb62cecac66","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWmpZMlU0WkRCak5UY3dOMk5tTVRSak1qa3hOekV4T0dKaVlUZGhNRFpoTUdaak5qY3hNVFE9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyWTJObE9HUXdZelUzTURkalpqRTBZekk1TVRjeE1UaGlZbUUzWVRBMllUQm1ZelkzTVRFMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2Y2NlOGQwYzU3MDdjZjE0YzI5MTcxMThiYmE3YTA2YTBmYzY3MTE0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDZjY2U4ZDBjNTcwN2NmMTRjMjkxNzExOGJiYTdhMDZhMGZjNjcxMTQ=","chain":"POLYGON","address":"0x6cce8d0c5707cf14c2917118bba7a06a0fc67114","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWm1PVEpoT0RoaE1tSmpabUV4WkdKbU1UVXhNMlJtWVdGaFlqQTVNRGxtTkRBMlpESmlPRFk9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyWmpreVlUZzRZVEppWTJaaE1XUmlaakUxTVROa1ptRmhZV0l3T1RBNVpqUXdObVF5WWpnMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2ZjkyYTg4YTJiY2ZhMWRiZjE1MTNkZmFhYWIwOTA5ZjQwNmQyYjg2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDZmOTJhODhhMmJjZmExZGJmMTUxM2RmYWFhYjA5MDlmNDA2ZDJiODY=","chain":"POLYGON","address":"0x6f92a88a2bcfa1dbf1513dfaaab0909f406d2b86","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEWm1ZekUyWmpJNE0yVm1ZMlprTVdObU5URTFNRGMxTTJZeVptSTJZVFl5TVRnMFlqVXhabUU9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGcyWm1NeE5tWXlPRE5sWm1ObVpERmpaalV4TlRBM05UTm1NbVppTm1FMk1qRTROR0kxTVdaaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg2ZmMxNmYyODNlZmNmZDFjZjUxNTA3NTNmMmZiNmE2MjE4NGI1MWZh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDZmYzE2ZjI4M2VmY2ZkMWNmNTE1MDc1M2YyZmI2YTYyMTg0YjUxZmE=","chain":"POLYGON","address":"0x6fc16f283efcfd1cf5150753f2fb6a62184b51fa","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEY3dNVGd5TW1abU5qRm1ObVV6TkdaallUVmpaVGRtTTJFd056RmtNMlEzTlRkaVlqQmhOelk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczTURFNE1qSm1aall4WmpabE16Um1ZMkUxWTJVM1pqTmhNRGN4WkROa056VTNZbUl3WVRjMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3MDE4MjJmZjYxZjZlMzRmY2E1Y2U3ZjNhMDcxZDNkNzU3YmIwYTc2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDcwMTgyMmZmNjFmNmUzNGZjYTVjZTdmM2EwNzFkM2Q3NTdiYjBhNzY=","chain":"POLYGON","address":"0x701822ff61f6e34fca5ce7f3a071d3d757bb0a76","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEY3lNRGhsTVRJMlpETmpORFV4TlRRM1lXTXhPVFJsTm1Sa1pXRTFOMlE0WWpjeE1qVmxZVGc9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczTWpBNFpURXlObVF6WXpRMU1UVTBOMkZqTVRrMFpUWmtaR1ZoTlRka09HSTNNVEkxWldFNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3MjA4ZTEyNmQzYzQ1MTU0N2FjMTk0ZTZkZGVhNTdkOGI3MTI1ZWE4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDcyMDhlMTI2ZDNjNDUxNTQ3YWMxOTRlNmRkZWE1N2Q4YjcxMjVlYTg=","chain":"POLYGON","address":"0x7208e126d3c451547ac194e6ddea57d8b7125ea8","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEYzJZV1kyWXpNMllUbGtORFZsWXpkaU16QmhOR05sTW1ReVlXTmtNelF6TVdObU1Ua3hNbVk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczTm1GbU5tTXpObUU1WkRRMVpXTTNZak13WVRSalpUSmtNbUZqWkRNME16RmpaakU1TVRKbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3NmFmNmMzNmE5ZDQ1ZWM3YjMwYTRjZTJkMmFjZDM0MzFjZjE5MTJm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDc2YWY2YzM2YTlkNDVlYzdiMzBhNGNlMmQyYWNkMzQzMWNmMTkxMmY=","chain":"POLYGON","address":"0x76af6c36a9d45ec7b30a4ce2d2acd3431cf1912f","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEYzNOalE1WkdFeU9UWTRPRGswTTJFM1pXTmlNbUUzT0RVeFpHRmlPRFJpWWpreE9UTTBOV1k9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczTnpZME9XUmhNamsyT0RnNU5ETmhOMlZqWWpKaE56ZzFNV1JoWWpnMFltSTVNVGt6TkRWbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3NzY0OWRhMjk2ODg5NDNhN2VjYjJhNzg1MWRhYjg0YmI5MTkzNDVm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDc3NjQ5ZGEyOTY4ODk0M2E3ZWNiMmE3ODUxZGFiODRiYjkxOTM0NWY=","chain":"POLYGON","address":"0x77649da29688943a7ecb2a7851dab84bb919345f","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEYzVOV1U1TkdRNE5USmlOVGswTjJNMk56azFaVFZtWVRZek1EaGxZVFV4TkRObU0ySTVNV1U9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczT1RWbE9UUmtPRFV5WWpVNU5EZGpOamM1TldVMVptRTJNekE0WldFMU1UUXpaak5pT1RGbF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3OTVlOTRkODUyYjU5NDdjNjc5NWU1ZmE2MzA4ZWE1MTQzZjNiOTFl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDc5NWU5NGQ4NTJiNTk0N2M2Nzk1ZTVmYTYzMDhlYTUxNDNmM2I5MWU=","chain":"POLYGON","address":"0x795e94d852b5947c6795e5fa6308ea5143f3b91e","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEZGpOak00TXpjMVlUazFOV05sT1RsaE1XRTFaRFUwT1RrNFptSTJaVFl6WVRFME1qQXhPR1E9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczWXpZek9ETTNOV0U1TlRWalpUazVZVEZoTldRMU5EazVPR1ppTm1VMk0yRXhOREl3TVRoa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3YzYzODM3NWE5NTVjZTk5YTFhNWQ1NDk5OGZiNmU2M2ExNDIwMThk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDdjNjM4Mzc1YTk1NWNlOTlhMWE1ZDU0OTk4ZmI2ZTYzYTE0MjAxOGQ=","chain":"POLYGON","address":"0x7c638375a955ce99a1a5d54998fb6e63a142018d","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEZGtZV05rTVdVMllqUXdPVGRpWW1GbE1HWXlabVJtTkdSbVl6QTVNRGt5WkRnd1lqVmlOams9X1VTRA==","quantity":100.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGczWkdGalpERmxObUkwTURrM1ltSmhaVEJtTW1aa1pqUmtabU13T1RBNU1tUTRNR0kxWWpZNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg3ZGFjZDFlNmI0MDk3YmJhZTBmMmZkZjRkZmMwOTA5MmQ4MGI1YjY5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDdkYWNkMWU2YjQwOTdiYmFlMGYyZmRmNGRmYzA5MDkyZDgwYjViNjk=","chain":"POLYGON","address":"0x7dacd1e6b4097bbae0f2fdf4dfc09092d80b5b69","name":"GawdBlood","symbol":"BLOOD","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEZ3lNRE16TkRNNVptRmhPRGc0WkRsa1pqazFZVFk1TURCa01qazVOekU1TlRFME1EQmlaVE09X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0TWpBek16UXpPV1poWVRnNE9HUTVaR1k1TldFMk9UQXdaREk1T1RjeE9UVXhOREF3WW1Vel9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4MjAzMzQzOWZhYTg4OGQ5ZGY5NWE2OTAwZDI5OTcxOTUxNDAwYmUz","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDgyMDMzNDM5ZmFhODg4ZDlkZjk1YTY5MDBkMjk5NzE5NTE0MDBiZTM=","chain":"POLYGON","address":"0x82033439faa888d9df95a6900d29971951400be3","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEZzBNbVUyTXpsbVl6ZzNPVGswT0RJelkyWmlNVGM0T0RReU5UQTBaR0ZpWlRNNVpUTmlOVFk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0TkRKbE5qTTVabU00TnprNU5EZ3lNMk5tWWpFM09EZzBNalV3TkdSaFltVXpPV1V6WWpVMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4NDJlNjM5ZmM4Nzk5NDgyM2NmYjE3ODg0MjUwNGRhYmUzOWUzYjU2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDg0MmU2MzlmYzg3OTk0ODIzY2ZiMTc4ODQyNTA0ZGFiZTM5ZTNiNTY=","chain":"POLYGON","address":"0x842e639fc87994823cfb178842504dabe39e3b56","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEaGhaVFppT0dJMU1UazVPR001TnpSaE5URTNabUUyTVdNek16TTBZVGs0T0RSalkyRTJOalU9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0WVdVMllqaGlOVEU1T1Roak9UYzBZVFV4TjJaaE5qRmpNek16TkdFNU9EZzBZMk5oTmpZMV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4YWU2YjhiNTE5OThjOTc0YTUxN2ZhNjFjMzMzNGE5ODg0Y2NhNjY1","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDhhZTZiOGI1MTk5OGM5NzRhNTE3ZmE2MWMzMzM0YTk4ODRjY2E2NjU=","chain":"POLYGON","address":"0x8ae6b8b51998c974a517fa61c3334a9884cca665","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEaG1OREEyWXprME56aGlPV1kxT0dNME1EZGlZekEzWW1aak1URmpZVE5sTWpKaE1tWXdaR0U9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0WmpRd05tTTVORGM0WWpsbU5UaGpOREEzWW1Nd04ySm1ZekV4WTJFelpUSXlZVEptTUdSaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4ZjQwNmM5NDc4YjlmNThjNDA3YmMwN2JmYzExY2EzZTIyYTJmMGRh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDhmNDA2Yzk0NzhiOWY1OGM0MDdiYzA3YmZjMTFjYTNlMjJhMmYwZGE=","chain":"POLYGON","address":"0x8f406c9478b9f58c407bc07bfc11ca3e22a2f0da","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEaG1aVEkzWWpneE56SmxaakEyTTJSbE5UVTBPR0ZqTVRFM04yTTJNell3TlRJMU5qTTFOV0U9X1VTRA==","quantity":1500.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0Wm1VeU4ySTRNVGN5WldZd05qTmtaVFUxTkRoaFl6RXhOemRqTmpNMk1EVXlOVFl6TlRWaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4ZmUyN2I4MTcyZWYwNjNkZTU1NDhhYzExNzdjNjM2MDUyNTYzNTVh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDhmZTI3YjgxNzJlZjA2M2RlNTU0OGFjMTE3N2M2MzYwNTI1NjM1NWE=","chain":"POLYGON","address":"0x8fe27b8172ef063de5548ac1177c63605256355a","name":"Product Test Token","symbol":"PTT","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEaG1aak0yWXpnMU5HUTVNREUyTWpBME56WXlNekJrWkRobVltRTJOemd3WVdRME5HTTFPR0U9X1VTRA==","quantity":20.2,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc0Wm1Zek5tTTROVFJrT1RBeE5qSXdORGMyTWpNd1pHUTRabUpoTmpjNE1HRmtORFJqTlRoaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg4ZmYzNmM4NTRkOTAxNjIwNDc2MjMwZGQ4ZmJhNjc4MGFkNDRjNThh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDhmZjM2Yzg1NGQ5MDE2MjA0NzYyMzBkZDhmYmE2NzgwYWQ0NGM1OGE=","chain":"POLYGON","address":"0x8ff36c854d901620476230dd8fba6780ad44c58a","name":"Giggly Token Rewards","symbol":"GTR","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3hNakE0T0daaE0yWTFPRE5tT1dNMk9UTmtOamN5WldNNU9HRTNPRGRtTVdJek16QmxZak09X1VTRA==","quantity":1.0E-6,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TVRJd09EaG1ZVE5tTlRnelpqbGpOamt6WkRZM01tVmpPVGhoTnpnM1pqRmlNek13WldJel9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MTIwODhmYTNmNTgzZjljNjkzZDY3MmVjOThhNzg3ZjFiMzMwZWIz","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkxMjA4OGZhM2Y1ODNmOWM2OTNkNjcyZWM5OGE3ODdmMWIzMzBlYjM=","chain":"POLYGON","address":"0x912088fa3f583f9c693d672ec98a787f1b330eb3","name":"InCoin","symbol":"InC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3hNelk1TVRZNU4yUXhOR1JpWW1aa1l6TXpZMkV6TURKalpEWTJZbVk1TlRjMk1ERm1OV0k9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TVRNMk9URTJPVGRrTVRSa1ltSm1aR016TTJOaE16QXlZMlEyTm1KbU9UVTNOakF4WmpWaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MTM2OTE2OTdkMTRkYmJmZGMzM2NhMzAyY2Q2NmJmOTU3NjAxZjVi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkxMzY5MTY5N2QxNGRiYmZkYzMzY2EzMDJjZDY2YmY5NTc2MDFmNWI=","chain":"POLYGON","address":"0x913691697d14dbbfdc33ca302cd66bf957601f5b","name":"ARCADIUM","symbol":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3hZMlkxWVRkbU1qWXdObUl3WVdNMU1ERmhaR1EwTlRRMk5XUTNZVEl6TlRKbE9XVXpZV009X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TVdObU5XRTNaakkyTURaaU1HRmpOVEF4WVdSa05EVTBOalZrTjJFeU16VXlaVGxsTTJGal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MWNmNWE3ZjI2MDZiMGFjNTAxYWRkNDU0NjVkN2EyMzUyZTllM2Fj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkxY2Y1YTdmMjYwNmIwYWM1MDFhZGQ0NTQ2NWQ3YTIzNTJlOWUzYWM=","chain":"POLYGON","address":"0x91cf5a7f2606b0ac501add45465d7a2352e9e3ac","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3haVEl3T0RGaFpXTTJZekptWlRKaVlXVTBOV1E1TlRZNU1ETmtNV1l3T0dFeE5URTVZakE9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TVdVeU1EZ3hZV1ZqTm1NeVptVXlZbUZsTkRWa09UVTJPVEF6WkRGbU1EaGhNVFV4T1dJd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MWUyMDgxYWVjNmMyZmUyYmFlNDVkOTU2OTAzZDFmMDhhMTUxOWIw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkxZTIwODFhZWM2YzJmZTJiYWU0NWQ5NTY5MDNkMWYwOGExNTE5YjA=","chain":"POLYGON","address":"0x91e2081aec6c2fe2bae45d956903d1f08a1519b0","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3lZV0UwWmpNeU5qRTRaRGhsWkRVMFpXSTROekl3TmpNd00yRTVOV1ZtWVdKak1qZGhNVGs9X1VTRA==","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TW1GaE5HWXpNall4T0dRNFpXUTFOR1ZpT0RjeU1EWXpNRE5oT1RWbFptRmlZekkzWVRFNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MmFhNGYzMjYxOGQ4ZWQ1NGViODcyMDYzMDNhOTVlZmFiYzI3YTE5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkyYWE0ZjMyNjE4ZDhlZDU0ZWI4NzIwNjMwM2E5NWVmYWJjMjdhMTk=","chain":"POLYGON","address":"0x92aa4f32618d8ed54eb87206303a95efabc27a19","name":"USDC","symbol":"USDC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEa3pNelk0WWpNeU1qYzVOMlZtTldOaFpqSXpNRFk1T0dZek1XWmxZVFJqTlRjMk4yRTBNV009X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TXpNMk9HSXpNakkzT1RkbFpqVmpZV1l5TXpBMk9UaG1NekZtWldFMFl6VTNOamRoTkRGal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5MzM2OGIzMjI3OTdlZjVjYWYyMzA2OThmMzFmZWE0YzU3NjdhNDFj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDkzMzY4YjMyMjc5N2VmNWNhZjIzMDY5OGYzMWZlYTRjNTc2N2E0MWM=","chain":"POLYGON","address":"0x93368b322797ef5caf230698f31fea4c5767a41c","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEazFPVE16T0dWa01XRmtaRGRsWVRsbVpEUTVaVEE0Tm1FMk1XRmhNV0V5TVdFNE1XRTFaVEE9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1TlRrek16aGxaREZoWkdRM1pXRTVabVEwT1dVd09EWmhOakZoWVRGaE1qRmhPREZoTldVd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5NTkzMzhlZDFhZGQ3ZWE5ZmQ0OWUwODZhNjFhYTFhMjFhODFhNWUw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDk1OTMzOGVkMWFkZDdlYTlmZDQ5ZTA4NmE2MWFhMWEyMWE4MWE1ZTA=","chain":"POLYGON","address":"0x959338ed1add7ea9fd49e086a61aa1a21a81a5e0","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEbGlNamd3Tmprd05HWXlOelZtTldKbVl6QTVZV1l4Wm1FeVpHSTJNVE0yTkRnellUZ3daR1k9X1VTRA==","quantity":60000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1WWpJNE1EWTVNRFJtTWpjMVpqVmlabU13T1dGbU1XWmhNbVJpTmpFek5qUTRNMkU0TUdSbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5YjI4MDY5MDRmMjc1ZjViZmMwOWFmMWZhMmRiNjEzNjQ4M2E4MGRm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDliMjgwNjkwNGYyNzVmNWJmYzA5YWYxZmEyZGI2MTM2NDgzYTgwZGY=","chain":"POLYGON","address":"0x9b2806904f275f5bfc09af1fa2db6136483a80df","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEbGlOVFJsWWpsak9UQmpZbU13TXpJM05HUmpPVEF5TlRJNU1ERmhNekV4WVRjeFpHTmxNV1k9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1WWpVMFpXSTVZemt3WTJKak1ETXlOelJrWXprd01qVXlPVEF4WVRNeE1XRTNNV1JqWlRGbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5YjU0ZWI5YzkwY2JjMDMyNzRkYzkwMjUyOTAxYTMxMWE3MWRjZTFm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDliNTRlYjljOTBjYmMwMzI3NGRjOTAyNTI5MDFhMzExYTcxZGNlMWY=","chain":"POLYGON","address":"0x9b54eb9c90cbc03274dc90252901a311a71dce1f","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEbGxNbVF5Tmpaa05tTTVNR1kyWXpCa09EQmhPRGd4TlRsaU1UVTVOVGhtTnpFek5XSTRZV1k9X1VTRA==","quantity":923000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1WlRKa01qWTJaRFpqT1RCbU5tTXdaRGd3WVRnNE1UVTVZakUxT1RVNFpqY3hNelZpT0dGbV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5ZTJkMjY2ZDZjOTBmNmMwZDgwYTg4MTU5YjE1OTU4ZjcxMzViOGFm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDllMmQyNjZkNmM5MGY2YzBkODBhODgxNTliMTU5NThmNzEzNWI4YWY=","chain":"POLYGON","address":"0x9e2d266d6c90f6c0d80a88159b15958f7135b8af","name":"StakeShare","symbol":"SSX","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VEbG1aRFE1TmprMU56Tm1PV1JsWXpjNE9ESTBNRGszTURsak9XSXpOV1l5WkdNek1EYzBZMkU9X1VTRA==","quantity":628.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGc1Wm1RME9UWTVOVGN6Wmpsa1pXTTNPRGd5TkRBNU56QTVZemxpTXpWbU1tUmpNekEzTkdOaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHg5ZmQ0OTY5NTczZjlkZWM3ODgyNDA5NzA5YzliMzVmMmRjMzA3NGNh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDlmZDQ5Njk1NzNmOWRlYzc4ODI0MDk3MDljOWIzNWYyZGMzMDc0Y2E=","chain":"POLYGON","address":"0x9fd4969573f9dec7882409709c9b35f2dc3074ca","name":"TAO Coin","symbol":"TAO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHRXpaakl6Tm1SaU1HWXhOV1EyTlRabU9EYzVNRGMzTjJRd1lqZ3dZakUyTlRjd1lUQTFZelU9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhoTTJZeU16WmtZakJtTVRWa05qVTJaamczT1RBM056ZGtNR0k0TUdJeE5qVTNNR0V3TldNMV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhhM2YyMzZkYjBmMTVkNjU2Zjg3OTA3NzdkMGI4MGIxNjU3MGEwNWM1","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGEzZjIzNmRiMGYxNWQ2NTZmODc5MDc3N2QwYjgwYjE2NTcwYTA1YzU=","chain":"POLYGON","address":"0xa3f236db0f15d656f8790777d0b80b16570a05c5","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHRTFPRFU1TkRZNVlXSTBORGxpTjJabU56TmpNMkV5TW1aaFl6ZGtNV014TkRaall6STRaVEk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhoTlRnMU9UUTJPV0ZpTkRRNVlqZG1aamN6WXpOaE1qSm1ZV00zWkRGak1UUTJZMk15T0dVeV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhhNTg1OTQ2OWFiNDQ5YjdmZjczYzNhMjJmYWM3ZDFjMTQ2Y2MyOGUy","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGE1ODU5NDY5YWI0NDliN2ZmNzNjM2EyMmZhYzdkMWMxNDZjYzI4ZTI=","chain":"POLYGON","address":"0xa5859469ab449b7ff73c3a22fac7d1c146cc28e2","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHRTJaREZtWWpNNE1Ea3hPV0kzWkdOa1ltWXhZV0ZtTlRkaVl6ZGlZVFV3WXpZMk1URm1aalE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhoTm1ReFptSXpPREE1TVRsaU4yUmpaR0ptTVdGaFpqVTNZbU0zWW1FMU1HTTJOakV4Wm1ZMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhhNmQxZmIzODA5MTliN2RjZGJmMWFhZjU3YmM3YmE1MGM2NjExZmY0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGE2ZDFmYjM4MDkxOWI3ZGNkYmYxYWFmNTdiYzdiYTUwYzY2MTFmZjQ=","chain":"POLYGON","address":"0xa6d1fb380919b7dcdbf1aaf57bc7ba50c6611ff4","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHRTNORFE1WVdSbU1XTmlNR0pqTlRka01XWTNNMlppTldabFlqRm1Nall5TldReU9HRmlPV1E9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhoTnpRME9XRmtaakZqWWpCaVl6VTNaREZtTnpObVlqVm1aV0l4WmpJMk1qVmtNamhoWWpsa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhhNzQ0OWFkZjFjYjBiYzU3ZDFmNzNmYjVmZWIxZjI2MjVkMjhhYjlk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGE3NDQ5YWRmMWNiMGJjNTdkMWY3M2ZiNWZlYjFmMjYyNWQyOGFiOWQ=","chain":"POLYGON","address":"0xa7449adf1cb0bc57d1f73fb5feb1f2625d28ab9d","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHRm1NR1kzWm1Vek1qSTBZMlF4WVRkbVl6bGhOR0l5TXpnM01tUXlPR00zWXpVMVpqQmxaRGM9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhoWmpCbU4yWmxNekl5TkdOa01XRTNabU01WVRSaU1qTTROekprTWpoak4yTTFOV1l3WldRM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhhZjBmN2ZlMzIyNGNkMWE3ZmM5YTRiMjM4NzJkMjhjN2M1NWYwZWQ3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGFmMGY3ZmUzMjI0Y2QxYTdmYzlhNGIyMzg3MmQyOGM3YzU1ZjBlZDc=","chain":"POLYGON","address":"0xaf0f7fe3224cd1a7fc9a4b23872d28c7c55f0ed7","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSXhZMlUxTkdFMllXWXlNVGt4TVRFd05EWXlZV00zWlRsaVkyTTVPVEEzT0RFNE5HWTFaVFU9X1VTRA==","quantity":1.6E7,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTVdObE5UUmhObUZtTWpFNU1URXhNRFEyTW1Gak4yVTVZbU5qT1Rrd056Z3hPRFJtTldVMV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiMWNlNTRhNmFmMjE5MTExMDQ2MmFjN2U5YmNjOTkwNzgxODRmNWU1","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGIxY2U1NGE2YWYyMTkxMTEwNDYyYWM3ZTliY2M5OTA3ODE4NGY1ZTU=","chain":"POLYGON","address":"0xb1ce54a6af2191110462ac7e9bcc99078184f5e5","name":"WToken2","symbol":"WToken2","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSXlZamRoWm1Gak56QTJaV1F5TnpZMFlUaG1OREE1TWpZM056TTJOelZrTkdJNE5USm1OamM9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTW1JM1lXWmhZemN3Tm1Wa01qYzJOR0U0WmpRd09USTJOemN6TmpjMVpEUmlPRFV5WmpZM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiMmI3YWZhYzcwNmVkMjc2NGE4ZjQwOTI2NzczNjc1ZDRiODUyZjY3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGIyYjdhZmFjNzA2ZWQyNzY0YThmNDA5MjY3NzM2NzVkNGI4NTJmNjc=","chain":"POLYGON","address":"0xb2b7afac706ed2764a8f40926773675d4b852f67","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSXpPRFk0WldObU1ERXdaamc0Wm1ZMFpHRTJaalZrT1dVM01ERmxNR0V5WVRRNE9UQXhNbUk9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTXpnMk9HVmpaakF4TUdZNE9HWm1OR1JoTm1ZMVpEbGxOekF4WlRCaE1tRTBPRGt3TVRKaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiMzg2OGVjZjAxMGY4OGZmNGRhNmY1ZDllNzAxZTBhMmE0ODkwMTJi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGIzODY4ZWNmMDEwZjg4ZmY0ZGE2ZjVkOWU3MDFlMGEyYTQ4OTAxMmI=","chain":"POLYGON","address":"0xb3868ecf010f88ff4da6f5d9e701e0a2a489012b","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSXpPRGN5T0dNeU5UWmtZelpoTldJNU5tSTBNVEF5WXpSaU1XVTNOVEZrWXpBeU5HTmtNakE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTXpnM01qaGpNalUyWkdNMllUVmlPVFppTkRFd01tTTBZakZsTnpVeFpHTXdNalJqWkRJd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiMzg3MjhjMjU2ZGM2YTViOTZiNDEwMmM0YjFlNzUxZGMwMjRjZDIw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGIzODcyOGMyNTZkYzZhNWI5NmI0MTAyYzRiMWU3NTFkYzAyNGNkMjA=","chain":"POLYGON","address":"0xb38728c256dc6a5b96b4102c4b1e751dc024cd20","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSTBPR1k1TmpRM1pqVTJNV1F6WmpCa1pqRTRPV000TlRGbE56bG1ZekEzWkRjMVptWXhOR1U9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTkRobU9UWTBOMlkxTmpGa00yWXdaR1l4T0Rsak9EVXhaVGM1Wm1Nd04yUTNOV1ptTVRSbF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiNDhmOTY0N2Y1NjFkM2YwZGYxODljODUxZTc5ZmMwN2Q3NWZmMTRl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGI0OGY5NjQ3ZjU2MWQzZjBkZjE4OWM4NTFlNzlmYzA3ZDc1ZmYxNGU=","chain":"POLYGON","address":"0xb48f9647f561d3f0df189c851e79fc07d75ff14e","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSTFOVFU0T0RRNU1UTTRZamhqWVdGaE56RTFaR1EzTVdKbFlXSXpOekF6TlRVNFpURmtZMlk9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTlRVMU9EZzBPVEV6T0dJNFkyRmhZVGN4TldSa056RmlaV0ZpTXpjd016VTFPR1V4WkdObV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiNTU1ODg0OTEzOGI4Y2FhYTcxNWRkNzFiZWFiMzcwMzU1OGUxZGNm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGI1NTU4ODQ5MTM4YjhjYWFhNzE1ZGQ3MWJlYWIzNzAzNTU4ZTFkY2Y=","chain":"POLYGON","address":"0xb5558849138b8caaa715dd71beab3703558e1dcf","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSTFZamsyTURGaU5qSTVPRGhqT0RNME9HSXhNR1E0TVdVd01tWmhPRFF3TUdWa05UVTVPRE09X1VTRA==","quantity":0.81,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpTldJNU5qQXhZall5T1RnNFl6Z3pORGhpTVRCa09ERmxNREptWVRnME1EQmxaRFUxT1Rnel9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiNWI5NjAxYjYyOTg4YzgzNDhiMTBkODFlMDJmYTg0MDBlZDU1OTgz","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGI1Yjk2MDFiNjI5ODhjODM0OGIxMGQ4MWUwMmZhODQwMGVkNTU5ODM=","chain":"POLYGON","address":"0xb5b9601b62988c8348b10d81e02fa8400ed55983","name":"TinyHero","symbol":"TINY","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSTVOakF3WlRNeU9UbGpNV0ppTjJKa09USm1ZalF3WWpVMllXTTRZakl4TkRZelpERXpORFk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpT1RZd01HVXpNams1WXpGaVlqZGlaRGt5Wm1JME1HSTFObUZqT0dJeU1UUTJNMlF4TXpRMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiOTYwMGUzMjk5YzFiYjdiZDkyZmI0MGI1NmFjOGIyMTQ2M2QxMzQ2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGI5NjAwZTMyOTljMWJiN2JkOTJmYjQwYjU2YWM4YjIxNDYzZDEzNDY=","chain":"POLYGON","address":"0xb9600e3299c1bb7bd92fb40b56ac8b21463d1346","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSmhaR05qWlRNeU5XUmtaVEkyWXpjNVpqZGhOemRrTWpBME5USTRORGcwT1RFMk5EQXhaV1E9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpWVdSalkyVXpNalZrWkdVeU5tTTNPV1kzWVRjM1pESXdORFV5T0RRNE5Ea3hOalF3TVdWa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiYWRjY2UzMjVkZGUyNmM3OWY3YTc3ZDIwNDUyODQ4NDkxNjQwMWVk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGJhZGNjZTMyNWRkZTI2Yzc5ZjdhNzdkMjA0NTI4NDg0OTE2NDAxZWQ=","chain":"POLYGON","address":"0xbadcce325dde26c79f7a77d204528484916401ed","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSmpOVEkyTjJZMU9EZG1ZakptTW1KaVlUWTVNVGxrWkRNNU5HRXlNR0l6T0dGbE1tWTJNamc9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpWXpVeU5qZG1OVGczWm1JeVpqSmlZbUUyT1RFNVpHUXpPVFJoTWpCaU16aGhaVEptTmpJNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiYzUyNjdmNTg3ZmIyZjJiYmE2OTE5ZGQzOTRhMjBiMzhhZTJmNjI4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGJjNTI2N2Y1ODdmYjJmMmJiYTY5MTlkZDM5NGEyMGIzOGFlMmY2Mjg=","chain":"POLYGON","address":"0xbc5267f587fb2f2bba6919dd394a20b38ae2f628","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHSmtOalE0TkRVMlltRTFPRFExWW1ZNE16ZzROVGd3TURaaU1EZG1OV1V5TUdNd1pUTTBNelk9X1VTRA==","quantity":1.0E7,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhpWkRZME9EUTFObUpoTlRnME5XSm1PRE00T0RVNE1EQTJZakEzWmpWbE1qQmpNR1V6TkRNMl9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhiZDY0ODQ1NmJhNTg0NWJmODM4ODU4MDA2YjA3ZjVlMjBjMGUzNDM2","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGJkNjQ4NDU2YmE1ODQ1YmY4Mzg4NTgwMDZiMDdmNWUyMGMwZTM0MzY=","chain":"POLYGON","address":"0xbd648456ba5845bf838858006b07f5e20c0e3436","name":"JTBC News in Korea","symbol":"JTBC","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTXlPVEl6WlRGa1lUSmxZbUk1T0RSa01qSTVaR1F3TlRBd016VTNNekl4TWpObVl6YzFaR009X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTWpreU0yVXhaR0V5WldKaU9UZzBaREl5T1dSa01EVXdNRE0xTnpNeU1USXpabU0zTldSal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjMjkyM2UxZGEyZWJiOTg0ZDIyOWRkMDUwMDM1NzMyMTIzZmM3NWRj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGMyOTIzZTFkYTJlYmI5ODRkMjI5ZGQwNTAwMzU3MzIxMjNmYzc1ZGM=","chain":"POLYGON","address":"0xc2923e1da2ebb984d229dd050035732123fc75dc","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTTFaV1ZtTm1NNE1HSXlPV05rT0RaaVpERmtPR1U0WWpGaU5UVXlNVGM0TmpOa09XRmlPREE9X1VTRA==","quantity":80000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTldWbFpqWmpPREJpTWpsalpEZzJZbVF4WkRobE9HSXhZalUxTWpFM09EWXpaRGxoWWpnd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjNWVlZjZjODBiMjljZDg2YmQxZDhlOGIxYjU1MjE3ODYzZDlhYjgw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGM1ZWVmNmM4MGIyOWNkODZiZDFkOGU4YjFiNTUyMTc4NjNkOWFiODA=","chain":"POLYGON","address":"0xc5eef6c80b29cd86bd1d8e8b1b55217863d9ab80","name":"MYFRIENDS","symbol":"MYFRIENDS","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTTJaR1ZrWkRoak5qbGpNRFUyTnpSa1pUUm1aalZqTmpRMlpUQXdOV0V3WmpjNE16azRZMlU9X1VTRA==","quantity":100.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTm1SbFpHUTRZelk1WXpBMU5qYzBaR1UwWm1ZMVl6WTBObVV3TURWaE1HWTNPRE01T0dObF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjNmRlZGQ4YzY5YzA1Njc0ZGU0ZmY1YzY0NmUwMDVhMGY3ODM5OGNl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGM2ZGVkZDhjNjljMDU2NzRkZTRmZjVjNjQ2ZTAwNWEwZjc4Mzk4Y2U=","chain":"POLYGON","address":"0xc6dedd8c69c05674de4ff5c646e005a0f78398ce","name":"Demo Token","symbol":"DEMO","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTTNNR05sWmpRM05UTTBaV1V3WmpNd1pURTJOMlpoWXpRNVkyWXhOMlkxTkdZMlpERXlZemc9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTnpCalpXWTBOelV6TkdWbE1HWXpNR1V4TmpkbVlXTTBPV05tTVRkbU5UUm1ObVF4TW1NNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjNzBjZWY0NzUzNGVlMGYzMGUxNjdmYWM0OWNmMTdmNTRmNmQxMmM4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGM3MGNlZjQ3NTM0ZWUwZjMwZTE2N2ZhYzQ5Y2YxN2Y1NGY2ZDEyYzg=","chain":"POLYGON","address":"0xc70cef47534ee0f30e167fac49cf17f54f6d12c8","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTTNNVGd6T0RNeU5tWXhNakkyTmpabE5qWXlZak5oWkRZMU5EQXpaakk1WWpjMk5EZG1ORE09X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTnpFNE16Z3pNalptTVRJeU5qWTJaVFkyTW1JellXUTJOVFF3TTJZeU9XSTNOalEzWmpRel9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjNzE4MzgzMjZmMTIyNjY2ZTY2MmIzYWQ2NTQwM2YyOWI3NjQ3ZjQz","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGM3MTgzODMyNmYxMjI2NjZlNjYyYjNhZDY1NDAzZjI5Yjc2NDdmNDM=","chain":"POLYGON","address":"0xc71838326f122666e662b3ad65403f29b7647f43","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHTTNPVFpsT1dVM016UTROVFJpTVRBM05HSTBOamd3WVRRelpERmhaVEZtT1dJMU9HWTVaRGc9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhqTnprMlpUbGxOek0wT0RVMFlqRXdOelJpTkRZNE1HRTBNMlF4WVdVeFpqbGlOVGhtT1dRNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhjNzk2ZTllNzM0ODU0YjEwNzRiNDY4MGE0M2QxYWUxZjliNThmOWQ4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGM3OTZlOWU3MzQ4NTRiMTA3NGI0NjgwYTQzZDFhZTFmOWI1OGY5ZDg=","chain":"POLYGON","address":"0xc796e9e734854b1074b4680a43d1ae1f9b58f9d8","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUXdNalZpTUdRNVlXUXdNR1JpTURNME4ySTRPV05tWVdNMk1UUTNOR0kyTWpZM1lUaGhNR009X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrTURJMVlqQmtPV0ZrTURCa1lqQXpORGRpT0RsalptRmpOakUwTnpSaU5qSTJOMkU0WVRCal9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkMDI1YjBkOWFkMDBkYjAzNDdiODljZmFjNjE0NzRiNjI2N2E4YTBj","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGQwMjViMGQ5YWQwMGRiMDM0N2I4OWNmYWM2MTQ3NGI2MjY3YThhMGM=","chain":"POLYGON","address":"0xd025b0d9ad00db0347b89cfac61474b6267a8a0c","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUXpaalpsTmpRM09XVmlaakZqTXpFd01UbGtOVFEwWmpjM1l6TTVPRGN4T1RBelpUY3hZMlE9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrTTJZMlpUWTBOemxsWW1ZeFl6TXhNREU1WkRVME5HWTNOMk16T1RnM01Ua3dNMlUzTVdOa19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkM2Y2ZTY0NzllYmYxYzMxMDE5ZDU0NGY3N2MzOTg3MTkwM2U3MWNk","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGQzZjZlNjQ3OWViZjFjMzEwMTlkNTQ0Zjc3YzM5ODcxOTAzZTcxY2Q=","chain":"POLYGON","address":"0xd3f6e6479ebf1c31019d544f77c39871903e71cd","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VERTJPR1ZoTjJReVptRXdaVE0yT0RCak4yRTVPV0ZqWkRKaE9EVXhOMlZsWWpGak1XSTJZakk9X1VTRA==","quantity":110.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGd4TmpobFlUZGtNbVpoTUdVek5qZ3dZemRoT1RsaFkyUXlZVGcxTVRkbFpXSXhZekZpTm1JeV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHgxNjhlYTdkMmZhMGUzNjgwYzdhOTlhY2QyYTg1MTdlZWIxYzFiNmIy","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weDE2OGVhN2QyZmEwZTM2ODBjN2E5OWFjZDJhODUxN2VlYjFjMWI2YjI=","chain":"POLYGON","address":"0x168ea7d2fa0e3680c7a99acd2a8517eeb1c1b6b2","name":"Cielo Coin","symbol":"\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000 ","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUTFaRE01TWpObVkySmpNVEF4WmpNM1ptVTNNemN5WWpsalpURmlZakV5T1dabE0ySmtZekk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrTldRek9USXpabU5pWXpFd01XWXpOMlpsTnpNM01tSTVZMlV4WW1JeE1qbG1aVE5pWkdNeV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkNWQzOTIzZmNiYzEwMWYzN2ZlNzM3MmI5Y2UxYmIxMjlmZTNiZGMy","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGQ1ZDM5MjNmY2JjMTAxZjM3ZmU3MzcyYjljZTFiYjEyOWZlM2JkYzI=","chain":"POLYGON","address":"0xd5d3923fcbc101f37fe7372b9ce1bb129fe3bdc2","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUTNPR0l3TWpNeE56WXhZVE16WTJGaFltVm1OR1UyWldSbU9ETmhaakkyTWpBMlpXWTFNREE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrTnpoaU1ESXpNVGMyTVdFek0yTmhZV0psWmpSbE5tVmtaamd6WVdZeU5qSXdObVZtTlRBd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkNzhiMDIzMTc2MWEzM2NhYWJlZjRlNmVkZjgzYWYyNjIwNmVmNTAw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGQ3OGIwMjMxNzYxYTMzY2FhYmVmNGU2ZWRmODNhZjI2MjA2ZWY1MDA=","chain":"POLYGON","address":"0xd78b0231761a33caabef4e6edf83af26206ef500","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUmlZMkU1TTJJNU5XTTNZemcwTURRNE1XRTFOR0V4TjJNMFlXVm1Oamd6TXpkbVpqSmxNRFE9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrWW1OaE9UTmlPVFZqTjJNNE5EQTBPREZoTlRSaE1UZGpOR0ZsWmpZNE16TTNabVl5WlRBMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkYmNhOTNiOTVjN2M4NDA0ODFhNTRhMTdjNGFlZjY4MzM3ZmYyZTA0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGRiY2E5M2I5NWM3Yzg0MDQ4MWE1NGExN2M0YWVmNjgzMzdmZjJlMDQ=","chain":"POLYGON","address":"0xdbca93b95c7c840481a54a17c4aef68337ff2e04","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUmpZMlF3T0RJNU4yRTRNekF3WldObFpHTTBaRE0yTWpjNU9EWmhaRE0xTUdRMVlXWmxZMlk9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrWTJOa01EZ3lPVGRoT0RNd01HVmpaV1JqTkdRek5qSTNPVGcyWVdRek5UQmtOV0ZtWldObV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkY2NkMDgyOTdhODMwMGVjZWRjNGQzNjI3OTg2YWQzNTBkNWFmZWNm","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGRjY2QwODI5N2E4MzAwZWNlZGM0ZDM2Mjc5ODZhZDM1MGQ1YWZlY2Y=","chain":"POLYGON","address":"0xdccd08297a8300ecedc4d3627986ad350d5afecf","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUmtNVFkwTmpoa1pEZGtZakU1T0RCaE1qZGpPV05oTWpBeFltUXhaR1V3TUdSa1pEVmpZVEk9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrWkRFMk5EWTRaR1EzWkdJeE9UZ3dZVEkzWXpsallUSXdNV0prTVdSbE1EQmtaR1ExWTJFeV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkZDE2NDY4ZGQ3ZGIxOTgwYTI3YzljYTIwMWJkMWRlMDBkZGQ1Y2Ey","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGRkMTY0NjhkZDdkYjE5ODBhMjdjOWNhMjAxYmQxZGUwMGRkZDVjYTI=","chain":"POLYGON","address":"0xdd16468dd7db1980a27c9ca201bd1de00ddd5ca2","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUmtPV1pqWlRGaFl6STRZakk0T0dGbE9UQTFNR1JoWXpBek5USTNNR0pqWldNNE5HVTNZbUU9X1VTRA==","quantity":1000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrWkRsbVkyVXhZV015T0dJeU9EaGhaVGt3TlRCa1lXTXdNelV5TnpCaVkyVmpPRFJsTjJKaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkZDlmY2UxYWMyOGIyODhhZTkwNTBkYWMwMzUyNzBiY2VjODRlN2Jh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGRkOWZjZTFhYzI4YjI4OGFlOTA1MGRhYzAzNTI3MGJjZWM4NGU3YmE=","chain":"POLYGON","address":"0xdd9fce1ac28b288ae9050dac035270bcec84e7ba","name":"W3_test","symbol":"W3T_01","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHUmxOelZtWVRsak5qRTNOV1V6WW1ReFptVmlZV0ZpT1RrMk4ySmtNV1ZoWTJKa1lqVXhPR1U9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhrWlRjMVptRTVZell4TnpWbE0ySmtNV1psWW1GaFlqazVOamRpWkRGbFlXTmlaR0kxTVRobF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhkZTc1ZmE5YzYxNzVlM2JkMWZlYmFhYjk5NjdiZDFlYWNiZGI1MThl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGRlNzVmYTljNjE3NWUzYmQxZmViYWFiOTk2N2JkMWVhY2JkYjUxOGU=","chain":"POLYGON","address":"0xde75fa9c6175e3bd1febaab9967bd1eacbdb518e","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlJWUklSVkpGVlUxZk1IZ3dZMkk0WkRCaU16ZGpOelE0TjJJeE1XUTFOMll4WmpNelpHVm1ZVEppTVdRelkyWmpZMlpsX1VTRA==","quantity":2000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPa1ZVU0VWU1JWVk5YekI0TUdOaU9HUXdZak0zWXpjME9EZGlNVEZrTlRkbU1XWXpNMlJsWm1FeVlqRmtNMk5tWTJObVpRPT1fVVNE","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4MGNiOGQwYjM3Yzc0ODdiMTFkNTdmMWYzM2RlZmEyYjFkM2NmY2NmZQ==","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46RVRIRVJFVU1fMHgwY2I4ZDBiMzdjNzQ4N2IxMWQ1N2YxZjMzZGVmYTJiMWQzY2ZjY2Zl","chain":"ETHEREUM","address":"0x0cb8d0b37c7487b11d57f1f33defa2b1d3cfccfe","name":"Dank Token","symbol":"DANK","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHVTNOR1pqWkRrNFpUY3laamsxT1RReVptRTJNak5qWkRJNE56TmxNR1kwWlRJeU1XWm1NRGs9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhsTnpSbVkyUTVPR1UzTW1ZNU5UazBNbVpoTmpJelkyUXlPRGN6WlRCbU5HVXlNakZtWmpBNV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhlNzRmY2Q5OGU3MmY5NTk0MmZhNjIzY2QyODczZTBmNGUyMjFmZjA5","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGU3NGZjZDk4ZTcyZjk1OTQyZmE2MjNjZDI4NzNlMGY0ZTIyMWZmMDk=","chain":"POLYGON","address":"0xe74fcd98e72f95942fa623cd2873e0f4e221ff09","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHVTNPREkxTW1FeFpEUTFNbVk1TVRBMk9XUTRZemd4TUdWa01HSXpPR0U1T1RRMU1EZzNOR1U9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhsTnpneU5USmhNV1EwTlRKbU9URXdOamxrT0dNNE1UQmxaREJpTXpoaE9UazBOVEE0TnpSbF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhlNzgyNTJhMWQ0NTJmOTEwNjlkOGM4MTBlZDBiMzhhOTk0NTA4NzRl","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGU3ODI1MmExZDQ1MmY5MTA2OWQ4YzgxMGVkMGIzOGE5OTQ1MDg3NGU=","chain":"POLYGON","address":"0xe78252a1d452f91069d8c810ed0b38a99450874e","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHVTNZbVl3T1RjeU1XUTFPV1ZpWmpBeE9XRTFPRGcyWTJJeFpqUTVaREUyWm1Jek9USTNOak09X1VTRA==","quantity":250000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhsTjJKbU1EazNNakZrTlRsbFltWXdNVGxoTlRnNE5tTmlNV1kwT1dReE5tWmlNemt5TnpZel9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhlN2JmMDk3MjFkNTllYmYwMTlhNTg4NmNiMWY0OWQxNmZiMzkyNzYz","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGU3YmYwOTcyMWQ1OWViZjAxOWE1ODg2Y2IxZjQ5ZDE2ZmIzOTI3NjM=","chain":"POLYGON","address":"0xe7bf09721d59ebf019a5886cb1f49d16fb392763","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHVmlNVFpoTUdJME5qY3lNemxoWVRKaU0yWmtOREZqTVROaE5qaGxZalkyWTJZd056RXhZalE9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhsWWpFMllUQmlORFkzTWpNNVlXRXlZak5tWkRReFl6RXpZVFk0WldJMk5tTm1NRGN4TVdJMF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhlYjE2YTBiNDY3MjM5YWEyYjNmZDQxYzEzYTY4ZWI2NmNmMDcxMWI0","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGViMTZhMGI0NjcyMzlhYTJiM2ZkNDFjMTNhNjhlYjY2Y2YwNzExYjQ=","chain":"POLYGON","address":"0xeb16a0b467239aa2b3fd41c13a68eb66cf0711b4","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHWTFaalF5WVdZeU5XVTBNbUkzWVRRM016QmpNVEUzTldGaE56RmhObVk0TVRRMk9EQTFOVGM9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhtTldZME1tRm1NalZsTkRKaU4yRTBOek13WXpFeE56VmhZVGN4WVRabU9ERTBOamd3TlRVM19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhmNWY0MmFmMjVlNDJiN2E0NzMwYzExNzVhYTcxYTZmODE0NjgwNTU3","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGY1ZjQyYWYyNWU0MmI3YTQ3MzBjMTE3NWFhNzFhNmY4MTQ2ODA1NTc=","chain":"POLYGON","address":"0xf5f42af25e42b7a4730c1175aa71a6f814680557","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHWmhZamxtTnpsbVpESmpNakprTWpNeE5XSTVZVFUyTlRRMFlqTXhZak5pTVRVM05tTXpPR0k9X1VTRA==","quantity":1000000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhtWVdJNVpqYzVabVF5WXpJeVpESXpNVFZpT1dFMU5qVTBOR0l6TVdJellqRTFOelpqTXpoaV9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhmYWI5Zjc5ZmQyYzIyZDIzMTViOWE1NjU0NGIzMWIzYjE1NzZjMzhi","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGZhYjlmNzlmZDJjMjJkMjMxNWI5YTU2NTQ0YjMxYjNiMTU3NmMzOGI=","chain":"POLYGON","address":"0xfab9f79fd2c22d2315b9a56544b31b3b1576c38b","name":"TestUSDCoin","symbol":"USDC","standard":"ERC20","decimals":6,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHWmhaVEppWkRnNFltUTNaVGhpT0Rka01qRXlOalpqWkRVd05HRTFaV1psTWpnMVlqRmpaVEE9X1VTRA==","quantity":325000.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhtWVdVeVltUTRPR0prTjJVNFlqZzNaREl4TWpZMlkyUTFNRFJoTldWbVpUSTROV0l4WTJVd19VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhmYWUyYmQ4OGJkN2U4Yjg3ZDIxMjY2Y2Q1MDRhNWVmZTI4NWIxY2Uw","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGZhZTJiZDg4YmQ3ZThiODdkMjEyNjZjZDUwNGE1ZWZlMjg1YjFjZTA=","chain":"POLYGON","address":"0xfae2bd88bd7e8b87d21266cd504a5efe285b1ce0","name":"ARCADIUM","symbol":"ARCADIUM","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHWmlNMlU1WlRoa1pUUmlaVEZtTnpJek5EQmlPRE5rWVRaa1pEVXlPV0k1Wm1NNVlURmxPV0U9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhtWWpObE9XVTRaR1UwWW1VeFpqY3lNelF3WWpnelpHRTJaR1ExTWpsaU9XWmpPV0V4WlRsaF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhmYjNlOWU4ZGU0YmUxZjcyMzQwYjgzZGE2ZGQ1MjliOWZjOWExZTlh","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGZiM2U5ZThkZTRiZTFmNzIzNDBiODNkYTZkZDUyOWI5ZmM5YTFlOWE=","chain":"POLYGON","address":"0xfb3e9e8de4be1f72340b83da6dd529b9fc9a1e9a","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"},{"id":"VG9rZW5CYWxhbmNlOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Nl9WRzlyWlc0NlVFOU1XVWRQVGw4d2VHWmpOalEwTTJFek1qTXpPVGcwTldVd01EZ3laamt3WWpVM1pqbG1NRFkwTkRBMFpUY3pZemc9X1VTRA==","quantity":4.0,"denominatedValue":{"id":"QW1vdW50OjBfVVNE","currency":"USD","value":0.0,"__typename":"Amount"},"tokenProjectMarket":{"id":"VG9rZW5Qcm9qZWN0TWFya2V0OlZHOXJaVzVRY205cVpXTjBPbEJQVEZsSFQwNWZNSGhtWXpZME5ETmhNekl6TXprNE5EVmxNREE0TW1ZNU1HSTFOMlk1WmpBMk5EUXdOR1UzTTJNNF9VU0Q=","pricePercentChange":null,"tokenProject":{"isSpam":false,"id":"VG9rZW5Qcm9qZWN0OlBPTFlHT05fMHhmYzY0NDNhMzIzMzk4NDVlMDA4MmY5MGI1N2Y5ZjA2NDQwNGU3M2M4","logoUrl":null,"__typename":"TokenProject"},"__typename":"TokenProjectMarket"},"token":{"id":"VG9rZW46UE9MWUdPTl8weGZjNjQ0M2EzMjMzOTg0NWUwMDgyZjkwYjU3ZjlmMDY0NDA0ZTczYzg=","chain":"POLYGON","address":"0xfc6443a32339845e0082f90b57f9f064404e73c8","name":"DOGE6","symbol":"DOGE6","standard":"ERC20","decimals":18,"__typename":"Token"},"__typename":"TokenBalance"}],"__typename":"Portfolio"}]}} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/mini-portfolio/uniswapx_activity.json b/apps/web/cypress/fixtures/mini-portfolio/uniswapx_activity.json new file mode 100644 index 0000000..990e091 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/uniswapx_activity.json @@ -0,0 +1,102 @@ +{ + "data": { + "portfolios": [ + { + "id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==", + "assetActivities": [ + { + "id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNE9EZGpOemN5TlRRNU1qWTVNVEkwWVRkbVpUTXlNams1TjJJNU0yUTJabUV3TjJObE1UQXhOamxrTjJJd1pXUXhObUV6TldabU16SmtOMk13TWpBeVh6QjRaREkzTXpnek1EUTRaalF4WldZMlpXRXhaV1EzWWpBeFltVTVOemRqTjJVME1HSXdaRGswTmw4d2VEUTNZVFF5TVdKalpXTTJORE5oWWpSallURmpZamc0TmpOaU4yWm1PV0ppWm1SaU5HVmlNVE09", + "timestamp": 1691001923, + "type": "SWAP_ORDER", + "chain": "ETHEREUM", + "details": { + "__typename": "TransactionDetails", + "id": "VHJhbnNhY3Rpb246MHg4ODdjNzcyNTQ5MjY5MTI0YTdmZTMyMjk5N2I5M2Q2ZmEwN2NlMTAxNjlkN2IwZWQxNmEzNWZmMzJkN2MwMjAyXzB4ZDI3MzgzMDQ4ZjQxZWY2ZWExZWQ3YjAxYmU5NzdjN2U0MGIwZDk0Nl8weDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTM=", + "type": "SWAP_ORDER", + "from": "0xd27383048f41ef6ea1ed7b01be977c7e40b0d946", + "to": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13", + "hash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "nonce": 439, + "status": "CONFIRMED" + }, + "assetChanges": [ + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweDgwYmVjYjgwOGJmYWRlNDE0MzE4M2U1OGQxOGYyMDgwZTg0ZTU3YTFfMHg0N2E0MjFiY2VjNjQzYWI0Y2ExY2I4ODYzYjdmZjliYmZkYjRlYjEzXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4", + "name": "USD Coin", + "symbol": "USDC", + "address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + "decimals": 6, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OA==", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4L2xvZ28ucG5n", + "url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "ERC20", + "quantity": "300.0", + "sender": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "recipient": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13", + "direction": "OUT", + "transactedValue": { + "id": "QW1vdW50OjMwMC4xNDkxNTIwOTE5NDE2M19VU0Q=", + "currency": "USD", + "value": 300.14915209194163, + "__typename": "Amount" + } + }, + { + "__typename": "TokenTransfer", + "id": "VG9rZW5UcmFuc2ZlcjoweDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTNfMHg4MGJlY2I4MDhiZmFkZTQxNDMxODNlNThkMThmMjA4MGU4NGU1N2ExXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==", + "asset": { + "id": "VG9rZW46RVRIRVJFVU1fMHg2YjE3NTQ3NGU4OTA5NGM0NGRhOThiOTU0ZWVkZWFjNDk1MjcxZDBm", + "name": "Dai Stablecoin", + "symbol": "DAI", + "address": "0x6b175474e89094c44da98b954eedeac495271d0f", + "decimals": 18, + "chain": "ETHEREUM", + "standard": null, + "project": { + "id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NmIxNzU0NzRlODkwOTRjNDRkYTk4Yjk1NGVlZGVhYzQ5NTI3MWQwZg==", + "isSpam": false, + "logo": { + "id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHg2QjE3NTQ3NEU4OTA5NEM0NERhOThiOTU0RWVkZUFDNDk1MjcxZDBGL2xvZ28ucG5n", + "url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png", + "__typename": "Image" + }, + "__typename": "TokenProject" + }, + "__typename": "Token" + }, + "tokenStandard": "ERC20", + "quantity": "280.573117586837733376", + "sender": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13", + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "direction": "IN", + "transactedValue": { + "id": "QW1vdW50OjI4MC42ODc3OTU0NTg2ODE4X1VTRA==", + "currency": "USD", + "value": 280.6877954586818, + "__typename": "Amount" + } + } + ], + "__typename": "AssetActivity" + } + ], + "__typename": "Portfolio" + } + ] + }, + "errors": [] +} diff --git a/apps/web/cypress/fixtures/mini-portfolio/unitag.json b/apps/web/cypress/fixtures/mini-portfolio/unitag.json new file mode 100644 index 0000000..8fada58 --- /dev/null +++ b/apps/web/cypress/fixtures/mini-portfolio/unitag.json @@ -0,0 +1,3 @@ +{ + "username": "hayden" +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/cancelledStatusResponse.json b/apps/web/cypress/fixtures/uniswapx/cancelledStatusResponse.json new file mode 100644 index 0000000..f293fe1 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/cancelledStatusResponse.json @@ -0,0 +1,26 @@ +{ + "orders": [ + { + "outputs": [ + { + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "startAmount": "91371770080538616664", + "endAmount": "90914911230135923580", + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + } + ], + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b", + "input": { + "endAmount": "100000000", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "100000000" + }, + "orderStatus": "cancelled", + "createdAt": 1686339087, + "chainId": 1, + "orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19", + "type": "Dutch" + } + ] +} diff --git a/apps/web/cypress/fixtures/uniswapx/expiredStatusResponse.json b/apps/web/cypress/fixtures/uniswapx/expiredStatusResponse.json new file mode 100644 index 0000000..bf6a422 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/expiredStatusResponse.json @@ -0,0 +1,26 @@ +{ + "orders": [ + { + "outputs": [ + { + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "startAmount": "91371770080538616664", + "endAmount": "90914911230135923580", + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + } + ], + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b", + "input": { + "endAmount": "100000000", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "100000000" + }, + "orderStatus": "expired", + "createdAt": 1686339087, + "chainId": 1, + "orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19", + "type": "Dutch" + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/feeQuote.json b/apps/web/cypress/fixtures/uniswapx/feeQuote.json new file mode 100644 index 0000000..d2894a5 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/feeQuote.json @@ -0,0 +1,562 @@ +{ + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F", + "nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633", + "deadline": 1697481666, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1697481594, + "decayEndTime": 1697481654, + "exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb", + "exclusivityOverrideBps": "100", + "input": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "200000000", + "endAmount": "200000000" + }, + "outputs": [ + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": "123803169993201727", + "endAmount": "117908377342236273", + "recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F" + }, + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": "185983730585681", + "endAmount": "177128258400955", + "recipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + } + ] + }, + "encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327", + "quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890", + "requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f", + "orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9", + "startTimeBufferSecs": 45, + "auctionPeriodSecs": 60, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amount": { + "type": "BigNumber", + "hex": "0x0bebc200" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401" + }, + "deadline": 1697481666, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F", + "nonce": { + "type": "BigNumber", + "hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401" + }, + "deadline": 1697481666, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1697481594, + "decayEndTime": 1697481654, + "exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x64" + }, + "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x0bebc200" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x0bebc200" + }, + "outputs": [ + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": { + "type": "BigNumber", + "hex": "0x01b7d653c183183f" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x01a2e50b6386d671" + }, + "recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F" + }, + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": { + "type": "BigNumber", + "hex": "0xa926b6321051" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0xa118e2ebf2bb" + }, + "recipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + } + ] + } + } + }, + "portionBips": 15, + "portionAmount": "185983730585681", + "portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + }, + "requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f", + "allQuotes": [ + { + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F", + "nonce": "1993353164669688581970088190602701610528397285201889446578254799128576197633", + "deadline": 1697481666, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1697481594, + "decayEndTime": 1697481654, + "exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb", + "exclusivityOverrideBps": "100", + "input": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "200000000", + "endAmount": "200000000" + }, + "outputs": [ + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": "123803169993201727", + "endAmount": "117908377342236273", + "recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F" + }, + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": "185983730585681", + "endAmount": "177128258400955", + "recipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + } + ] + }, + "encodedOrder": "0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000652d837a00000000000000000000000000000000000000000000000000000000652d83b6000000000000000000000000aafb85ad4a412dd8adc49611496a7695a22f4aeb0000000000000000000000000000000000000000000000000000000000000064000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000000000000000000000000000000000000bebc200000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c40000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f046832aa305880d33daa871e5041a0cd4853599a9ead518917239e206765040100000000000000000000000000000000000000000000000000000000652d83c2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b7d653c183183f00000000000000000000000000000000000000000000000001a2e50b6386d6710000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a926b63210510000000000000000000000000000000000000000000000000000a118e2ebf2bb00000000000000000000000037a8f295612602f2774d331e562be9e61b83a327", + "quoteId": "7b924043-f2d8-4f2e-abaa-9f65fbe5f890", + "requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f", + "orderHash": "0xb5b4e3be188f6eb9dbe7e1489595829184a9ebfb5389185ed7ba7c03142278c9", + "startTimeBufferSecs": 45, + "auctionPeriodSecs": 60, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amount": { + "type": "BigNumber", + "hex": "0x0bebc200" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401" + }, + "deadline": 1697481666, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F", + "nonce": { + "type": "BigNumber", + "hex": "0x046832aa305880d33daa871e5041a0cd4853599a9ead518917239e2067650401" + }, + "deadline": 1697481666, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1697481594, + "decayEndTime": 1697481654, + "exclusiveFiller": "0xaAFb85ad4a412dd8adC49611496a7695A22f4aeb", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x64" + }, + "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x0bebc200" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x0bebc200" + }, + "outputs": [ + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": { + "type": "BigNumber", + "hex": "0x01b7d653c183183f" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x01a2e50b6386d671" + }, + "recipient": "0x0938a82F93D5DAB110Dc6277FC236b5b082DC10F" + }, + { + "token": "0x0000000000000000000000000000000000000000", + "startAmount": { + "type": "BigNumber", + "hex": "0xa926b6321051" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0xa118e2ebf2bb" + }, + "recipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + } + ] + } + } + }, + "portionBips": 15, + "portionAmount": "185983730585681", + "portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327" + } + }, + { + "routing": "CLASSIC", + "quote": { + "methodParameters": { + "calldata": "0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000652d85d0000000000000000000000000000000000000000000000000000000000000000308060c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000180000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000bebc20000000000000000000000000000000000000000000000000001bdf1285753b47400000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000000000000000060000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000037a8f295612602f2774d331e562be9e61b83a327000000000000000000000000000000000000000000000000000000000000000f00000000000000000000000000000000000000000000000000000000000000400000000000000000000000000938a82f93d5dab110dc6277fc236b5b082dc10f00000000000000000000000000000000000000000000000001bd45ea74e458eb", + "value": "0x00", + "to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" + }, + "blockNumber": "18364784", + "amount": "200000000", + "amountDecimals": "200", + "quote": "126149127803342909", + "quoteDecimals": "0.126149127803342909", + "quoteGasAdjusted": "122888348391508943", + "quoteGasAdjustedDecimals": "0.122888348391508943", + "quoteGasAndPortionAdjusted": "122699124699803928", + "quoteGasAndPortionAdjustedDecimals": "0.122699124699803928", + "gasUseEstimateQuote": "3260779411833966", + "gasUseEstimateQuoteDecimals": "0.003260779411833966", + "gasUseEstimate": "240911", + "gasUseEstimateUSD": "5.153332510477604328", + "simulationStatus": "SUCCESS", + "simulationError": false, + "gasPriceWei": "13535203506", + "route": [ + [ + { + "type": "v2-pool", + "address": "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "reserve0": { + "token": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "quotient": "27487668611269" + }, + "reserve1": { + "token": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "quotient": "17390022942803382004255" + }, + "amountIn": "200000000", + "amountOut": "125959904111637894" + } + ] + ], + "routeString": "[V2] 100.00% = USDC -- [0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc] --> WETH", + "quoteId": "f46cf31c-251e-470c-bd57-13209015694e", + "portionBips": 15, + "portionRecipient": "0x37a8f295612602f2774d331e562be9e61B83a327", + "portionAmount": "189223691705014", + "portionAmountDecimals": "0.000189223691705014", + "requestId": "a02ca0ca-7855-4dd0-9330-8b818aaeb59f", + "tradeType": "EXACT_INPUT", + "slippage": 0.5 + } + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/fillTransactionReceipt.json b/apps/web/cypress/fixtures/uniswapx/fillTransactionReceipt.json new file mode 100644 index 0000000..d6e02b0 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/fillTransactionReceipt.json @@ -0,0 +1,114 @@ +{ + "to": "0xbD7F9D0239f81C94b728d827a87b9864972661eC", + "from": "0xa17Fbb0b5a251A7ACA3BD7377e7eCC4F700A2C09", + "contractAddress": null, + "transactionIndex": 61, + "gasUsed": { + "type": "BigNumber", + "hex": "0x03e0c8" + }, + "logsBloom": + "0x00000000000000000000008000200100000020000000000000000000000000000000000000000000000000010000000000000000000020000000000001000000000280000000000808000008000000000000000000000000000000000000200010000000100000000008000000000004402000080000000000000010000800000000000000000800000800000000000000000000010000000000000000000000000000000000200000000000005000000000000000000000000000000000000000000002000000000000000000000000040002000000000000000100000000090000000400000000000400000020080000000000000000000000000000000000", + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c", + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "logs": [ + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000005f5e100", + "logIndex": 103, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + }, + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0xbD7F9D0239f81C94b728d827a87b9864972661eC", + "topics": [ + "0x78ad7ec0e9f89e74012afa58738b6b661c024cb0fd185ee2f616c0a28924bd66", + "0xd10e1d90145460003d98ba4b788564e9549cc93c65a12c9b297720a9d6a586de", + "0x000000000000000000000000a17fbb0b5a251a7aca3bd7377e7ecc4f700a2c09", + "0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1" + ], + "data": "0x8e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de00", + "logIndex": 104, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + }, + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168", + "0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf" + ], + "data": "0x0000000000000000000000000000000000000000000000056b9a675be430b502", + "logIndex": 105, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + }, + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf", + "0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000005f5e100", + "logIndex": 106, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + }, + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168", + "topics": [ + "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67", + "0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45", + "0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf" + ], + "data": "0xfffffffffffffffffffffffffffffffffffffffffffffffa946598a41bcf4afe0000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000010c7063b90a5e90d13830000000000000000000000000000000000000000000071b57cb2bb0b5b28224ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc89c", + "logIndex": 107, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + }, + { + "transactionIndex": 61, + "blockNumber": 17444757, + "transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf", + "0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1" + ], + "data": "0x000000000000000000000000000000000000000000000004f409bcc7a52b6358", + "logIndex": 108, + "blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c" + } + ], + "blockNumber": 17444757, + "confirmations": 392238, + "cumulativeGasUsed": { + "type": "BigNumber", + "hex": "0x4065ac" + }, + "effectiveGasPrice": { + "type": "BigNumber", + "hex": "0x04aa792df0" + }, + "status": 1, + "type": 2, + "byzantium": true +} diff --git a/apps/web/cypress/fixtures/uniswapx/filledStatusResponse.json b/apps/web/cypress/fixtures/uniswapx/filledStatusResponse.json new file mode 100644 index 0000000..90cad3d --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/filledStatusResponse.json @@ -0,0 +1,33 @@ +{ + "orders": [ + { + "outputs": [ + { + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "startAmount": "91371770080538616664", + "endAmount": "90914911230135923580", + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + } + ], + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b", + "input": { + "endAmount": "100000000", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "100000000" + }, + "settledAmounts": [ + { + "tokenOut": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "amountOut": "91371770080538616664" + } + ], + "orderStatus": "filled", + "txHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6", + "createdAt": 1686339087, + "chainId": 1, + "orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19", + "type": "Dutch" + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/insufficientFundsStatusResponse.json b/apps/web/cypress/fixtures/uniswapx/insufficientFundsStatusResponse.json new file mode 100644 index 0000000..9a5ac9e --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/insufficientFundsStatusResponse.json @@ -0,0 +1,26 @@ +{ + "orders": [ + { + "outputs": [ + { + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "startAmount": "91371770080538616664", + "endAmount": "90914911230135923580", + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + } + ], + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b", + "input": { + "endAmount": "100000000", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "100000000" + }, + "orderStatus": "insufficient-funds", + "createdAt": 1686339087, + "chainId": 1, + "orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19", + "type": "Dutch" + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/openStatusResponse.json b/apps/web/cypress/fixtures/uniswapx/openStatusResponse.json new file mode 100644 index 0000000..899e4ab --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/openStatusResponse.json @@ -0,0 +1,26 @@ +{ + "orders": [ + { + "outputs": [ + { + "recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1", + "startAmount": "91371770080538616664", + "endAmount": "90914911230135923580", + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F" + } + ], + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1", + "signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b", + "input": { + "endAmount": "100000000", + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "100000000" + }, + "orderStatus": "open", + "createdAt": 1686339087, + "chainId": 1, + "orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19", + "type": "Dutch" + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/orderResponse.json b/apps/web/cypress/fixtures/uniswapx/orderResponse.json new file mode 100644 index 0000000..7851da4 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/orderResponse.json @@ -0,0 +1 @@ +{"hash":"0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19"} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/pricingQuoteDAI.json b/apps/web/cypress/fixtures/uniswapx/pricingQuoteDAI.json new file mode 100644 index 0000000..665d943 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/pricingQuoteDAI.json @@ -0,0 +1,203 @@ +{ + "routing": "CLASSIC", + "quote": { + "methodParameters": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000301010c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000024dce54d34a1a00000000000000000000000000000000000000000000000014bf539990eba0f1a45f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000646b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000068155a43676e00000000000000000000000000000000000000000000000003a8d8fe3b38ba218b0e00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f46b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000002b5e3af16b1880000", + "value": "0x00", + "to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" + }, + "blockNumber": "19164578", + "amount": "50000000000000000000", + "amountDecimals": "50", + "quote": "114685045008808904983035", + "quoteDecimals": "114685.045008808904983035", + "quoteGasAdjusted": "114712627172980673791567", + "quoteGasAdjustedDecimals": "114712.627172980673791567", + "gasUseEstimateQuote": "27582164171768808532", + "gasUseEstimateQuoteDecimals": "27.582164171768808532", + "gasUseEstimate": "407876", + "gasUseEstimateUSD": "27.582164171768808531", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "29582260870", + "route": [ + [ + { + "type": "v3-pool", + "address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168", + "tokenIn": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "tokenOut": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "fee": "100", + "liquidity": "455161791973686033270872", + "sqrtRatioX96": "79228239540809686649257", + "tickCurrent": "-276325", + "amountIn": "97489235602775826155972" + }, + { + "type": "v3-pool", + "address": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "12687822722543096229", + "sqrtRatioX96": "1654855764068853531248850694676721", + "tickCurrent": "198947", + "amountOut": "42500000000000000000" + } + ], + [ + { + "type": "v3-pool", + "address": "0x60594a405d53811d3BC4766596EFD80fd545A270", + "tokenIn": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "405602215381884532648035", + "sqrtRatioX96": "1655768895199418237140666958", + "tickCurrent": "-77366", + "amountIn": "17195809406033078827063", + "amountOut": "7500000000000000000" + } + ] + ], + "routeString": "[V3] 85.00% = DAI -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> USDC -- 0.05% [0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640] --> WETH, [V3] 15.00% = DAI -- 0.05% [0x60594a405d53811d3BC4766596EFD80fd545A270] --> WETH", + "quoteId": "8e33b7d6-5181-4bbb-98a9-aff7f04a38bc", + "hitsCachedRoutes": true, + "requestId": "4ec0db79-7309-4851-821d-392191d293f9", + "tradeType": "EXACT_OUTPUT", + "slippage": 0.5 + }, + "requestId": "4ec0db79-7309-4851-821d-392191d293f9", + "allQuotes": [ + { + "routing": "CLASSIC", + "quote": { + "methodParameters": { + "calldata": "0x24856bc300000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000301010c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001a000000000000000000000000000000000000000000000000000000000000002c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000024dce54d34a1a00000000000000000000000000000000000000000000000014bf539990eba0f1a45f00000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000042c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000646b175474e89094c44da98b954eedeac495271d0f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000068155a43676e00000000000000000000000000000000000000000000000003a8d8fe3b38ba218b0e00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f46b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000002b5e3af16b1880000", + "value": "0x00", + "to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" + }, + "blockNumber": "19164578", + "amount": "50000000000000000000", + "amountDecimals": "50", + "quote": "114685045008808904983035", + "quoteDecimals": "114685.045008808904983035", + "quoteGasAdjusted": "114712627172980673791567", + "quoteGasAdjustedDecimals": "114712.627172980673791567", + "gasUseEstimateQuote": "27582164171768808532", + "gasUseEstimateQuoteDecimals": "27.582164171768808532", + "gasUseEstimate": "407876", + "gasUseEstimateUSD": "27.582164171768808531", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "29582260870", + "route": [ + [ + { + "type": "v3-pool", + "address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168", + "tokenIn": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "tokenOut": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "fee": "100", + "liquidity": "455161791973686033270872", + "sqrtRatioX96": "79228239540809686649257", + "tickCurrent": "-276325", + "amountIn": "97489235602775826155972" + }, + { + "type": "v3-pool", + "address": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "12687822722543096229", + "sqrtRatioX96": "1654855764068853531248850694676721", + "tickCurrent": "198947", + "amountOut": "42500000000000000000" + } + ], + [ + { + "type": "v3-pool", + "address": "0x60594a405d53811d3BC4766596EFD80fd545A270", + "tokenIn": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "405602215381884532648035", + "sqrtRatioX96": "1655768895199418237140666958", + "tickCurrent": "-77366", + "amountIn": "17195809406033078827063", + "amountOut": "7500000000000000000" + } + ] + ], + "routeString": "[V3] 85.00% = DAI -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> USDC -- 0.05% [0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640] --> WETH, [V3] 15.00% = DAI -- 0.05% [0x60594a405d53811d3BC4766596EFD80fd545A270] --> WETH", + "quoteId": "8e33b7d6-5181-4bbb-98a9-aff7f04a38bc", + "hitsCachedRoutes": true, + "requestId": "4ec0db79-7309-4851-821d-392191d293f9", + "tradeType": "EXACT_OUTPUT", + "slippage": 0.5 + } + } + ] +} diff --git a/apps/web/cypress/fixtures/uniswapx/pricingQuoteUSDC.json b/apps/web/cypress/fixtures/uniswapx/pricingQuoteUSDC.json new file mode 100644 index 0000000..99db947 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/pricingQuoteUSDC.json @@ -0,0 +1,115 @@ +{ + "routing": "CLASSIC", + "quote": { + "methodParameters": { + "calldata": "0x24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002010c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000002b5e3af16b18800000000000000000000000000000000000000000000000000000000001ac6769ffe00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000002b5e3af16b1880000", + "value": "0x00", + "to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" + }, + "blockNumber": "19164502", + "amount": "50000000000000000000", + "amountDecimals": "50", + "quote": "114426679274", + "quoteDecimals": "114426.679274", + "quoteGasAdjusted": "114442912487", + "quoteGasAdjustedDecimals": "114442.912487", + "gasUseEstimateQuote": "16233213", + "gasUseEstimateQuoteDecimals": "16.233213", + "gasUseEstimate": "186938", + "gasUseEstimateUSD": "16.238136991567515431", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "37970650516", + "route": [ + [ + { + "type": "v3-pool", + "address": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "12541370727930662304", + "sqrtRatioX96": "1656723958203820357489371175266863", + "tickCurrent": "198970", + "amountIn": "114426679274", + "amountOut": "50000000000000000000" + } + ] + ], + "routeString": "[V3] 100.00% = USDC -- 0.05% [0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640] --> WETH", + "quoteId": "c0e9f879-823b-4a83-ad30-7fc735ceb6ca", + "hitsCachedRoutes": true, + "requestId": "0dbeaad0-9cdb-4ace-90f6-3406dafee7a5", + "tradeType": "EXACT_OUTPUT", + "slippage": 0.5 + }, + "requestId": "0dbeaad0-9cdb-4ace-90f6-3406dafee7a5", + "allQuotes": [ + { + "routing": "CLASSIC", + "quote": { + "methodParameters": { + "calldata": "0x24856bc3000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000002010c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000016000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000002b5e3af16b18800000000000000000000000000000000000000000000000000000000001ac6769ffe00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002bc02aaa39b223fe8d0a0e5c4f27ead9083c756cc20001f4a0b86991c6218b36c1d19d4a2e9eb0ce3606eb4800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000002b5e3af16b1880000", + "value": "0x00", + "to": "0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD" + }, + "blockNumber": "19164502", + "amount": "50000000000000000000", + "amountDecimals": "50", + "quote": "114426679274", + "quoteDecimals": "114426.679274", + "quoteGasAdjusted": "114442912487", + "quoteGasAdjustedDecimals": "114442.912487", + "gasUseEstimateQuote": "16233213", + "gasUseEstimateQuoteDecimals": "16.233213", + "gasUseEstimate": "186938", + "gasUseEstimateUSD": "16.238136991567515431", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "37970650516", + "route": [ + [ + { + "type": "v3-pool", + "address": "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "fee": "500", + "liquidity": "12541370727930662304", + "sqrtRatioX96": "1656723958203820357489371175266863", + "tickCurrent": "198970", + "amountIn": "114426679274", + "amountOut": "50000000000000000000" + } + ] + ], + "routeString": "[V3] 100.00% = USDC -- 0.05% [0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640] --> WETH", + "quoteId": "c0e9f879-823b-4a83-ad30-7fc735ceb6ca", + "hitsCachedRoutes": true, + "requestId": "0dbeaad0-9cdb-4ace-90f6-3406dafee7a5", + "tradeType": "EXACT_OUTPUT", + "slippage": 0.5 + } + } + ] +} diff --git a/apps/web/cypress/fixtures/uniswapx/quote1.json b/apps/web/cypress/fixtures/uniswapx/quote1.json new file mode 100644 index 0000000..ee09e61 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/quote1.json @@ -0,0 +1,493 @@ +{ + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD", + "nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368", + "deadline": 1690902198, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1690902126, + "decayEndTime": 1690902186, + "exclusiveFiller": "0x0000000000000000000000000000000000000000", + "exclusivityOverrideBps": "0", + "input": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "300000000", + "endAmount": "300000000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": "289951120815684452958", + "endAmount": "267060007981523637666", + "recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD" + } + ] + }, + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd", + "quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a", + "requestId": "2d16f993-6429-4755-ba50-1383789459dc", + "auctionPeriodSecs": 60, + "startTimeBufferSecs": 30, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amount": { + "type": "BigNumber", + "hex": "0x11e1a300" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300" + }, + "deadline": 1690902198, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD", + "nonce": { + "type": "BigNumber", + "hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300" + }, + "deadline": 1690902198, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1690902126, + "decayEndTime": 1690902186, + "exclusiveFiller": "0x0000000000000000000000000000000000000000", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x00" + }, + "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x11e1a300" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x11e1a300" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": { + "type": "BigNumber", + "hex": "0x0fb7e15027ad3e025e" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x0e7a33be508bb395a2" + }, + "recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD" + } + ] + } + } + } + }, + "requestId": "2d16f993-6429-4755-ba50-1383789459dc", + "allQuotes": [ + { + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD", + "nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368", + "deadline": 1690902198, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1690902126, + "decayEndTime": 1690902186, + "exclusiveFiller": "0x0000000000000000000000000000000000000000", + "exclusivityOverrideBps": "0", + "input": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "startAmount": "300000000", + "endAmount": "300000000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": "289951120815684452958", + "endAmount": "267060007981523637666", + "recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD" + } + ] + }, + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd", + "quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a", + "requestId": "2d16f993-6429-4755-ba50-1383789459dc", + "auctionPeriodSecs": 60, + "startTimeBufferSecs": 30, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "amount": { + "type": "BigNumber", + "hex": "0x11e1a300" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300" + }, + "deadline": 1690902198, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD", + "nonce": { + "type": "BigNumber", + "hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300" + }, + "deadline": 1690902198, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1690902126, + "decayEndTime": 1690902186, + "exclusiveFiller": "0x0000000000000000000000000000000000000000", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x00" + }, + "inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x11e1a300" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x11e1a300" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": { + "type": "BigNumber", + "hex": "0x0fb7e15027ad3e025e" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x0e7a33be508bb395a2" + }, + "recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD" + } + ] + } + } + } + } + }, + { + "routing": "CLASSIC", + "quote": { + "blockNumber": "17820918", + "amount": "300000000", + "amountDecimals": "300", + "quote": "299952256425393549464", + "quoteDecimals": "299.952256425393549464", + "quoteGasAdjusted": "289922128602824170541", + "quoteGasAdjustedDecimals": "289.922128602824170541", + "gasUseEstimateQuote": "10030127822569378922", + "gasUseEstimateQuoteDecimals": "10.030127822569378922", + "gasUseEstimate": "128000", + "gasUseEstimateUSD": "10.031724", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "42803167855", + "route": [ + [ + { + "type": "v3-pool", + "address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168", + "tokenIn": { + "chainId": 1, + "decimals": "6", + "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "symbol": "USDC" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "fee": "100", + "liquidity": "534676532046235168447130", + "sqrtRatioX96": "79230505815006815109584", + "tickCurrent": "-276324", + "amountIn": "300000000", + "amountOut": "299952256425393549464" + } + ] + ], + "routeString": "[V3] 100.00% = USDC -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> DAI", + "quoteId": "1dd3bd14-780e-41c6-88e1-30a763f97482", + "requestId": "2d16f993-6429-4755-ba50-1383789459dc", + "tradeType": "EXACT_INPUT", + "slippage": 0.5 + } + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/fixtures/uniswapx/quote2.json b/apps/web/cypress/fixtures/uniswapx/quote2.json new file mode 100644 index 0000000..43c4f64 --- /dev/null +++ b/apps/web/cypress/fixtures/uniswapx/quote2.json @@ -0,0 +1,493 @@ +{ + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0000000000000000000000000000000000000000", + "nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881", + "deadline": 1691176812, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1691176740, + "decayEndTime": 1691176800, + "exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181", + "exclusivityOverrideBps": "100", + "input": { + "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "startAmount": "1000000000000000000", + "endAmount": "1000000000000000000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": "929502510517534478575", + "endAmount": "919795986077127665276", + "recipient": "0x0000000000000000000000000000000000000000" + } + ] + }, + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000", + "quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84", + "requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1", + "auctionPeriodSecs": 60, + "startTimeBufferSecs": 30, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "amount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001" + }, + "deadline": 1691176812, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0000000000000000000000000000000000000000", + "nonce": { + "type": "BigNumber", + "hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001" + }, + "deadline": 1691176812, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1691176740, + "decayEndTime": 1691176800, + "exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x64" + }, + "inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": { + "type": "BigNumber", + "hex": "0x3263704899af6e50ef" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x31dcbbc80c9555e67c" + }, + "recipient": "0x0000000000000000000000000000000000000000" + } + ] + } + } + } + }, + "requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1", + "allQuotes": [ + { + "routing": "DUTCH_LIMIT", + "quote": { + "orderInfo": { + "chainId": 1, + "permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3", + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0000000000000000000000000000000000000000", + "nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881", + "deadline": 1691176812, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x", + "decayStartTime": 1691176740, + "decayEndTime": 1691176800, + "exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181", + "exclusivityOverrideBps": "100", + "input": { + "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "startAmount": "1000000000000000000", + "endAmount": "1000000000000000000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": "929502510517534478575", + "endAmount": "919795986077127665276", + "recipient": "0x0000000000000000000000000000000000000000" + } + ] + }, + "encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000", + "quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84", + "requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1", + "auctionPeriodSecs": 60, + "startTimeBufferSecs": 30, + "deadlineBufferSecs": 12, + "slippageTolerance": "0.5", + "permitData": { + "domain": { + "name": "Permit2", + "chainId": 1, + "verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3" + }, + "types": { + "PermitWitnessTransferFrom": [ + { + "name": "permitted", + "type": "TokenPermissions" + }, + { + "name": "spender", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "witness", + "type": "ExclusiveDutchOrder" + } + ], + "TokenPermissions": [ + { + "name": "token", + "type": "address" + }, + { + "name": "amount", + "type": "uint256" + } + ], + "ExclusiveDutchOrder": [ + { + "name": "info", + "type": "OrderInfo" + }, + { + "name": "decayStartTime", + "type": "uint256" + }, + { + "name": "decayEndTime", + "type": "uint256" + }, + { + "name": "exclusiveFiller", + "type": "address" + }, + { + "name": "exclusivityOverrideBps", + "type": "uint256" + }, + { + "name": "inputToken", + "type": "address" + }, + { + "name": "inputStartAmount", + "type": "uint256" + }, + { + "name": "inputEndAmount", + "type": "uint256" + }, + { + "name": "outputs", + "type": "DutchOutput[]" + } + ], + "OrderInfo": [ + { + "name": "reactor", + "type": "address" + }, + { + "name": "swapper", + "type": "address" + }, + { + "name": "nonce", + "type": "uint256" + }, + { + "name": "deadline", + "type": "uint256" + }, + { + "name": "additionalValidationContract", + "type": "address" + }, + { + "name": "additionalValidationData", + "type": "bytes" + } + ], + "DutchOutput": [ + { + "name": "token", + "type": "address" + }, + { + "name": "startAmount", + "type": "uint256" + }, + { + "name": "endAmount", + "type": "uint256" + }, + { + "name": "recipient", + "type": "address" + } + ] + }, + "values": { + "permitted": { + "token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "amount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + } + }, + "spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "nonce": { + "type": "BigNumber", + "hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001" + }, + "deadline": 1691176812, + "witness": { + "info": { + "reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4", + "swapper": "0x0000000000000000000000000000000000000000", + "nonce": { + "type": "BigNumber", + "hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001" + }, + "deadline": 1691176812, + "additionalValidationContract": "0x0000000000000000000000000000000000000000", + "additionalValidationData": "0x" + }, + "decayStartTime": 1691176740, + "decayEndTime": 1691176800, + "exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181", + "exclusivityOverrideBps": { + "type": "BigNumber", + "hex": "0x64" + }, + "inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "inputStartAmount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + }, + "inputEndAmount": { + "type": "BigNumber", + "hex": "0x0de0b6b3a7640000" + }, + "outputs": [ + { + "token": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "startAmount": { + "type": "BigNumber", + "hex": "0x3263704899af6e50ef" + }, + "endAmount": { + "type": "BigNumber", + "hex": "0x31dcbbc80c9555e67c" + }, + "recipient": "0x0000000000000000000000000000000000000000" + } + ] + } + } + } + } + }, + { + "routing": "CLASSIC", + "quote": { + "blockNumber": "17843654", + "amount": "1000000000000000000", + "amountDecimals": "1", + "quote": "931181529570145926787", + "quoteDecimals": "931.181529570145926787", + "quoteGasAdjusted": "929033336026294051828", + "quoteGasAdjustedDecimals": "929.033336026294051828", + "gasUseEstimateQuote": "2148193543851874958", + "gasUseEstimateQuoteDecimals": "2.148193543851874958", + "gasUseEstimate": "128000", + "gasUseEstimateUSD": "4.174934", + "simulationStatus": "UNATTEMPTED", + "simulationError": false, + "gasPriceWei": "17811260539", + "route": [ + [ + { + "type": "v3-pool", + "address": "0xD8de6af55F618a7Bc69835D55DDC6582220c36c0", + "tokenIn": { + "chainId": 1, + "decimals": "18", + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "symbol": "WETH" + }, + "tokenOut": { + "chainId": 1, + "decimals": "18", + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "symbol": "DAI" + }, + "fee": "3000", + "liquidity": "62287359628325896425115", + "sqrtRatioX96": "2591813593283507889384697884", + "tickCurrent": "-68403", + "amountIn": "1000000000000000000", + "amountOut": "931181529570145926787" + } + ] + ], + "routeString": "[V3] 100.00% = WETH -- 0.3% [0xD8de6af55F618a7Bc69835D55DDC6582220c36c0] --> DAI", + "quoteId": "414e5f1c-120a-4e35-9760-c54d4b09e91d", + "requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1", + "tradeType": "EXACT_INPUT", + "slippage": 0.5 + } + } + ] +} \ No newline at end of file diff --git a/apps/web/cypress/staging/t9n.test.ts b/apps/web/cypress/staging/t9n.test.ts new file mode 100644 index 0000000..028d9e4 --- /dev/null +++ b/apps/web/cypress/staging/t9n.test.ts @@ -0,0 +1,21 @@ +import { getTestSelector } from '../utils' + +describe('translations', () => { + // TODO re-enable web test + it.skip('loads locale from the query param', () => { + cy.visit('/?lng=fr-FR') + cy.contains('Échanger') + cy.contains('Uniswap disponible en : English') + }) + + // TODO re-enable web test + it.skip('loads locale from menu', () => { + cy.visit('/') + cy.get(getTestSelector('web3-status-connected')).click() + cy.get(getTestSelector('wallet-settings')).click() + cy.get(getTestSelector('wallet-language-item')).contains('français').click({ force: true }) + cy.location('search').should('match', /\?lng=fr-FR$/) + cy.contains('Échanger') + cy.contains('Uniswap disponible en : English') + }) +}) diff --git a/apps/web/cypress/support/commands.ts b/apps/web/cypress/support/commands.ts new file mode 100644 index 0000000..14de0c9 --- /dev/null +++ b/apps/web/cypress/support/commands.ts @@ -0,0 +1,114 @@ +import 'cypress-hardhat/lib/browser' + +import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge' + +import { FeatureFlag } from '../../src/featureFlags' +import { initialState, UserState } from '../../src/state/user/reducer' +import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state' + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace Cypress { + interface ApplicationWindow { + ethereum: Eip1193Bridge + } + interface Chainable { + /** + * Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event. + * + * @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED + * @param {number} timeout - The maximum amount of time (in ms) to wait for the event. + * @returns {Chainable} + */ + waitForAmplitudeEvent(eventName: string, requiredProperties?: string[]): Chainable + /** + * Intercepts a specific graphql operation and responds with the given fixture. + * @param {string} operationName - The name of the graphql operation to intercept. + * @param {string} fixturePath - The path to the fixture to respond with. + */ + interceptGraphqlOperation(operationName: string, fixturePath: string): Chainable + } + interface VisitOptions { + serviceWorker?: true + featureFlags?: Array<{ name: FeatureFlag; value: boolean }> + /** + * Initial user state. + * @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE} + */ + userState?: Partial + } + } +} + +// sets up the injected provider to be a mock ethereum provider with the given mnemonic/index +// eslint-disable-next-line no-undef +Cypress.Commands.overwrite( + 'visit', + (original, url: string | Partial, options?: Partial) => { + if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.') + + return cy + .intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 }) + .provider() + .then((provider) => + original({ + ...options, + url, + onBeforeLoad(win) { + options?.onBeforeLoad?.(win) + + setInitialUserState(win, { + ...initialState, + ...CONNECTED_WALLET_USER_STATE, + ...(options?.userState ?? {}), + }) + + // Set feature flags, if configured. + if (options?.featureFlags) { + const featureFlags = options.featureFlags.reduce( + (flags, flag) => ({ ...flags, [flag.name]: flag.value ? 'enabled' : 'control' }), + {} + ) + win.localStorage.setItem('featureFlags', JSON.stringify(featureFlags)) + } + + // Inject the mock ethereum provider. + win.ethereum = provider + }, + }) + ) + } +) + +Cypress.Commands.add('waitForAmplitudeEvent', (eventName, requiredProperties) => { + function findAndDiscardEventsUpToTarget() { + const events = Cypress.env('amplitudeEventCache') + const targetEventIndex = events.findIndex((event) => { + if (event.event_type !== eventName) return false + if (requiredProperties) { + return requiredProperties.every((prop) => event.event_properties[prop]) + } + return true + }) + + if (targetEventIndex !== -1) { + const event = events[targetEventIndex] + Cypress.env('amplitudeEventCache', events.slice(targetEventIndex + 1)) + return cy.wrap(event) + } else { + // If not found, retry after waiting for more events to be sent. + return cy.wait('@amplitude').then(findAndDiscardEventsUpToTarget) + } + } + return findAndDiscardEventsUpToTarget() +}) + +Cypress.Commands.add('interceptGraphqlOperation', (operationName, fixturePath) => { + return cy.intercept(/(?:interface|beta).gateway.uniswap.org\/v1\/graphql/, (req) => { + if (req.body.operationName === operationName) { + req.reply({ fixture: fixturePath }) + } else { + req.continue() + } + }) +}) diff --git a/apps/web/cypress/support/e2e.ts b/apps/web/cypress/support/e2e.ts new file mode 100644 index 0000000..bb1377b --- /dev/null +++ b/apps/web/cypress/support/e2e.ts @@ -0,0 +1,23 @@ +// *********************************************************** +// This file is processed and loaded automatically before your test files. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +import './commands' +import './setupTests' + +// Squelch logs from fetches, as they clutter the logs so much as to make them unusable. +// See https://docs.cypress.io/api/commands/intercept#Disabling-logs-for-a-request. +// TODO(https://github.com/cypress-io/cypress/issues/26069): Squelch only wildcard logs once Cypress allows it. +const log = Cypress.log +Cypress.log = function (options, ...args) { + if (options.displayName === 'script' || options.name === 'request') return + return log(options, ...args) +} as typeof log + +Cypress.on('uncaught:exception', () => { + // returning false here prevents Cypress from failing the test + return false +}) diff --git a/apps/web/cypress/support/setupTests.ts b/apps/web/cypress/support/setupTests.ts new file mode 100644 index 0000000..b0babe6 --- /dev/null +++ b/apps/web/cypress/support/setupTests.ts @@ -0,0 +1,70 @@ +// @ts-ignore +import TokenListJSON from '@uniswap/default-token-list' +import { CyHttpMessages } from 'cypress/types/net-stubbing' + +beforeEach(() => { + // Many API calls enforce that requests come from our app, so we must mock Origin and Referer. + cy.intercept('*', (req) => { + req.headers['referer'] = 'https://app.uniswap.org' + req.headers['origin'] = 'https://app.uniswap.org' + }) + + // Network RPCs are disabled for cypress tests - calls should be routed through the connected wallet instead. + cy.intercept(/infura.io/, { statusCode: 404 }) + cy.intercept(/quiknode.pro/, { statusCode: 404 }) + + // Log requests to hardhat. + cy.intercept(/:8545/, logJsonRpc) + + Cypress.env('amplitudeEventCache', []) + + // Mock analytics responses to avoid analytics in tests. + cy.intercept('https://interface.gateway.uniswap.org/v1/amplitude-proxy', (req) => { + const requestBody = JSON.stringify(req.body) + const byteSize = new Blob([requestBody]).size + req.alias = 'amplitude' + req.reply( + JSON.stringify({ + code: 200, + server_upload_time: Date.now(), + payload_size_bytes: byteSize, + events_ingested: req.body.events.length, + }), + { + 'origin-country': 'US', + } + ) + + Cypress.env('amplitudeEventCache').push(...req.body.events) + }).intercept('https://*.sentry.io', { statusCode: 200 }) + + // Mock statsig to allow us to mock flags. + cy.intercept(/statsig/, { statusCode: 409 }) + + // Mock our own token list responses to avoid the latency of IPFS. + cy.intercept('https://gateway.ipfs.io/ipns/tokens.uniswap.org', TokenListJSON) + .intercept('https://gateway.ipfs.io/ipns/extendedtokens.uniswap.org', { statusCode: 404 }) + .intercept('https://gateway.ipfs.io/ipns/unsupportedtokens.uniswap.org', { statusCode: 404 }) + + // Reset hardhat between tests to ensure isolation. + // This resets the fork, as well as options like automine. + cy.hardhat().then((hardhat) => hardhat.reset()) +}) + +function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) { + req.alias = req.body.method + const log = Cypress.log({ + autoEnd: false, + name: req.body.method, + message: req.body.params?.map((param: any) => + typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10) + ), + }) + req.on('after:response', (res) => { + if (res.statusCode === 200) { + log.end() + } else { + log.error(new Error(`${res.statusCode}: ${res.statusMessage}`)) + } + }) +} diff --git a/apps/web/cypress/tsconfig.json b/apps/web/cypress/tsconfig.json new file mode 100644 index 0000000..90aec36 --- /dev/null +++ b/apps/web/cypress/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": false, + "incremental": true, + "isolatedModules": false, + "noImplicitAny": false, + "sourceMap": false, + "target": "ES6", + "tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo + "types": ["cypress", "node"], + }, + "include": ["**/*.ts"], +} diff --git a/apps/web/cypress/utils/graphql-test-utils.ts b/apps/web/cypress/utils/graphql-test-utils.ts new file mode 100644 index 0000000..b5b2039 --- /dev/null +++ b/apps/web/cypress/utils/graphql-test-utils.ts @@ -0,0 +1,12 @@ +// Utility to match GraphQL mutation based on the query name +export const hasQuery = (req: any, queryName: string) => { + const { body } = req + return Object.prototype.hasOwnProperty.call(body, 'query') && body.query.includes(queryName) +} + +// Alias query if queryName matches +export const aliasQuery = (req: any, queryName: string) => { + if (hasQuery(req, queryName)) { + req.alias = `${queryName}Query` + } +} diff --git a/apps/web/cypress/utils/index.ts b/apps/web/cypress/utils/index.ts new file mode 100644 index 0000000..2c3fc22 --- /dev/null +++ b/apps/web/cypress/utils/index.ts @@ -0,0 +1,13 @@ +import { Currency } from '@uniswap/sdk-core' + +export const getTestSelector = (selectorId: string) => `[data-testid=${selectorId}]` + +export const getTestSelectorStartsWith = (selectorId: string) => `[data-testid^=${selectorId}]` + +/** Gets the balance of a token as a Chainable. */ +export function getBalance(token: Currency) { + return cy + .hardhat() + .then((hardhat) => hardhat.getBalance(hardhat.wallet, token)) + .then((balance) => Number(balance.toFixed(1))) +} diff --git a/apps/web/cypress/utils/user-state.ts b/apps/web/cypress/utils/user-state.ts new file mode 100644 index 0000000..3cab361 --- /dev/null +++ b/apps/web/cypress/utils/user-state.ts @@ -0,0 +1,39 @@ +import { connectionMetaKey } from '../../src/connection/meta' +import { ConnectionType } from '../../src/connection/types' +import { UserState } from '../../src/state/user/reducer' + +export const CONNECTED_WALLET_USER_STATE: Partial = { + recentConnectionMeta: { type: ConnectionType.INJECTED }, +} + +export const DISCONNECTED_WALLET_USER_STATE: Partial = { recentConnectionMeta: undefined } + +/** + * This sets the initial value of the "user" slice in IndexedDB. + * Other persisted slices are not set, so they will be filled with their respective initial values + * when the app runs. + */ +export function setInitialUserState(win: Cypress.AUTWindow, state: UserState) { + // Selected wallet should also be reflected in localStorage, so that eager connections work. + if (state.recentConnectionMeta) { + win.localStorage.setItem(connectionMetaKey, JSON.stringify(state.recentConnectionMeta)) + } + + win.indexedDB.deleteDatabase('redux') + const dbRequest = win.indexedDB.open('redux') + dbRequest.onsuccess = function () { + const db = dbRequest.result + const transaction = db.transaction('keyvaluepairs', 'readwrite') + const store = transaction.objectStore('keyvaluepairs') + store.put( + { + user: state, + }, + 'persist:interface' + ) + } + dbRequest.onupgradeneeded = function () { + const db = dbRequest.result + db.createObjectStore('keyvaluepairs') + } +} diff --git a/apps/web/eslint_rules/no-undefined-or.js b/apps/web/eslint_rules/no-undefined-or.js new file mode 100644 index 0000000..0bae173 --- /dev/null +++ b/apps/web/eslint_rules/no-undefined-or.js @@ -0,0 +1,44 @@ +/* eslint-env node */ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Enforce the use of optional object fields', + category: 'Best Practices', + recommended: true, + }, + fixable: 'code', + schema: [], + }, + create(context) { + return { + 'TSPropertySignature > TSTypeAnnotation > TSUnionType': (node) => { + const types = node.types + const hasUndefined = types.some((typeNode) => typeNode.type === 'TSUndefinedKeyword') + + if (hasUndefined) { + const typesWithoutUndefined = types.filter((typeNode) => typeNode.type !== 'TSUndefinedKeyword') + + // If there is more than one type left after removing 'undefined', + // join them together with ' | ' to create a new union type. + const newTypeSource = + typesWithoutUndefined.length > 1 + ? typesWithoutUndefined.map((typeNode) => context.getSourceCode().getText(typeNode)).join(' | ') + : context.getSourceCode().getText(typesWithoutUndefined[0]) + + context.report({ + node, + message: `Prefer optional properties to "Type | undefined".`, + fix(fixer) { + const propertySignature = node.parent.parent + const isAlreadyOptional = propertySignature.optional + const newTypeAnnotation = isAlreadyOptional ? `: ${newTypeSource}` : `?: ${newTypeSource}` + return fixer.replaceText(node.parent, newTypeAnnotation) + }, + }) + } + }, + } + }, +} diff --git a/apps/web/functions/README.md b/apps/web/functions/README.md new file mode 100644 index 0000000..54c0913 --- /dev/null +++ b/apps/web/functions/README.md @@ -0,0 +1,55 @@ +# Cloudflare Cloud Functions + +## Purpose + +These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities. + +## Functions + +Currently, there are 2 types of cloudflare functions developed + +- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages. + - Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages + - These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected +- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages + - Currently supports NFT Assets, NFT Collections, and Token Detail Pages + - These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called. + - Can be found in the `api/image` folder. + +## Testing + +Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Tests run against a proxy server, so you'll need to start it before running tests: + +- Manually run `yarn start:cloud` to setup wrangler on `localhost:3000` +- Run unit tests with `yarn test:cloud` + +## Deployment + +Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit. + +## Miscellaneous + +- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API. + +## Scripts + +- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment + - `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions + - `--node-compat`: wrangler option that enables compatibility with Node.js modules + - `--proxy:3001`: telling the proxy to listen on port 3001 + - `--port=3000`: telling wrangler to run our proxy on port 3000 + - `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4 + - `PORT-3001 --yarn start`: runs default yarn start on port 3001 + - when exiting Miniflare, may need to clean up process on port 3001 separately: `kill $(lsof -t -i:3001)` +- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest + + - `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly + - `--config=functions/jest.config.json`: specifying which config file to use + + ## Additional Documents + + - [Open Graph Protocol](https://ogp.me/) + - [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) + - [Cloudflare Workers](https://developers.cloudflare.com/workers/) + - [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/) + - [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/) diff --git a/apps/web/functions/[[index]].ts b/apps/web/functions/[[index]].ts new file mode 100644 index 0000000..21c4474 --- /dev/null +++ b/apps/web/functions/[[index]].ts @@ -0,0 +1,29 @@ +/* eslint-disable import/no-unused-modules */ +import { paths } from '../src/pages/paths' +import { MetaTagInjector } from './components/metaTagInjector' + +function doesMatchPath(path: string): boolean { + const regexPaths = paths.map((p) => '^' + p.replace(/:[^/]+/g, '[^/]+').replace(/\*/g, '.*') + '$') + + return regexPaths.some((regex) => new RegExp(regex).test(path)) +} + +export const onRequest: PagesFunction = async ({ request, next }) => { + const requestURL = new URL(request.url) + const imageUri = requestURL.origin + '/images/1200x630_Rich_Link_Preview_Image.png' + const data = { + title: 'Uniswap Interface', + image: imageUri, + url: request.url, + description: 'Swap or provide liquidity on the Uniswap Protocol', + } + const res = next() + try { + const content = new HTMLRewriter().on('head', new MetaTagInjector(data, request)).transform(await res).body + return new Response(content, { + status: doesMatchPath(requestURL.pathname) || requestURL.pathname.includes('.') ? 200 : 404, + }) + } catch (e) { + return res + } +} diff --git a/apps/web/functions/api/image/nfts/asset/[[index]].tsx b/apps/web/functions/api/image/nfts/asset/[[index]].tsx new file mode 100644 index 0000000..bc66b3a --- /dev/null +++ b/apps/web/functions/api/image/nfts/asset/[[index]].tsx @@ -0,0 +1,75 @@ +/* eslint-disable import/no-unused-modules */ +import { ImageResponse } from '@vercel/og' + +import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist' +import { WATERMARK_URL } from '../../../../constants' +import getAsset from '../../../../utils/getAsset' +import getFont from '../../../../utils/getFont' +import { getRequest } from '../../../../utils/getRequest' + +export const onRequest: PagesFunction = async ({ params, request }) => { + try { + const origin = new URL(request.url).origin + const { index } = params + const collectionAddress = index[0]?.toString() + const tokenId = index[1]?.toString() + const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId + + if (blocklistedCollections.includes(collectionAddress)) { + return new Response('Collection unsupported.', { status: 404 }) + } + + const data = await getRequest( + cacheUrl, + () => getAsset(collectionAddress, tokenId, cacheUrl), + (data): data is NonNullable>> => Boolean(data.ogImage) + ) + + if (!data) { + return new Response('Asset not found.', { status: 404 }) + } + + const fontData = await getFont(origin) + + return new ImageResponse( + ( +

+ {data.title} +
+ Uniswap +
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Inter', + data: fontData, + style: 'normal', + }, + ], + } + ) as Response + } catch (error: any) { + return new Response(error.message || error.toString(), { status: 500 }) + } +} diff --git a/apps/web/functions/api/image/nfts/asset/nftImage.test.ts b/apps/web/functions/api/image/nfts/asset/nftImage.test.ts new file mode 100644 index 0000000..ed247ae --- /dev/null +++ b/apps/web/functions/api/image/nfts/asset/nftImage.test.ts @@ -0,0 +1,29 @@ +const assetImageUrl = [ + 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/804', + 'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947', +] + +test.each(assetImageUrl)('assetImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('image/png') +}) + +const invalidAssetImageUrl = [ + 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/10001', + 'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/44700', +] + +test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(404) +}) + +const blockedAssetImageUrl = [ + 'http://127.0.0.1:3000/api/image/nfts/asset/0xd4d871419714b778ebec2e22c7c53572b573706e/276', +] + +test.each(blockedAssetImageUrl)('blockedAssetImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(404) +}) diff --git a/apps/web/functions/api/image/nfts/collection/[index].tsx b/apps/web/functions/api/image/nfts/collection/[index].tsx new file mode 100644 index 0000000..2475e48 --- /dev/null +++ b/apps/web/functions/api/image/nfts/collection/[index].tsx @@ -0,0 +1,121 @@ +/* eslint-disable import/no-unused-modules */ +import { ImageResponse } from '@vercel/og' + +import { blocklistedCollections } from '../../../../../src/nft/utils/blocklist' +import { getColor } from '../../../../../src/utils/getColor' +import { CHECK_URL, WATERMARK_URL } from '../../../../constants' +import getCollection from '../../../../utils/getCollection' +import getFont from '../../../../utils/getFont' +import { getRequest } from '../../../../utils/getRequest' + +export const onRequest: PagesFunction = async ({ params, request }) => { + try { + const origin = new URL(request.url).origin + const { index } = params + const collectionAddress = index?.toString() + const cacheUrl = origin + '/nfts/collection/' + collectionAddress + + if (blocklistedCollections.includes(collectionAddress)) { + return new Response('Collection unsupported.', { status: 404 }) + } + + const data = await getRequest( + cacheUrl, + () => getCollection(collectionAddress, cacheUrl), + (data): data is NonNullable>> => + Boolean(data.ogImage && data.name && data.isVerified) + ) + + if (!data) { + return new Response('Collection not found.', { status: 404 }) + } + + const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage)]) + + // Split name into words to wrap them since satori does not support inline text wrapping + const words = data.name.split(' ') + + return new ImageResponse( + ( +
+
+
+ {data.name} +
+
+ {words.map((word: string) => ( + {word} + ))} + {data.isVerified && } +
+ Uniswap +
+
+
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Inter', + data: fontData, + style: 'normal', + }, + ], + } + ) as Response + } catch (error: any) { + return new Response(error.message || error.toString(), { status: 500 }) + } +} diff --git a/apps/web/functions/api/image/nfts/collection/collectionImage.test.ts b/apps/web/functions/api/image/nfts/collection/collectionImage.test.ts new file mode 100644 index 0000000..f5b67db --- /dev/null +++ b/apps/web/functions/api/image/nfts/collection/collectionImage.test.ts @@ -0,0 +1,34 @@ +import * as matchers from 'jest-extended' +expect.extend(matchers) + +const collectionImageUrls = [ + 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544', + 'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + 'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b', +] + +test.each([...collectionImageUrls])('collectionImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('image/png') +}) + +const nonexistentImageUrls = [ + 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545', +] + +const invalidCollectionImageUrls = ['http://127.0.0.1:3000/api/image/nfts/collection/0xd3adb33f'] + +test.each([...invalidCollectionImageUrls, ...nonexistentImageUrls])('invalidAssetImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBeOneOf([404, 500]) +}) + +const blockedCollectionImageUrls = [ + 'http://127.0.0.1:3000/api/image/nfts/collection/0xd4d871419714b778ebec2e22c7c53572b573706e', +] + +test.each(blockedCollectionImageUrls)('blockedCollectionImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBeOneOf([404, 500]) +}) diff --git a/apps/web/functions/api/image/tokens/[[index]].tsx b/apps/web/functions/api/image/tokens/[[index]].tsx new file mode 100644 index 0000000..d02172b --- /dev/null +++ b/apps/web/functions/api/image/tokens/[[index]].tsx @@ -0,0 +1,176 @@ +/* eslint-disable import/no-unused-modules */ +import { ImageResponse } from '@vercel/og' + +import { getColor } from '../../../../src/utils/getColor' +import { WATERMARK_URL } from '../../../constants' +import getFont from '../../../utils/getFont' +import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL' +import { getRequest } from '../../../utils/getRequest' +import getToken from '../../../utils/getToken' + +export const onRequest: PagesFunction = async ({ params, request }) => { + try { + const origin = new URL(request.url).origin + const { index } = params + const networkName = String(index[0]) + const tokenAddress = String(index[1]) + + const cacheUrl = origin + '/tokens/' + networkName + '/' + tokenAddress + + const data = await getRequest( + cacheUrl, + () => getToken(networkName, tokenAddress, cacheUrl), + (data): data is NonNullable>> => Boolean(data.symbol && data.name) + ) + + if (!data) { + return new Response('Token not found.', { status: 404 }) + } + + const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage, true)]) + + const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin) + + // Capitalize name such that each word starts with a capital letter + let words = data.name.split(' ') + words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) + let name = words.join(' ') + name = name.trim() + + return new ImageResponse( + ( +
+
+
+ {data.ogImage ? ( + + {networkLogo != '' && ( + + )} + + ) : ( +
+
+ {data.name.slice(0, 3).toUpperCase()} +
+ {networkLogo != '' && ( + + )} +
+ )} +
+ {name} +
+
+
+ {data.symbol} +
+ Uniswap +
+
+
+
+ ), + { + width: 1200, + height: 630, + fonts: [ + { + name: 'Inter', + data: fontData, + style: 'normal', + }, + ], + } + ) as Response + } catch (error: any) { + return new Response(error.message || error.toString(), { status: 500 }) + } +} diff --git a/apps/web/functions/api/image/tokens/tokenImage.test.ts b/apps/web/functions/api/image/tokens/tokenImage.test.ts new file mode 100644 index 0000000..3c77378 --- /dev/null +++ b/apps/web/functions/api/image/tokens/tokenImage.test.ts @@ -0,0 +1,22 @@ +const tokenImageUrl = [ + 'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + 'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE', +] + +test.each(tokenImageUrl)('tokenImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(200) + expect(response.headers.get('content-type')).toBe('image/png') +}) + +const invalidTokenImageUrl = [ + 'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49', + 'http://127.0.0.1:3000/api/image/tokens/ethereum', + 'http://127.0.0.1:3000/api/image/tokens/ethereun', + 'http://127.0.0.1:3000/api/image/tokens/potato/?potato=1', +] + +test.each(invalidTokenImageUrl)('invalidAssetImageUrl', async (url) => { + const response = await fetch(new Request(url)) + expect(response.status).toBe(404) +}) diff --git a/apps/web/functions/babel.config.js b/apps/web/functions/babel.config.js new file mode 100644 index 0000000..4c450d9 --- /dev/null +++ b/apps/web/functions/babel.config.js @@ -0,0 +1 @@ +export const presets = ['@babel/preset-env'] diff --git a/apps/web/functions/client.ts b/apps/web/functions/client.ts new file mode 100644 index 0000000..327a9d5 --- /dev/null +++ b/apps/web/functions/client.ts @@ -0,0 +1,20 @@ +import { ApolloClient, InMemoryCache } from '@apollo/client' +const GRAPHQL_ENDPOINT = 'https://interface.gateway.uniswap.org/v1/graphql' + +//TODO: Figure out how to make ApolloClient global variable +export default new ApolloClient({ + connectToDevTools: false, + uri: GRAPHQL_ENDPOINT, + headers: { + 'Content-Type': 'application/json', + Origin: 'https://app.uniswap.org', + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36', + }, + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + fetchPolicy: 'cache-first', + }, + }, +}) diff --git a/apps/web/functions/components/metaTagInjector.test.ts b/apps/web/functions/components/metaTagInjector.test.ts new file mode 100644 index 0000000..e0a9af5 --- /dev/null +++ b/apps/web/functions/components/metaTagInjector.test.ts @@ -0,0 +1,60 @@ +import { MetaTagInjector } from './metaTagInjector' + +test('should append meta tag to element', () => { + const element = { + append: jest.fn(), + } as unknown as Element + const property = 'property' + const content = 'content' + const injector = new MetaTagInjector( + { + title: 'test', + url: 'testUrl', + image: 'testImage', + description: 'testDescription', + }, + new Request('http://localhost') + ) + injector.append(element, property, content) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + + injector.element(element) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { + html: true, + }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + + expect(element.append).toHaveBeenCalledWith(``, { + html: true, + }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) + + expect(element.append).toHaveBeenCalledTimes(13) +}) + +test('should pass through header blocked paths', () => { + const element = { + append: jest.fn(), + } as unknown as Element + const request = new Request('http://localhost') + request.headers.set('x-blocked-paths', '/') + const injector = new MetaTagInjector( + { + title: 'test', + url: 'testUrl', + image: 'testImage', + description: 'testDescription', + }, + request + ) + injector.element(element) + expect(element.append).toHaveBeenCalledWith(``, { html: true }) +}) diff --git a/apps/web/functions/components/metaTagInjector.ts b/apps/web/functions/components/metaTagInjector.ts new file mode 100644 index 0000000..2be810a --- /dev/null +++ b/apps/web/functions/components/metaTagInjector.ts @@ -0,0 +1,47 @@ +type MetaTagInjectorInput = { + title: string + image?: string + url: string + description?: string +} + +/** + * Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter} + * to inject meta tags into the of an HTML document. + */ +export class MetaTagInjector implements HTMLRewriterElementContentHandlers { + constructor(private input: MetaTagInjectorInput, private request: Request) {} + + append(element: Element, property: string, content: string) { + element.append(``, { html: true }) + } + + element(element: Element) { + //Open Graph Tags + this.append(element, 'og:title', this.input.title) + if (this.input.description) { + this.append(element, 'og:description', this.input.description) + } + if (this.input.image) { + this.append(element, 'og:image', this.input.image) + this.append(element, 'og:image:width', '1200') + this.append(element, 'og:image:height', '630') + this.append(element, 'og:image:alt', this.input.title) + } + this.append(element, 'og:type', 'website') + this.append(element, 'og:url', this.input.url) + + //Twitter Tags + this.append(element, 'twitter:card', 'summary_large_image') + this.append(element, 'twitter:title', this.input.title) + if (this.input.image) { + this.append(element, 'twitter:image', this.input.image) + this.append(element, 'twitter:image:alt', this.input.title) + } + + const blockedPaths = this.request.headers.get('x-blocked-paths') + if (blockedPaths) { + this.append(element, 'x:blocked-paths', blockedPaths) + } + } +} diff --git a/apps/web/functions/constants.ts b/apps/web/functions/constants.ts new file mode 100644 index 0000000..23e1ce7 --- /dev/null +++ b/apps/web/functions/constants.ts @@ -0,0 +1,2 @@ +export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png' +export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg' diff --git a/apps/web/functions/default.test.ts b/apps/web/functions/default.test.ts new file mode 100644 index 0000000..96a8ca1 --- /dev/null +++ b/apps/web/functions/default.test.ts @@ -0,0 +1,22 @@ +const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pool'] + +test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => { + const body = await fetch(new Request(defaultUrl)).then((res) => res.text()) + expect(body).toContain(``) + expect(body).toContain( + `` + ) + expect(body).toContain( + `` + ) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain( + `` + ) + expect(body).toContain(``) +}) diff --git a/apps/web/functions/jest.config.json b/apps/web/functions/jest.config.json new file mode 100644 index 0000000..07ef871 --- /dev/null +++ b/apps/web/functions/jest.config.json @@ -0,0 +1,10 @@ +{ + "setupFilesAfterEnv": ["/setupAfterEnv.ts"], + "preset": "ts-jest", + "transform": { + "'^.+\\.(ts|tsx)?$'": "ts-jest", + "^.+\\.(js|jsx)$": "babel-jest" + }, + "testTimeout": 360000, + "cacheDirectory": "../node_modules/.cache/cloud-jest" +} diff --git a/apps/web/functions/nfts/asset/[[index]].ts b/apps/web/functions/nfts/asset/[[index]].ts new file mode 100644 index 0000000..c7a15a6 --- /dev/null +++ b/apps/web/functions/nfts/asset/[[index]].ts @@ -0,0 +1,15 @@ +/* eslint-disable import/no-unused-modules */ +import getAsset from '../../utils/getAsset' +import { getMetadataRequest } from '../../utils/getRequest' + +export const onRequest: PagesFunction = async ({ params, request, next }) => { + const res = next() + try { + const { index } = params + const collectionAddress = index[0]?.toString() + const tokenId = index[1]?.toString() + return getMetadataRequest(res, request, () => getAsset(collectionAddress, tokenId, request.url)) + } catch (e) { + return res + } +} diff --git a/apps/web/functions/nfts/asset/nft.test.ts b/apps/web/functions/nfts/asset/nft.test.ts new file mode 100644 index 0000000..9f5b2c1 --- /dev/null +++ b/apps/web/functions/nfts/asset/nft.test.ts @@ -0,0 +1,72 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const assets = [ + { + address: '0xed5af388653567af2f388e6224dc7c4b3241c544', + assetId: '2550', + collectionName: 'Azuki', + image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550', + }, + { + address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + assetId: '3735', + collectionName: 'Bored Ape Yacht Club', + image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735', + }, + { + address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb', + assetId: '3947', + collectionName: 'CryptoPunk', + image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947', + }, +] + +// TODO re-enable web tests +// eslint-disable-next-line jest/no-commented-out-tests +// test.each(assets)('should inject metadata for valid assets', async (nft) => { +// const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId +// const body = await fetch(new Request(url)).then((res) => res.text()) +// expect(body).toMatchSnapshot(nft.collectionName) +// expect(body).toContain(``) +// expect(body).not.toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// expect(body).toContain(``) +// }) + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const invalidAssets = [ + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000', + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544', + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545', + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1', + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//', + 'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550', +] + +test('pass', () => { + expect(0).toBe(0) +}) + +// TODO re-enable web tests +// eslint-disable-next-line jest/no-commented-out-tests +// test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => { +// const body = await fetch(new Request(url)).then((res) => res.text()) +// expect(body).not.toContain('og:title') +// expect(body).not.toContain('og:image') +// expect(body).not.toContain('og:image:width') +// expect(body).not.toContain('og:image:height') +// expect(body).not.toContain('og:type') +// expect(body).not.toContain('og:url') +// expect(body).not.toContain('og:image:alt') +// expect(body).not.toContain('twitter:card') +// expect(body).not.toContain('twitter:title') +// expect(body).not.toContain('twitter:image') +// expect(body).not.toContain('twitter:image:alt') +// }) diff --git a/apps/web/functions/nfts/collection/[index].ts b/apps/web/functions/nfts/collection/[index].ts new file mode 100644 index 0000000..381dcca --- /dev/null +++ b/apps/web/functions/nfts/collection/[index].ts @@ -0,0 +1,14 @@ +/* eslint-disable import/no-unused-modules */ +import getCollection from '../../utils/getCollection' +import { getMetadataRequest } from '../../utils/getRequest' + +export const onRequest: PagesFunction = async ({ params, request, next }) => { + const res = next() + try { + const { index } = params + const collectionAddress = index?.toString() + return getMetadataRequest(res, request, () => getCollection(collectionAddress, request.url)) + } catch (e) { + return res + } +} diff --git a/apps/web/functions/nfts/collection/__snapshots__/collection.test.ts.snap b/apps/web/functions/nfts/collection/__snapshots__/collection.test.ts.snap new file mode 100644 index 0000000..7fe0dc4 --- /dev/null +++ b/apps/web/functions/nfts/collection/__snapshots__/collection.test.ts.snap @@ -0,0 +1,442 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should inject metadata for collections 1`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; + +exports[`should inject metadata for collections 2`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; + +exports[`should inject metadata for collections 3`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; diff --git a/apps/web/functions/nfts/collection/collection.test.ts b/apps/web/functions/nfts/collection/collection.test.ts new file mode 100644 index 0000000..7213288 --- /dev/null +++ b/apps/web/functions/nfts/collection/collection.test.ts @@ -0,0 +1,66 @@ +const collections = [ + { + address: '0xed5af388653567af2f388e6224dc7c4b3241c544', + collectionName: 'Azuki', + image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544', + }, + { + address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + collectionName: 'Bored Ape Yacht Club', + image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d', + }, + { + address: '0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b', + collectionName: 'CLONE X - X TAKASHI MURAKAMI', + image: 'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b', + }, +] + +test.each([...collections])('should inject metadata for collections', async (collection) => { + const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address + const body = await fetch(new Request(url)).then((res) => res.text()) + expect(body).toMatchSnapshot() + expect(body).toContain(``) + expect(body).not.toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) +}) + +const nonexistentCollections = [ + { + address: '0xed5af388653567af2f388e6224dc7c4b3241c545', + }, +] + +const invalidCollections = [ + { + address: '0xd3adb33f', + }, +] + +test.each([...invalidCollections, ...nonexistentCollections])( + 'should not inject metadata for nonexistent collections', + async (collection) => { + const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address + const body = await fetch(new Request(url)).then((res) => res.text()) + expect(body).not.toContain('og:title') + expect(body).not.toContain('og:image') + expect(body).not.toContain('og:image:width') + expect(body).not.toContain('og:image:height') + expect(body).not.toContain('og:type') + expect(body).not.toContain('og:url') + expect(body).not.toContain('og:image:alt') + expect(body).not.toContain('twitter:card') + expect(body).not.toContain('twitter:title') + expect(body).not.toContain('twitter:image') + expect(body).not.toContain('twitter:image:alt') + } +) diff --git a/apps/web/functions/setupAfterEnv.ts b/apps/web/functions/setupAfterEnv.ts new file mode 100644 index 0000000..05366db --- /dev/null +++ b/apps/web/functions/setupAfterEnv.ts @@ -0,0 +1 @@ +jest.retryTimes(3) diff --git a/apps/web/functions/tokens/[[index]].ts b/apps/web/functions/tokens/[[index]].ts new file mode 100644 index 0000000..da2dce5 --- /dev/null +++ b/apps/web/functions/tokens/[[index]].ts @@ -0,0 +1,18 @@ +/* eslint-disable import/no-unused-modules */ +import { getMetadataRequest } from '../utils/getRequest' +import getToken from '../utils/getToken' + +export const onRequest: PagesFunction = async ({ params, request, next }) => { + const res = next() + try { + const { index } = params + const networkName = index[0]?.toString() + const tokenAddress = index[1]?.toString() + if (!tokenAddress) { + return res + } + return getMetadataRequest(res, request, () => getToken(networkName, tokenAddress, request.url)) + } catch (e) { + return res + } +} diff --git a/apps/web/functions/tokens/__snapshots__/token.test.ts.snap b/apps/web/functions/tokens/__snapshots__/token.test.ts.snap new file mode 100644 index 0000000..a9b35cc --- /dev/null +++ b/apps/web/functions/tokens/__snapshots__/token.test.ts.snap @@ -0,0 +1,589 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`should inject metadata for valid tokens 1`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; + +exports[`should inject metadata for valid tokens 2`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; + +exports[`should inject metadata for valid tokens 3`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; + +exports[`should inject metadata for valid tokens 4`] = ` +" + + + + + Uniswap Interface + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + +" +`; diff --git a/apps/web/functions/tokens/token.test.ts b/apps/web/functions/tokens/token.test.ts new file mode 100644 index 0000000..3c74f6f --- /dev/null +++ b/apps/web/functions/tokens/token.test.ts @@ -0,0 +1,73 @@ +import { NATIVE_CHAIN_ID } from '../../src/constants/tokens' + +const tokens = [ + { + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + network: 'ethereum', + symbol: 'USDC', + image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + }, + { + address: NATIVE_CHAIN_ID, + network: 'ethereum', + symbol: 'ETH', + image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE', + }, + { + address: NATIVE_CHAIN_ID, + network: 'polygon', + symbol: 'MATIC', + image: 'http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE', + }, + { + address: '0x6982508145454ce325ddbe47a25d4ec3d2311933', + network: 'ethereum', + symbol: 'PEPE', + image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933', + }, +] + +test.each(tokens)('should inject metadata for valid tokens', async (token) => { + const url = 'http://127.0.0.1:3000/tokens/' + token.network + '/' + token.address + const body = await fetch(new Request(url)).then((res) => res.text()) + expect(body).toMatchSnapshot() + expect(body).toContain(``) + expect(body).not.toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) + expect(body).toContain(``) +}) + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const invalidTokens = [ + 'http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49', + 'http://127.0.0.1:3000/tokens/ethereum', + 'http://127.0.0.1:3000/tokens/ethereun', + 'http://127.0.0.1:3000/tokens/ethereum/0x0', + 'http://127.0.0.1:3000/tokens/ethereum//', + 'http://127.0.0.1:3000/tokens/potato/?potato=1', +] + +// TODO re-enable web tests +// eslint-disable-next-line jest/no-commented-out-tests +// test.each(invalidTokens)('should not inject metadata for invalid tokens', async (url) => { +// const body = await fetch(new Request(url)).then((res) => res.text()) +// expect(body).not.toContain('og:title') +// expect(body).not.toContain('og:image') +// expect(body).not.toContain('og:image:width') +// expect(body).not.toContain('og:image:height') +// expect(body).not.toContain('og:type') +// expect(body).not.toContain('og:url') +// expect(body).not.toContain('og:image:alt') +// expect(body).not.toContain('twitter:card') +// expect(body).not.toContain('twitter:title') +// expect(body).not.toContain('twitter:image') +// expect(body).not.toContain('twitter:image:alt') +// }) diff --git a/apps/web/functions/tsconfig.json b/apps/web/functions/tsconfig.json new file mode 100644 index 0000000..7306413 --- /dev/null +++ b/apps/web/functions/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "composite": false, + "incremental": true, + "isolatedModules": false, + "tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo + "types": ["jest", "node", "@cloudflare/workers-types"] + }, + "include": ["**/*.ts", "**/*.tsx"], +} diff --git a/apps/web/functions/types.d.ts b/apps/web/functions/types.d.ts new file mode 100644 index 0000000..ea74d69 --- /dev/null +++ b/apps/web/functions/types.d.ts @@ -0,0 +1 @@ +declare module 'colorthief/src/color-thief-node' diff --git a/apps/web/functions/utils/cache.test.ts b/apps/web/functions/utils/cache.test.ts new file mode 100644 index 0000000..97355f2 --- /dev/null +++ b/apps/web/functions/utils/cache.test.ts @@ -0,0 +1,43 @@ +import CacheMock from 'browser-cache-mock' + +import { mocked } from '../../src/test-utils/mocked' +import Cache from './cache' + +const cacheMock = new CacheMock() + +const data = { + title: 'test', + image: 'testImage', + url: 'testUrl', +} + +beforeAll(() => { + const globalAny: any = global + globalAny.caches = { + open: async () => cacheMock, + ...cacheMock, + } +}) + +test('Should put cache properly', async () => { + jest.spyOn(cacheMock, 'put') + await Cache.put(data, 'https://example.com') + expect(cacheMock.put).toHaveBeenCalledWith('https://example.com', expect.anything()) + const call = mocked(cacheMock.put).mock.calls[0] + const response = JSON.parse(await (call[1] as Response).clone().text()) + expect(response).toStrictEqual(data) + + await expect(Cache.match('https://example.com')).resolves.toStrictEqual(data) +}) + +test('Should match cache properly', async () => { + jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify(data))) + const response = await Cache.match('https://example.com') + expect(response).toStrictEqual(data) +}) + +test('Should return undefined if not all data is present', async () => { + jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify({ ...data, title: undefined }))) + const response = await Cache.match('https://example.com') + expect(response).toBeUndefined() +}) diff --git a/apps/web/functions/utils/cache.ts b/apps/web/functions/utils/cache.ts new file mode 100644 index 0000000..0701482 --- /dev/null +++ b/apps/web/functions/utils/cache.ts @@ -0,0 +1,32 @@ +export interface Data { + title: string + image: string + url: string + name?: string + ogImage?: string + isVerified?: boolean + symbol?: string +} + +const CACHE_NAME = 'functions-cache' as const + +class Cache { + async match(request: string): Promise { + const cache = await caches.open(CACHE_NAME) + const response = await cache.match(request) + if (!response) return undefined + const data: Data = JSON.parse(await response.text()) + if (!data.title || !data.image || !data.url) return undefined + return data + } + + async put(data: Data, request: string) { + // Set max age to 1 week + const response = new Response(JSON.stringify(data)) + response.headers.set('Cache-Control', 'max-age=604800') + const cache = await caches.open(CACHE_NAME) + await cache.put(request, response) + } +} + +export default new Cache() diff --git a/apps/web/functions/utils/getAsset.ts b/apps/web/functions/utils/getAsset.ts new file mode 100644 index 0000000..d7fb47e --- /dev/null +++ b/apps/web/functions/utils/getAsset.ts @@ -0,0 +1,41 @@ +import { AssetDocument, AssetQuery } from '../../src/graphql/data/__generated__/types-and-hooks' +import client from '../client' + +function formatTitleName(name: string | undefined, collectionName: string | undefined, tokenId: string) { + if (name) { + return name + } + if (collectionName && tokenId) { + return collectionName + ' #' + tokenId + } + if (tokenId) { + return 'Asset #' + tokenId + } + return 'View NFT on Uniswap' +} + +export default async function getAsset(collectionAddress: string, tokenId: string, url: string) { + const origin = new URL(url).origin + const image = origin + '/api/image/nfts/asset/' + collectionAddress + '/' + tokenId + const { data } = await client.query({ + query: AssetDocument, + variables: { + address: collectionAddress, + filter: { + tokenIds: [tokenId], + }, + }, + }) + const asset = data?.nftAssets?.edges[0]?.node + if (!asset) { + return undefined + } + const title = formatTitleName(asset.name, asset.collection?.name, asset.tokenId) + const formattedAsset = { + title, + image, + url, + ogImage: asset.image?.url ?? origin + '/images/192x192_App_Icon.png', + } + return formattedAsset +} diff --git a/apps/web/functions/utils/getCollection.ts b/apps/web/functions/utils/getCollection.ts new file mode 100644 index 0000000..5d767d6 --- /dev/null +++ b/apps/web/functions/utils/getCollection.ts @@ -0,0 +1,26 @@ +import { CollectionDocument, CollectionQuery } from '../../src/graphql/data/__generated__/types-and-hooks' +import client from '../client' + +export default async function getCollection(collectionAddress: string, url: string) { + const origin = new URL(url).origin + const image = origin + '/api/image/nfts/collection/' + collectionAddress + const { data } = await client.query({ + query: CollectionDocument, + variables: { + addresses: collectionAddress, + }, + }) + const collection = data?.nftCollections?.edges[0]?.node + if (!collection || !collection.name) { + return undefined + } + const formattedAsset = { + title: collection.name + ' on Uniswap', + image, + url, + name: collection.name ?? 'Collection', + ogImage: collection.image?.url ?? origin + '/images/192x192_App_Icon.png', + isVerified: collection.isVerified ?? false, + } + return formattedAsset +} diff --git a/apps/web/functions/utils/getFont.ts b/apps/web/functions/utils/getFont.ts new file mode 100644 index 0000000..18c8823 --- /dev/null +++ b/apps/web/functions/utils/getFont.ts @@ -0,0 +1,5 @@ +export default async function getFont(origin: string) { + const url = origin + '/fonts/Inter-normal.var.ttf' + const font = await fetch(url) + return font.arrayBuffer() +} diff --git a/apps/web/functions/utils/getNetworkLogoURL.ts b/apps/web/functions/utils/getNetworkLogoURL.ts new file mode 100644 index 0000000..8581267 --- /dev/null +++ b/apps/web/functions/utils/getNetworkLogoURL.ts @@ -0,0 +1,16 @@ +import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks' + +export default function getNetworkLogoUrl(network: string, origin: string) { + switch (network) { + case Chain.Polygon: + return origin + '/images/logos/Polygon_Logo.png' + case Chain.Arbitrum: + return origin + '/images/logos/Arbitrum_Logo.png' + case Chain.Optimism: + return origin + '/images/logos/Optimism_Logo.png' + case Chain.Celo: + return origin + '/images/logos/Celo_Logo.png' + default: + return '' + } +} diff --git a/apps/web/functions/utils/getRequest.test.ts b/apps/web/functions/utils/getRequest.test.ts new file mode 100644 index 0000000..a8b09c7 --- /dev/null +++ b/apps/web/functions/utils/getRequest.test.ts @@ -0,0 +1,38 @@ +import * as matchers from 'jest-extended' +expect.extend(matchers) + +import { mocked } from '../../src/test-utils/mocked' +import Cache, { Data } from './cache' +import { getRequest } from './getRequest' + +jest.mock('./cache', () => ({ + match: jest.fn(), + put: jest.fn(), +})) + +test('should call Cache.match before calling getData when request is not cached', async () => { + const url = 'https://example.com' + const getData = jest.fn().mockResolvedValueOnce({ + title: 'test', + image: 'testImage', + url: 'testUrl', + }) + await getRequest(url, getData, (data): data is Data => true) + expect(Cache.match).toHaveBeenCalledWith(url) + expect(getData).toHaveBeenCalled() + expect(Cache.match).toHaveBeenCalledBefore(getData) + expect(Cache.put).toHaveBeenCalledAfter(getData) +}) + +test('getData should not be called when request is cached', async () => { + const url = 'https://example.com' + mocked(Cache.match).mockResolvedValueOnce({ + title: 'test', + image: 'testImage', + url: 'testUrl', + }) + const getData = jest.fn() + await getRequest(url, getData, (data): data is Data => true) + expect(Cache.match).toHaveBeenCalledWith(url) + expect(getData).not.toHaveBeenCalled() +}) diff --git a/apps/web/functions/utils/getRequest.ts b/apps/web/functions/utils/getRequest.ts new file mode 100644 index 0000000..f1b88cc --- /dev/null +++ b/apps/web/functions/utils/getRequest.ts @@ -0,0 +1,41 @@ +import { MetaTagInjector } from '../components/metaTagInjector' +import Cache, { Data } from './cache' + +export async function getMetadataRequest( + res: Promise, + request: Request, + getData: () => Promise +) { + try { + const cachedData = await getRequest(request.url, getData, (data): data is Data => true) + if (cachedData) { + return new HTMLRewriter().on('head', new MetaTagInjector(cachedData, request)).transform(await res) + } else { + return res + } + } catch (e) { + return res + } +} + +export async function getRequest( + url: string, + getData: () => Promise, + validateData: (data: Data) => data is T +): Promise { + try { + const cachedData = await Cache.match(url) + if (cachedData && validateData(cachedData)) { + return cachedData + } else { + const data = await getData() + if (!data) { + return undefined + } + await Cache.put(data, url) + return data + } + } catch (e) { + return undefined + } +} diff --git a/apps/web/functions/utils/getToken.ts b/apps/web/functions/utils/getToken.ts new file mode 100644 index 0000000..da125f4 --- /dev/null +++ b/apps/web/functions/utils/getToken.ts @@ -0,0 +1,57 @@ +import { NATIVE_CHAIN_ID } from '../../src/constants/tokens' +import { Chain, TokenDocument, TokenQuery } from '../../src/graphql/data/__generated__/types-and-hooks' +import client from '../client' + +function formatTitleName(symbol: string | undefined, name: string | undefined) { + if (symbol) { + return 'Get ' + symbol + ' on Uniswap' + } + if (name) { + return 'Get ' + name + ' on Uniswap' + } + return 'View Token on Uniswap' +} + +const convertTokenAddress = (networkName: string, tokenAddress: string) => { + if (tokenAddress === NATIVE_CHAIN_ID) { + switch (networkName) { + case Chain.Celo: + return '0x471EcE3750Da237f93B8E339c536989b8978a438' + case Chain.Polygon: + return '0x0000000000000000000000000000000000001010' + default: + return undefined + } + } + return tokenAddress +} + +export default async function getToken(networkName: string, tokenAddress: string, url: string) { + const origin = new URL(url).origin + const image = origin + '/api/image/tokens/' + networkName + '/' + tokenAddress + const uppercaseNetworkName = networkName.toUpperCase() + const convertedTokenAddress = convertTokenAddress(uppercaseNetworkName, tokenAddress) + const { data } = await client.query({ + query: TokenDocument, + variables: { + chain: uppercaseNetworkName, + address: convertedTokenAddress, + }, + }) + const asset = data?.token + if (!asset) { + return undefined + } + + const title = formatTitleName(asset.symbol, asset.name) + + const formattedAsset = { + title, + image, + url, + symbol: asset.symbol ?? 'UNK', + ogImage: asset.project?.logoUrl, + name: asset.name ?? 'Token', + } + return formattedAsset +} diff --git a/apps/web/graphql.data.codegen.config.ts b/apps/web/graphql.data.codegen.config.ts new file mode 100644 index 0000000..b3f8e69 --- /dev/null +++ b/apps/web/graphql.data.codegen.config.ts @@ -0,0 +1,26 @@ +/* eslint-env node */ + +import type { CodegenConfig } from '@graphql-codegen/cli' + +// Generates TS objects from the schemas returned by graphql queries +// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project +const config: CodegenConfig = { + overwrite: true, + schema: './src/graphql/data/schema.graphql', + documents: ['./src/graphql/data/**', '!./src/graphql/data/__generated__/**', '!**/thegraph/**'], + generates: { + 'src/graphql/data/__generated__/types-and-hooks.ts': { + plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'], + config: { + withHooks: true, + // This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null + maybeValue: 'T', + immutableTypes: true, + }, + }, + }, +} + +// This is used in package.json when generating apollo schemas however the linter stills flags this as unused +// eslint-disable-next-line import/no-unused-modules +export default config diff --git a/apps/web/graphql.data.config.js b/apps/web/graphql.data.config.js new file mode 100644 index 0000000..7844861 --- /dev/null +++ b/apps/web/graphql.data.config.js @@ -0,0 +1,8 @@ +/* eslint-env node */ + +module.exports = { + src: './src', + language: 'typescript', + schema: './src/graphql/data/schema.graphql', + exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/thegraph/**'], +} diff --git a/apps/web/graphql.thegraph.codegen.config.ts b/apps/web/graphql.thegraph.codegen.config.ts new file mode 100644 index 0000000..677a6bd --- /dev/null +++ b/apps/web/graphql.thegraph.codegen.config.ts @@ -0,0 +1,25 @@ +/* eslint-env node */ + +import type { CodegenConfig } from '@graphql-codegen/cli' + +// Generates TS objects from the schemas returned by graphql queries +// To learn more: https://www.apollographql.com/docs/react/development-testing/static-typing/#setting-up-your-project +const config: CodegenConfig = { + overwrite: true, + schema: './src/graphql/thegraph/schema.graphql', + documents: ['!./src/graphql/data/**', '!./src/graphql/thegraph/__generated__/**', './src/graphql/thegraph/**'], + generates: { + 'src/graphql/thegraph/__generated__/types-and-hooks.ts': { + plugins: ['typescript', 'typescript-operations', 'typescript-react-apollo'], + config: { + withHooks: true, + // This avoid all generated schemas being wrapped in Maybe https://the-guild.dev/graphql/codegen/plugins/typescript/typescript#maybevalue-string-default-value-t--null + maybeValue: 'T', + }, + }, + }, +} + +// This is used in package.json when generating apollo schemas however the linter stills flags this as unused +// eslint-disable-next-line import/no-unused-modules +export default config diff --git a/apps/web/graphql.thegraph.config.js b/apps/web/graphql.thegraph.config.js new file mode 100644 index 0000000..ae185b9 --- /dev/null +++ b/apps/web/graphql.thegraph.config.js @@ -0,0 +1,10 @@ +/* eslint-env node */ + +const defaultConfig = require('./graphql.data.config') + +module.exports = { + src: defaultConfig.src, + language: defaultConfig.language, + schema: './src/graphql/thegraph/schema.graphql', + exclude: ['**/node_modules/**', '**/__mocks__/**', '**/__generated__/**', '**/data/**'], +} diff --git a/apps/web/hardhat.config.js b/apps/web/hardhat.config.js new file mode 100644 index 0000000..269aa28 --- /dev/null +++ b/apps/web/hardhat.config.js @@ -0,0 +1,40 @@ +import { ChainId } from '@uniswap/sdk-core' +import { UNIVERSAL_ROUTER_CREATION_BLOCK } from '@uniswap/universal-router-sdk' + +/* eslint-env node */ +require('dotenv').config() + +const forkingConfig = { + httpHeaders: { + Origin: 'localhost:3000', // infura allowlists requests by origin + }, +} + +const forks = { + [ChainId.MAINNET]: { + url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + ...forkingConfig, + }, + [ChainId.POLYGON]: { + url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`, + blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.POLYGON), + ...forkingConfig, + }, +} + +module.exports = { + forks, + networks: { + hardhat: { + chainId: ChainId.MAINNET, + forking: forks[ChainId.MAINNET], + accounts: { + count: 2, + }, + mining: { + auto: true, // automine to make tests easier to write. + interval: 0, // do not interval mine so that tests remain deterministic + }, + }, + }, +} diff --git a/apps/web/lingui.config.ts b/apps/web/lingui.config.ts new file mode 100644 index 0000000..289ebbb --- /dev/null +++ b/apps/web/lingui.config.ts @@ -0,0 +1,64 @@ +/* eslint-env node */ + +const linguiConfig = { + catalogs: [ + { + path: '/src/locales/{locale}', + include: ['/src/**/*.ts', '/src/**/*.tsx'], + exclude: [ + '/src/**/*.d.ts', + '/src/**/*.test.*', + '/src/types/v3/**', + '/src/abis/types/**', + '/src/graphql/**/__generated__/**', + ], + }, + ], + compileNamespace: 'cjs', + fallbackLocales: { + default: 'en-US', + }, + format: 'po', + formatOptions: { + lineNumbers: false, + }, + locales: [ + 'af-ZA', + 'ar-SA', + 'ca-ES', + 'cs-CZ', + 'da-DK', + 'el-GR', + 'en-US', + 'es-ES', + 'fi-FI', + 'fr-FR', + 'he-IL', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'nl-NL', + 'no-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ro-RO', + 'ru-RU', + 'sr-SP', + 'sv-SE', + 'sw-TZ', + 'tr-TR', + 'uk-UA', + 'vi-VN', + 'zh-CN', + 'zh-TW', + ], + orderBy: 'messageId', + rootDir: '.', + runtimeConfigModule: ['@lingui/core', 'i18n'], + sourceLocale: 'en-US', +} + +export default linguiConfig diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..09ebfc8 --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,306 @@ +{ + "name": "@uniswap/interface", + "version": "1.1.0", + "description": "Uniswap Interface", + "license": "GPL-3.0-or-later", + "scripts": { + "ajv": "node scripts/compile-ajv-validators.js", + "check:deps:usage": "depcheck", + "contracts:compile:abi": "typechain --target ethers-v5 --out-dir src/abis/types \"./src/abis/**/*.json\"", + "contracts:compile:v3": "typechain --target ethers-v5 --out-dir src/types/v3 \"../../node_modules/@uniswap/**/artifacts/contracts/**/*[!dbg].json\"", + "contracts": "yarn contracts:compile:abi && yarn contracts:compile:v3", + "graphql:schema": "node scripts/fetch-schema.js", + "graphql:generate:data": "graphql-codegen --config graphql.data.codegen.config.ts", + "graphql:generate:thegraph": "graphql-codegen --config graphql.thegraph.codegen.config.ts", + "graphql:generate": "yarn graphql:generate:data && yarn graphql:generate:thegraph", + "graphql": "yarn graphql:schema && yarn graphql:generate", + "sitemap:generate": "node scripts/generate-sitemap.js", + "i18n:extract": "lingui extract --locale en-US", + "i18n:compile": "lingui compile", + "i18n": "yarn i18n:extract --clean && yarn i18n:compile", + "prepare": "concurrently \"npm:ajv\" \"npm:i18n\"", + "start": "craco start", + "start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn start", + "build:production": "craco build", + "analyze": "source-map-explorer 'build/static/js/*.js' --no-border-checks --gzip", + "serve": "serve build -s -l 3000", + "lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .", + "typecheck": "tsc && yarn typecheck:cloud && yarn typecheck:cypress", + "typecheck:cloud": "tsc -p functions/tsconfig.json", + "typecheck:cypress": "tsc -p cypress/tsconfig.json", + "test": "craco test", + "test:cloud": "yarn jest functions --config=functions/jest.config.json", + "cypress:open": "cypress open --browser chrome --e2e", + "cypress:run": "cypress run --browser chrome --e2e", + "deduplicate": "yarn-deduplicate --strategy=highest" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "yarn.lock": [ + "yarn deduplicate" + ] + }, + "jest": { + "collectCoverageFrom": [ + "src/**/*.ts*", + "!src/**/*.d.ts", + "!src/abis/types/**", + "!src/constants/**/*.ts", + "!src/graphql/**/__generated__/**", + "!src/locales/**", + "!src/test-utils/**", + "!src/types/v3/**" + ], + "coveragePathIgnorePatterns": [ + ".snap" + ], + "coverageThreshold": { + "global": { + "branches": 4, + "functions": 6, + "lines": 9, + "statements": 9 + } + } + }, + "browserslist": { + "production": [ + ">0.5%", + "not dead" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@babel/preset-env": "7.23.3", + "@cloudflare/workers-types": "4.20231025.0", + "@craco/craco": "7.1.0", + "@ethersproject/experimental": "5.7.0", + "@lingui/cli": "4.5.0", + "@lingui/swc-plugin": "4.0.4", + "@swc/core": "1.3.72", + "@swc/jest": "0.2.29", + "@swc/plugin-styled-components": "1.5.97", + "@testing-library/jest-dom": "5.17.0", + "@testing-library/react": "13.4.0", + "@testing-library/user-event": "14.5.1", + "@typechain/ethers-v5": "7.2.0", + "@types/array.prototype.flat": "1.2.5", + "@types/array.prototype.flatmap": "1.2.6", + "@types/d3": "7.4.3", + "@types/jest": "29.5.0", + "@types/lingui__core": "2.7.1", + "@types/lingui__macro": "2.7.4", + "@types/lingui__react": "2.8.3", + "@types/ms": "0.7.31", + "@types/multicodec": "1.0.0", + "@types/node": "18.16.0", + "@types/qs": "6.9.2", + "@types/react": "^18.0.15", + "@types/react-dom": "^18.0.6", + "@types/react-redux": "7.1.30", + "@types/react-table": "7.7.12", + "@types/react-virtualized-auto-sizer": "1.0.0", + "@types/react-window": "1.8.2", + "@types/rebass": "4.0.7", + "@types/styled-components": "5.1.25", + "@types/testing-library__cypress": "5.0.13", + "@types/uuid": "9.0.1", + "@types/wcag-contrast": "3.0.0", + "@types/xml2js": "0.4.14", + "@uniswap/default-token-list": "11.11.0", + "@uniswap/eslint-config": "workspace:^", + "@vanilla-extract/jest-transform": "1.1.1", + "@vanilla-extract/webpack-plugin": "2.3.1", + "@vercel/og": "0.5.8", + "@walletconnect/types": "2.11.2", + "babel-jest": "29.6.1", + "browser-cache-mock": "0.1.7", + "concurrently": "^8.0.1", + "cypress": "12.12.0", + "cypress-hardhat": "2.5.0", + "dotenv": "16.0.3", + "eslint": "8.44.0", + "eslint-plugin-import": "2.27.5", + "eslint-plugin-rulesdir": "0.2.2", + "hardhat": "2.14.0", + "husky": "^8.0.3", + "jest": "29.7.0", + "jest-extended": "4.0.1", + "jest-fail-on-console": "3.1.1", + "jest-fetch-mock": "3.0.3", + "jest-styled-components": "7.2.0", + "jpeg-js": "0.4.4", + "lint-staged": "^14.0.0", + "mini-css-extract-plugin": "^2.7.6", + "path-browserify": "1.0.1", + "png-ts": "0.0.3", + "postinstall-postinstall": "2.1.0", + "prettier": "latest", + "process": "0.11.10", + "react-scripts": "5.0.1", + "resize-observer-polyfill": "1.5.1", + "serve": "^14.2.0", + "source-map-explorer": "2.5.3", + "start-server-and-test": "2.0.0", + "swc-loader": "^0.2.3", + "terser": "5.24.0", + "terser-webpack-plugin": "5.3.9", + "ts-jest": "^29.1.1", + "tsafe": "1.6.4", + "typescript": "5.3.3", + "webpack": "5.89.0", + "webpack-retry-chunk-load-plugin": "3.1.1", + "wrangler": "3.15.0", + "yarn-deduplicate": "6.0.0" + }, + "dependencies": { + "@apollo/client": "3.9.6", + "@coinbase/wallet-sdk": "3.7.2", + "@graphql-codegen/cli": "^3.3.1", + "@graphql-codegen/client-preset": "^3.0.1", + "@graphql-codegen/typescript": "^3.0.4", + "@graphql-codegen/typescript-operations": "^3.0.2", + "@graphql-codegen/typescript-react-apollo": "^3.3.7", + "@graphql-codegen/typescript-resolvers": "^3.2.1", + "@juggle/resize-observer": "3.4.0", + "@lingui/core": "4.5.0", + "@lingui/macro": "4.5.0", + "@lingui/react": "4.5.0", + "@looksrare/sdk": "0.10.4", + "@metamask/jazzicon": "2.0.0", + "@opensea/seaport-js": "1.3.0", + "@popperjs/core": "2.11.8", + "@reach/dialog": "0.10.5", + "@reach/portal": "0.10.5", + "@reduxjs/toolkit": "1.9.3", + "@rive-app/react-canvas": "4.6.2", + "@sentry/browser": "7.80.0", + "@sentry/core": "7.80.0", + "@sentry/react": "7.80.0", + "@sentry/types": "7.80.0", + "@tanstack/react-table": "8.10.7", + "@types/poisson-disk-sampling": "2.2.4", + "@types/react-scroll-sync": "0.8.7", + "@types/react-window-infinite-loader": "1.0.6", + "@uniswap/analytics": "1.7.0", + "@uniswap/analytics-events": "2.31.0", + "@uniswap/governance": "1.0.2", + "@uniswap/liquidity-staker": "1.0.2", + "@uniswap/merkle-distributor": "1.0.1", + "@uniswap/permit2-sdk": "1.2.0", + "@uniswap/redux-multicall": "1.1.8", + "@uniswap/router-sdk": "1.8.0", + "@uniswap/sdk-core": "4.1.2", + "@uniswap/smart-order-router": "3.17.3", + "@uniswap/token-lists": "1.0.0-beta.33", + "@uniswap/uniswapx-sdk": "1.4.1", + "@uniswap/universal-router-sdk": "1.7.1", + "@uniswap/v2-core": "1.0.1", + "@uniswap/v2-periphery": "1.1.0-beta.0", + "@uniswap/v2-sdk": "4.1.0", + "@uniswap/v3-core": "1.0.1", + "@uniswap/v3-periphery": "1.4.4", + "@uniswap/v3-sdk": "3.10.2", + "@vanilla-extract/css": "1.14.0", + "@vanilla-extract/dynamic": "2.1.0", + "@vanilla-extract/sprinkles": "1.6.1", + "@visx/group": "2.17.0", + "@visx/responsive": "2.17.0", + "@visx/shape": "2.18.0", + "@web3-react/coinbase-wallet": "8.2.3", + "@web3-react/core": "8.2.3", + "@web3-react/eip1193": "8.2.3", + "@web3-react/empty": "8.2.3", + "@web3-react/gnosis-safe": "8.2.4", + "@web3-react/metamask": "8.2.4", + "@web3-react/network": "8.2.3", + "@web3-react/types": "8.2.3", + "@web3-react/walletconnect-v2": "8.5.1", + "ajv": "8.11.0", + "ajv-formats": "2.1.1", + "array.prototype.flat": "1.3.2", + "array.prototype.flatmap": "1.3.2", + "buffer": "6.0.3", + "cids": "1.1.6", + "clsx": "1.2.1", + "copy-to-clipboard": "3.3.3", + "d3": "7.6.1", + "date-fns": "2.30.0", + "ethers": "5.7.2", + "fancy-canvas": "2.1.0", + "focus-visible": "5.2.0", + "framer-motion": "10.17.6", + "get-graphql-schema": "^2.1.2", + "graphql": "16.6.0", + "immer": "9.0.6", + "inter-ui": "3.19.3", + "jotai": "1.3.7", + "jsbi": "3.2.5", + "lightweight-charts": "4.1.1", + "localforage": "1.10.0", + "make-plural": "7.0.0", + "ms": "2.1.3", + "multicodec": "3.2.1", + "multihashes": "4.0.2", + "nock": "13.3.3", + "poisson-disk-sampling": "2.3.1", + "polished": "3.3.2", + "polyfill-object.fromentries": "1.0.1", + "qrcode.react": "3.1.0", + "qs": "6.9.4", + "query-string": "7.1.3", + "rc-slider": "10.4.0", + "react": "18.2.0", + "react-dom": "18.2.0", + "react-feather": "2.0.10", + "react-helmet-async": "2.0.4", + "react-infinite-scroll-component": "6.1.0", + "react-is": "18.2.0", + "react-markdown": "4.3.1", + "react-native-gesture-handler": "2.15.0", + "react-popper": "2.3.0", + "react-query": "3.39.1", + "react-redux": "8.0.5", + "react-router-dom": "6.10.0", + "react-scroll-sync": "0.11.2", + "react-spring": "9.7.3", + "react-table": "7.8.0", + "react-use-gesture": "6.0.14", + "react-virtualized-auto-sizer": "1.0.20", + "react-window": "1.8.9", + "react-window-infinite-loader": "1.0.9", + "rebass": "4.0.7", + "redux": "4.2.1", + "redux-persist": "6.0.0", + "statsig-react": "1.32.0", + "styled-components": "5.3.11", + "tiny-invariant": "1.3.1", + "uniswap": "workspace:^", + "use-resize-observer": "9.1.0", + "utilities": "workspace:^", + "uuid": "9.0.0", + "video-extensions": "1.2.0", + "wcag-contrast": "3.0.0", + "web-vitals": "2.1.4", + "workbox-core": "6.6.0", + "workbox-navigation-preload": "6.6.0", + "workbox-precaching": "6.6.0", + "workbox-routing": "6.6.0", + "xml2js": "0.6.2", + "zone.js": "0.12.0", + "zustand": "4.4.6" + }, + "engines": { + "npm": "please-use-yarn", + "node": "18.x", + "yarn": ">=1.22" + } +} diff --git a/apps/web/public/.well-known/assetlinks.json b/apps/web/public/.well-known/assetlinks.json new file mode 100644 index 0000000..dfcfe7c --- /dev/null +++ b/apps/web/public/.well-known/assetlinks.json @@ -0,0 +1,29 @@ +[ + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.uniswap.mobile", + "sha256_cert_fingerprints": + ["49:D9:3D:5D:FB:AA:64:A4:64:80:85:0F:39:A8:C1:D9:25:D3:D4:BC:8E:6B:1F:45:0C:EA:AF:B1:0C:27:DF:B8", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"] + } + }, + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.uniswap.mobile.beta", + "sha256_cert_fingerprints": + ["75:41:9C:2D:01:4A:88:4E:8D:C6:EF:E5:51:54:28:6B:99:05:31:43:AD:84:B4:EB:39:28:B8:C3:C4:CE:48:E3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"] + } + }, + { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.uniswap.mobile.dev", + "sha256_cert_fingerprints": + ["45:F8:15:02:C5:4F:AD:82:E7:51:F0:9C:D1:CA:77:C8:C9:BF:06:A6:D9:5A:55:4F:9E:B8:5F:81:33:2B:D0:DB", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"] + } + } +] \ No newline at end of file diff --git a/apps/web/public/451.html b/apps/web/public/451.html new file mode 100644 index 0000000..19918c3 --- /dev/null +++ b/apps/web/public/451.html @@ -0,0 +1,10 @@ + + + + + Unavailable For Legal Reasons + + +

Unavailable For Legal Reasons

+ + diff --git a/apps/web/public/app-sitemap.xml b/apps/web/public/app-sitemap.xml new file mode 100644 index 0000000..bc83f2f --- /dev/null +++ b/apps/web/public/app-sitemap.xml @@ -0,0 +1,129 @@ + + + + https://app.uniswap.org/ + 2023-10-11T19:57:27.976Z + weekly + 1.0 + + + https://app.uniswap.org/explore + 2024-03-08T18:44:55.418Z + weekly + 0.8 + + + https://app.uniswap.org/explore/tokens + 2024-03-08T18:44:55.418Z + weekly + 0.8 + + + https://app.uniswap.org/explore/pools + 2024-03-08T18:44:55.418Z + weekly + 0.8 + + + https://app.uniswap.org/explore/transactions + 2024-03-08T18:44:55.418Z + weekly + 0.8 + + + https://app.uniswap.org/send + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/limit + 2024-02-21T20:00:00.976Z + weekly + 0.6 + + + https://app.uniswap.org/limits + 2024-02-21T20:00:00.976Z + weekly + 0.6 + + + https://app.uniswap.org/swap + 2023-10-11T19:57:27.976Z + weekly + 0.9 + + + https://app.uniswap.org/pool/v2/find + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/pool/v2 + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/pool + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/pools/v2/find + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/pools/v2 + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/pools + 2023-10-11T19:57:27.976Z + weekly + 0.7 + + + https://app.uniswap.org/add/v2 + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/add + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/migrate/v2 + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/nfts + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/nfts/profile + 2023-10-11T19:57:27.976Z + weekly + 0.6 + + + https://app.uniswap.org/create-proposal + 2023-10-11T19:57:27.976Z + weekly + 0.5 + + diff --git a/apps/web/public/apple-app-site-association b/apps/web/public/apple-app-site-association new file mode 100644 index 0000000..9554d3e --- /dev/null +++ b/apps/web/public/apple-app-site-association @@ -0,0 +1,46 @@ +{ + "applinks": { + "details": [ + { + "appIDs": [ + "JH3UHGZD75.com.uniswap.mobile", + "JH3UHGZD75.com.uniswap.mobile.dev" + ], + "components": [ + { + "#": "/nfts/asset/*", + "comment": "NFT Item" + }, + { + "#": "/nfts/collection/*", + "comment": "NFT Collection" + }, + { + "#": "/tokens/*", + "comment": "Token address" + }, + { + "#": "/address/*", + "comment": "Wallet address" + }, + { + "/": "/nfts/asset/*", + "comment": "NFT Item" + }, + { + "/": "/nfts/collection/*", + "comment": "NFT Collection" + }, + { + "/": "/tokens/*", + "comment": "Token address" + }, + { + "/": "/address/*", + "comment": "Wallet address" + } + ] + } + ] + } +} diff --git a/apps/web/public/csp.json b/apps/web/public/csp.json new file mode 100644 index 0000000..3492364 --- /dev/null +++ b/apps/web/public/csp.json @@ -0,0 +1,95 @@ +{ + "defaultSrc": [ + "'self'" + ], + "scriptSrc": [ + "'self'", + "'unsafe-eval'", + "'unsafe-inline'", + "'wasm-unsafe-eval'", + "data:", + "https://translate.googleapis.com/", + "https://vercel.com", + "https://vercel.live/", + "https://www.google-analytics.com", + "https://www.googletagmanager.com" + ], + "styleSrc": [ + "'self'", + "'unsafe-inline'", + "'unsafe-eval'" + ], + "imgSrc": [ + "*", + "'self'", + "blob:", + "data:", + "https://assets.coingecko.com/", + "https://cdn.center.app/", + "https://ethereum-optimism.github.io/", + "https://explorer-api.walletconnect.com/", + "https://i.seadn.io/", + "https://lh3.googleusercontent.com/", + "https://openseauserdata.com/", + "https://raw.githubusercontent.com/", + "https://raw.seadn.io/", + "https://s2.coinmarketcap.com/", + "https://static.optimism.io/", + "https://vercel.com", + "https://vercel.live/", + "https://trustwallet.com/", + "https://cloudflare-ipfs.com/" + ], + "frameSrc": [ + "'self'", + "https://buy.moonpay.com/", + "https://vercel.com", + "https://vercel.live/", + "https://verify.walletconnect.com/", + "https://verify.walletconnect.org/" + ], + "connectSrc": [ + "*", + "'self'", + "blob:", + "data:", + "https://*.gateway.uniswap.org", + "https://gateway.uniswap.org", + "https://statsigapi.net", + "https://api.moonpay.com/", + "https://api.opensea.io", + "https://api.thegraph.com/", + "https://api.uniswap.org", + "https://arbitrum-mainnet.infura.io/", + "https://avalanche-mainnet.infura.io/", + "https://base-mainnet.infura.io/", + "https://bridge.arbitrum.io", + "https://celo-org.github.io", + "https://cloudflare-ipfs.com", + "https://explorer-api.walletconnect.com", + "https://forno.celo.org/", + "https://gateway.ipfs.io/", + "https://interface.gateway.uniswap.org", + "https://mainnet.infura.io", + "https://o1037921.ingest.sentry.io", + "https://old-wispy-arrow.bsc.quiknode.pro/", + "https://optimism-mainnet.infura.io/", + "https://polygon-mainnet.infura.io/", + "https://raw.githubusercontent.com", + "https://static.optimism.io", + "https://temp.api.uniswap.org/", + "https://tokenlist.arbitrum.io", + "https://tokens.coingecko.com", + "https://ultra-blue-flower.quiknode.pro", + "https://us-central1-uniswap-mobile.cloudfunctions.net/", + "https://vercel.com", + "https://vercel.live/", + "https://www.gemini.com", + "wss://relay.walletconnect.com/", + "wss://www.walletlink.org/rpc" + ], + "workerSrc": [ + "'self'", + "blob:" + ] +} diff --git a/apps/web/public/favicon.png b/apps/web/public/favicon.png new file mode 100644 index 0000000..47c6e1f Binary files /dev/null and b/apps/web/public/favicon.png differ diff --git a/apps/web/public/fonts/Basel-Grotesk-Book.woff b/apps/web/public/fonts/Basel-Grotesk-Book.woff new file mode 100644 index 0000000..3f5a304 Binary files /dev/null and b/apps/web/public/fonts/Basel-Grotesk-Book.woff differ diff --git a/apps/web/public/fonts/Basel-Grotesk-Book.woff2 b/apps/web/public/fonts/Basel-Grotesk-Book.woff2 new file mode 100644 index 0000000..31d5903 Binary files /dev/null and b/apps/web/public/fonts/Basel-Grotesk-Book.woff2 differ diff --git a/apps/web/public/fonts/Basel-Grotesk-Medium.woff b/apps/web/public/fonts/Basel-Grotesk-Medium.woff new file mode 100644 index 0000000..11147d0 Binary files /dev/null and b/apps/web/public/fonts/Basel-Grotesk-Medium.woff differ diff --git a/apps/web/public/fonts/Basel-Grotesk-Medium.woff2 b/apps/web/public/fonts/Basel-Grotesk-Medium.woff2 new file mode 100644 index 0000000..b79bb72 Binary files /dev/null and b/apps/web/public/fonts/Basel-Grotesk-Medium.woff2 differ diff --git a/apps/web/public/fonts/Inter-normal.var.ttf b/apps/web/public/fonts/Inter-normal.var.ttf new file mode 100644 index 0000000..600b384 Binary files /dev/null and b/apps/web/public/fonts/Inter-normal.var.ttf differ diff --git a/apps/web/public/images/1200x630_Rich_Link_Preview_Image.png b/apps/web/public/images/1200x630_Rich_Link_Preview_Image.png new file mode 100644 index 0000000..12328b3 Binary files /dev/null and b/apps/web/public/images/1200x630_Rich_Link_Preview_Image.png differ diff --git a/apps/web/public/images/192x192_App_Icon.png b/apps/web/public/images/192x192_App_Icon.png new file mode 100644 index 0000000..75eaaf8 Binary files /dev/null and b/apps/web/public/images/192x192_App_Icon.png differ diff --git a/apps/web/public/images/256x256_App_Icon_Pink.svg b/apps/web/public/images/256x256_App_Icon_Pink.svg new file mode 100644 index 0000000..8c82868 --- /dev/null +++ b/apps/web/public/images/256x256_App_Icon_Pink.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/public/images/324x74_App_Watermark.png b/apps/web/public/images/324x74_App_Watermark.png new file mode 100644 index 0000000..cda3444 Binary files /dev/null and b/apps/web/public/images/324x74_App_Watermark.png differ diff --git a/apps/web/public/images/512x512_App_Icon.png b/apps/web/public/images/512x512_App_Icon.png new file mode 100644 index 0000000..f24aada Binary files /dev/null and b/apps/web/public/images/512x512_App_Icon.png differ diff --git a/apps/web/public/images/54x54_Verified_Check.svg b/apps/web/public/images/54x54_Verified_Check.svg new file mode 100644 index 0000000..edf59dc --- /dev/null +++ b/apps/web/public/images/54x54_Verified_Check.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/images/logos/Arbitrum_Logo.png b/apps/web/public/images/logos/Arbitrum_Logo.png new file mode 100644 index 0000000..eebe97c Binary files /dev/null and b/apps/web/public/images/logos/Arbitrum_Logo.png differ diff --git a/apps/web/public/images/logos/Celo_Logo.png b/apps/web/public/images/logos/Celo_Logo.png new file mode 100644 index 0000000..8639b11 Binary files /dev/null and b/apps/web/public/images/logos/Celo_Logo.png differ diff --git a/apps/web/public/images/logos/Optimism_Logo.png b/apps/web/public/images/logos/Optimism_Logo.png new file mode 100644 index 0000000..fe22521 Binary files /dev/null and b/apps/web/public/images/logos/Optimism_Logo.png differ diff --git a/apps/web/public/images/logos/Polygon_Logo.png b/apps/web/public/images/logos/Polygon_Logo.png new file mode 100644 index 0000000..4b86751 Binary files /dev/null and b/apps/web/public/images/logos/Polygon_Logo.png differ diff --git a/apps/web/public/images/noise-color.png b/apps/web/public/images/noise-color.png new file mode 100644 index 0000000..84f1dba Binary files /dev/null and b/apps/web/public/images/noise-color.png differ diff --git a/apps/web/public/images/noise-light.png b/apps/web/public/images/noise-light.png new file mode 100644 index 0000000..395991c Binary files /dev/null and b/apps/web/public/images/noise-light.png differ diff --git a/apps/web/public/index.html b/apps/web/public/index.html new file mode 100644 index 0000000..a90c0f6 --- /dev/null +++ b/apps/web/public/index.html @@ -0,0 +1,143 @@ + + + + + + Uniswap Interface + + + + + + + + + <% const cspConfig = require('./csp.json'); %> + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + diff --git a/apps/web/public/manifest.json b/apps/web/public/manifest.json new file mode 100644 index 0000000..64e0ec0 --- /dev/null +++ b/apps/web/public/manifest.json @@ -0,0 +1,30 @@ +{ + "background_color": "#fff", + "display": "standalone", + "homepage_url": "https://app.uniswap.org", + "providedBy": { + "name": "Uniswap", + "url": "https://uniswap.org" + }, + "icons": [ + { + "src": "./images/192x192_App_Icon.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "./images/512x512_App_Icon.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "orientation": "portrait", + "name": "Uniswap", + "description": "Swap or provide liquidity on the Uniswap Protocol", + "iconPath": "./images/256x256_App_Icon_Pink.svg", + "short_name": "Uniswap", + "start_url": ".", + "theme_color": "#FC72FF" +} \ No newline at end of file diff --git a/apps/web/public/nft/svgs/gem.svg b/apps/web/public/nft/svgs/gem.svg new file mode 100644 index 0000000..e61ab4d --- /dev/null +++ b/apps/web/public/nft/svgs/gem.svg @@ -0,0 +1 @@ +Design diff --git a/apps/web/public/nft/svgs/marketplaces/looksrare-grey.svg b/apps/web/public/nft/svgs/marketplaces/looksrare-grey.svg new file mode 100644 index 0000000..026109b --- /dev/null +++ b/apps/web/public/nft/svgs/marketplaces/looksrare-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/nft/svgs/marketplaces/opensea-grey.svg b/apps/web/public/nft/svgs/marketplaces/opensea-grey.svg new file mode 100644 index 0000000..62b3eb6 --- /dev/null +++ b/apps/web/public/nft/svgs/marketplaces/opensea-grey.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/public/nft/svgs/marketplaces/x2y2-grey.svg b/apps/web/public/nft/svgs/marketplaces/x2y2-grey.svg new file mode 100644 index 0000000..93a7023 --- /dev/null +++ b/apps/web/public/nft/svgs/marketplaces/x2y2-grey.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/nfts-sitemap.xml b/apps/web/public/nfts-sitemap.xml new file mode 100644 index 0000000..5e72adb --- /dev/null +++ b/apps/web/public/nfts-sitemap.xml @@ -0,0 +1,633 @@ + + + + https://app.uniswap.org/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x60e4d786628fea6478f785a6d7e704777c86a7c6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x34d85c9cdeb23fa97cb08333b511ac86e1c4e258 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x99a9b7c1116f9ceeb1652de04d5969cce509b069 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb7f7f6c52f2e2fdb1963eab30438024864c313f6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x23581767a106ae21c074b2276d25e5c3e136a68b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x8a90cab2b38dba80c64b7734e58ee1db38b8992e + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xba30e5f9bb24caa003e9f2f0497ad287fdf95623 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xbd3531da5cf5857e7cfaa92426877b022e612cf8 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7bd29408f11d2bfc23c34f18275bbf23bb716bc7 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x306b1ea3ecdf94ab739f1910bbda052ed4a9f949 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x1a92f7381b9f03921564a437210bb9396471050c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x5cc5b05a8a13e3fbdb0bb9fccd98d38e50f90c38 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x5af0d9827e0c53e4799bb226655a1de152a425a5 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x3bf2922f4520a8ba0c2efc3d2a1539678dad5e9d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xe785e82358879f061bc3dcac6f0444462d4b5330 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x76be3b62873462d2142405439777e971754e8e77 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xfd43af6d3fe1b916c026f6ac35b3ede068d1ca01 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x1cb1a5e65610aeff2551a50f76a87a7d3fb649c6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xff9c1b15b16263c61d017ee9f65c50e4ae0113d7 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x6339e5e072086621540d0362c4e3cea0d643e114 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb932a70a57673d89f4acffbe830e8ed7f75fb9e0 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x79fcdef22feed20eddacbb2587640e45491b757f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xa3aee8bce55beea1951ef834b99f3ac60d1abeeb + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x769272677fab02575e84945f03eca517acc544cc + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4db1f25d3d98600140dfc18deb7515be5bd293af + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x34eebee6942d8def3c125458d1a86e0a897fd6f9 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x59468516a8259058bad1ca5f8f4bff190d30e066 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x394e3d3044fc89fcdd966d3cb35ac0b32b0cda91 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x60bb1e2aa1c9acafb4d34f71585d7e959f387769 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x28472a58a490c5e09a238847f66a68a47cc76f0f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x341a1c534248966c4b6afad165b98daed4b964ef + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x82c7a8f707110f5fbb16184a5933e9f78a34c6ab + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xccc441ac31f02cd96c153db6fd5fe0a2f4e6a68d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x764aeebcf425d56800ef2c84f2578689415a2daa + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x160c404b2b49cbc3240055ceaee026df1e8497a0 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xd2f668a8461d6761115daf8aeb3cdf5f40c532c6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x39ee2c7b3cb80254225884ca001f57118c8f21b6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xd774557b647330c91bf44cfeab205095f7e6c367 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x1792a96e5668ad7c167ab804a100ce42395ce54d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xf87e31492faf9a91b02ee0deaad50d51d56d5d4d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x04afa589e2b933f9463c5639f412b183ec062505 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xe75512aa3bec8f00434bbd6ad8b0a3fbff100ad6 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x348fc118bcc65a92dc033a951af153d14d945312 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x892848074ddea461a15f337250da3ce55580ca85 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x5946aeaab44e65eb370ffaa6a7ef2218cff9b47d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x282bdd42f4eb70e7a9d9f40c8fea0825b7f68c5d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4b15a9c28034dc83db40cd810001427d3bd7163d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7ea3cca10668b8346aec0bf1844a49e995527c8b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb852c6b5892256c264cc2c888ea462189154d8d7 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x9378368ba6b85c1fba5b131b530f5f5bedf21a18 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x2acab3dea77832c09420663b0e1cb386031ba17b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x0c2e57efddba8c768147d1fdf9176a0a6ebd5d83 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x08d7c0242953446436f34b4c78fe9da38c73668d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x8943c7bac1914c9a7aba750bf2b6b09fd21037e0 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x364c828ee171616a39897688a831c2499ad972ec + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7f36182dee28c45de6072a34d29855bae76dbe2f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xf61f24c2d93bf2de187546b14425bf631f28d6dc + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x797a48c46be32aafcedcfd3d8992493d8a1f256b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x123b30e25973fecd8354dd5f41cc45a3065ef88c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x6632a9d63e142f17a668064d41a21193b49b41a0 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xf4ee95274741437636e748ddac70818b4ed7d043 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x57a204aa1042f6e66dd7730813f4024114d74f37 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xd1258db6ac08eb0e625b75b371c023da478e94a9 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x75e95ba5997eb235f40ecf8347cdb11f18ff640b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xd532b88607b1877fe20c181cba2550e3bbd6b31c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xa1d4657e0e6507d5a94d06da93e94dc7c8c44b51 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xedb61f74b0d09b2558f1eeb79b247c1f363ae452 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7d8820fa92eb1584636f4f5b8515b5476b75171a + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x231d3559aa848bf10366fb9868590f01d34bf240 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xad9fd7cb4fc7a0fbce08d64068f60cbde22ed34c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x0e9d6552b85be180d941f1ca73ae3e318d2d4f1f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb716600ed99b4710152582a124c697a7fe78adbf + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xaadc2d4261199ce24a4b0a57370c4fcf43bb60aa + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4e1f41613c9084fdb9e34e11fae9412427480e56 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x79986af15539de2db9a5086382daeda917a9cf0c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc99c679c50033bbc5321eb88752e89a93e9e83c5 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc36cf0cfcb5d905b8b513860db0cfe63f6cf9f5c + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x9c8ff314c9bc7f6e59a9d9225fb22946427edc03 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x3110ef5f612208724ca51f5761a69081809f03b7 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x036721e5a769cc48b3189efbb9cce4471e8a48b1 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x524cab2ec69124574082676e6f654a18df49a048 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7ab2352b1d2e185560494d5e577f9d3c238b78c5 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x32973908faee0bf825a343000fe412ebe56f802a + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x7daec605e9e2a1717326eedfd660601e2753a057 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc1caf0c19a8ac28c41fe59ba6c754e4b9bd54de9 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x33fd426905f149f8376e227d0c9d3340aad17af1 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x466cfcd0525189b573e794f554b8a751279213ac + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x6be69b2a9b153737887cfcdca7781ed1511c7e36 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x80336ad7a747236ef41f47ed2c7641828a480baa + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x9401518f4ebba857baa879d9f76e1cc8b31ed197 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4b61413d4392c806e6d0ff5ee91e6073c21d6430 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc3f733ca98e0dad0386979eb96fb1722a1a05e69 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x09233d553058c2f42ba751c87816a8e9fae7ef10 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x960b7a6bcd451c9968473f7bbfd9be826efd549a + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x36d30b3b85255473d27dd0f7fd8f35e36a9d6f06 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x698fbaaca64944376e2cdc4cad86eaa91362cf54 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x497a9a79e82e6fc0ff10a16f6f75e6fcd5ae65a8 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x41a322b28d0ff354040e2cbc676f0320d8c8850d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xa9c0a07a7cb84ad1f2ffab06de3e55aab7d523e8 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x942bc2d3e7a589fe5bd4a5c6ef9727dfd82f5c8a + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x8821bee2ba0df28761afff119d66390d594cd280 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x8c6def540b83471664edc6d5cf75883986932674 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x8d9710f0e193d3f95c0723eaaf1a81030dc9116d + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x86825dfca7a6224cfbd2da48e85df2fc3aa7c4b1 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x629a673a8242c2ac4b7b8c5d8735fbeac21a6205 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x9a534628b4062e123ce7ee2222ec20b86e16ca8f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc2c747e0f7004f9e8817db2ca4997657a7746928 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x73da73ef3a6982109c4d5bdb0db9dd3e3783f313 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xc92ceddfb8dd984a89fb494c376f9a48b999aafc + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x3248e8ba90facc4fdd3814518c14f8cc4d980e4b + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x67d9417c9c3c250f61a83c7e8658dac487b56b09 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb6a37b5d14d502c3ab0ae6f3a0e058bc9517786e + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x86c10d10eca1fca9daf87a279abccabe0063f247 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4b3406a41399c7fd2ba65cbc93697ad9e7ea61e5 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xb0640e8b5f24bedc63c33d371923d68fde020303 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xd3d9ddd0cf0a5f0bfb8f7fceae075df687eaebab + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xa5c0bd78d1667c13bfb403e2a3336871396713c5 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x4d7d2e237d64d1484660b55c0a4cc092fa5e6716 + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0xfcb1315c4273954f74cb16d5b663dbf479eec62e + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x66d1db16101502ed0ca428842c619ca7b62c8fef + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x128675d4fddbc4a0d3f8aa777d8ee0fb8b427c2f + 2024-03-08T18:44:55.418Z + 0.7 + + + https://app.uniswap.org/nfts/collection/0x19b86299c21505cdf59ce63740b240a9c822b5e4 + 2024-03-08T18:44:55.418Z + 0.7 + + \ No newline at end of file diff --git a/apps/web/public/pools-sitemap.xml b/apps/web/public/pools-sitemap.xml new file mode 100644 index 0000000..2989ee4 --- /dev/null +++ b/apps/web/public/pools-sitemap.xml @@ -0,0 +1,3543 @@ + + + + https://app.uniswap.org/explore/pools/ethereum/0xcbcdf9626bc03e24f779434178a73a0b4bad62ed + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4e68ccd3e89f51c3074ca5072bbac773960dfa36 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4585fe77225b41b697c938b018e2ac67ac5a20c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc63b0708e2f7e69cb8a1df0e1389a98c35a76d52 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x99ac8ca7087fa4a2a1fb6357269965a2014abc35 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x11b815efb8f581194ae79006d24e0d814b7697f6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa6cc3c2531fdaa6ae1a3ca84c2855806728693e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x5777d92f208679db4b9778590fa3cab3ac9e2168 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc2e9f25be6257c210d7adf0d4cd6e3e881ba25f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x11950d141ecb863f01007add7d1a342041227b58 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc5c134a1f112efa96003f8559dba6fac0ba77692 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x109830a1aaad605bbf02a9dfa7b0b92ec2fb7daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x1df4c6e36d61416813b42fe32724ef11e363eddc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x12d6867fa648d269835cf69b49f125147754b54d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x3416cf6c708da44db2624d63ea0aaef7113527c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe8c6c9227491c0a8156a0106a0204d881bb7e531 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x04708077eca6bb527a5bbbd6358ffb043a9c1c14 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9db9e0e53058c89e5b94e29621a205198648425b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xf239009a101b6b930a527deaab6961b6e7dec8a6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xfe0df74636bc25c7f2400f22fe7dae32d39443d2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xf4c5e0f4590b6679b3030d29a84857f226087fef + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x5764a6f2212d502bc5970f9f129ffcd61e5d7563 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa3f558aebaecaf0e11ca4b2199cc5ed341edfd74 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x99132b53ab44694eeb372e87bced3929e4ab8456 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x6c6bc977e13df9b0de53b251522280bb72383700 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9d96880952b4c80a55099b9c258250f2cc5813ec + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x3afdc5e6dfc0b0a507a8e023c9dce2cafc310316 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x290a6a7460b308ee3f19023d2d00de604bcf5b42 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xac4b3dacb91461209ae9d41ec517c2b9cb1b7daf + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x60594a405d53811d3bc4766596efd80fd545a270 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x331399c614ca67dee86733e5a2fba40dbb16827c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4b5ab61593a2401b1075b90c04cbcdd3f87ce011 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x844eb5c280f38c7462316aad3f338ef9bda62668 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe936f0073549ad8b1fa53583600d629ba9375161 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x2f62f2b4c5fcd7570a709dec05d68ea19c82a9ec + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x381fe4eb128db1621647ca00965da3f9e09f4fac + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x97e7d56a0408570ba1a7852de36350f7713906ec + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xcd423f3ab39a11ff1d9208b7d37df56e902c932b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe15e6583425700993bd08f51bf6e7b73cd5da91b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x69d91b94f0aaf8e8a2586909fa77a5c2c89818d5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe42318ea3b998e8355a3da364eb9d48ec725eb45 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xad9ef19e289dcbc9ab27b83d2df53cdeff60f02d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x3b685307c8611afb2a9e83ebc8743dc20480716e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7bea39867e4169dbe237d55c8242a8f2fcdcc387 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7b1e5d984a43ee732de195628d20d05cfabc3cc7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7858e59e0c01ea06df3af3d20ac7b0003275d4bf + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xae2a25cbdb19d0dc0dddd1d2f6b08a6e48c4a9a9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x14af1804dbbf7d621ecc2901eef292a24a0260ea + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x80a9ae39310abf666a87c743d6ebbd0e8c42158e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc31e54c7a869b9fcbecc14363cf510d1c41fa443 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2f5e87c9312fa29aed5c179e456625d79015299c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc6962004f452be9203591991d15f6b388e09e8d0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc6f780497a95e246eb9449f5e4770916dcd6396a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x641c00a822e8b671738d32a431a4fb6074e5c79d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x92c63d0e701caae670c9415d91c474f686298f00 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x1aeedd3727a6431b8f070c0afaa81cc74f273882 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xcda53b1f66614552f834ceef361a8d12a0b8dad8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x35218a1cbac5bbc3e57fd9bd38219d37571b3537 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x17c14d2c404d167802b16c450d3c99f88f2c4f4d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x468b88941e7cc0b88c1869d68ab6b570bcef62ff + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xdbaeb7f0dfe3a0aafd798ccecb5b22e708f7852c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x149e36e72726e0bcea5c59d40df2c43f60f5a22d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xbaaf1fc002e31cb12b99e4119e5e350911ec575b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa67f72f21bd9f91db2da2d260590da5e6c437009 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x92fd143a8fa0c84e016c2765648b9733b0aa519e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x7cf803e8d82a50504180f417b8bc7a493c0a0503 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x81c48d31365e6b526f6bbadc5c9aafd822134863 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x446bf9748b4ea044dd759d9b9311c70491df8f29 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc82819f72a9e77e2c0c3a69b3196478f44303cf4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x50c7390dfdd3756139e6efb5a461c2eb7331ceb4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x1dfc1054e0e2a10e33c9ca21aad5aa8a1cce91e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc91b7b39bbb2c733f0e7459348fd0c80259c8471 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x59d72ddb29da32847a4665d08ffc8464a7185fae + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x09ba302a3f5ad2bf8853266e271b005a5b3716fe + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa77d77c9773c35e910acc2e30cefe52b54a58414 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8da66e470403b3d3eee66c67e2c61fda6e248ad1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2f020e708811c054f146eebcc4d5a215fd4eec26 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x7e7fb3cceca5f2ac952edf221fd2a9f62e411980 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x68c685fd52a56f04665b491d491355a624540e85 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa8328bf492ba1b77ad6381b3f7567d942b000baf + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc0cf0f380ddb44dbcaf19a86d094c8bba3efa04a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa169d1ab5c948555954d38700a6cdaa7a4e0c3a0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x1862200e8e7ce1c0827b792d0f9546156f44f892 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x05bbaaa020ff6bea107a9a1e06d2feb7bfd79ed2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xd02a4969dc12bb889754361f8bcf3385ac1b2077 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc24f7d8e51a64dc1238880bd00bb961d54cbeb29 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x7c06736e41236fecd681dd3353aa77ecd19ea565 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc473e2aee3441bf9240be85eb122abb059a3b57c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x14353445c8329df76e6f15e9ead18fa2d45a8bb6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2039f8c9cd32ba9cd2ea7e575d5b1abea93f7527 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xd3e11119d2680c963f1cdcffece0c4ade823fb58 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8e295789c9465487074a65b1ae9ce0351172393f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x97bca422ec0ee4851f2110ea743c1cd0a14835a1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xbe3ad6a5669dc0b8b12febc03608860c31e2eef6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x56ebd63a756b94d3de9cea194896b4920b64fb01 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xe2ddd33585b441b9245085588169f35108f85a6e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x84436a2af97f37018db116ae8e1b691666db3d00 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x68f5c0a2de713a54991e01858fd27a3832401849 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x4533bad2dc588f0fadf8d2e72386d4cd6a19b519 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x85149247691df622eaf1a8bd0cafd40bc45154a9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0392b358ce4547601befa962680bede836606ae2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x1c3140ab59d6caf9fa7459c6f83d4b52ba881d36 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd1f1bad4c9e6c44dec1e9bf3b94902205c5cd6c3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x03af20bdaaffb4cc0a521796a223f7d85e2aac31 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x73b14a78a0d396c521f954532d43fd5ffe385216 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xac85eaf55e9c60ed40a683de7e549d23fdfbeb33 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x04f6c85a1b00f6d9b75f91fd23835974cc07e65c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x730691cdac3cbd4d41fc5eb9d8abbb0cea795b94 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x535541f1aa08416e69dc4d610131099fa2ae7222 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xfc1f3296458f9b2a27a0b91dd7681c4020e09d05 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x85c31ffa3706d1cce9d525a00f1c7d4a2911754c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd52533a3309b393afebe3176620e8ccfb6159f8a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xff7fbdf7832ae524deda39ca402e03d92adff7a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb589969d38ce76d3d7aa319de7133bc9755fd840 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xf334f6104a179207ddacfb41fa3567feea8595c2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x1fb3cf6e48f1e7b10213e7b6d87d4c073c7fdb7b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd4344ea0c5ade7e22b9b275f0bde7a145dec5a23 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x5b42a63d6741416ce9a7b9f4f16d8c9231ccddd4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x252cbdff917169775be2b552ec9f6781af95e7f6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2ab22ac86b25bd448a4d9dc041bd2384655299c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc858a329bf053be78d6239c4a4343b8fbd21472b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa73c628eaf6e283e26a7b1f8001cf186aa4c0e8e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb533c12fb4e7b53b5524eab9b47d93ff6c7a456f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2ae3d6096d8215ac2acddf30c60caa984ea5debe + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x19ea026886cbb7a900ecb2458636d72b5cae223b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x6f32061f59a21086c334d0d45f804089ce374aaf + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xfaf037caafa9620bfaebc04c298bf4a104963613 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xadb35413ec50e0afe41039eac8b930d313e94fa4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe9e3893921de87b1194a8108f9d70c24bde71c27 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xf1f199342687a7d78bcc16fce79fa2665ef870e1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xf44acaa38be5e965c5ddf374e7a2ba270e580684 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x36e42931a765022790b797963e42c5522d6b585a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x5adba6c5589c50791dd65131df29677595c7efa7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x3249e3e3e4133ee18e65347daf586610cc265f54 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xca1b837c87c6563910c2befa48834fa2a8c3d72d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x6ef7b14bcd8d989cef8f8ec8ba4bf371b2ac95fd + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x37ffd11972128fd624337ebceb167c8c0a5115ff + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe62bd99a9501ca33d98913105fc2bec5bae6e5dd + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb2ac2e5a3684411254d58b1c5a542212b782114d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb0efaf46a1de55c54f333f93b1f0641e73bc16d0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd0fa3b5264ccde31e8b094b86bca4a1e97d3c603 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xad4c666fc170b468b19988959eb931a3676f0e9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x790fde1fd6d2568050061a88c375d5c2e06b140b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xaefc1edaede6adadcdf3bb344577d45a80b19582 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa8a5356ee5d02fe33d72355e4f698782f8f199e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x55bc964fe3b0c8cc2d4c63d65f1be7aef9bb1a3c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x95d9d28606ee55de7667f0f176ebfc3215cfd9c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x45dda9cb7c25131df268515131f647d726f50608 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x50eaedb835021e4a108b7290636d62e9765cc6d7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x167384319b41f7094e62f7506409eb38079abff8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa374094527e1673a86de625aa59517c5de346d32 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x86f1d8390222a3691c28938ec7404a1661e618e0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xeda1094f59a4781456734e5d258b95e6be20b983 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x847b64f9d3a95e977d157866447a5c0a5dfa0ee5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x94ab9e4553ffb839431e37cc79ba8905f45bfbea + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x0e44ceb592acfc5d3f09d996302eb4c499ff8c10 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x1e5bd2ab4c308396c06c182e1b7e7ba8b2935b83 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9b08288c3be4f62bbf8d1c20ac9c5e6f9467d8b7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb6e57ed85c4c9dbfef2a68711e9d6f36c56e0fcb + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x3e31ab7f37c048fc6574189135d108df80f0ea26 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xd36ec33c8bed5a9f7b6630855f1533455b98a418 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xdac8a8e6dbf8c690ec6815e0ff03491b2770255d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xfe343675878100b344802a6763fd373fdeed07a4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x0a28c2f5e0e8463e047c203f00f649812ae67e4f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x88f3c15523544835ff6c738ddb30995339ad57d6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x98b9162161164de1ed182a0dfa08f5fbf0f733ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xeef1a9507b3d505f0062f2be9453981255b503c8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc4c06c9a239f94fc0a1d3e04d23c159ebe8316f1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x849ec65748107aedc518dbc42961f358ea1361a7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2db87c4831b2fec2e35591221455834193b50d1b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa4d8c89f0c20efbe54cba9e7e7a7e509056228d9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x642f28a89fa9d0fa30e664f71804bfdd7341d21f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2aceda63b5e958c45bd27d916ba701bc1dc08f7a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x781067ef296e5c4a4203f81c593274824b7c185d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x4ccd010148379ea531d6c587cfdd60180196f9b1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xd866fac7db79994d08c0ca2221fee08935595b4b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x941061770214613ba0ca3db9a700c39587bb89b6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa9077cdb3d13f45b8b9d87c43e11bce0e73d8631 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa01f64fa1b923dd9c5c7618b39a6ba8098a88863 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa830ff28bb7a46570a7e43dc24a35a663b9cfc2e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x8837a61644d523cbe5216dde226f8f85e3aa9be3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xca5d44977d6de1846530eb434167b208752fba7d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x4d05f2a005e6f36633778416764e82d1d12e7fbb + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x41e64a5bc929fa8e6a9c8d7e3b81a13b21ff3045 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x3ea34cfc9322273311f7843826a2581c4a00fd39 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x785061ed819414dc4269d2a5d5974069c0daea96 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x3f5228d0e7d75467366be7de2c31d0d098ba2c23 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2e3f22e9a1c2470b2e293351f48c99e1fd788f32 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2a08c38c7e1fa969325e2b64047abb085dec3756 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe6c36eed27c2e8ecb9a233bf12da06c9730b5955 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xefa98fdf168f372e5e9e9b910fcdfd65856f3986 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x76fa081e510f43ac8335efdb4db88c9ff1894413 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc6832ef0af793336aa44a936e54b992bff47e7cd + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x865f456479a21e2b3d866561d7171a3d0a7b112d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xbd934a7778771a7e2d9bf80596002a214d8c9304 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9ab9f658104467604b5afa9a3e1df62f35f7b208 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x6e430d59ba145c59b73a6db674fe3d53c1f31cae + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9e37cb775a047ae99fc5a24dded834127c4180cd + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x48413707b70355597404018e7c603b261fcadf3f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xade9bcd4b968ee26bed102dd43a55f6a8c2416df + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xda679706ff21114ac9fac5198bff24543f357a16 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xba3f945812a83471d709bce9c3ca699a19fb46f7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc9034c3e7f58003e6ae0c8438e7c8f4598d5acaa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4c36388be6f416a29c8d8eee81c771ce6be14b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa1b2457c0b627f97f6cc892946a382451e979014 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4b0aaf3ebb163dd45f663b38b6d93f6093ebc2d3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xae2ce200bdb67c472030b31f602f0756c9aeb61c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3bc5180d5439b500f381f9a46f15dd6608101671 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x5122e02898ece3bc62df8c1efdb29a9e914244d3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x24e1cbd6fed006ceed9af0dce688acc7951d57a9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2556230ac694093d4d3b7b965a2f2d77d4c403a4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xdaca082c2c7d052a96fa83ea9d3a7b6839e39586 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa555149210075702a734968f338d5e1cbd509354 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x10648ba41b8565907cfa1496765fa4d95390aa0d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x00bcec1526dae1e170a53017b8775a93b7810d7c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x20e068d76f9e90b90604500b84c7e19dcb923e7e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x6b93950a9b589bc32b82a5df4e5148f98a7fae27 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd9caa6dbe6791fcb7fc9fb59d1a6b3dd8c1c2339 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x62e81e93136ac42a1ada48d4098f5f9e703e7455 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x84206d33845c9d811438b6fe4e7a0c634748dc50 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd0b53d9277642d899df5c87a3966a349a798f224 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xcfa7c4bb565915f1c4f9475e2a0536d31efad776 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa7de21f28ca460b45373b217cd4eb111c3faeff8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xb64dff20dd5c47e6dbb56ead80d23568006dec1e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xad4e969f4193878e5cc89cefb57faf6c7c0048da + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xdf5eb97e3e23ca7f5a5fd2264680377c211310ba + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xf16baaae8eb7b37f4280e72924479f69e7a61f32 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe745a591970e0fa981204cf525e170a2b9e4fb93 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x64b74c66b9ba60ca668b781289767ae7298f37ae + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x17e1ebd791e7253a5e606fd94c5b66c14d873136 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x46715bd57b9ec01deadb35fe096fb44acda79414 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3447accd4b8e735329d1065244aad2ed630f0122 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2feb7f3ffc243f7de94d5ea5975533d301584e07 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0d5959a52e7004b601f0be70618d01ac3cdce976 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2170ca774e48a3f51559917ada6f9d7ae8f7bfea + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x62a76dfa8951aefcff787e790782db3633ebf422 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x8073679e0b3b2d1d665777cf1b2b5b1c2d3d2d0c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x143f1a6f3fb32e6ab3f22d3cc6b417b5c2197599 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x82ad659c2f152aad59bb37cbc5e7663a2de0c607 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa4efe9e8e2a2d5a2ac46805f233b8e49d0e11955 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xfcc89a1f250d76de198767d33e1ca9138a7fb54b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2faa2b42b782d578a160f61bb7cd763a17476730 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xdd44c0e83c2570062d1e6fdd440b4724862e8f31 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe3930a14641786e123e7bbe842d701fa1cbfe2df + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x6d03360ce4764e862ed81660c1f76cc2711b14b6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc055f66f228105072315247785c00299d0ce27e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xcae1d141ab11cef0a415cf0440025e1e5e962e06 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0f338ec12d3f7c3d77a4b9fcc1f95f3fb6ad0ea6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4eaa90264d6a3567228dcb5cfc242200da586437 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x6fe9e9de56356f7edbfcbb29fab7cd69471a4869 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xf420603317a0996a3fce1b1a80993eaef6f7ae1a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x47a90a2d92a8367a91efa1906bfc8c1e05bf10c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x41bf5eeae051fbd2e97b76b5f8f0fdcc1a1e526b + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x28df0835942396b7a1b7ae1cd068728e6ddbbafd + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa3f3664a52f01b42557524bd14556e379daf5669 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x1fd22fa7274bafebdfb1881321709f1219744829 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xe39cfc1a2e51a09ecbd060a24ee4eef5a97697bb + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x06396509195eb9e07c38a016694dc9ff535b128a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5a1c486edefda2f09d3b349fadc38524f1743826 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5bf1cf153c102a79d9e18b7fb7c79ba57fa70d0c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2c3c320d49019d4f9a92352e947c7e5acfe47d68 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4141325bac36affe9db165e854982230a14e6d48 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x17507bef4c3abc1bc715be723ee1baf571256e05 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x8149b92ea743cc382aada523b68b8834733b9015 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc98f01bf2141e1140ef8f8cad99d4b021d10718f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x7f9d307973cdabe42769d9712df8ee1cc1a28d10 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5c87da28a45e5089b762dcbbd86f743d14c54317 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2cd97604ef77bbcb1fa0cff47545dff8ec7def08 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x7862d9b4be2156b15d54f41ee4ede2d5b0b455e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x554548b404213c7efcdbab933f52edfe3c581834 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x63008c5ea4e47f5421e0e1428b1c5043a507d0d0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0350ca994791c4b07a5b02b08aaf9d6fc8ab510e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x32776ed4d96ed069a2d812773f0ad8ad9ef83cf8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x84f3ca9b7a1579ff74059bd0e8929424d3fa330e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5289a8dbf7029ee0b0498a84777ed3941d9acfec + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xb2bc284ab4c953b7f7a06d59c0ceb2de26405f22 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x508acf810857fefa86281499068ad5d19ebce325 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xccdfcd1aac447d5b29980f64b831c532a6a33726 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4fb87838a29b37598099ef5aa6b3fbeeef987c50 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x515e94dc736b9d8b7d28ecf1cece0aba3d75da97 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xfd6e5b7c30538dff2752058e425ad01a56b831cc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xcb99fe720124129520f7a09ca3cbef78d58ed934 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xd2f21358c1549be193537b2a4c5dc7f0228ae011 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x93094ed1c907e4bca7eb041cb659da94f7e1b58e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xd37e6ecb991d1a0e7610c89666817665713362a7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x73234630bd159384c8d43f145407312d64614f43 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xad1ddf00c4ae50573e4dc98e6c5ee93baa04a0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa765593c821f7df9ad81119509a37961e7ffa6c5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9b501a7ad3087d603ceb34424b7b2a6c348ad0b7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xafebb7cfa1a15fcac4121b609b456cbce3137c20 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0adaf134ae0c4583b3a38fc3168a83e33162651e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xf9878a5dd55edc120fde01893ea713a4f032229c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x84e47c7f2fe86f6b5efbe14fee46b8bb871b2e05 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xf3e5bec78654049990965f666b0612e116b94fb2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x33e59edd3214e97cb68450c6d3d6c167de072aba + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2ca76c7e466e560e0cb11a91269bb953e41254bc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xbb124e35ab9e85f8d59ba83500e559dc052b9368 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xd88d5f9e6c10e6febc9296a454f6c2589b1e8fae + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb90fe7da36ac89448e6dfd7f2bb1e90a66659977 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xbd6313d0796984c578cae6bc5b5e23b27c5540c5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x1f18cd7d1c7ba0dbe3d9abe0d3ec84ce1ad10066 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x7da99753ff017f1b7afb2c8c0542718dc9f15f21 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x079e7a44f42e9cd2442c3b9536244be634e8f888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x1c8dafd358d308b880f71edb5170b010b106ca60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xbd0f6f34baa3c1329448a69bab90111a20756f01 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x3420720e561f3082f1e514a4545f0f2e0c955a5d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xea3fb6e3313a2a90757e4ca3d6749efd0107b0b6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xf130f72f8190f662522774c3367e6e8814f5e219 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4a46c053bd5c10a959aea258228217b9d3405f3d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb83258bf5940c98abf54f26c5a02710bd6b83b2c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x6a209c5329f0a225fa1890d4177823c096016f34 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xdb24905b1b080f65dedb0ad978aad5c76363d3c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xddff2cdad11898b901a661e32e9fa010780263a0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x72dd8fe09b5b493012e5816068dfc6fb26a2a9e6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x54fc722a66abfb6500a36d8b7b2646129d0e836a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x53b612b32233c80ec439a64325a29766ce95be7f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xe5edcbe72d1bc223097a1bed1fe6c0e404b4290c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb928c37b8bd9754d321dc3d3c6ef374d332fe761 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x2d70cbabf4d8e61d5317b62cbe912935fd94e0fe + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x953e2937f0515c43ca7995e80c84aedcbbb9385e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x84394d80830ae963b599ded7d9149b90059f182f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa1777e082fa1746eb78dd9c1fbb515419cf6e538 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x112466c8b6e5abe42c78c47eb1b9d40baa3f943c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9491d57c5687ab75726423b55ac2d87d1cda2c3f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x978799f1845c00c9a4d9fd2629b9ce18df66e488 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xdc55d1fd1c04e005051a40bd59c5f95623257bc5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x34757893070b0fc5de37aaf2844255ff90f7f1e0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x7faf167615419228f3f7d71d52d840dab154913c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa4d7b6a50dd4c55334ca6f175dbc6561f269d264 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x0ed413cefde954d8e5c54d981d7d182b587e98e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x524375d0c6a04439128428f400b00eae81a2e9e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4b7a4530d56ff55a4dce089d917ede812e543307 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x84bb5b9bf1b6782c87cfa3e396f2f571c8e49646 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x723292eea7e1576ae482a5c317934054c0199e24 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9b42940e8184d866aac6595a91f8d8952a59d3b9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x37622453c614f625d288151101ffe48fd222ced1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4a94130b9e8eb0a0959c2c0f1ee9583213773fd9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x51514b3dc24afc1db95586242b99f0063bea17c5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc130254e9196d48bbd9f91240390a6e8203132e9 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x60ac25da2ada3be14a2a8c04e45b072bed965966 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4e392a3883a84225260ff857318517eb50e5d128 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xca0aa06385a42242fe9523cd7015f6d01cd8f6b2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x3e448c17043ce1481bbe53c0fd19481bad8b98a6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x81060e6bf2a683f208b8799a33c7c09830cabed1 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x463fe9f646b61ccfb43a022bf947075411cd71c7 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x21b8065d10f73ee2e260e5b47d3344d3ced7596e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x0d4a11d5eeaac28ec3f61d100daf4d40471f1852 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x517f9dd285e75b599234f7221227339478d0fcc8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa43fe16908251ee70ef74718545e4fe6c5ccec9f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x0af81cd5d9c124b4859d65697a4cd10ee223746a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xca7c2771d248dcbe09eabe0ce57a62e18da178c0 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x09d1d767edf8fa23a64c51fa559e0688e526812f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x7b73644935b8e68019ac6356c40661e1bc315860 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x180efc1349a69390ade25667487a826164c9c6e4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9c4fe5ffd9a9fc5678cfbd93aa2d4fd684b67c4c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa478c2975ab1ea89e8196811f51a7b7ade33eb11 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xbb2b8038a1640196fbe3e38816f3e67cba72d940 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9ec9367b8c4dd45ec8e7b800b1f719251053ad60 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc91ef786fbf6d62858262c82c63de45085dea659 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x197d7010147df7b99e9025c724f13723b29313f8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x25647e01bd0967c1b9599fa3521939871d1d0888 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x2f0b1417aa42ebf0b4ca1154212847f6094d708d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x6ada49aeccf6e556bb7a35ef0119cc8ca795294a + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x2a6c340bcbb0a79d3deecd3bc5cbc2605ea9259f + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xda2d09fbbf8ee4b5051a0e9b562c5fcb4b393b18 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x48d20b3e529fb3dd7d91293f80638df582ab2daa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4028daac072e492d34a3afdbef0ba7e35d8b55c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc2eab7d33d3cb97692ecb231a5d0e4a649cb539d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc5be99a02c6857f9eac67bbce58df5572498f40c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xe4b8583ccb95b25737c016ac88e539d0605949e8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x8dbee21e8586ee356130074aaa789c33159921ca + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x43de4318b6eb91a7cf37975dbb574396a7b5b5c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x9ff68f61ca5eb0c6606dc517a9d44001e564bb66 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa29fe6ef9592b5d408cca961d0fb9b1faf497d6d + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x1b1137dd16faa651e38a9dfb5d9ffff7767fdf62 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x470e8de2ebaef52014a47cb5e6af86884947f08c + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x8fb8e9921922d2ffb529a95d28a0d06d275d7a59 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xd3d2e2692501a5c9ca623199d38826e513033a17 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x97e1fcb93ae7267dbafad23f7b9afaa08264cfd8 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa5e9c917b4b821e4e0a5bbefce078ab6540d6b5e + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x2cc846fff0b08fb3bffad71f53a60b4b6e6d6482 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x959873fb4fc11825fba83c80c4c632db1e936e15 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xa7480aafa8ad2af3ce24ac6853f960ae6ac7f0c4 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xc7e6b676bfc73ae40bcc4577f22aab1682c691c6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x570febdf89c07f256c75686caca215289bb11cfc + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x343fd171caf4f0287ae6b87d75a8964dc44516ab + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xcaa004418eb42cdf00cb057b7c9e28f0ffd840a5 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xe3d3551bb608e7665472180a20280630d9e938aa + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xb6b0c651c37ec4ca81c0a128420e02001a57fac2 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x4e34da137f0b317c633838458e0c923a5e088752 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0xfe9e7931e55c514c33d489c88582fa36e84bd8e3 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x5281e311734869c64ca60ef047fd87759397efe6 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x149148acc3b06b8cc73af3a10e84189243a35925 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x8ef79d6c328c25da633559c20c75f638a4863462 + 2024-03-08T18:54:07.408Z + 0.8 + + + https://app.uniswap.org/explore/pools/ethereum/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/arbitrum/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0xbf16ef186e715668aa29cef57e2fd7f9d48adfe6 + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/optimism/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x5645dcb64c059aa11212707fbf4e7f984440a8cf + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/polygon/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x3ad4913fa896391c9822a81d8d869cc0d783bdd7 + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/base/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/bnb/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + + https://app.uniswap.org/explore/pools/celo/0x0f23d49bc92ec52ff591d091b3e16c937034496e + 2024-03-08T20:34:37.448Z + 0.8 + + \ No newline at end of file diff --git a/apps/web/public/rive/landing-page.riv b/apps/web/public/rive/landing-page.riv new file mode 100644 index 0000000..5c7e9fc Binary files /dev/null and b/apps/web/public/rive/landing-page.riv differ diff --git a/apps/web/public/robots.txt b/apps/web/public/robots.txt new file mode 100644 index 0000000..72c434b --- /dev/null +++ b/apps/web/public/robots.txt @@ -0,0 +1,22 @@ +# * +User-agent: * + +# Analytics +Disallow: https://statsigapi.net +Disallow: https://api.uniswap.org/v1/statsig-proxy +Disallow: https://api.uniswap.org/v1/amplitude-proxy +Disallow: https://sentry.io + +# Token Lists +Disallow: https://cloudflare-ipfs.com +Disallow: https://static.optimism.io +Disallow: https://raw.githubusercontent.com/plasmadlt/plasma-finance-token-list/master/bnb.json +Disallow: https://raw.githubusercontent.com/ethereum-optimism/ethereum-optimism.github.io/master/optimism.tokenlist.json +Disallow: https://tokenlist.arbitrum.io +Disallow: https://raw.githubusercontent.com/compound-finance/token-list/master/compound.tokenlist.json +Disallow: https://raw.githubusercontent.com/ava-labs/avalanche-bridge-resources/main/token_list.json +Disallow: https://raw.githubusercontent.com/SetProtocol/uniswap-tokenlist/main/set.tokenlist.json +Disallow: https://celo-org.github.io + +# Sitemaps +Sitemap: https://app.uniswap.org/sitemap.xml \ No newline at end of file diff --git a/apps/web/public/sitemap.xml b/apps/web/public/sitemap.xml new file mode 100644 index 0000000..f344621 --- /dev/null +++ b/apps/web/public/sitemap.xml @@ -0,0 +1,15 @@ + + + + https://app.uniswap.org/app-sitemap.xml + + + https://app.uniswap.org/tokens-sitemap.xml + + + https://app.uniswap.org/pools-sitemap.xml + + + https://app.uniswap.org/nfts-sitemap.xml + + diff --git a/apps/web/public/tokens-sitemap.xml b/apps/web/public/tokens-sitemap.xml new file mode 100644 index 0000000..fa34f63 --- /dev/null +++ b/apps/web/public/tokens-sitemap.xml @@ -0,0 +1,2278 @@ + + + + https://app.uniswap.org/explore/tokens/ethereum/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xdac17f958d2ee523a2206206994597c13d831ec7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6b175474e89094c44da98b954eedeac495271d0f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6123b0049f904d730db3c36a31167d9d4121fa6b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xcf0c122c6b73ff809c693db761e7baebe62b6a2e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xfaba6f8e4a5e8ab82f62fe7c39859fa577269be3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x58cb30368ceb2d194740b144eab4c2da8a917dcb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4c9edd5852cd905f086c759e8383e09bff1e68b3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xaaee1a9723aadb7afa2810263653a34ba2c21c7a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x514910771af9ca656af840dff83e8264ecf986ca + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5b7533812759b45c2b44c19e320ba2cd2681b542 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xae78736cd615f374d3085123a210448e74fc6393 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb9f599ce614feb2e1bbe58f180f370d05b39344e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd5f7838f5c461feff7fe49ea5ebaf7728bb0adfa + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd31a59c85ae9d8edefec411d448f90841571b89c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6a7eff1e2c355ad6eb91bebb5ded49257f3fed98 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x576e2bed8f7b46d34016198911cdf9886f78bea7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1258d60b224c0c5cd888d37bbf31aa5fcfb7e870 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x62d0a8458ed7719fdaf978fe5929c6d342b0bfce + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x77e06c9eccf2e797fd462a92b6d7642ef85b0a44 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x24fcfc492c1393274b6bcd568ac9e225bec93584 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x27702a26126e0b3702af63ee09ac4d1a084ef628 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd46ba6d942050d489dbd938a2c909a5d5039a161 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbe9895146f7af43049ca1c1ae358b0541ea49704 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x72f713d11480dcf08b37e1898670e736688d218d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x0001a500a6b18995b03f44bb040a5ffc28e45cb0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x9e9fbde7c7a83c43913bddc8779158f1368f0413 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5f98805a4e8be255a32880fdec7f6728c6568ba0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2b591e99afe9f32eaa6214f7b7629768c40eeb39 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1ae7e1d0ce06364ced9ad58225a1705b3e5db92b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x046eee2cc3188071c02bfc1745a6b17c656e3f3d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x84018071282d4b2996272659d9c01cb08dd7327f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x12970e6868f88f6557b76120662c1b3e50a646bf + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xaea46a60368a7bd060eec7df8cba43b7ef41ad85 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x6de037ef9ad2725eb40118bb1702ebb27e4aeb24 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc01154b4ccb518232d6bbfc9b9e6c5068b766f82 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x5a98fcbea516cf06857215779fd812ca3bef1b32 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x102c776ddb30c754ded4fdcc77a19230a60d4e4f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x72e4f9f808c49a2a61de9c5896298920dc4eeea9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x467719ad09025fcc6cf6f8311755809d45a5e5f3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf19308f923582a6f7c465e5ce7a9dc1bec6665b1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x710287d1d39dcf62094a83ebb3e736e79400068a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf951e335afb289353dc249e82926178eac7ded78 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf017d3690346eb8234b85f74cee5e15821fee1f4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8c282c35b5e1088bb208991c151182a782637699 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xeaa63125dd63f10874f99cdbbb18410e7fc79dd3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xde342a3e269056fc3305f9e315f4c40d917ba521 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2dff88a56767223a5529ea5960da7a3f5f766406 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x626e8036deb333b408be468f951bdb42433cbf18 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xdd66781d0e9a08d4fbb5ec7bac80b691be27f21d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb23d80f5fefcddaa212212f028021b41ded428cf + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xbaac2b4491727d78d2b78815144570b9f2fe8899 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf8ebf4849f1fa4faf0dff2106a173d3a6cb2eb3a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb90b2a35c65dbc466b04240097ca756ad2005295 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1614f18fc94f47967a3fbe5ffcd46d4e7da3d787 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf1df7305e4bab3885cab5b1e4dfc338452a67891 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x91fbb2503ac69702061f1ac6885759fc853e6eae + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa9e8acf069c58aec8825542845fd754e41a9489a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2c95d751da37a5c1d9c5a7fd465c1d50f3d96160 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe453c3409f8ad2b1fe1ed08e189634d359705a5b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x89d584a1edb3a70b3b07963f9a3ea5399e38b136 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4507cef57c46789ef8d1a19ea45f4216bae2b528 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xd1d2eb1b1e90b638588728b4130137d262c87cae + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe92344b4edf545f3209094b192e46600a19e7c2d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8a0a9b663693a22235b896f70a229c4a22597623 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1bbe973bef3a977fc51cbed703e8ffdefe001fed + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa41d2f8ee4f47d3b860a149765a7df8c3287b7f0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x761d38e5ddf6ccf6cf7c55759d5210750b5d60f3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc18360217d8f7ab5e7c516566761ea12ce7f9d72 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xe28b3b32b6c345a34ff64674606124dd5aceca30 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x168e209d7b2f58f1f24b8ae7b7d35e662bbf11cc + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xb131f4a55907b10d1f0a50d8ab8fa09ec342cd74 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3472a5a71965499acd81997a54bba8d852c6e53d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7dd9c5cba05e151c895fde1cf355c9a1d5da6429 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x19efa7d0fc88ffe461d1091f8cbe56dc2708a84f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x14fee680690900ba0cccfc76ad70fd1b95d10e16 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3c3a81e81dc49a522a592e7622a7e711c06bf354 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xa1290d69c65a6fe4df752f95823fae25cb99e5a7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x92f419fb7a750aed295b0ddf536276bf5a40124f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x2c06ba9e7f0daccbc1f6a33ea67e85bb68fbee3a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x3d658390460295fb963f54dc0899cfb1c30776df + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x8e870d67f660d95d5be530380d0ec0bd388289e1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x853d955acef822db058eb8505911ed77f175b99e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x1294f4183763743c7c9519bec51773fb3acd78fd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x4e15361fd6b4bb609fa63c81a2be19d873717870 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x695d38eb4e57e0f137e36df7c1f0f2635981246b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x40a7df3df8b56147b781353d379cb960120211d7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xaaef88cea01475125522e117bfe45cf32044e238 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x163f8c2467924be0ae7b5347228cabf260318753 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x30672ae2680c319ec1028b69670a4a786baa0f35 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xc944e90c64b2c07662a292be6244bdf05cda44a7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x15e6e0d4ebeac120f9a97e71faa6a0235b85ed12 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x7d225c4cc612e61d26523b099b0718d03152edef + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x82af49447d8a07e3bd95bd0d56f35241523fbab1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xaf88d065e77c8cc2239327c5edb3a432268e5831 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xff970a61a04b1ca14834a43f5de4533ebddb5cc8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x912ce59144191c1204e64559fe8253a0e49e6548 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x2f2a2543b76a4166549f7aab2e75bef0aefc5b0f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x5979d7b546e38e414f7e9822514be443a4800529 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x35751007a407ca6feffe80b3cb397736d2cf4dbe + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xeb466342c4d449bc9f53a865d5cb90586f405215 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xfc5a1a6eb076a2c7ad06ed22c90d7e710e35ad0a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x0c880f6761f1af8d9aa9c466984b80dab9a8c9e8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xf97f4df75117a78c1a5a0dbb814af92458539fb4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9623063377ad1b27544c965ccd7342f7ea7e88c7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x539bde0d7dbd336b79148aa742883198bbf60342 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3082cc23568ea640225c2467653db90e9250aaa0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x18c11fd286c5ec11c3b683caa813b77f5163a122 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x289ba1701c2f088cf0faf8b3705246331cb8a839 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4cb9a7ae498cedcbb5eae9f25736ae7d428c9d66 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x00cbcf7b3d37844e44b888bc747bdd75fcf4e555 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xfa7f8980b0f1e64a2062791cc3b0871572f1f7f0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd79bb960dc8a206806c3a428b31bca49934d18d7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3096e7bfd0878cc65be71f8899bc4cfb57187ba3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x13ad51ed4f1b7e9dc168d8a00cb3f4ddd85efa60 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4e352cf164e64adcbad318c3a1e222e9eba4ce42 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x11cdb42b0eb46d95f990bedd4695a6e3fa034978 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xba5ddd1f9d7f570dc94a51479a000e3bce967196 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xc8ccbd97b96834b976c995a67bf46e5754e2c48e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd07d35368e04a839dee335e213302b21ef14bb4a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x323665443cef804a3b5206103304bd4872ea4253 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x83d6c8c06ac276465e4c92e7ac8c23740f435140 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x87aaffdf26c6885f6010219208d5b161ec7609c0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1b8d516e2146d7a32aca0fcbf9482db85fd42c3a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xafccb724e3aec1657fc9514e3e53a0e71e80622d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4425742f1ec8d98779690b5a3a6276db85ddc01a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xec70dcb4a1efa46b8f2d97c310c9c4790ba5ffa8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3b60ff35d3f7f62d636b067dd0dc0dfdad670e4e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x58b9cb810a68a7f3e1e4f8cb45d1b9b3c79705e8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xfa5ed56a203466cbbc2430a43c66b9d8723528e7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x95146881b86b3ee99e63705ec87afe29fcc044d9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x088cd8f5ef3652623c22d48b1605dcfe860cd704 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x6daf586b7370b14163171544fca24abcc0862ac5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9d2f299715d94d8a7e6f5eaa8e654e8c74a988a7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x580e933d90091b9ce380740e3a4a39c67eb85b4c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x655a6beebf2361a19549a99486ff65f709bd2646 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9e64d3b9e8ec387a9a58ced80b71ed815f8d82b5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x2297aebd383787a160dd0d9f71508148769342e3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x6694340fc020c5e6b96567843da2df01b2ce1eb6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x772598e9e62155d7fdfe65fdf01eb5a53a8465be + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x431402e8b9de9aa016c743880e04e517074d8cec + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd74f5255d557944cf7dd0e45ff521520002d5748 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x6fd58f5a2f3468e35feb098b5f59f04157002407 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x561877b6b3dd7651313794e5f2894b2f18be0766 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xf9ca0ec182a94f6231df9b14bd147ef7fb9fa17c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd77b108d4f6cefaa0cae9506a934e825becca46e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd56734d7f9979dd94fae3d67c7e928234e71cd4c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xf1264873436a0771e440e2b28072fafcc5eebd01 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x5575552988a3a80504bbaeb1311674fcfd40ad4b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x0341c0c0ec423328621788d4854119b97f44e391 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x764bfc309090e7f93edce53e5befa374cdcb7b8e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xaaa6c1e32c55a7bfa8066a6fae9b42650f262418 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x9e20461bc2c4c980f62f1b279d71734207a6a356 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x7fb7ede54259cb3d4e1eaf230c7e2b1ffc951e9a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3a18dcc9745edcd1ef33ecb93b0b6eba5671e7ca + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x000000000026839b3f4181f2cf69336af6153b99 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x8b0e6f19ee57089f7649a455d89d7bc6314d04e8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x31c91d8fb96bff40955dd2dbc909b36e8b104dde + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x25d887ce7a35172c62febfd67a1856f20faebb00 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd4d42f0b6def4ce0383636770ef773390d85c61a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xf8388c2b6edf00e2e27eef5200b1befb24ce141d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x619c82392cb6e41778b7d088860fea8447941f4c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xad4b9c1fbf4923061814dd9d5732eb703faa53d4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd7a892f28dedc74e6b7b33f93be08abfc394a360 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3269a3c00ab86c753856fd135d97b87facb0d848 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4568ca00299819998501914690d6010ae48a59ba + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x21e60ee73f17ac0a411ae5d690f908c3ed66fe12 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xd3188e0df68559c0b63361f6160c57ad88b239d8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x2b41806cbf1ffb3d9e31a9ece6b738bf9d6f645f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xf19547f9ed24aa66b03c3a552d181ae334fbb8db + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x35e6a59f786d9266c7961ea28c7b768b33959cbb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x59a729658e9245b0cf1f8cb9fb37945d2b06ea27 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb56c29413af8778977093b9b4947efeea7136c36 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x43ab8f7d2a8dd4102ccea6b438f6d747b1b9f034 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1d987200df3b744cfa9c14f713f5334cb4bc4d5d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3404149e9ee6f17fb41db1ce593ee48fbdcd9506 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x080f6aed32fc474dd5717105dba5ea57268f46eb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb5a628803ee72d82098d4bcaf29a42e63531b441 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x1622bf67e6e5747b81866fe0b85178a93c7f86e3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x7dd747d63b094971e6638313a6a2685e80c7fb2e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xa2f9ecf83a48b86265ff5fd36cdbaaa1f349916c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x17a8541b82bf67e10b0874284b4ae66858cb1fd5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xbcd4d5ac29e06e4973a1ddcd782cd035d04bc0b7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x42069d11a2cc72388a2e06210921e839cfbd3280 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xbbea044f9e7c0520195e49ad1e561572e7e1b948 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xe85b662fe97e8562f4099d8a1d5a92d4b453bf30 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x3d9907f9a368ad0a51be60f7da3b97cf940982d8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0x4e51ac49bc5e2d87e0ef713e9e5ab2d71ef4f336 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000006 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x7f5c764cbc14f9669b88837ca1490cca17c31607 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x4200000000000000000000000000000000000042 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x0b2c639c533813f4aa9d7837caf62653d097ff85 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x1f32b1c2345538c0c6f582fcb022739c4a194ebb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x68f180fcce6836688e9084f035309e29bf0a2095 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x94b008aa00579c1307b0ef2c499ad98a8ce58e58 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xda10009cbd5d07dd0cecc66161fc93d7c9000da1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xdc6ff44d5d932cbd77b52e5612ba0529dc6226f1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x8c6f28f2f1a3c87f0f938b96d27520d9751ec8d9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x8700daec35af8ff88c16bdf0418774cb3d7599b4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x920cf626a271321c151d027030d5d08af699456b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x6c84a8f1c29108f47a79964b5fe888d4f4d0de40 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x9e1028f5f1d5ede59748ffcee5532509976840e0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xeb466342c4d449bc9f53a865d5cb90586f405215 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x350a791bfc2c21f9ed5d10980dad2e2638ffa7f6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x17aabf6838a6303fc6e9c5a227dc1eb6d95c829a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xf467c7d5a4a9c4687ffc7986ac6ad5a4c81e1404 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x76fb31fb4af56892a25e32cfc43de717950c9278 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xc5b001dc33727f8f26880b184090d3e252470d45 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x9560e827af36c94d2ac33a39bce1fe78631088db + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x9bcef72be871e61ed4fbbc7630889bee758eb81d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x50c5725949a6f0c72e6c4a641f24049a917db0cb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xf98dcd95217e15e05d8638da4c91125e59590b07 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x4b03afc91295ed778320c2824bad5eb5a1d852dd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xc40f949f8a4e094d1b49a23ea9241d289b7b2819 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x323665443cef804a3b5206103304bd4872ea4253 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x50bce64397c75488465253c0a034b8097fea6578 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x296f55f8fb28e498b858d0bcda06d955b2cb3f97 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x2598c30330d5771ae9f983979209486ae26de875 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x0994206dfe8de6ec6920ff4d779b0d950605fb53 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xc3248a1bd9d72fa3da6e6ba701e58cbf818354eb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x6fd9d7ad17242c41f7131d257212c54a0e816691 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x14778860e937f509e651192a90589de711fb88a9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xdfa46478f9e5ea86d57387849598dbfb2e964b02 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x9b88d293b7a791e40d36a39765ffd5a1b9b5c349 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x3eb398fec5f7327c6b15099a9681d9568ded2e82 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x217d47011b23bb961eb6d93ca9945b7501a5bb11 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x1cef2d62af4cd26673c7416957cc4ec619a696a7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x9fd22a17b4a96da3f83797d122172c450381fb88 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0xaddb6a0412de1ba0f936dcaeb8aaa24578dcf3b2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x2791bca1f2de4661ed88a30c99a7a9449aa84174 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x7ceb23fd6bc0add59e62ac25578270cff1b9f619 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x1bfd67037b42cf73acf2047067bd4f2c47d9bfd6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xc2132d05d31c914a87c6611c10748aeb04b58e8f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x53e0bca35ec356bd5dddfebbd1fc0fd03fabad39 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x61299774020da444af134c82fa83e3810b309991 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xd6df932a45c0f255f85145f286ea0b292b21c90b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x2ad2934d5bfb7912304754479dd1f096d5c807da + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xc3c7d422809852031b44ab29eec9f1eff2a58756 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8f3cf7ad23cd3cadbd9735aff958023239c6a063 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x750e4c4984a9e0f12978ea6742bc1c5d248f40ed + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x111111517e4929d3dcbdfa7cce55d30d4b6bc4d6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xd0258a3fd00f38aa8090dfee343f10a9d4d30d3f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x430ef9263e76dae63c84292c3409d61c598e9682 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb33eaad8d922b1083446dc23f610c2567fb5180f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xdc3326e71d45186f113a2f448984ca0e8d201995 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x311434160d7537be358930def317afb606c0d737 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0b3f868e0be5597d5db7feb59e1cadbb0fdda50a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe3f2b1b2229c0333ad17d03f179b87500e7c5e01 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xac0f66379a6d7801d7726d5a943356a172549adb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf88332547c680f755481bf489d890426248bb275 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe5417af564e4bfda1c483642db72007871397896 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe261d618a959afffd53168cd07d12e37b26761db + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe0b52e49357fd4daf2c15e02058dce6bc0057db4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xbbba073c31bf03b8acf7c28ef0738decf3695683 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe238ecb42c424e877652ad82d8a939183a04c35f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x3b56a704c01d650147ade2b8cee594066b3f9421 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x5fe2b58c013d7601147dcdd68c143a77499f5531 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x172370d5cd63279efa6d502dab29171933a610af + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x53df32548214f51821cf1fe4368109ac5ddea1ff + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xff76c0b48363a7c7307868a81548d340049b0023 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x6f8a06447ff6fcf75d803135a7de15ce88c1d4ec + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x50b728d8d964fd00c2d0aad81718b71311fef68a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x03b54a6e9a984069379fae1a4fc4dbae93b3bccd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xd93f7e271cb87c23aaa73edc008a79646d1f9912 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x200c234721b5e549c3693ccc93cf191f90dc2af9 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x11cd37bb86f65419713f30673a480ea33c826872 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8a16d4bf8a0a716017e8d2262c4ac32927797a2f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x9a71012b13ca4d3d0cdc72a177df3ef03b0e76a3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xa1c57f48f0deb89f569dfbe6e2b7f46d33606fd4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x190eb8a183d22a4bdf278c6791b152228857c033 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x2f6f07cdcf3588944bf4c42ac74ff24bf56e7590 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x235737dbb56e8517391473f7c964db31fa6ef280 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0b220b82f3ea3b7f6d9a1d8ab58930c064a2b5bf + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8bff1bd27e2789fe390acabc379c380a83b68e84 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb58458c52b6511dc723d7d6f3be8c36d7383b4a8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x323665443cef804a3b5206103304bd4872ea4253 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x2760e46d9bb43dafcbecaad1f64b93207f9f0ed7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x18ec0a6e18e5bc3784fdd3a3634b31245ab704f6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x431d5dff03120afa4bdf332c61a6e1766ef37bdb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x6f7c932e7684666c9fd1d44527765433e01ff61d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xeee3371b89fc43ea970e908536fcddd975135d8a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe5b49820e5a1063f6f4ddf851327b5e8b2301048 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xaa3717090cddc9b227e49d0d84a28ac0a996e6ff + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x62a872d9977db171d9e213a5dc2b782e72ca0033 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x381caf412b45dac0f62fbeec89de306d3eabe384 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe0bceef36f3a6efdd5eebfacd591423f8549b9d5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x23d29d30e35c5e8d321e1dc9a8a61bfd846d4c5c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x282d8efce846a88b159800bd4130ad77443fa1a1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x74dd45dd579cad749f9381d6227e7e02277c944b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x714db550b574b3e927af3d93e26127d15721d4c2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xfa68fb4628dff1028cfec22b4162fccd0d45efb6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe631dabef60c37a37d70d3b4f812871df663226f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xdb725f82818de83e99f1dac22a9b5b51d3d04dd4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x3c59798620e5fec0ae6df1a19c6454094572ab92 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0d0b8488222f7f83b23e365320a4021b12ead608 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xa380c0b01ad15c8cf6b46890bddab5f0868e87f3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8a953cfe442c5e8855cc6c61b1293fa648bae472 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x45c32fa6df82ead1e2ef74d17b76547eddfaff89 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x11cd72f7a4b699c67f225ca8abb20bc9f8db90c7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0c9c7712c83b3c70e7c5e11100d33d9401bdf9dd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x77a6f2e9a9e44fd5d5c3f9be9e52831fc1c3c0a0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xbfc70507384047aa74c29cdc8c5cb88d0f7213ac + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xfcb54da3f4193435184f3f647467e12b50754575 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x9a6a40cdf21a0af417f1b815223fd92c85636c58 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe111178a87a3bff0c8d18decba5798827539ae99 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x82617aa52dddf5ed9bb7b370ed777b3182a30fd1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x2ab0e9e4ee70fff1fb9d67031e44f6410170d00e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xa486c6bc102f409180ccb8a94ba045d39f8fc7cb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xc4a206a306f0db88f98a3591419bc14832536862 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf0059cc2b3e980065a906940fbce5f9db7ae40a7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x16eccfdbb4ee1a85a33f3a9b21175cd7ae753db4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x553d3d295e0f695b9228246232edf400ed3560b5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x14af1f2f02dccb1e43402339099a05a5e363b83c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x7bdf330f423ea880ff95fc41a280fd5ecfd3d09f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x8505b9d2254a7ae468c0e9dd10ccea3a837aef5c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xe2aa7db6da1dae97c5f5c6914d285fbfcc32a128 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xb7b31a6bc18e48888545ce79e83e06003be70930 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x1631244689ec1fecbdd22fb5916e920dfc9b8d30 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf6372cdb9c1d3674e83842e3800f2a62ac9f3c66 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x692ac1e363ae34b6b489148152b12e2785a3d8d6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x0266f4f08d82372cf0fcbccc0ff74309089c74d1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x7fbc10850cae055b27039af31bd258430e714c62 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xa3fa99a148fa48d14ed51d610c367c61876997f1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x9dbfc1cbf7a1e711503a29b4b5f9130ebeccac96 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/polygon/0xf86df9b91f002cfeb2aed0e6d05c4c4eaef7cf02 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4200000000000000000000000000000000000006 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x6921b130d297cc43754afba22e5eac0fbf8db75b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x5babfc2f240bc5de90eb7e19d789412db1dec402 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x532f27101965dd16442e59d40670faf5ebb142e4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4ed4e862860bed51a9570b96d89af5e1b0efefed + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xc1cba3fcea344f92d9239c08c0568f6f2f0ee452 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xac1bd2486aaf3b5c0fc3fd868558b082a531b2b4 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x0d97f261b1e88845184f678e2d1e7a98d9fd38de + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8129b94753f22ec4e62e2c4d099ffe6773969ebc + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3f14920c99beb920afa163031c4e47a3e03b3e4a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x940181a94a35a4569e4529a3cdfb74e38fd98631 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3419875b4d3bca7f3fdda2db7a476a79fd31b4fe + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xa067436db77ab18b1a315095e4b816791609897c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xafb89a09d82fbde58f18ac6437b3fc81724e4df6 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x489fe42c267fe0366b16b0c39e7aeef977e841ef + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x2ae3f1ec7f1f5012cfeab0185bfc7aa3cf0dec22 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xdc46c1e93b71ff9209a0f8076a9951569dc35855 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x91f45aa2bde7393e0af1cc674ffe75d746b93567 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x236aa50979d5f3de3bd1eeb40e81137f22ab794b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xf6e932ca12afa26665dc4dde7e27be02a7c02e50 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x524d524b4c9366be706d3a90dcf70076ca037ae3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x5b5dee44552546ecea05edea01dcd7be7aa6144a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x2598c30330d5771ae9f983979209486ae26de875 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xfa980ced6895ac314e7de34ef1bfae90a5add21b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x469fda1fb46fcb4befc0d8b994b516bd28c87003 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x4e496c0256fb9d4cc7ba2fdf931bc9cbb7731660 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x27d2decb4bfc9c76f0309b8e88dec3a601fe25a8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xbfd5206962267c7b4b4a8b3d76ac2e1b2a5c4d5e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x9e1028f5f1d5ede59748ffcee5532509976840e0 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x3c3aa127e6ee3d2f2e432d0184dd36f2d2076b52 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xba5e6fa2f33f3955f0cef50c63dcc84861eab663 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x97c806e7665d3afd84a8fe1837921403d59f3dcc + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8ee73c484a26e0a5df2ee2a4960b789967dd0415 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x00e57ec29ef2ba7df07ad10573011647b2366f6d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x8f019931375454fe4ee353427eb94e2e0c9e0a8c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0x93e6407554b2f02640ab806cd57bd83e848ec65d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x55d398326f99059ff775485246999027b3197955 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x2170ed0880ac9a755fd29b2688956bd959f933f8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xfdc66a08b0d0dc44c17bbd471b88f49f50cdd20f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x7130d2a12b9bcbfae4f2634d864a1ee1ce3ead9c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x1d2f0da169ceb9fc7b3144628db156f3f6c60dbe + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xe9e7cea3dedca5984780bafc599bd69add087d56 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xfa54ff1a158b5189ebba6ae130ced6bbd3aea76e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x570a5d26f7765ecb712c0924e4de545b89fd43df + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x47c454ca6be2f6def6f32b638c80f91c9c3c5949 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xad86d0e9764ba90ddd68747d64bffbd79879a238 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xf8a0bf9cf54bb92f17374d9e9a321e6a111a51bd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xd691d9a68c887bdf34da8c36f63487333acfd103 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x1294f4183763743c7c9519bec51773fb3acd78fd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xb04906e95ab5d797ada81508115611fee694c2b3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x111111111117dc0aa78b770fa6a738034120c302 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xcc42724c6683b7e57334c4e856f4c9965ed682bd + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x90c97f71e18723b0cf0dfa30ee176ab653e89f40 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0e09fabb73bd3ade0a17ecc321fd13a19e81ce82 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x2b72867c32cf673f7b02d208b26889fed353b1f8 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x031b41e504677879370e9dbcf937283a8691fa7f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x1ce0c2827e2ef14d5c4f29a091d735a204794041 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xcf3bb6ac0f6d987a5727e2d15e39c2d6061d5bec + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x8ff795a6f4d97e7887c79bea79aba5cc76444adf + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x2dff88a56767223a5529ea5960da7a3f5f766406 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x003d87d02a2a01e9e8a20f507c83e15dd83a33d1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x4b0f1812e5df2a09796481ff14017e6005508003 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbf5140a22578168fd562dccf235e5d43a02ce9b1 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xca1c644704febf4ab81f85daca488d1623c28e63 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x51e72dd1f2628295cc2ef931cb64fdbdc3a0c599 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbbca42c60b5290f2c48871a596492f93ff0ddc82 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x555296de6a86e72752e5c5dc091fe49713aa145c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0808bf94d57c905f1236212654268ef82e1e594e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x8457ca5040ad67fdebbcc8edce889a335bc0fbfb + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xcebef3df1f3c5bfd90fde603e71f31a53b11944d + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x90ed8f1dc86388f14b64ba8fb4bbd23099f18240 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x9840652dc04fb9db2c43853633f0f62be6f00f98 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xba2ae424d960c26247dd6c32edc70b295c744c43 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0782b6d8c4551b9760e74c0545a9bcd90bdc41e5 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xbe2b6c5e31f292009f495ddbda88e28391c9815e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x8f0528ce5ef7b51152a59745befdd91d97091d2f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xffeecbf8d7267757c2dc3d13d730e97e15bfdf7f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0eb3a705fc54725037cc9e008bdede697f62f335 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xf21768ccbc73ea5b6fd3c687208a7c2def2d966e + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x0000028a2eb8346cd5c0267856ab7594b7a55308 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x76a797a59ba2c17726896976b7b3747bfd1d220f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xc79d1fd14f514cd713b5ca43d288a782ae53eab2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xad29abb318791d579433d831ed122afeaf29dcfe + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x3203c9e46ca618c8c1ce5dc67e7e9d75f5da2377 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xdb021b1b247fe2f1fa57e0a87c748cc1e321f07f + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x7083609fce4d1d8dc0c979aab8c869ea2c873402 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xc5f0f7b66764f6ec8c8dff7ba683102295e16409 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xe29142e14e52bdfbb8108076f66f49661f10ec10 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0xb0d502e938ed5f4df2e681fe6e419ff29631d62b + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x6730f7a6bbb7b9c8e60843948f7feb4b6a17b7f7 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/bnb/0x1613957159e9b0ac6c80e824f7eea748a32a0ae2 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x471ece3750da237f93b8e339c536989b8978a438 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x765de816845861e75a25fca122bb6898b8b1282a + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x66803fb87abd4aac3cbb3fad7c3aa01f6f3fb207 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0xd8763cba276a3738e6de85b4b3bf5fded6d6ca73 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x37f750b7cc259a2f741af45294f6a16572cf5cad + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0xd71ffd0940c920786ec4dbb5a12306669b5b81ef + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0xe8537a3d056da446677b9e9d6c5db704eaab4787 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x4f604735c1cf31399c6e711d5962b2b3e0225ad3 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0x02de4766c272abc10bc88c220d214a26960a7e92 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0xceba9300f2b948710d2653dd7b07f33a8b32118c + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/celo/0xc16b81af351ba9e64c1a069e3ab18c244a1e3049 + 2024-03-08T18:44:55.418Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x728f30fa2f100742c7949d1961804fa8e0b1387d + 2024-03-08T20:33:55.117Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0x41ea5d41eeacc2d5c4072260945118a13bb7ebce + 2024-03-08T20:33:55.117Z + 0.8 + + + https://app.uniswap.org/explore/tokens/ethereum/0xf21661d0d1d76d3ecb8e1b9f1c923dbfffae4097 + 2024-03-08T20:33:55.117Z + 0.8 + + + https://app.uniswap.org/explore/tokens/arbitrum/0xb0ecc6ac0073c063dcfc026ccdc9039cae2998e1 + 2024-03-08T20:33:55.117Z + 0.8 + + + https://app.uniswap.org/explore/tokens/optimism/0x00f932f0fe257456b32deda4758922e56a4f4b42 + 2024-03-08T20:33:55.117Z + 0.8 + + + https://app.uniswap.org/explore/tokens/base/0xa4af354d466e8a68090dd9eb2cb7caf162f4c8c2 + 2024-03-08T20:33:55.117Z + 0.8 + + \ No newline at end of file diff --git a/apps/web/scripts/compile-ajv-validators.js b/apps/web/scripts/compile-ajv-validators.js new file mode 100644 index 0000000..965a479 --- /dev/null +++ b/apps/web/scripts/compile-ajv-validators.js @@ -0,0 +1,20 @@ +/* eslint-env node */ + +const fs = require('fs') +const path = require('path') +const Ajv = require('ajv') +const standaloneCode = require('ajv/dist/standalone').default +const addFormats = require('ajv-formats') +const schema = require('@uniswap/token-lists/dist/tokenlist.schema.json') + +const tokenListAjv = new Ajv({ code: { source: true, esm: true } }) +addFormats(tokenListAjv) +const validateTokenList = tokenListAjv.compile(schema) +let tokenListModuleCode = standaloneCode(tokenListAjv, validateTokenList) +fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokenList.js'), tokenListModuleCode) + +const tokensAjv = new Ajv({ code: { source: true, esm: true } }) +addFormats(tokensAjv) +const validateTokens = tokensAjv.compile({ ...schema, required: ['tokens'] }) +let tokensModuleCode = standaloneCode(tokensAjv, validateTokens) +fs.writeFileSync(path.join(__dirname, '../src/utils/__generated__/validateTokens.js'), tokensModuleCode) diff --git a/apps/web/scripts/fetch-schema.js b/apps/web/scripts/fetch-schema.js new file mode 100644 index 0000000..10f3996 --- /dev/null +++ b/apps/web/scripts/fetch-schema.js @@ -0,0 +1,28 @@ +/* eslint-env node */ + +require('dotenv').config({ path: '.env.production' }) +const child_process = require('child_process') +const fs = require('fs/promises') +const { promisify } = require('util') +const dataConfig = require('../graphql.data.config') +const thegraphConfig = require('../graphql.thegraph.config') + +const exec = promisify(child_process.exec) + +function fetchSchema(url, outputFile) { + exec(`npx --silent get-graphql-schema -h Origin=https://app.uniswap.org ${url}`) + .then(({ stderr, stdout }) => { + if (stderr) { + throw new Error(stderr) + } else { + fs.writeFile(outputFile, stdout) + } + }) + .catch((err) => { + console.error(err) + console.error(`Failed to fetch schema from ${url}`) + }) +} + +fetchSchema(process.env.THE_GRAPH_SCHEMA_ENDPOINT, thegraphConfig.schema) +fetchSchema(process.env.REACT_APP_AWS_API_ENDPOINT, dataConfig.schema) diff --git a/apps/web/scripts/generate-sitemap.js b/apps/web/scripts/generate-sitemap.js new file mode 100644 index 0000000..14dad6f --- /dev/null +++ b/apps/web/scripts/generate-sitemap.js @@ -0,0 +1,212 @@ +/* eslint-env node */ + +const fs = require('fs') +const { parseStringPromise, Builder } = require('xml2js') + +const weekMs = 7 * 24 * 60 * 60 * 1000 +const nowISO = new Date().toISOString() + +const getTopTokensQuery = (chain) => ` + query { + topTokens(pageSize: 100, page: 1, chain: ${chain}, orderBy: VOLUME) { + address + } + }` + +const getTopPoolsQuery = (v3Chain) => ` + query { + topV3Pools(first: 50, chain: ${v3Chain}) { + id + address + } + topV2Pairs(first: 50, chain: ETHEREUM) { + address + } + } +` + +const chains = ['ETHEREUM', 'ARBITRUM', 'OPTIMISM', 'POLYGON', 'BASE', 'BNB', 'CELO'] + +const nftTopCollectionsQuery = ` + query { + topCollections(first: 100, duration: MAX) { + edges { + node { + nftContracts { + address + } + } + } + } + } +` + +fs.readFile('./public/tokens-sitemap.xml', 'utf8', async (err, data) => { + const tokenURLs = {} + try { + const sitemap = await parseStringPromise(data) + if (sitemap.urlset.url) { + sitemap.urlset.url.forEach((url) => { + const lastMod = new Date(url.lastmod).getTime() + if (lastMod < Date.now() - weekMs) { + url.lastmod = nowISO + } + tokenURLs[url.loc] = true + }) + } + + for (const chainName of chains) { + const tokensResponse = await fetch('https://api.uniswap.org/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: 'https://app.uniswap.org', + }, + body: JSON.stringify({ query: getTopTokensQuery(chainName) }), + }) + const tokensJSON = await tokensResponse.json() + const tokenAddresses = tokensJSON.data.topTokens.map((token) => token.address.toLowerCase()) + + tokenAddresses.forEach((address) => { + const tokenURL = `https://app.uniswap.org/explore/tokens/${chainName.toLowerCase()}/${address}` + if (!(tokenURL in tokenURLs)) { + sitemap.urlset.url.push({ + loc: [tokenURL], + lastmod: [nowISO], + priority: [0.8], + }) + } + }) + } + + const builder = new Builder() + const xml = builder.buildObject(sitemap) + const path = './public/tokens-sitemap.xml' + fs.writeFile(path, xml, (error) => { + if (error) throw error + const stats = fs.statSync(path) + const fileSizeBytes = stats.size + const fileSizeMegabytes = fileSizeBytes / (1024 * 1024) + + if (fileSizeMegabytes > 50) { + throw new Error('Generated tokens-sitemap.xml file size exceeds 50MB') + } + console.log('Tokens sitemap updated') + }) + } catch (e) { + console.error(e) + } +}) + +fs.readFile('./public/nfts-sitemap.xml', 'utf8', async (err, data) => { + const collectionURLs = {} + try { + const sitemap = await parseStringPromise(data) + if (sitemap.urlset.url) { + sitemap.urlset.url.forEach((url) => { + const lastMod = new Date(url.lastmod).getTime() + if (lastMod < Date.now() - weekMs) { + url.lastmod = nowISO + } + collectionURLs[url.loc] = true + }) + } + + const nftResponse = await fetch('https://interface.gateway.uniswap.org/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: 'https://app.uniswap.org', + }, + body: JSON.stringify({ query: nftTopCollectionsQuery }), + }) + const nftJSON = await nftResponse.json() + const collectionAddresses = nftJSON.data.topCollections.edges.map((edge) => edge.node.nftContracts[0].address) + collectionAddresses.forEach((address) => { + const collectionURL = `https://app.uniswap.org/nfts/collection/${address}` + if (!(collectionURL in collectionURLs)) { + sitemap.urlset.url.push({ + loc: [collectionURL], + lastmod: [nowISO], + priority: [0.7], + }) + } + }) + + const builder = new Builder() + const xml = builder.buildObject(sitemap) + const path = './public/nfts-sitemap.xml' + fs.writeFile(path, xml, (error) => { + if (error) throw error + const stats = fs.statSync(path) + const fileSizeBytes = stats.size + const fileSizeMegabytes = fileSizeBytes / (1024 * 1024) + + if (fileSizeMegabytes > 50) { + throw new Error('Generated nfts-sitemap.xml file size exceeds 50MB') + } + console.log('NFT collections sitemap updated') + }) + } catch (e) { + console.error(e) + } +}) + +fs.readFile('./public/pools-sitemap.xml', 'utf8', async (err, data) => { + const poolURLs = {} + try { + const sitemap = await parseStringPromise(data) + if (sitemap.urlset.url) { + sitemap.urlset.url.forEach((url) => { + const lastMod = new Date(url.lastmod).getTime() + if (lastMod < Date.now() - weekMs) { + url.lastmod = nowISO + } + poolURLs[url.loc] = true + }) + } + + for (const chainName of chains) { + const poolsResponse = await fetch('https://api.uniswap.org/v1/graphql', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Origin: 'https://app.uniswap.org', + }, + body: JSON.stringify({ query: getTopPoolsQuery(chainName) }), + }) + const poolsJSON = await poolsResponse.json() + const v3PoolAddresses = poolsJSON.data.topV3Pools?.map((pool) => pool.address.toLowerCase()) ?? [] + const v2PoolAddresses = poolsJSON.data.topV2Pairs?.map((pool) => pool.address.toLowerCase()) ?? [] + const poolAddresses = v3PoolAddresses.concat(v2PoolAddresses) + + poolAddresses.forEach((address) => { + const poolUrl = `https://app.uniswap.org/explore/pools/${chainName.toLowerCase()}/${address}` + if (!(poolUrl in poolURLs)) { + sitemap.urlset.url.push({ + loc: [poolUrl], + lastmod: [nowISO], + priority: [0.8], + }) + } + }) + } + + const builder = new Builder() + const xml = builder.buildObject(sitemap) + const path = './public/pools-sitemap.xml' + fs.writeFile(path, xml, (error) => { + if (error) throw error + const stats = fs.statSync(path) + const fileSizeBytes = stats.size + const fileSizeMegabytes = fileSizeBytes / (1024 * 1024) + + if (fileSizeMegabytes > 50) { + throw new Error('Generated pools-sitemap.xml file size exceeds 50MB') + } + console.log('Pools sitemap updated') + }) + } catch (e) { + console.error(e) + } +}) diff --git a/apps/web/scripts/terser-loader.js b/apps/web/scripts/terser-loader.js new file mode 100644 index 0000000..969068b --- /dev/null +++ b/apps/web/scripts/terser-loader.js @@ -0,0 +1,15 @@ +/* eslint-env node */ + +const { minify } = require('terser') + +module.exports = async function terserLoader(source, map, meta) { + const callback = this.async() + const options = this.getOptions() + try { + const data = await minify(source, options) + const { code } = data || {} + callback(null, code, map, meta) + } catch (e) { + callback(e) + } +} diff --git a/apps/web/src/analytics/index.tsx b/apps/web/src/analytics/index.tsx new file mode 100644 index 0000000..bc9c0bd --- /dev/null +++ b/apps/web/src/analytics/index.tsx @@ -0,0 +1,59 @@ +import { + TraceEvent as AnalyticsEvent, + Trace as AnalyticsTrace, + sendAnalyticsEvent as sendAnalyticsTraceEvent, +} from '@uniswap/analytics' +import { atomWithStorage, useAtomValue } from 'jotai/utils' +import { memo } from 'react' + +export { + OriginApplication, + getDeviceId, + initializeAnalytics, + useTrace, + user, + type ITraceContext, +} from '@uniswap/analytics' + +const allowAnalyticsAtomKey = 'allow_analytics' +export const allowAnalyticsAtom = atomWithStorage(allowAnalyticsAtomKey, true) + +export const Trace = memo((props: React.ComponentProps) => { + const allowAnalytics = useAtomValue(allowAnalyticsAtom) + const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false + + return +}) + +Trace.displayName = 'Trace' + +export const TraceEvent = memo((props: React.ComponentProps) => { + const allowAnalytics = useAtomValue(allowAnalyticsAtom) + const shouldLogImpression = allowAnalytics ? props.shouldLogImpression : false + + return +}) + +TraceEvent.displayName = 'TraceEvent' + +export const sendAnalyticsEvent: typeof sendAnalyticsTraceEvent = (event, properties) => { + let allowAnalytics = true + + try { + const value = localStorage.getItem(allowAnalyticsAtomKey) + + if (typeof value === 'string') { + allowAnalytics = JSON.parse(value) + } + // eslint-disable-next-line no-empty + } catch {} + + if (allowAnalytics) { + sendAnalyticsTraceEvent(event, properties) + } +} + +// This is only used for initial page load so we can get the user's country +export const sendInitializationEvent: typeof sendAnalyticsTraceEvent = (event, properties) => { + sendAnalyticsTraceEvent(event, properties) +} diff --git a/apps/web/src/assets/images/404-page-dark.png b/apps/web/src/assets/images/404-page-dark.png new file mode 100644 index 0000000..308bcf2 Binary files /dev/null and b/apps/web/src/assets/images/404-page-dark.png differ diff --git a/apps/web/src/assets/images/404-page-light.png b/apps/web/src/assets/images/404-page-light.png new file mode 100644 index 0000000..dc854f1 Binary files /dev/null and b/apps/web/src/assets/images/404-page-light.png differ diff --git a/apps/web/src/assets/images/airdopBackground.png b/apps/web/src/assets/images/airdopBackground.png new file mode 100644 index 0000000..859fab3 Binary files /dev/null and b/apps/web/src/assets/images/airdopBackground.png differ diff --git a/apps/web/src/assets/images/arbitrumCircle.png b/apps/web/src/assets/images/arbitrumCircle.png new file mode 100644 index 0000000..3a69836 Binary files /dev/null and b/apps/web/src/assets/images/arbitrumCircle.png differ diff --git a/apps/web/src/assets/images/arrow-right.svg b/apps/web/src/assets/images/arrow-right.svg new file mode 100644 index 0000000..89e10e6 --- /dev/null +++ b/apps/web/src/assets/images/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/images/big_unicorn.png b/apps/web/src/assets/images/big_unicorn.png new file mode 100644 index 0000000..09e9137 Binary files /dev/null and b/apps/web/src/assets/images/big_unicorn.png differ diff --git a/apps/web/src/assets/images/blue-loader.svg b/apps/web/src/assets/images/blue-loader.svg new file mode 100644 index 0000000..3dc43e2 --- /dev/null +++ b/apps/web/src/assets/images/blue-loader.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/images/buy.svg b/apps/web/src/assets/images/buy.svg new file mode 100644 index 0000000..838c7f1 --- /dev/null +++ b/apps/web/src/assets/images/buy.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/images/dropdown.svg b/apps/web/src/assets/images/dropdown.svg new file mode 100644 index 0000000..d55772b --- /dev/null +++ b/apps/web/src/assets/images/dropdown.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/images/ethereum-logo.png b/apps/web/src/assets/images/ethereum-logo.png new file mode 100644 index 0000000..760946e Binary files /dev/null and b/apps/web/src/assets/images/ethereum-logo.png differ diff --git a/apps/web/src/assets/images/gnosis.png b/apps/web/src/assets/images/gnosis.png new file mode 100644 index 0000000..ff5244e Binary files /dev/null and b/apps/web/src/assets/images/gnosis.png differ diff --git a/apps/web/src/assets/images/menu.svg b/apps/web/src/assets/images/menu.svg new file mode 100644 index 0000000..1545717 --- /dev/null +++ b/apps/web/src/assets/images/menu.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/assets/images/noise.png b/apps/web/src/assets/images/noise.png new file mode 100644 index 0000000..33a6489 Binary files /dev/null and b/apps/web/src/assets/images/noise.png differ diff --git a/apps/web/src/assets/images/sizingImage.png b/apps/web/src/assets/images/sizingImage.png new file mode 100644 index 0000000..75e5350 Binary files /dev/null and b/apps/web/src/assets/images/sizingImage.png differ diff --git a/apps/web/src/assets/images/token-logo.png b/apps/web/src/assets/images/token-logo.png new file mode 100644 index 0000000..35c017d Binary files /dev/null and b/apps/web/src/assets/images/token-logo.png differ diff --git a/apps/web/src/assets/images/uniwallet.png b/apps/web/src/assets/images/uniwallet.png new file mode 100644 index 0000000..a62a6b4 Binary files /dev/null and b/apps/web/src/assets/images/uniwallet.png differ diff --git a/apps/web/src/assets/images/uniwallet_modal_icon.png b/apps/web/src/assets/images/uniwallet_modal_icon.png new file mode 100644 index 0000000..16bba8a Binary files /dev/null and b/apps/web/src/assets/images/uniwallet_modal_icon.png differ diff --git a/apps/web/src/assets/images/walletAnnouncementBannerQR.png b/apps/web/src/assets/images/walletAnnouncementBannerQR.png new file mode 100644 index 0000000..d02d976 Binary files /dev/null and b/apps/web/src/assets/images/walletAnnouncementBannerQR.png differ diff --git a/apps/web/src/assets/images/wallet_banner_phone_image.png b/apps/web/src/assets/images/wallet_banner_phone_image.png new file mode 100644 index 0000000..a436eef Binary files /dev/null and b/apps/web/src/assets/images/wallet_banner_phone_image.png differ diff --git a/apps/web/src/assets/images/welcomeModal-dark.jpg b/apps/web/src/assets/images/welcomeModal-dark.jpg new file mode 100644 index 0000000..d7b8075 Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-dark.jpg differ diff --git a/apps/web/src/assets/images/welcomeModal-dark@2x.jpg b/apps/web/src/assets/images/welcomeModal-dark@2x.jpg new file mode 100644 index 0000000..5043bb6 Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-dark@2x.jpg differ diff --git a/apps/web/src/assets/images/welcomeModal-dark@3x.jpg b/apps/web/src/assets/images/welcomeModal-dark@3x.jpg new file mode 100644 index 0000000..1ce51e7 Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-dark@3x.jpg differ diff --git a/apps/web/src/assets/images/welcomeModal-light.jpg b/apps/web/src/assets/images/welcomeModal-light.jpg new file mode 100644 index 0000000..849db5b Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-light.jpg differ diff --git a/apps/web/src/assets/images/welcomeModal-light@2x.jpg b/apps/web/src/assets/images/welcomeModal-light@2x.jpg new file mode 100644 index 0000000..b8ae653 Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-light@2x.jpg differ diff --git a/apps/web/src/assets/images/welcomeModal-light@3x.jpg b/apps/web/src/assets/images/welcomeModal-light@3x.jpg new file mode 100644 index 0000000..ea60618 Binary files /dev/null and b/apps/web/src/assets/images/welcomeModal-light@3x.jpg differ diff --git a/apps/web/src/assets/images/x.svg b/apps/web/src/assets/images/x.svg new file mode 100644 index 0000000..1345e9b --- /dev/null +++ b/apps/web/src/assets/images/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/assets/images/xl_uni.png b/apps/web/src/assets/images/xl_uni.png new file mode 100644 index 0000000..86e43cb Binary files /dev/null and b/apps/web/src/assets/images/xl_uni.png differ diff --git a/apps/web/src/assets/svg/app-store-badge.svg b/apps/web/src/assets/svg/app-store-badge.svg new file mode 100644 index 0000000..6b74a93 --- /dev/null +++ b/apps/web/src/assets/svg/app-store-badge.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/avax_logo.svg b/apps/web/src/assets/svg/avax_logo.svg new file mode 100644 index 0000000..041ded0 --- /dev/null +++ b/apps/web/src/assets/svg/avax_logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/base_background_icon.svg b/apps/web/src/assets/svg/base_background_icon.svg new file mode 100644 index 0000000..16b66d8 --- /dev/null +++ b/apps/web/src/assets/svg/base_background_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/blank_token.svg b/apps/web/src/assets/svg/blank_token.svg new file mode 100644 index 0000000..8d0e038 --- /dev/null +++ b/apps/web/src/assets/svg/blank_token.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/bnb-logo.svg b/apps/web/src/assets/svg/bnb-logo.svg new file mode 100644 index 0000000..f39dba1 --- /dev/null +++ b/apps/web/src/assets/svg/bnb-logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/bolt.svg b/apps/web/src/assets/svg/bolt.svg new file mode 100644 index 0000000..2e09866 --- /dev/null +++ b/apps/web/src/assets/svg/bolt.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/assets/svg/candlestick-chart-icon.svg b/apps/web/src/assets/svg/candlestick-chart-icon.svg new file mode 100644 index 0000000..1766d9d --- /dev/null +++ b/apps/web/src/assets/svg/candlestick-chart-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/celo_logo.svg b/apps/web/src/assets/svg/celo_logo.svg new file mode 100644 index 0000000..bd24e97 --- /dev/null +++ b/apps/web/src/assets/svg/celo_logo.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/apps/web/src/assets/svg/contract-interaction.svg b/apps/web/src/assets/svg/contract-interaction.svg new file mode 100644 index 0000000..0ba8b77 --- /dev/null +++ b/apps/web/src/assets/svg/contract-interaction.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/apps/web/src/assets/svg/dot_line.svg b/apps/web/src/assets/svg/dot_line.svg new file mode 100644 index 0000000..f8a0f9b --- /dev/null +++ b/apps/web/src/assets/svg/dot_line.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/expando-icon-closed.svg b/apps/web/src/assets/svg/expando-icon-closed.svg new file mode 100644 index 0000000..63311a8 --- /dev/null +++ b/apps/web/src/assets/svg/expando-icon-closed.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/expando-icon-opened.svg b/apps/web/src/assets/svg/expando-icon-opened.svg new file mode 100644 index 0000000..4ea0cc9 --- /dev/null +++ b/apps/web/src/assets/svg/expando-icon-opened.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/eye.svg b/apps/web/src/assets/svg/eye.svg new file mode 100644 index 0000000..40bb466 --- /dev/null +++ b/apps/web/src/assets/svg/eye.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/web/src/assets/svg/fiat_mask.svg b/apps/web/src/assets/svg/fiat_mask.svg new file mode 100644 index 0000000..46ca98f --- /dev/null +++ b/apps/web/src/assets/svg/fiat_mask.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/web/src/assets/svg/filter.svg b/apps/web/src/assets/svg/filter.svg new file mode 100644 index 0000000..3cfaf73 --- /dev/null +++ b/apps/web/src/assets/svg/filter.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/gas-icon.svg b/apps/web/src/assets/svg/gas-icon.svg new file mode 100644 index 0000000..d90e095 --- /dev/null +++ b/apps/web/src/assets/svg/gas-icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/web/src/assets/svg/line-chart-icon.svg b/apps/web/src/assets/svg/line-chart-icon.svg new file mode 100644 index 0000000..7664a78 --- /dev/null +++ b/apps/web/src/assets/svg/line-chart-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/logo.svg b/apps/web/src/assets/svg/logo.svg new file mode 100644 index 0000000..d93197e --- /dev/null +++ b/apps/web/src/assets/svg/logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/matic-token-icon.svg b/apps/web/src/assets/svg/matic-token-icon.svg new file mode 100644 index 0000000..8fc95da --- /dev/null +++ b/apps/web/src/assets/svg/matic-token-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/moonpay.svg b/apps/web/src/assets/svg/moonpay.svg new file mode 100644 index 0000000..3f0e2c7 --- /dev/null +++ b/apps/web/src/assets/svg/moonpay.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/assets/svg/papers-text.svg b/apps/web/src/assets/svg/papers-text.svg new file mode 100644 index 0000000..dfd7400 --- /dev/null +++ b/apps/web/src/assets/svg/papers-text.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/phantom-icon.svg b/apps/web/src/assets/svg/phantom-icon.svg new file mode 100644 index 0000000..3d9309f --- /dev/null +++ b/apps/web/src/assets/svg/phantom-icon.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/play-store-badge.svg b/apps/web/src/assets/svg/play-store-badge.svg new file mode 100644 index 0000000..b6a5ea0 --- /dev/null +++ b/apps/web/src/assets/svg/play-store-badge.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/assets/svg/search.svg b/apps/web/src/assets/svg/search.svg new file mode 100644 index 0000000..fb9275b --- /dev/null +++ b/apps/web/src/assets/svg/search.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/web/src/assets/svg/socks.svg b/apps/web/src/assets/svg/socks.svg new file mode 100644 index 0000000..e755ba0 --- /dev/null +++ b/apps/web/src/assets/svg/socks.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/static_route.svg b/apps/web/src/assets/svg/static_route.svg new file mode 100644 index 0000000..2880c87 --- /dev/null +++ b/apps/web/src/assets/svg/static_route.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/tooltip_triangle.svg b/apps/web/src/assets/svg/tooltip_triangle.svg new file mode 100644 index 0000000..06f84dd --- /dev/null +++ b/apps/web/src/assets/svg/tooltip_triangle.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/assets/svg/uniswap_app_logo.svg b/apps/web/src/assets/svg/uniswap_app_logo.svg new file mode 100644 index 0000000..f44535c --- /dev/null +++ b/apps/web/src/assets/svg/uniswap_app_logo.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/uniswapx_error.svg b/apps/web/src/assets/svg/uniswapx_error.svg new file mode 100644 index 0000000..833f69c --- /dev/null +++ b/apps/web/src/assets/svg/uniswapx_error.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/svg/winter-uni.svg b/apps/web/src/assets/svg/winter-uni.svg new file mode 100644 index 0000000..9250e60 --- /dev/null +++ b/apps/web/src/assets/svg/winter-uni.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/assets/svg/x.svg b/apps/web/src/assets/svg/x.svg new file mode 100644 index 0000000..9f607a1 --- /dev/null +++ b/apps/web/src/assets/svg/x.svg @@ -0,0 +1 @@ + diff --git a/apps/web/src/assets/wallets/brave-icon.svg b/apps/web/src/assets/wallets/brave-icon.svg new file mode 100644 index 0000000..b4fb5b6 --- /dev/null +++ b/apps/web/src/assets/wallets/brave-icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/browser-wallet-dark.svg b/apps/web/src/assets/wallets/browser-wallet-dark.svg new file mode 100644 index 0000000..af517ca --- /dev/null +++ b/apps/web/src/assets/wallets/browser-wallet-dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/assets/wallets/browser-wallet-light.svg b/apps/web/src/assets/wallets/browser-wallet-light.svg new file mode 100644 index 0000000..290de6a --- /dev/null +++ b/apps/web/src/assets/wallets/browser-wallet-light.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/assets/wallets/coinbase-icon.svg b/apps/web/src/assets/wallets/coinbase-icon.svg new file mode 100644 index 0000000..5e2727a --- /dev/null +++ b/apps/web/src/assets/wallets/coinbase-icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/ledger-icon.svg b/apps/web/src/assets/wallets/ledger-icon.svg new file mode 100644 index 0000000..078db76 --- /dev/null +++ b/apps/web/src/assets/wallets/ledger-icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/metamask-icon.svg b/apps/web/src/assets/wallets/metamask-icon.svg new file mode 100644 index 0000000..d574d33 --- /dev/null +++ b/apps/web/src/assets/wallets/metamask-icon.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/rabby-icon.svg b/apps/web/src/assets/wallets/rabby-icon.svg new file mode 100644 index 0000000..d61f871 --- /dev/null +++ b/apps/web/src/assets/wallets/rabby-icon.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/trustwallet-icon.svg b/apps/web/src/assets/wallets/trustwallet-icon.svg new file mode 100644 index 0000000..b574317 --- /dev/null +++ b/apps/web/src/assets/wallets/trustwallet-icon.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/apps/web/src/assets/wallets/uniswap-wallet-icon.png b/apps/web/src/assets/wallets/uniswap-wallet-icon.png new file mode 100644 index 0000000..9f9f89c Binary files /dev/null and b/apps/web/src/assets/wallets/uniswap-wallet-icon.png differ diff --git a/apps/web/src/assets/wallets/walletconnect-icon.svg b/apps/web/src/assets/wallets/walletconnect-icon.svg new file mode 100644 index 0000000..3f524a4 --- /dev/null +++ b/apps/web/src/assets/wallets/walletconnect-icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/components/AccountDetails/TransactionSummary.tsx b/apps/web/src/components/AccountDetails/TransactionSummary.tsx new file mode 100644 index 0000000..6258d8d --- /dev/null +++ b/apps/web/src/components/AccountDetails/TransactionSummary.tsx @@ -0,0 +1,379 @@ +import { Trans } from '@lingui/macro' +import { Fraction, TradeType } from '@uniswap/sdk-core' +import { BigNumber } from 'ethers/lib/ethers' +import JSBI from 'jsbi' + +import { nativeOnChain } from '../../constants/tokens' +import { useCurrency, useToken } from '../../hooks/Tokens' +import useENSName from '../../hooks/useENSName' +import { VoteOption } from '../../state/governance/types' +import { + AddLiquidityV2PoolTransactionInfo, + AddLiquidityV3PoolTransactionInfo, + ApproveTransactionInfo, + ClaimTransactionInfo, + CollectFeesTransactionInfo, + CreateV3PoolTransactionInfo, + DelegateTransactionInfo, + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + ExecuteTransactionInfo, + MigrateV2LiquidityToV3TransactionInfo, + QueueTransactionInfo, + RemoveLiquidityV3TransactionInfo, + SendTransactionInfo, + TransactionInfo, + TransactionType, + VoteTransactionInfo, + WrapTransactionInfo, +} from '../../state/transactions/types' + +function formatAmount(amountRaw: string, decimals: number, sigFigs: number): string { + return new Fraction(amountRaw, JSBI.exponentiate(JSBI.BigInt(10), JSBI.BigInt(decimals))).toSignificant(sigFigs) +} + +function FormattedCurrencyAmount({ + rawAmount, + symbol, + decimals, + sigFigs, +}: { + rawAmount: string + symbol: string + decimals: number + sigFigs: number +}) { + return ( + <> + {formatAmount(rawAmount, decimals, sigFigs)} {symbol} + + ) +} + +function FormattedCurrencyAmountManaged({ + rawAmount, + currencyId, + sigFigs = 6, +}: { + rawAmount: string + currencyId: string + sigFigs: number +}) { + const currency = useCurrency(currencyId) + return currency ? ( + + ) : null +} + +function ClaimSummary({ info: { recipient, uniAmountRaw } }: { info: ClaimTransactionInfo }) { + const { ENSName } = useENSName() + return typeof uniAmountRaw === 'string' ? ( + + Claim for{' '} + {ENSName ?? recipient} + + ) : ( + Claim UNI reward for {ENSName ?? recipient} + ) +} + +function SubmitProposalTransactionSummary() { + return Submit new proposal +} + +function ApprovalSummary({ info }: { info: ApproveTransactionInfo }) { + const token = useToken(info.tokenAddress) + + return BigNumber.from(info.amount)?.eq(0) ? ( + Revoke {token?.symbol} + ) : ( + Approve {token?.symbol} + ) +} + +function VoteSummary({ info }: { info: VoteTransactionInfo }) { + const proposalKey = `${info.governorAddress}/${info.proposalId}` + if (info.reason && info.reason.trim().length > 0) { + switch (info.decision) { + case VoteOption.For: + return Vote for proposal {proposalKey} + case VoteOption.Abstain: + return Vote to abstain on proposal {proposalKey} + case VoteOption.Against: + return Vote against proposal {proposalKey} + } + } else { + switch (info.decision) { + case VoteOption.For: + return ( + + Vote for proposal {proposalKey} with reason "{info.reason}" + + ) + case VoteOption.Abstain: + return ( + + Vote to abstain on proposal {proposalKey} with reason "{info.reason}" + + ) + case VoteOption.Against: + return ( + + Vote against proposal {proposalKey} with reason "{info.reason}" + + ) + } + } +} + +function QueueSummary({ info }: { info: QueueTransactionInfo }) { + const proposalKey = `${info.governorAddress}/${info.proposalId}` + return Queue proposal {proposalKey}. +} + +function ExecuteSummary({ info }: { info: ExecuteTransactionInfo }) { + const proposalKey = `${info.governorAddress}/${info.proposalId}` + return Execute proposal {proposalKey}. +} + +function DelegateSummary({ info: { delegatee } }: { info: DelegateTransactionInfo }) { + const { ENSName } = useENSName(delegatee) + return Delegate voting power to {ENSName ?? delegatee} +} + +function WrapSummary({ info: { chainId, currencyAmountRaw, unwrapped } }: { info: WrapTransactionInfo }) { + const native = chainId ? nativeOnChain(chainId) : undefined + + if (unwrapped) { + return ( + + Unwrap{' '} + {' '} + to {native?.symbol ?? 'ETH'} + + ) + } else { + return ( + + Wrap{' '} + {' '} + to {native?.wrapped?.symbol ?? 'WETH'} + + ) + } +} + +function DepositLiquidityStakingSummary() { + // not worth rendering the tokens since you can should no longer deposit liquidity in the staking contracts + // todo: deprecate and delete the code paths that allow this, show user more information + return Deposit liquidity +} + +function WithdrawLiquidityStakingSummary() { + return Withdraw deposited liquidity +} + +function MigrateLiquidityToV3Summary({ + info: { baseCurrencyId, quoteCurrencyId }, +}: { + info: MigrateV2LiquidityToV3TransactionInfo +}) { + const baseCurrency = useCurrency(baseCurrencyId) + const quoteCurrency = useCurrency(quoteCurrencyId) + + return ( + + Migrate {baseCurrency?.symbol}/{quoteCurrency?.symbol} liquidity to V3 + + ) +} + +function CreateV3PoolSummary({ info: { quoteCurrencyId, baseCurrencyId } }: { info: CreateV3PoolTransactionInfo }) { + const baseCurrency = useCurrency(baseCurrencyId) + const quoteCurrency = useCurrency(quoteCurrencyId) + + return ( + + Create {baseCurrency?.symbol}/{quoteCurrency?.symbol} V3 pool + + ) +} + +function CollectFeesSummary({ info: { currencyId0, currencyId1 } }: { info: CollectFeesTransactionInfo }) { + const currency0 = useCurrency(currencyId0) + const currency1 = useCurrency(currencyId1) + + return ( + + Collect {currency0?.symbol}/{currency1?.symbol} fees + + ) +} + +function RemoveLiquidityV3Summary({ + info: { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, +}: { + info: RemoveLiquidityV3TransactionInfo +}) { + return ( + + Remove{' '} + and{' '} + + + ) +} + +function AddLiquidityV3PoolSummary({ + info: { createPool, quoteCurrencyId, baseCurrencyId }, +}: { + info: AddLiquidityV3PoolTransactionInfo +}) { + const baseCurrency = useCurrency(baseCurrencyId) + const quoteCurrency = useCurrency(quoteCurrencyId) + + return createPool ? ( + + Create pool and add {baseCurrency?.symbol}/{quoteCurrency?.symbol} V3 liquidity + + ) : ( + + Add {baseCurrency?.symbol}/{quoteCurrency?.symbol} V3 liquidity + + ) +} + +function AddLiquidityV2PoolSummary({ + info: { quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw, baseCurrencyId }, +}: { + info: AddLiquidityV2PoolTransactionInfo +}) { + return ( + + Add {' '} + and {' '} + to Uniswap V2 + + ) +} + +function SendSummary({ info }: { info: SendTransactionInfo }) { + return ( + + Sent + to{' '} + {info.recipient} + + ) +} + +function SwapSummary({ info }: { info: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo }) { + if (info.tradeType === TradeType.EXACT_INPUT) { + return ( + + Swap exactly{' '} + {' '} + for{' '} + + + ) + } else { + return ( + + Swap{' '} + {' '} + for exactly{' '} + + + ) + } +} + +export function TransactionSummary({ info }: { info: TransactionInfo }) { + switch (info.type) { + case TransactionType.ADD_LIQUIDITY_V3_POOL: + return + + case TransactionType.ADD_LIQUIDITY_V2_POOL: + return + + case TransactionType.CLAIM: + return + + case TransactionType.DEPOSIT_LIQUIDITY_STAKING: + return + + case TransactionType.WITHDRAW_LIQUIDITY_STAKING: + return + + case TransactionType.SWAP: + return + + case TransactionType.APPROVAL: + return + + case TransactionType.VOTE: + return + + case TransactionType.DELEGATE: + return + + case TransactionType.WRAP: + return + + case TransactionType.CREATE_V3_POOL: + return + + case TransactionType.MIGRATE_LIQUIDITY_V3: + return + + case TransactionType.COLLECT_FEES: + return + + case TransactionType.REMOVE_LIQUIDITY_V3: + return + + case TransactionType.QUEUE: + return + + case TransactionType.EXECUTE: + return + + case TransactionType.SUBMIT_PROPOSAL: + return + + case TransactionType.SEND: + return + } +} diff --git a/apps/web/src/components/AccountDrawer/ActionTile.tsx b/apps/web/src/components/AccountDrawer/ActionTile.tsx new file mode 100644 index 0000000..5dcf067 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/ActionTile.tsx @@ -0,0 +1,124 @@ +import { ButtonEmphasis, ButtonSize, LoadingButtonSpinner, ThemeButton } from 'components/Button' +import Column from 'components/Column' +import Row from 'components/Row' +import Tooltip from 'components/Tooltip' +import { SupportArticleURL } from 'constants/supportArticles' +import { ReactNode, useReducer } from 'react' +import { Info } from 'react-feather' +import { Text } from 'rebass' +import styled from 'styled-components' +import { ExternalLink } from 'theme/components' +import { ThemedText } from 'theme/components/text' + +const Container = styled(Column)` + position: relative; + height: 100%; + width: 100%; +` +const Tile = styled(ThemeButton)` + height: 100%; + width: 100%; + + display: flex; + justify-content: flex-start; + padding: 12px; + + border-color: transparent; + border-radius: 16px; + border-style: solid; + border-width: 1px; +` +const StyledLoadingButtonSpinner = styled(LoadingButtonSpinner)` + height: 28px; + width: 28px; + fill: ${({ theme }) => theme.accent1}; +` +const ActionName = styled(Text)` + font-size: 16px; + font-style: normal; + font-weight: 535; + line-height: 24px; +` +const ErrorContainer = styled(Row)` + width: 100%; + position: absolute; + bottom: -24px; + display: flex; + justify-content: center; + align-items: center; +` +const ErrorText = styled(ThemedText.LabelMicro)` + color: ${({ theme }) => theme.neutral2}; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +` +const ErrorLink = styled(ExternalLink)` + align-items: center; + display: flex; + height: 14px; + justify-content: center; + margin-left: 6px; + width: 14px; +` +const StyledInfoIcon = styled(Info)` + height: 12px; + width: 12px; + flex: 1 1 auto; + stroke: ${({ theme }) => theme.neutral2}; +` +export function ActionTile({ + dataTestId, + Icon, + name, + onClick, + loading, + disabled, + error, + errorMessage, + errorTooltip, +}: { + dataTestId: string + Icon: ReactNode + name: string + onClick: () => void + loading?: boolean + disabled?: boolean + error?: boolean + errorMessage?: string + errorTooltip?: string +}) { + const [showTooltip, toggleTooltip] = useReducer((isOpen) => !isOpen, false) + + return ( + + + + {loading ? : Icon} + {name} + + + {error && ( + + {errorMessage} + + + + + + + )} + + ) +} diff --git a/apps/web/src/components/AccountDrawer/AnalyticsToggle.tsx b/apps/web/src/components/AccountDrawer/AnalyticsToggle.tsx new file mode 100644 index 0000000..de616d8 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/AnalyticsToggle.tsx @@ -0,0 +1,18 @@ +import { t } from '@lingui/macro' +import { allowAnalyticsAtom } from 'analytics' +import { useAtom } from 'jotai' + +import { SettingsToggle } from './SettingsToggle' + +export function AnalyticsToggle() { + const [allowAnalytics, updateAllowAnalytics] = useAtom(allowAnalyticsAtom) + + return ( + void updateAllowAnalytics((value) => !value)} + /> + ) +} diff --git a/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx new file mode 100644 index 0000000..03deb69 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/AuthenticatedHeader.tsx @@ -0,0 +1,255 @@ +import { t, Trans } from '@lingui/macro' +import { BrowserEvent, InterfaceElementName, InterfaceEventName, SharedEventName } from '@uniswap/analytics-events' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { sendAnalyticsEvent, TraceEvent } from 'analytics' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Column from 'components/Column' +import { CreditCardIcon } from 'components/Icons/CreditCard' +import { ImagesIcon } from 'components/Icons/Images' +import { Power } from 'components/Icons/Power' +import { Settings } from 'components/Icons/Settings' +import Row, { AutoRow } from 'components/Row' +import { LoadingBubble } from 'components/Tokens/loading' +import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' +import { getConnection } from 'connection' +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import useENSName from 'hooks/useENSName' +import { useProfilePageState, useSellAsset, useWalletCollections } from 'nft/hooks' +import { ProfilePageStateType } from 'nft/types' +import { useCallback, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAppDispatch } from 'state/hooks' +import { setRecentConnectionDisconnected } from 'state/user/reducer' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { useUnitagByAddressWithoutFlag } from 'uniswap/src/features/unitags/hooksWithoutFlags' +import { isPathBlocked } from 'utils/blockedPaths' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { useCloseModal, useFiatOnrampAvailability, useOpenModal, useToggleModal } from '../../state/application/hooks' +import { ApplicationModal } from '../../state/application/reducer' +import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks' +import { useCachedPortfolioBalancesQuery } from '../PrefetchBalancesWrapper/PrefetchBalancesWrapper' +import { ActionTile } from './ActionTile' +import IconButton, { IconHoverText, IconWithConfirmTextButton } from './IconButton' +import MiniPortfolio from './MiniPortfolio' +import { useToggleAccountDrawer } from './MiniPortfolio/hooks' +import { portfolioFadeInAnimation } from './MiniPortfolio/PortfolioRow' +import { Status } from './Status' + +const AuthenticatedHeaderWrapper = styled.div` + padding: 20px 16px; + display: flex; + flex-direction: column; + flex: 1; +` + +const WalletButton = styled(ThemeButton)` + border-radius: 12px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 4px; + color: white; + border: none; +` + +const UNIButton = styled(WalletButton)` + border-radius: 12px; + padding-top: 10px; + padding-bottom: 10px; + margin-top: 4px; + color: white; + border: none; + background: linear-gradient(to right, #9139b0 0%, #4261d6 100%); +` + +const IconContainer = styled.div` + display: flex; + align-items: center; + & > a, + & > button { + margin-right: 8px; + } + + & > button:last-child { + margin-right: 0px; + ${IconHoverText}:last-child { + left: 0px; + } + } +` + +const HeaderWrapper = styled.div` + margin-bottom: 20px; + display: flex; + justify-content: space-between; + align-items: flex-start; +` + +const FadeInColumn = styled(Column)` + ${portfolioFadeInAnimation} +` + +const PortfolioDrawerContainer = styled(Column)` + flex: 1; +` + +export default function AuthenticatedHeader({ account, openSettings }: { account: string; openSettings: () => void }) { + const { connector } = useWeb3React() + const { ENSName } = useENSName(account) + const dispatch = useAppDispatch() + const navigate = useNavigate() + const closeModal = useCloseModal() + const setSellPageState = useProfilePageState((state) => state.setProfilePageState) + const resetSellAssets = useSellAsset((state) => state.reset) + const clearCollectionFilters = useWalletCollections((state) => state.clearCollectionFilters) + const shouldShowBuyFiatButton = !isPathBlocked('/buy') + const { formatNumber, formatDelta } = useFormatter() + + const shouldDisableNFTRoutes = useDisableNFTRoutes() + + const unclaimedAmount: CurrencyAmount | undefined = useUserUnclaimedAmount(account) + const isUnclaimed = useUserHasAvailableClaim(account) + const connection = getConnection(connector) + const openClaimModal = useToggleModal(ApplicationModal.ADDRESS_CLAIM) + const disconnect = useCallback(() => { + connector.deactivate?.() + connector.resetState() + dispatch(setRecentConnectionDisconnected()) + }, [connector, dispatch]) + + const toggleWalletDrawer = useToggleAccountDrawer() + + const navigateToProfile = useCallback(() => { + toggleWalletDrawer() + resetSellAssets() + setSellPageState(ProfilePageStateType.VIEWING) + clearCollectionFilters() + navigate('/nfts/profile') + closeModal() + }, [clearCollectionFilters, closeModal, navigate, resetSellAssets, setSellPageState, toggleWalletDrawer]) + + const openFiatOnrampModal = useOpenModal(ApplicationModal.FIAT_ONRAMP) + const openFoRModalWithAnalytics = useCallback(() => { + toggleWalletDrawer() + sendAnalyticsEvent(InterfaceEventName.FIAT_ONRAMP_WIDGET_OPENED) + openFiatOnrampModal() + }, [openFiatOnrampModal, toggleWalletDrawer]) + + const [shouldCheck, setShouldCheck] = useState(false) + const { + available: fiatOnrampAvailable, + availabilityChecked: fiatOnrampAvailabilityChecked, + error, + loading: fiatOnrampAvailabilityLoading, + } = useFiatOnrampAvailability(shouldCheck, openFoRModalWithAnalytics) + + const handleBuyCryptoClick = useCallback(() => { + if (!fiatOnrampAvailabilityChecked) { + setShouldCheck(true) + } else if (fiatOnrampAvailable) { + openFoRModalWithAnalytics() + } + }, [fiatOnrampAvailabilityChecked, fiatOnrampAvailable, openFoRModalWithAnalytics]) + const disableBuyCryptoButton = Boolean( + error || (!fiatOnrampAvailable && fiatOnrampAvailabilityChecked) || fiatOnrampAvailabilityLoading + ) + + const { data: portfolioBalances } = useCachedPortfolioBalancesQuery({ account }) + const portfolio = portfolioBalances?.portfolios?.[0] + const totalBalance = portfolio?.tokensTotalDenominatedValue?.value + const absoluteChange = portfolio?.tokensTotalDenominatedValueChange?.absolute?.value + const percentChange = portfolio?.tokensTotalDenominatedValueChange?.percentage?.value + const [showDisconnectConfirm, setShowDisconnectConfirm] = useState(false) + + const { unitag } = useUnitagByAddressWithoutFlag(account, Boolean(account)) + + return ( + + + + + + + + + + + + {totalBalance !== undefined ? ( + + + {formatNumber({ + input: totalBalance, + type: NumberType.PortfolioBalance, + })} + + + {absoluteChange !== 0 && percentChange && ( + <> + + + {`${formatNumber({ + input: Math.abs(absoluteChange as number), + type: NumberType.PortfolioBalance, + })} (${formatDelta(percentChange)})`} + + + )} + + + ) : ( + + + + + )} + + {shouldShowBuyFiatButton && ( + } + name={t`Buy`} + onClick={handleBuyCryptoClick} + disabled={disableBuyCryptoButton} + loading={fiatOnrampAvailabilityLoading} + error={Boolean(!fiatOnrampAvailable && fiatOnrampAvailabilityChecked)} + errorMessage={t`Restricted region`} + errorTooltip={t`Moonpay is not available in some regions. Click to learn more.`} + /> + )} + {!shouldDisableNFTRoutes && ( + } + name={t`View NFTs`} + onClick={navigateToProfile} + /> + )} + + + {isUnclaimed && ( + + Claim {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} reward + + )} + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/DefaultMenu.tsx b/apps/web/src/components/AccountDrawer/DefaultMenu.tsx new file mode 100644 index 0000000..8dc404b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/DefaultMenu.tsx @@ -0,0 +1,95 @@ +import { useWeb3React } from '@web3-react/core' +import { LimitsMenu } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu' +import Column from 'components/Column' +import WalletModal from 'components/WalletModal' +import { useCallback, useEffect, useMemo } from 'react' +import styled from 'styled-components' + +import { sendAnalyticsEvent } from 'analytics' +import { atom, useAtom } from 'jotai' +import AuthenticatedHeader from './AuthenticatedHeader' +import LanguageMenu from './LanguageMenu' +import LocalCurrencyMenu from './LocalCurrencyMenu' +import SettingsMenu from './SettingsMenu' + +const DefaultMenuWrap = styled(Column)` + width: 100%; + height: 100%; +` + +export enum MenuState { + DEFAULT = 'default', + SETTINGS = 'settings', + LANGUAGE_SETTINGS = 'language_settings', + LOCAL_CURRENCY_SETTINGS = 'local_currency_settings', + LIMITS = 'limits', +} + +export const miniPortfolioMenuStateAtom = atom(MenuState.DEFAULT) + +function DefaultMenu({ drawerOpen }: { drawerOpen: boolean }) { + const { account } = useWeb3React() + const isAuthenticated = !!account + + const [menu, setMenu] = useAtom(miniPortfolioMenuStateAtom) + const openSettings = useCallback(() => setMenu(MenuState.SETTINGS), [setMenu]) + const closeSettings = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu]) + const openLanguageSettings = useCallback(() => setMenu(MenuState.LANGUAGE_SETTINGS), [setMenu]) + const openLocalCurrencySettings = useCallback(() => setMenu(MenuState.LOCAL_CURRENCY_SETTINGS), [setMenu]) + const closeLimitsMenu = useCallback(() => setMenu(MenuState.DEFAULT), [setMenu]) + + useEffect(() => { + if (!drawerOpen && menu !== MenuState.DEFAULT) { + // wait for the drawer to close before resetting the menu + const timer = setTimeout(() => { + closeSettings() + }, 250) + return () => clearTimeout(timer) + } + return + }, [drawerOpen, menu, closeSettings]) + + useEffect(() => { + if (menu === MenuState.DEFAULT) return // menu is closed, don't log + + sendAnalyticsEvent('Portfolio Menu Opened', { name: menu }) + }, [menu]) + + const SubMenu = useMemo(() => { + switch (menu) { + case MenuState.DEFAULT: + return isAuthenticated ? ( + + ) : ( + + ) + case MenuState.SETTINGS: + return ( + + ) + case MenuState.LANGUAGE_SETTINGS: + return + case MenuState.LOCAL_CURRENCY_SETTINGS: + return + case MenuState.LIMITS: + return isAuthenticated ? : null + } + }, [ + account, + closeLimitsMenu, + closeSettings, + isAuthenticated, + menu, + openLanguageSettings, + openLocalCurrencySettings, + openSettings, + ]) + + return {SubMenu} +} + +export default DefaultMenu diff --git a/apps/web/src/components/AccountDrawer/DownloadButton.tsx b/apps/web/src/components/AccountDrawer/DownloadButton.tsx new file mode 100644 index 0000000..2b0b702 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/DownloadButton.tsx @@ -0,0 +1,55 @@ +import { InterfaceElementName } from '@uniswap/analytics-events' +import { PropsWithChildren, useCallback } from 'react' +import styled from 'styled-components' +import { ClickableStyle } from 'theme/components' +import { openDownloadApp } from 'utils/openDownloadApp' + +const StyledButton = styled.button<{ padded?: boolean; branded?: boolean }>` + ${ClickableStyle} + width: 100%; + display: flex; + justify-content: center; + flex-direction: row; + gap: 6px; + padding: 8px 24px; + border: none; + white-space: nowrap; + background: ${({ theme, branded }) => (branded ? theme.accent1 : theme.surface3)}; + border-radius: 12px; + + font-weight: 535; + font-size: 14px; + line-height: 16px; + color: ${({ theme, branded }) => (branded ? theme.deprecated_accentTextLightPrimary : theme.neutral1)}; +` + +function BaseButton({ onClick, branded, children }: PropsWithChildren<{ onClick?: () => void; branded?: boolean }>) { + return ( + + {children} + + ) +} + +// Launches App/Play Store if on an iOS/Android device, else navigates to Uniswap Wallet microsite +export function DownloadButton({ + onClick, + text = 'Download', + element, +}: { + onClick?: () => void + text?: string + element: InterfaceElementName +}) { + const onButtonClick = useCallback(() => { + // handles any actions required by the parent, i.e. cancelling wallet connection attempt or dismissing an ad + onClick?.() + openDownloadApp({ element }) + }, [element, onClick]) + + return ( + + {text} + + ) +} diff --git a/apps/web/src/components/AccountDrawer/GitVersionRow.tsx b/apps/web/src/components/AccountDrawer/GitVersionRow.tsx new file mode 100644 index 0000000..140bd53 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/GitVersionRow.tsx @@ -0,0 +1,28 @@ +import { Trans } from '@lingui/macro' +import Tooltip from 'components/Tooltip' +import useCopyClipboard from 'hooks/useCopyClipboard' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const Container = styled.div` + width: 100%; + cursor: pointer; +` + +export function GitVersionRow() { + const [isCopied, staticCopy] = useCopyClipboard() + return process.env.REACT_APP_GIT_COMMIT_HASH ? ( + { + staticCopy(process.env.REACT_APP_GIT_COMMIT_HASH as string) + }} + > + + + Version: + {' ' + process.env.REACT_APP_GIT_COMMIT_HASH.substring(0, 6)} + + + + ) : null +} diff --git a/apps/web/src/components/AccountDrawer/IconButton.tsx b/apps/web/src/components/AccountDrawer/IconButton.tsx new file mode 100644 index 0000000..86c7dc8 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/IconButton.tsx @@ -0,0 +1,261 @@ +import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react' +import { Icon } from 'react-feather' +import styled, { DefaultTheme, css } from 'styled-components' +import useResizeObserver from 'use-resize-observer' + +import { TRANSITION_DURATIONS } from '../../theme/styles' +import Row from '../Row' + +export const IconHoverText = styled.span` + color: ${({ theme }) => theme.neutral1}; + position: absolute; + top: 28px; + border-radius: 8px; + transform: translateX(-50%); + opacity: 0; + font-size: 12px; + padding: 5px; + left: 10px; +` + +const getWidthTransition = ({ theme }: { theme: DefaultTheme }) => + `width ${theme.transition.timing.inOut} ${theme.transition.duration.fast}` + +const IconStyles = css<{ hideHorizontal?: boolean }>` + background-color: ${({ theme }) => theme.surface1}; + transition: ${getWidthTransition}; + border-radius: 12px; + display: flex; + padding: 0; + cursor: pointer; + position: relative; + overflow: hidden; + height: 32px; + width: ${({ hideHorizontal }) => (hideHorizontal ? '0px' : '32px')}; + color: ${({ theme }) => theme.neutral2}; + :hover { + background-color: ${({ theme }) => theme.surface2}; + transition: ${({ + theme: { + transition: { duration, timing }, + }, + }) => `${duration.fast} background-color ${timing.in}, ${getWidthTransition}`}; + + ${IconHoverText} { + opacity: 1; + } + } + :active { + background-color: ${({ theme }) => theme.surface1}; + transition: background-color ${({ theme }) => theme.transition.duration.fast} linear, ${getWidthTransition}; + } +` + +const IconBlockLink = styled.a` + ${IconStyles}; +` + +const IconBlockButton = styled.button` + ${IconStyles}; + border: none; + outline: none; +` + +const IconWrapper = styled.span` + width: 24px; + height: 24px; + margin: auto; + display: flex; + align-items: center; + justify-content: center; +` +interface BaseProps { + Icon: Icon + hideHorizontal?: boolean + children?: React.ReactNode +} + +interface IconLinkProps extends React.ComponentPropsWithoutRef<'a'>, BaseProps {} +interface IconButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseProps {} + +type IconBlockProps = React.ComponentPropsWithoutRef<'a' | 'button'> + +const IconBlock = forwardRef(function IconBlock(props, ref) { + if ('href' in props) { + return } {...props} /> + } + // ignoring 'button' 'type' conflict between React and styled-components + // @ts-ignore + return +}) + +const IconButton = ({ Icon, ...rest }: IconButtonProps | IconLinkProps) => ( + + + + + +) + +type IconWithTextProps = (IconButtonProps | IconLinkProps) & { + text: string + onConfirm?: () => void + onShowConfirm?: (on: boolean) => void + dismissOnHoverOut?: boolean + dismissOnHoverDurationMs?: number +} + +const TextWrapper = styled.div` + display: flex; + flex-shrink: 0; + overflow: hidden; + min-width: min-content; + font-weight: 485; +` + +const TextHide = styled.div` + overflow: hidden; + transition: width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast}, + max-width ${({ theme }) => theme.transition.timing.inOut} ${({ theme }) => theme.transition.duration.fast}; +` + +/** + * Allows for hiding and showing some text next to an IconButton + * Note that for width transitions to animate in CSS we need to always specify the width (no auto) + * so there's resize observing and measuring going on here. + */ +export const IconWithConfirmTextButton = ({ + Icon, + text, + onConfirm, + onShowConfirm, + onClick, + dismissOnHoverOut, + dismissOnHoverDurationMs = TRANSITION_DURATIONS.slow, + ...rest +}: IconWithTextProps) => { + const [showText, setShowTextWithoutCallback] = useState(false) + const [frame, setFrame] = useState() + const frameObserver = useResizeObserver() + const hiddenObserver = useResizeObserver() + + const setShowText = useCallback( + (val: boolean) => { + setShowTextWithoutCallback(val) + onShowConfirm?.(val) + }, + [onShowConfirm] + ) + + const dimensionsRef = useRef({ + frame: 0, + innerText: 0, + }) + const dimensions = (() => { + // once opened, we avoid updating it to prevent constant resize loop + if (!showText) { + dimensionsRef.current = { + frame: frameObserver.width || 0, + innerText: hiddenObserver.width || 0, + } + } + return dimensionsRef.current + })() + + // keyboard action to cancel + useEffect(() => { + if (typeof window === 'undefined') return + if (!showText || !frame) return + + const closeAndPrevent = (e: Event) => { + setShowText(false) + e.preventDefault() + e.stopPropagation() + } + + const clickHandler = (e: MouseEvent) => { + const { target } = e + const shouldClose = !(target instanceof HTMLElement) || !frame.contains(target) + if (shouldClose) { + closeAndPrevent(e) + } + } + + const keyHandler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeAndPrevent(e) + } + } + + window.addEventListener('click', clickHandler, { capture: true }) + window.addEventListener('keydown', keyHandler, { capture: true }) + + return () => { + window.removeEventListener('click', clickHandler, { capture: true }) + window.removeEventListener('keydown', keyHandler, { capture: true }) + } + }, [frame, setShowText, showText]) + + const xPad = showText ? 8 : 0 + const width = showText ? dimensions.frame + dimensions.innerText + xPad * 2 : 32 + const mouseLeaveTimeout = useRef() + + return ( + { + frameObserver.ref(node) + setFrame(node) + }} + {...rest} + style={{ + width, + paddingLeft: xPad, + paddingRight: xPad, + }} + // @ts-ignore MouseEvent is valid, its a subset of the two mouse events, + // even manually typing this all out more specifically it still gets mad about any casting for some reason + onClick={(e: MouseEvent) => { + if (showText) { + onConfirm?.() + } else { + onClick?.(e) + setShowText(!showText) + } + }} + {...(dismissOnHoverOut && { + onMouseLeave() { + mouseLeaveTimeout.current = setTimeout(() => { + setShowText(false) + }, dismissOnHoverDurationMs) + }, + onMouseEnter() { + if (mouseLeaveTimeout.current) { + clearTimeout(mouseLeaveTimeout.current) + } + }, + })} + > + + + + + + {/* this outer div is so we can cut it off but keep the inner text width full-width so we can measure it */} + + {text} + + + + ) +} + +export default IconButton diff --git a/apps/web/src/components/AccountDrawer/LanguageMenu.tsx b/apps/web/src/components/AccountDrawer/LanguageMenu.tsx new file mode 100644 index 0000000..8723dcc --- /dev/null +++ b/apps/web/src/components/AccountDrawer/LanguageMenu.tsx @@ -0,0 +1,43 @@ +import { Trans } from '@lingui/macro' +import { LOCALE_LABEL, SUPPORTED_LOCALES, SupportedLocale } from 'constants/locales' +import { useActiveLocale } from 'hooks/useActiveLocale' +import { useLocationLinkProps } from 'hooks/useLocationLinkProps' + +import { MenuColumn, MenuItem } from './shared' +import { SlideOutMenu } from './SlideOutMenu' + +function LanguageMenuItem({ locale, isActive }: { locale: SupportedLocale; isActive: boolean }) { + const { to, onClick } = useLocationLinkProps(locale) + + return ( + + ) +} + +export function LanguageMenuItems() { + const activeLocale = useActiveLocale() + + return ( + <> + {SUPPORTED_LOCALES.map((locale) => ( + + ))} + + ) +} + +export default function LanguageMenu({ onClose }: { onClose: () => void }) { + return ( + Language} onClose={onClose}> + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx new file mode 100644 index 0000000..1d5de96 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/LocalCurrencyMenu.tsx @@ -0,0 +1,61 @@ +import { Trans } from '@lingui/macro' +import { getLocalCurrencyIcon, SUPPORTED_LOCAL_CURRENCIES, SupportedLocalCurrency } from 'constants/localCurrencies' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { useLocalCurrencyLinkProps } from 'hooks/useLocalCurrencyLinkProps' +import { useMemo } from 'react' +import styled from 'styled-components' + +import { MenuColumn, MenuItem } from './shared' +import { SlideOutMenu } from './SlideOutMenu' + +const StyledLocalCurrencyIcon = styled.div` + width: 20px; + height: 20px; + border-radius: 100%; + overflow: hidden; +` + +function LocalCurrencyMenuItem({ + localCurrency, + isActive, +}: { + localCurrency: SupportedLocalCurrency + isActive: boolean +}) { + const { to, onClick } = useLocalCurrencyLinkProps(localCurrency) + + const LocalCurrencyIcon = useMemo(() => { + return {getLocalCurrencyIcon(localCurrency)} + }, [localCurrency]) + + if (!to) return null + + return ( + + ) +} + +export default function LocalCurrencyMenu({ onClose }: { onClose: () => void }) { + const activeLocalCurrency = useActiveLocalCurrency() + + return ( + Currency} onClose={onClose}> + + {SUPPORTED_LOCAL_CURRENCIES.map((localCurrency) => ( + + ))} + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx new file mode 100644 index 0000000..d9684dd --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/ActivityRow.tsx @@ -0,0 +1,98 @@ +import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events' +import { TraceEvent } from 'analytics' +import Column from 'components/Column' +import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' +import { LoaderV2 } from 'components/Icons/LoadingSpinner' +import Row from 'components/Row' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import useENSName from 'hooks/useENSName' +import { useCallback } from 'react' +import { SignatureType } from 'state/signatures/types' +import styled from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { shortenAddress } from 'utilities/src/addresses' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { PortfolioLogo } from '../PortfolioLogo' +import PortfolioRow from '../PortfolioRow' +import { useOpenOffchainActivityModal } from './OffchainActivityModal' +import { useTimeSince } from './parseRemote' +import { Activity } from './types' + +const ActivityRowDescriptor = styled(ThemedText.BodySmall)` + color: ${({ theme }) => theme.neutral2}; + ${EllipsisStyle} +` + +const StyledTimestamp = styled(ThemedText.BodySmall)` + color: ${({ theme }) => theme.neutral2}; + font-variant: small; + font-feature-settings: 'tnum' on, 'lnum' on, 'ss02' on; +` + +function StatusIndicator({ activity: { status, timestamp, offchainOrderDetails } }: { activity: Activity }) { + const timeSince = useTimeSince(timestamp) + + switch (status) { + case TransactionStatus.Pending: + if (offchainOrderDetails?.type === SignatureType.SIGN_LIMIT) return null + return + case TransactionStatus.Confirmed: + return {timeSince} + case TransactionStatus.Failed: + return + } +} + +export function ActivityRow({ activity }: { activity: Activity }) { + const { chainId, title, descriptor, logos, otherAccount, currencies, hash, prefixIconSrc, offchainOrderDetails } = + activity + + const openOffchainActivityModal = useOpenOffchainActivityModal() + + const { ENSName } = useENSName(otherAccount) + const explorerUrl = getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION) + + const onClick = useCallback(() => { + if (offchainOrderDetails) { + openOffchainActivityModal(offchainOrderDetails, { + inputLogo: activity?.logos?.[0], + outputLogo: activity?.logos?.[1], + }) + return + } + + window.open(getExplorerLink(chainId, hash, ExplorerDataType.TRANSACTION), '_blank') + }, [activity?.logos, chainId, hash, offchainOrderDetails, openOffchainActivityModal]) + + return ( + + + + + } + title={ + + {prefixIconSrc && } + {title} + + } + descriptor={ + + {descriptor} + {ENSName ?? shortenAddress(otherAccount)} + + } + right={} + onClick={onClick} + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.test.tsx new file mode 100644 index 0000000..735d079 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.test.tsx @@ -0,0 +1,67 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { + CancelLimitsDialog, + CancellationState, +} from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog' +import { DAI } from 'constants/tokens' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' +import { render, screen } from 'test-utils/render' + +const mockOrderDetails: UniswapXOrderDetails = { + type: SignatureType.SIGN_LIMIT, + orderHash: '0x1234', + status: UniswapXOrderStatus.OPEN, + swapInfo: { + isUniswapXOrder: true, + type: 1, + tradeType: 0, + inputCurrencyId: DAI.address, + outputCurrencyId: WETH9[ChainId.MAINNET].address, + inputCurrencyAmountRaw: '252074033564766400000', + expectedOutputCurrencyAmountRaw: '106841079134757921', + minimumOutputCurrencyAmountRaw: '106841079134757921', + settledOutputCurrencyAmountRaw: '106841079134757921', + }, + txHash: '0x1234', + encodedOrder: '0xencodedOrder', + id: '0x1234', + addedTime: 3, + chainId: ChainId.MAINNET, + expiry: 4, + offerer: '0x1234', +} + +jest.mock('hooks/useTransactionGasFee', () => ({ + ...jest.requireActual('hooks/useTransactionGasFee'), + useTransactionGasFee: jest.fn(), +})) + +jest.mock('components/AccountDrawer/MiniPortfolio/Activity/utils', () => ({ + useCreateCancelTransactionRequest: jest.fn(), +})) + +// TODO(WEB-3741): figure out why this test is failing locally, but not on CI +// eslint-disable-next-line jest/no-disabled-tests +describe.skip('CancelLimitsDialog', () => { + it('should render correctly', async () => { + const mockOnCancel = jest.fn() + const mockOnConfirm = jest.fn() + render( + + ) + + expect(document.body).toMatchSnapshot() + expect( + screen.getByText( + 'Your swap could execute before cancellation is processed. Your network costs cannot be refunded. Do you wish to proceed?' + ) + ).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx new file mode 100644 index 0000000..5e20123 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog.tsx @@ -0,0 +1,181 @@ +import { Plural, Trans } from '@lingui/macro' +import { ChainId, CurrencyAmount } from '@uniswap/sdk-core' +import { ConfirmedIcon, LogoContainer, SubmittedIcon } from 'components/AccountDrawer/MiniPortfolio/Activity/Logos' +import { useCancelLimitsGasEstimate } from 'components/AccountDrawer/MiniPortfolio/Limits/hooks/useCancelLimitsGasEstimate' +import GetHelp from 'components/Button/GetHelp' +import Column from 'components/Column' +import { Container, Dialog, DialogButtonType, DialogProps } from 'components/Dialog/Dialog' +import { LoaderV3 } from 'components/Icons/LoadingSpinner' +import Modal from 'components/Modal' +import Row from 'components/Row' +import { DetailLineItem } from 'components/swap/DetailLineItem' +import { nativeOnChain } from 'constants/tokens' +import { useStablecoinValue } from 'hooks/useStablecoinPrice' +import { Slash } from 'react-feather' +import { UniswapXOrderDetails } from 'state/signatures/types' +import styled, { useTheme } from 'styled-components' +import { CloseIcon, ExternalLink, ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const GasEstimateContainer = styled(Row)` + border-top: 1px solid ${({ theme }) => theme.surface3}; + margin-top: 16px; + padding-top: 16px; +` + +export enum CancellationState { + NOT_STARTED = 'not_started', + REVIEWING_CANCELLATION = 'reviewing_cancellation', + PENDING_SIGNATURE = 'pending_cancellation_signature', + PENDING_CONFIRMATION = 'pending_cancellation_confirmation', + CANCELLED = 'cancelled', +} + +type CancelLimitsDialogProps = Partial> & + Pick + +function useCancelLimitsDialogContent( + state: CancellationState, + orders: UniswapXOrderDetails[] +): { title?: JSX.Element; icon: JSX.Element } { + const theme = useTheme() + switch (state) { + case CancellationState.REVIEWING_CANCELLATION: + return { + title: ( + + ), + icon: , + } + case CancellationState.PENDING_SIGNATURE: + return { + title: Confirm cancellation, + icon: , + } + case CancellationState.PENDING_CONFIRMATION: + return { + title: Cancellation submitted, + icon: , + } + case CancellationState.CANCELLED: + return { + title: Cancellation Successful, + icon: , + } + default: + return { + title: undefined, + icon: , + } + } +} + +export function CancelLimitsDialog( + props: CancelLimitsDialogProps & { + orders: UniswapXOrderDetails[] + cancelState: CancellationState + cancelTxHash?: string + onConfirm: () => void + } +) { + const { orders, cancelState, cancelTxHash, onConfirm, onCancel } = props + + const { title, icon } = useCancelLimitsDialogContent(cancelState, orders) + + const gasEstimate = useCancelLimitsGasEstimate(orders) + + if ( + [CancellationState.PENDING_SIGNATURE, CancellationState.PENDING_CONFIRMATION, CancellationState.CANCELLED].includes( + cancelState + ) + ) { + const cancelSubmitted = + (cancelState === CancellationState.CANCELLED || cancelState === CancellationState.PENDING_CONFIRMATION) && + cancelTxHash + return ( + + + + + + + {icon} + + {title} + + + {cancelSubmitted ? ( + + View on Explorer + + ) : ( + + Proceed in your wallet + + )} + + + + ) + } else if (cancelState === CancellationState.REVIEWING_CANCELLATION) { + return ( + + + + + } + buttonsConfig={{ + left: { + title: Nevermind, + onClick: onCancel, + textColor: 'neutral1', + }, + right: { + title: Proceed, + onClick: onConfirm, + type: DialogButtonType.Error, + disabled: cancelState !== CancellationState.REVIEWING_CANCELLATION, + textColor: 'white', + }, + }} + /> + ) + } else { + // CancellationState.NOT_STARTED + return null + } +} + +function GasEstimateDisplay({ gasEstimateValue, chainId }: { gasEstimateValue?: string; chainId: ChainId }) { + const gasFeeCurrencyAmount = CurrencyAmount.fromRawAmount(nativeOnChain(chainId), gasEstimateValue ?? '0') + const gasFeeUSD = useStablecoinValue(gasFeeCurrencyAmount) + const { formatCurrencyAmount } = useFormatter() + const gasFeeFormatted = formatCurrencyAmount({ + amount: gasFeeUSD, + type: NumberType.PortfolioBalance, + }) + return ( + + Network cost, + Value: () => {gasEstimateValue ? gasFeeFormatted : '-'}, + }} + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx new file mode 100644 index 0000000..17159df --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/Logos.tsx @@ -0,0 +1,88 @@ +import { LoaderV3 } from 'components/Icons/LoadingSpinner' +import styled, { css, useTheme } from 'styled-components' +import { FadePresence, FadePresenceAnimationType } from 'theme/components/FadePresence' + +export const LogoContainer = styled.div` + height: 64px; + width: 64px; + position: relative; + display: flex; + justify-content: center; + border-radius: 50%; + overflow: visible; +` + +const LoadingIndicator = styled(LoaderV3)` + stroke: ${({ theme }) => theme.neutral3}; + fill: ${({ theme }) => theme.neutral3}; + width: calc(100% + 8px); + height: calc(100% + 8px); + top: -4px; + left: -4px; + position: absolute; +` + +export function LoadingIndicatorOverlay() { + return ( + + + + ) +} + +export function ConfirmedIcon({ className }: { className?: string }) { + const theme = useTheme() + return ( + + + + + + ) +} + +export function SubmittedIcon({ className }: { className?: string }) { + const theme = useTheme() + return ( + + + + + + ) +} + +const IconCss = css` + height: 64px; + width: 64px; +` + +export const AnimatedEntranceConfirmationIcon = styled(ConfirmedIcon)` + ${IconCss} +` + +export const AnimatedEntranceSubmittedIcon = styled(SubmittedIcon)` + ${IconCss} +` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx new file mode 100644 index 0000000..54cc5d3 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.test.tsx @@ -0,0 +1,123 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTimestamp' +import { DAI, WRAPPED_NATIVE_CURRENCY } from 'constants/tokens' +import { Maybe } from 'graphql/jsutils/Maybe' +import { useCurrency } from 'hooks/Tokens' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { SignatureType } from 'state/signatures/types' +import { mocked } from 'test-utils/mocked' +import { render } from 'test-utils/render' + +import { OrderContent } from './OffchainActivityModal' + +jest.mock('hooks/Tokens', () => ({ + useCurrency: jest.fn(), +})) +jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => ({ + formatTimestamp: jest.fn(), +})) + +describe('OrderContent', () => { + beforeEach(() => { + mocked(useCurrency).mockImplementation((currencyId: Maybe) => { + if (currencyId === WETH9[ChainId.MAINNET].address) { + return WRAPPED_NATIVE_CURRENCY[ChainId.MAINNET] + } else { + return DAI + } + }) + mocked(formatTimestamp).mockImplementation(() => { + return 'Mock Date' // This ensures consistent test behavior across local and CI + }) + }) + it('should render without error, filled order', () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + expect(container).toHaveTextContent('Order executed') + }) + it('should render without error, open order', () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + expect(container).toHaveTextContent('Order pending') + expect(container).toHaveTextContent('Cancel order') + }) + + it('should render without error, limit order', () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + expect(container).toHaveTextContent('Limit pending') + expect(container).toHaveTextContent('Cancel limit') + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx new file mode 100644 index 0000000..f6722e8 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal.tsx @@ -0,0 +1,396 @@ +import { Trans } from '@lingui/macro' +import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { + CancellationState, + CancelLimitsDialog, +} from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog' +import { formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTimestamp' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Column, { AutoColumn } from 'components/Column' +import { OpacityHoverState } from 'components/Common' +import Modal from 'components/Modal' +import Row from 'components/Row' +import { Field } from 'components/swap/constants' +import { SwapModalHeaderAmount } from 'components/swap/SwapModalHeaderAmount' +import { useCurrency } from 'hooks/Tokens' +import { useUSDPrice } from 'hooks/useUSDPrice' +import { atom } from 'jotai' +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { ReactNode, useCallback, useMemo, useState } from 'react' +import { ArrowDown, X } from 'react-feather' +import { useOrder } from 'state/signatures/hooks' +import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' +import styled, { useTheme } from 'styled-components' +import { Divider, ThemedText } from 'theme/components' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { sendAnalyticsEvent } from 'analytics' +import { cancelMultipleUniswapXOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/utils' +import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' +import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' +import { ContractTransaction } from 'ethers/lib/ethers' +import { useContract } from 'hooks/useContract' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types/Permit2' +import { PortfolioLogo } from '../PortfolioLogo' +import { OffchainOrderLineItem, OffchainOrderLineItemProps, OffchainOrderLineItemType } from './OffchainOrderLineItem' + +type Logos = { + inputLogo?: string + outputLogo?: string +} + +type SelectedOrderInfo = { + modalOpen?: boolean + order?: UniswapXOrderDetails + logos?: Logos +} + +const selectedOrderAtom = atom(undefined) + +export function useOpenOffchainActivityModal() { + const setSelectedOrder = useUpdateAtom(selectedOrderAtom) + + return useCallback( + (order: UniswapXOrderDetails, logos?: Logos) => { + sendAnalyticsEvent('UniswapX Order Details Sheet Opened', { + order: order.orderHash, + }) + setSelectedOrder({ order, logos, modalOpen: true }) + }, + [setSelectedOrder] + ) +} + +const Wrapper = styled(AutoColumn).attrs({ gap: 'md', grow: true })` + padding: 12px 20px 20px 20px; + width: 100%; + background-color: ${({ theme }) => theme.surface1}; +` + +const StyledXButton = styled(X)` + cursor: pointer; + justify-self: flex-end; + + color: ${({ theme }) => theme.neutral1}; + ${OpacityHoverState}; +` + +const OffchainModalDivider = styled(Divider)` + margin: 28px 0; +` + +const OffchainModalBottomButton = styled(ThemeButton)` + margin-top: 16px; + border-radius: 12px; +` + +const InsufficientFundsCopyContainer = styled(Row)` + margin-top: 16px; + padding: 12px; + border: 1.3px solid ${({ theme }) => theme.surface3}; + border-radius: 20px; + gap: 12px; + justify-content: space-between; + align-items: flex-start; +` + +const AlertIconContainer = styled.div` + display: flex; + flex-shrink: 0; + background-color: #1f1e02; + width: 40px; + height: 40px; + justify-content: center; + align-items: center; + border-radius: 12px; +` + +export function useOrderAmounts(order?: UniswapXOrderDetails): + | { + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + } + | undefined { + const inputCurrency = useCurrency(order?.swapInfo?.inputCurrencyId || 'ETH', order?.chainId) + const outputCurrency = useCurrency(order?.swapInfo?.outputCurrencyId || 'ETH', order?.chainId) + + if (!order || !order?.swapInfo) return undefined + + if (!inputCurrency || !outputCurrency) { + console.error(`Could not find token(s) for order ${order.txHash}`) + return undefined + } + + const { swapInfo } = order + + if (swapInfo.tradeType === TradeType.EXACT_INPUT) { + return { + inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.inputCurrencyAmountRaw), + outputAmount: CurrencyAmount.fromRawAmount( + outputCurrency, + swapInfo.settledOutputCurrencyAmountRaw ?? swapInfo.expectedOutputCurrencyAmountRaw + ), + } + } else { + return { + inputAmount: CurrencyAmount.fromRawAmount(inputCurrency, swapInfo.expectedInputCurrencyAmountRaw), + outputAmount: CurrencyAmount.fromRawAmount(outputCurrency, swapInfo.outputCurrencyAmountRaw), + } + } +} + +function getOrderTitle(order: UniswapXOrderDetails): ReactNode { + switch (order.status) { + case UniswapXOrderStatus.OPEN: + return order.type === SignatureType.SIGN_LIMIT ? Limit pending : Order pending + case UniswapXOrderStatus.EXPIRED: + return order.type === SignatureType.SIGN_LIMIT ? Limit expired : Order expired + case UniswapXOrderStatus.INSUFFICIENT_FUNDS: + case UniswapXOrderStatus.CANCELLED: + return order.type === SignatureType.SIGN_LIMIT ? Limit cancelled : Order cancelled + case UniswapXOrderStatus.FILLED: + return order.type === SignatureType.SIGN_LIMIT ? Limit executed : Order executed + default: + return null + } +} + +function useCancelOrder(order?: UniswapXOrderDetails): () => Promise { + const { provider } = useWeb3React() + const permit2 = useContract(PERMIT2_ADDRESS, PERMIT2_ABI, true) + return useCallback(async () => { + if (!order) return undefined + return await cancelMultipleUniswapXOrders({ + encodedOrders: [order.encodedOrder as string], + chainId: order.chainId, + provider, + permit2, + }) + }, [order, permit2, provider]) +} + +export function OrderContent({ + order, + logos, + onCancel, +}: { + order: UniswapXOrderDetails + logos?: Logos + onCancel?: () => void +}) { + const amounts = useOrderAmounts(order) + const amountsDefined = !!amounts?.inputAmount?.currency && !!amounts?.outputAmount?.currency + const fiatValueInput = useUSDPrice(amounts?.inputAmount) + const fiatValueOutput = useUSDPrice(amounts?.outputAmount) + const theme = useTheme() + + const explorerLink = order?.txHash + ? getExplorerLink(order.chainId, order.txHash, ExplorerDataType.TRANSACTION) + : undefined + + const createdAt = formatTimestamp(order.addedTime) + + const details: Array = useMemo(() => { + const details = [] + if (amountsDefined) { + details.push({ type: OffchainOrderLineItemType.EXCHANGE_RATE, amounts } as OffchainOrderLineItemProps) + } + if (order.status === UniswapXOrderStatus.OPEN) { + details.push({ + type: OffchainOrderLineItemType.EXPIRY, + order, + } as OffchainOrderLineItemProps) + } + details.push({ + type: OffchainOrderLineItemType.NETWORK_COST, + } as OffchainOrderLineItemProps) + if (explorerLink) { + details.push({ + type: OffchainOrderLineItemType.TRANSACTION_ID, + explorerLink, + order, + } as OffchainOrderLineItemProps) + } + return details + }, [amounts, amountsDefined, explorerLink, order]) + + const currencies = useMemo( + () => [amounts?.inputAmount.currency, amounts?.outputAmount.currency], + [amounts?.inputAmount.currency, amounts?.outputAmount.currency] + ) + + if (!amounts?.inputAmount || !amounts?.outputAmount) { + return null + } + return ( + + + + + {getOrderTitle(order)} + + {createdAt} + + + + + + + + + + + + {details.map((detail) => ( + + ))} + + {Boolean(order.status === UniswapXOrderStatus.OPEN && order.encodedOrder) && ( + + {order.type === SignatureType.SIGN_LIMIT ? Cancel limit : Cancel order} + + )} + {order.status === UniswapXOrderStatus.INSUFFICIENT_FUNDS ? ( + + + + + + + Insufficient balance + + + This order was canceled because your balance went below the input amount. + + + + ) : ( + + )} + + ) +} + +/* Returns the order currently selected in the UI synced with updates from order status polling */ +function useSyncedSelectedOrder(): UniswapXOrderDetails | undefined { + const selectedOrder = useAtomValue(selectedOrderAtom) + const localPendingOrder = useOrder(selectedOrder?.order?.orderHash ?? '') + + return useMemo(() => { + if (!selectedOrder?.order) return undefined + + return { + ...selectedOrder.order, + ...localPendingOrder, + } + }, [localPendingOrder, selectedOrder]) +} + +/** + * This is the modal that appears when you click on an X order in the activity tab. + * + * It needs to handle multiple types of X orders: + * - Pending orders initiated locally i.e. UniswapXOrderDetails + * - Pending/expired/cancelled orders initiated remotely and tracked locally i.e. SwapOrderDetailsParts from the Activity query + * - Filled orders i.e. TransactionDetailsParts from the Activity query. + * + * Because of this, we try to converge the different cases into the one type, UniswapXOrderDetails, + * which can be passed around within the Activity in the case of remote records. However, all the fields may not + * be defined in the remote cases. + */ +export function OffchainActivityModal() { + const selectedOrderAtomValue = useAtomValue(selectedOrderAtom) + const [cancelState, setCancelState] = useState(CancellationState.NOT_STARTED) + const [cancelTxHash, setCancelTxHash] = useState() + + const syncedSelectedOrder = useSyncedSelectedOrder() + const setSelectedOrder = useUpdateAtom(selectedOrderAtom) + + const reset = useCallback(() => { + setSelectedOrder((order) => order && { ...order, modalOpen: false }) + }, [setSelectedOrder]) + + const cancelOrder = useCancelOrder(syncedSelectedOrder) + + return ( + <> + {syncedSelectedOrder && selectedOrderAtomValue?.modalOpen && ( + { + setCancelState(CancellationState.NOT_STARTED) + if (cancelState !== CancellationState.REVIEWING_CANCELLATION) { + reset() + } + }} + onConfirm={async () => { + setCancelState(CancellationState.PENDING_SIGNATURE) + const transactions = await cancelOrder() + if (transactions && transactions.length > 0) { + setCancelState(CancellationState.PENDING_CONFIRMATION) + setCancelTxHash(transactions[0].hash) + try { + await transactions[0].wait(1) + } catch { + setCancelState(CancellationState.REVIEWING_CANCELLATION) + } + setCancelState(CancellationState.CANCELLED) + } else { + setCancelState(CancellationState.REVIEWING_CANCELLATION) + } + }} + cancelState={cancelState} + cancelTxHash={cancelTxHash} + /> + )} + + + + + Transaction details + + + + {syncedSelectedOrder && ( + { + setCancelState(CancellationState.REVIEWING_CANCELLATION) + }} + /> + )} + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx new file mode 100644 index 0000000..723a237 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.test.tsx @@ -0,0 +1,93 @@ +import { ChainId, CurrencyAmount, WETH9 } from '@uniswap/sdk-core' +import { DAI, USDC_MAINNET } from 'constants/tokens' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { SignatureType } from 'state/signatures/types' +import { render, screen } from 'test-utils/render' + +import { OffchainOrderLineItem, OffchainOrderLineItemType } from './OffchainOrderLineItem' + +describe('OffchainOrderLineItem', () => { + it('should render type EXCHANGE_RATE', () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + expect(screen.getByText('Rate')).toBeInTheDocument() + }) + + it('should render type EXPIRY', () => { + render( + + ) + expect(screen.getByText('Expiry')).toBeInTheDocument() + }) + + it('should render type NETWORK_COST', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + expect(screen.getByText('Network cost')).toBeInTheDocument() + }) + + it('should render type TRANSACTION_ID', () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + expect(screen.getByText('Transaction ID')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx new file mode 100644 index 0000000..8e1db6a --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/OffchainOrderLineItem.tsx @@ -0,0 +1,86 @@ +import { Trans } from '@lingui/macro' +import { Currency, CurrencyAmount, Price } from '@uniswap/sdk-core' +import { DetailLineItem, LineItemData } from 'components/swap/DetailLineItem' +import TradePrice from 'components/swap/TradePrice' +import { UniswapXOrderDetails } from 'state/signatures/types' +import { ExternalLink } from 'theme/components' +import { ellipseMiddle } from 'utilities/src/addresses' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { formatTimestamp } from '../formatTimestamp' + +export enum OffchainOrderLineItemType { + EXCHANGE_RATE = 'EXCHANGE_RATE', + EXPIRY = 'EXPIRY', + NETWORK_COST = 'NETWORK_COST', + TRANSACTION_ID = 'TRANSACTION_ID', +} + +export type OffchainOrderLineItemProps = + | { + type: OffchainOrderLineItemType.EXCHANGE_RATE + amounts: { + inputAmount: CurrencyAmount + outputAmount: CurrencyAmount + } + } + | { + type: OffchainOrderLineItemType.EXPIRY + order: UniswapXOrderDetails + } + | { + type: OffchainOrderLineItemType.TRANSACTION_ID + explorerLink: string + order: UniswapXOrderDetails + } + | { + type: OffchainOrderLineItemType.NETWORK_COST + } + +function useLineItem(details: OffchainOrderLineItemProps): LineItemData | undefined { + const { formatNumber } = useFormatter() + switch (details.type) { + case OffchainOrderLineItemType.EXCHANGE_RATE: + return { + Label: () => Rate, + Value: () => ( + + ), + } + case OffchainOrderLineItemType.EXPIRY: + return { + Label: () => Expiry, + Value: () => {details.order.expiry && formatTimestamp(details.order.expiry * 1000)}, + } + case OffchainOrderLineItemType.NETWORK_COST: + return { + Label: () => Network cost, + Value: () => {formatNumber({ input: 0, type: NumberType.FiatGasPrice })}, + } + case OffchainOrderLineItemType.TRANSACTION_ID: + return { + Label: () => Transaction ID, + Value: () => ( + {ellipseMiddle(details.order.txHash ?? '')} + ), + } + default: + return undefined + } +} + +export function OffchainOrderLineItem(props: OffchainOrderLineItemProps) { + const LineItem = useLineItem(props) + if (!LineItem) return null + + return +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelLimitsDialog.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelLimitsDialog.test.tsx.snap new file mode 100644 index 0000000..dce5e54 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/CancelLimitsDialog.test.tsx.snap @@ -0,0 +1,627 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CancelLimitsDialog should render correctly 1`] = ` +.c5 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: 100%; + padding: 4px 0px; +} + +.c9 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c6 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -webkit-justify-content: end; + -ms-flex-pack: end; + justify-content: end; + padding: 4px 0px; + gap: 10px; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c19 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + gap: 12px; +} + +.c14 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c16 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c11 { + color: #222222; + cursor: pointer; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c11:hover { + opacity: 0.6; +} + +.c11:active { + opacity: 0.4; +} + +.c7 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c7:hover { + opacity: 0.6; +} + +.c7:active { + opacity: 0.4; +} + +.c8 { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + border-radius: 16px; + padding: 4px 6px; + font-size: 14px; + font-weight: 485; + line-height: 20px; + background: #F9F9F9; + color: #7D7D7D; + stroke: none; +} + +.c8:hover { + background: #22222212; + color: #222222; + opacity: unset; +} + +.c8:hover path { + fill: #222222; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 24px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c3 { + width: 100%; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c23 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c20 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #22222212; + border-radius: 16px; + border: 0; + color: #222222; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 14px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 16px; + padding: 8px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c20:active .c22 { + background-color: #B8C0DC3d; +} + +.c20:focus .c22 { + background-color: #B8C0DC3d; +} + +.c20:hover .c22 { + background-color: #98A1C014; +} + +.c20:disabled { + cursor: default; + opacity: 0.6; +} + +.c20:disabled:active .c22, +.c20:disabled:focus .c22, +.c20:disabled:hover .c22 { + background-color: transparent; +} + +.c24 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #FF5F52; + border-radius: 16px; + border: 0; + color: #222222; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 14px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 16px; + padding: 8px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c24:active .c22 { + background-color: #B8C0DC3d; +} + +.c24:focus .c22 { + background-color: #B8C0DC3d; +} + +.c24:hover .c22 { + background-color: #98A1C014; +} + +.c24:disabled { + cursor: default; + opacity: 0.6; +} + +.c24:disabled:active .c22, +.c24:disabled:focus .c22, +.c24:disabled:hover .c22 { + background-color: transparent; +} + +.c0 { + will-change: transform,opacity; +} + +.c0[data-reach-dialog-overlay] { + z-index: 1040; + background-color: transparent; + overflow: hidden; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + overflow-y: scroll; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + background-color: rgba(0,0,0,0.60); +} + +.c1 { + overflow-y: auto; +} + +.c1[data-reach-dialog-content] { + margin: auto; + background-color: #F9F9F9; + border: 1px solid #22222212; + box-shadow: 8px 12px 20px rgba(51,53,72,0.04),4px 6px 12px rgba(51,53,72,0.02),4px 4px 8px rgba(51,53,72,0.04); + padding: 0px; + width: 50vw; + overflow-y: auto; + overflow-x: hidden; + max-width: 420px; + max-height: 90vh; + display: inline-table; + border-radius: 20px; +} + +.c4 { + background-color: #FFFFFF; + outline: 1px solid #22222212; + border-radius: 20px; + padding: 16px 24px 24px 24px; + width: 100%; +} + +.c13 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + background-color: #22222212; + width: 48px; + height: 48px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border-radius: 12px; +} + +.c15 { + font-size: 24px; + line-height: 32px; + text-align: center; + font-weight: 500; +} + +.c17 { + font-size: 16px; + font-weight: 500; + line-height: 24px; + -webkit-letter-spacing: 0em; + -moz-letter-spacing: 0em; + -ms-letter-spacing: 0em; + letter-spacing: 0em; + text-align: center; +} + +.c21 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + height: 40px; +} + +@media screen and (max-width:640px) { + .c0[data-reach-dialog-overlay] { + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + } +} + +@media screen and (max-width:768px) { + .c1[data-reach-dialog-content] { + width: 65vw; + } +} + +@media screen and (max-width:640px) { + .c1[data-reach-dialog-content] { + margin: 0; + width: 100vw; + border-radius: 20px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } +} + + + + +
+
+
+
+ +
+
+
+
+ + +`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap new file mode 100644 index 0000000..f1ac86b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainActivityModal.test.tsx.snap @@ -0,0 +1,1803 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OrderContent should render without error, filled order 1`] = ` +.c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c11 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 12px; +} + +.c17 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c6 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c7 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c22 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c22:hover { + opacity: 0.6; +} + +.c22:active { + opacity: 0.4; +} + +.c8 { + width: 100%; + height: 1px; + border-width: 0; + margin: 0; + background-color: #22222212; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c19 { + cursor: auto; + color: #7D7D7D; +} + +.c20 { + text-align: right; + overflow-wrap: break-word; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c4 img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; +} + +.c4 img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; +} + +.c4 img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c5 { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.c15 { + width: 36px; + height: 36px; + border-radius: 50%; +} + +.c14 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c23 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c24 { + line-height: 16px; +} + +.c21 { + background-color: transparent; + border: none; + cursor: pointer; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 0; + grid-template-columns: 1fr auto; + grid-gap: 0.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + text-align: left; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.c9 { + margin: 28px 0; +} + +
+ + +
+
+
+
+ + +
+
+
+
+ Order executed +
+
+ Mock Date +
+
+
+
+
+
+
+
+
+ 252.074 + + DAI +
+
+ - +
+
+
+
+
+ +
+
+
+ + + + +
+
+
+
+ 0.10684 + + WETH +
+
+ - +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ Rate +
+
+ +
+
+
+
+ Network cost +
+
+ + $0 + +
+
+
+
+ Transaction ID +
+ +
+
+
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
+ + +
+`; + +exports[`OrderContent should render without error, limit order 1`] = ` +.c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c11 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 12px; +} + +.c17 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c6 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c7 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c28 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c28:hover { + opacity: 0.6; +} + +.c28:active { + opacity: 0.4; +} + +.c8 { + width: 100%; + height: 1px; + border-width: 0; + margin: 0; + background-color: #22222212; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c25 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c22 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #22222212; + border-radius: 16px; + border: 0; + color: #222222; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 16px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 20px; + padding: 10px 12px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c22:active .c24 { + background-color: #B8C0DC3d; +} + +.c22:focus .c24 { + background-color: #B8C0DC3d; +} + +.c22:hover .c24 { + background-color: #98A1C014; +} + +.c22:disabled { + cursor: default; + opacity: 0.6; +} + +.c22:disabled:active .c24, +.c22:disabled:focus .c24, +.c22:disabled:hover .c24 { + background-color: transparent; +} + +.c19 { + cursor: auto; + color: #7D7D7D; +} + +.c20 { + text-align: right; + overflow-wrap: break-word; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c4 img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; +} + +.c4 img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; +} + +.c4 img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c5 { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.c15 { + width: 36px; + height: 36px; + border-radius: 50%; +} + +.c14 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c26 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c27 { + line-height: 16px; +} + +.c21 { + background-color: transparent; + border: none; + cursor: pointer; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 0; + grid-template-columns: 1fr auto; + grid-gap: 0.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + text-align: left; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.c9 { + margin: 28px 0; +} + +.c23 { + margin-top: 16px; + border-radius: 12px; +} + +
+ + +
+
+
+
+ + +
+
+
+
+ Limit pending +
+
+ Mock Date +
+
+
+
+
+
+
+
+
+ 252.074 + + DAI +
+
+ - +
+
+
+
+
+ +
+
+
+ + + + +
+
+
+
+ 0.10684 + + WETH +
+
+ - +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ Rate +
+
+ +
+
+
+
+ Expiry +
+
+ +
+
+
+
+ Network cost +
+
+ + $0 + +
+
+
+ +
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
+ + +
+`; + +exports[`OrderContent should render without error, open order 1`] = ` +.c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c11 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 12px; +} + +.c17 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c6 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c7 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c28 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c28:hover { + opacity: 0.6; +} + +.c28:active { + opacity: 0.4; +} + +.c8 { + width: 100%; + height: 1px; + border-width: 0; + margin: 0; + background-color: #22222212; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c16 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c25 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c22 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #22222212; + border-radius: 16px; + border: 0; + color: #222222; + cursor: pointer; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + font-size: 16px; + font-weight: 535; + gap: 12px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + line-height: 20px; + padding: 10px 12px; + position: relative; + -webkit-transition: 150ms ease opacity; + transition: 150ms ease opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c22:active .c24 { + background-color: #B8C0DC3d; +} + +.c22:focus .c24 { + background-color: #B8C0DC3d; +} + +.c22:hover .c24 { + background-color: #98A1C014; +} + +.c22:disabled { + cursor: default; + opacity: 0.6; +} + +.c22:disabled:active .c24, +.c22:disabled:focus .c24, +.c22:disabled:hover .c24 { + background-color: transparent; +} + +.c19 { + cursor: auto; + color: #7D7D7D; +} + +.c20 { + text-align: right; + overflow-wrap: break-word; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c4 img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; +} + +.c4 img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; +} + +.c4 img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c5 { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.c15 { + width: 36px; + height: 36px; + border-radius: 50%; +} + +.c14 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c26 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c27 { + line-height: 16px; +} + +.c21 { + background-color: transparent; + border: none; + cursor: pointer; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 0; + grid-template-columns: 1fr auto; + grid-gap: 0.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + text-align: left; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.c9 { + margin: 28px 0; +} + +.c23 { + margin-top: 16px; + border-radius: 12px; +} + +
+ + +
+
+
+
+ + +
+
+
+
+ Order pending +
+
+ Mock Date +
+
+
+
+
+
+
+
+
+ 252.074 + + DAI +
+
+ - +
+
+
+
+
+ +
+
+
+ + + + +
+
+
+
+ 0.10684 + + WETH +
+
+ - +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ Rate +
+
+ +
+
+
+
+ Expiry +
+
+ +
+
+
+
+ Network cost +
+
+ + $0 + +
+
+
+ +
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
+ + +
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainOrderLineItem.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainOrderLineItem.test.tsx.snap new file mode 100644 index 0000000..6f0ccd1 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/OffchainOrderLineItem.test.tsx.snap @@ -0,0 +1,299 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OffchainOrderLineItem should render type EXCHANGE_RATE 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c3 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c4 { + cursor: auto; + color: #7D7D7D; +} + +.c5 { + text-align: right; + overflow-wrap: break-word; +} + +.c6 { + background-color: transparent; + border: none; + cursor: pointer; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 0; + grid-template-columns: 1fr auto; + grid-gap: 0.25rem; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + text-align: left; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + + + +
+
+ Rate +
+
+ +
+
+
+
+
+`; + +exports[`OffchainOrderLineItem should render type NETWORK_COST 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c3 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c4 { + cursor: auto; + color: #7D7D7D; +} + +.c5 { + text-align: right; + overflow-wrap: break-word; +} + + + +
+
+ Network cost +
+
+ + $0 + +
+
+
+
+
+`; + +exports[`OffchainOrderLineItem should render type TRANSACTION_ID 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c3 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c6 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c6:hover { + opacity: 0.6; +} + +.c6:active { + opacity: 0.4; +} + +.c4 { + cursor: auto; + color: #7D7D7D; +} + +.c5 { + text-align: right; + overflow-wrap: break-word; +} + + + +
+
+ Transaction ID +
+ +
+
+
+
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap new file mode 100644 index 0000000..2ec526c --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/__snapshots__/parseRemote.test.tsx.snap @@ -0,0 +1,454 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`parseRemote parseRemoteActivities should parse NFT Mint 1`] = ` +Object { + "chainId": 1, + "descriptor": "1 SomeCollectionName", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "imageUrl", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Minted", +} +`; + +exports[`parseRemote parseRemoteActivities should parse NFT approval 1`] = ` +Object { + "chainId": 1, + "descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Unknown Approval", +} +`; + +exports[`parseRemote parseRemoteActivities should parse NFT approval for all 1`] = ` +Object { + "chainId": 1, + "descriptor": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Unknown Approval", +} +`; + +exports[`parseRemote parseRemoteActivities should parse NFT receive 1`] = ` +Object { + "chainId": 1, + "currencies": undefined, + "descriptor": "1 SomeCollectionName from ", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "imageUrl", + ], + "nonce": 12345, + "otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Received", +} +`; + +exports[`parseRemote parseRemoteActivities should parse closed UniswapX order 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 DAI for 200 WETH", + "from": "someOfferer", + "hash": "someHash", + "logos": Array [ + "someUrl", + "someUrl", + ], + "offchainOrderDetails": Object { + "addedTime": 10000, + "chainId": 1, + "id": "someId", + "offerer": "someOfferer", + "orderHash": "someHash", + "status": "expired", + "swapInfo": Object { + "expectedOutputCurrencyAmountRaw": "200000000000000000000", + "inputCurrencyAmountRaw": "100000000000000000000", + "inputCurrencyId": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "isUniswapXOrder": true, + "minimumOutputCurrencyAmountRaw": "200000000000000000000", + "outputCurrencyId": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "settledOutputCurrencyAmountRaw": "200000000000000000000", + "tradeType": 0, + "type": 1, + }, + "txHash": "someHash", + "type": "signUniswapXOrder", + }, + "prefixIconSrc": "bolt.svg", + "status": "FAILED", + "statusMessage": "Your swap could not be fulfilled at this time. Please try again.", + "timestamp": 10000, + "title": "Swap expired", +} +`; + +exports[`parseRemote parseRemoteActivities should parse eth wrap 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + ExtendedEther { + "chainId": 1, + "decimals": 18, + "isNative": true, + "isToken": false, + "name": "Ethereum", + "symbol": "ETH", + }, + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 ETH for 100 WETH", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "https://token-icons.s3.amazonaws.com/eth.png", + "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Wrapped", +} +`; + +exports[`parseRemote parseRemoteActivities should parse moonpay purchase 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 WETH for 100", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "moonpay.svg", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Purchased", +} +`; + +exports[`parseRemote parseRemoteActivities should parse nft purchase 1`] = ` +Object { + "chainId": 1, + "descriptor": "1 SomeCollectionName", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "imageUrl", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Bought", +} +`; + +exports[`parseRemote parseRemoteActivities should parse receive 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 WETH from ", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + ], + "nonce": 12345, + "otherAccount": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Received", +} +`; + +exports[`parseRemote parseRemoteActivities should parse remove liquidity 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + ], + "descriptor": "100 WETH and 100 DAI", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + "logoUrl", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Removed Liquidity", +} +`; + +exports[`parseRemote parseRemoteActivities should parse send 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + ], + "descriptor": "100 DAI to ", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + ], + "nonce": 12345, + "otherAccount": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Sent", +} +`; + +exports[`parseRemote parseRemoteActivities should parse swap 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 DAI for 100 WETH", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Swapped", +} +`; + +exports[`parseRemote parseRemoteActivities should parse swap order 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + Token { + "address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "Wrapped Ether", + "sellFeeBps": undefined, + "symbol": "WETH", + }, + ], + "descriptor": "100 DAI for 100 WETH", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + ], + "nonce": 12345, + "offchainOrderDetails": Object { + "addedTime": 10000, + "chainId": 1, + "id": "transactionId", + "offerer": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "orderHash": "someHash", + "status": "filled", + "swapInfo": Object { + "expectedOutputCurrencyAmountRaw": "100000000000000000000", + "inputCurrencyAmountRaw": "100000000000000000000", + "inputCurrencyId": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "isUniswapXOrder": true, + "minimumOutputCurrencyAmountRaw": "100000000000000000000", + "outputCurrencyId": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "settledOutputCurrencyAmountRaw": "100000000000000000000", + "tradeType": 0, + "type": 1, + }, + "txHash": "someHash", + "type": "signUniswapXOrder", + }, + "prefixIconSrc": "bolt.svg", + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Swapped", +} +`; + +exports[`parseRemote parseRemoteActivities should parse token approval 1`] = ` +Object { + "chainId": 1, + "currencies": Array [ + Token { + "address": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "buyFeeBps": undefined, + "chainId": 1, + "decimals": 18, + "isNative": false, + "isToken": true, + "name": "DAI", + "sellFeeBps": undefined, + "symbol": "DAI", + }, + ], + "descriptor": "DAI", + "from": "0x50EC05ADe8280758E2077fcBC08D878D4aef79C3", + "hash": "someHash", + "isSpam": false, + "logos": Array [ + "logoUrl", + ], + "nonce": 12345, + "status": "CONFIRMED", + "timestamp": 10000, + "title": "Approved", +} +`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts new file mode 100644 index 0000000..c1c620b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/fixtures/activity.ts @@ -0,0 +1,576 @@ +import { ChainId, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, WETH9 } from '@uniswap/sdk-core' +import { DAI } from 'constants/tokens' +import { + AssetActivityPartsFragment, + Chain, + Currency, + NftApprovalPartsFragment, + NftApproveForAllPartsFragment, + NftStandard, + NftTransferPartsFragment, + SwapOrderStatus, + TokenApprovalPartsFragment, + TokenStandard, + TokenTransferPartsFragment, + TransactionDetailsPartsFragment, + TransactionDirection, + TransactionStatus, + TransactionType, +} from 'graphql/data/__generated__/types-and-hooks' + +import { MOONPAY_SENDER_ADDRESSES } from '../../constants' + +const MockOrderTimestamp = 10000 +const MockRecipientAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' +export const MockSenderAddress = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3' + +export const mockTransactionDetailsPartsFragment: TransactionDetailsPartsFragment = { + __typename: 'TransactionDetails', + id: 'tx123', + type: TransactionType.Swap, + from: '0xSenderAddress', + to: '0xRecipientAddress', + hash: '0xHashValue', + nonce: 123, + status: TransactionStatus.Confirmed, + assetChanges: [], +} + +const mockAssetActivityPartsFragment = { + __typename: 'AssetActivity', + id: 'activityId', + timestamp: MockOrderTimestamp, + chain: Chain.Ethereum, + details: { + __typename: 'SwapOrderDetails', + id: 'detailsId', + offerer: 'offererId', + hash: 'someHash', + inputTokenQuantity: '100', + outputTokenQuantity: '200', + orderStatus: SwapOrderStatus.Open, + inputToken: { + __typename: 'Token', + id: 'tokenId', + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + }, + outputToken: { + __typename: 'Token', + id: 'tokenId', + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + }, + }, +} + +const mockSwapOrderDetailsPartsFragment = { + __typename: 'SwapOrderDetails', + id: 'someId', + offerer: 'someOfferer', + hash: 'someHash', + inputTokenQuantity: '100', + outputTokenQuantity: '200', + orderStatus: SwapOrderStatus.Open, + inputToken: { + __typename: 'Token', + id: DAI.address, + name: 'DAI', + symbol: DAI.symbol, + address: DAI.address, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'projectId', + isSpam: false, + logo: { + __typename: 'Image', + id: 'imageId', + url: 'someUrl', + }, + }, + }, + outputToken: { + __typename: 'Token', + id: WETH9[1].address, + name: 'Wrapped Ether', + symbol: 'WETH', + address: WETH9[1].address, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'projectId', + isSpam: false, + logo: { + __typename: 'Image', + id: 'imageId', + url: 'someUrl', + }, + }, + }, +} + +const mockNftApprovalPartsFragment: NftApprovalPartsFragment = { + __typename: 'NftApproval', + id: 'approvalId', + nftStandard: NftStandard.Erc721, // Replace with actual enum value + approvedAddress: '0xApprovedAddress', + asset: { + __typename: 'NftAsset', + id: 'assetId', + name: 'SomeNftName', + tokenId: 'tokenId123', + nftContract: { + __typename: 'NftContract', + id: 'nftContractId', + chain: Chain.Ethereum, // Replace with actual enum value + address: '0xContractAddress', + }, + image: { + __typename: 'Image', + id: 'imageId', + url: 'imageUrl', + }, + collection: { + __typename: 'NftCollection', + id: 'collectionId', + name: 'SomeCollectionName', + }, + }, +} + +const mockNftApproveForAllPartsFragment: NftApproveForAllPartsFragment = { + __typename: 'NftApproveForAll', + id: 'approveForAllId', + nftStandard: NftStandard.Erc721, // Replace with actual enum value + operatorAddress: '0xOperatorAddress', + approved: true, + asset: { + __typename: 'NftAsset', + id: 'assetId', + name: 'SomeNftName', + tokenId: 'tokenId123', + nftContract: { + __typename: 'NftContract', + id: 'nftContractId', + chain: Chain.Ethereum, // Replace with actual enum value + address: '0xContractAddress', + }, + image: { + __typename: 'Image', + id: 'imageId', + url: 'imageUrl', + }, + collection: { + __typename: 'NftCollection', + id: 'collectionId', + name: 'SomeCollectionName', + }, + }, +} + +const mockNftTransferPartsFragment: NftTransferPartsFragment = { + __typename: 'NftTransfer', + id: 'transferId', + nftStandard: NftStandard.Erc721, + sender: MockSenderAddress, + recipient: MockRecipientAddress, + direction: TransactionDirection.Out, + asset: { + __typename: 'NftAsset', + id: 'assetId', + name: 'SomeNftName', + tokenId: 'tokenId123', + nftContract: { + __typename: 'NftContract', + id: 'nftContractId', + chain: Chain.Ethereum, + address: '0xContractAddress', + }, + image: { + __typename: 'Image', + id: 'imageId', + url: 'imageUrl', + }, + collection: { + __typename: 'NftCollection', + id: 'collectionId', + name: 'SomeCollectionName', + }, + }, +} + +const mockSpamNftTransferPartsFragment: NftTransferPartsFragment = { + ...mockNftTransferPartsFragment, + asset: { + ...mockNftTransferPartsFragment.asset, + isSpam: true, + }, +} + +export const mockTokenTransferOutPartsFragment: TokenTransferPartsFragment = { + __typename: 'TokenTransfer', + id: 'tokenTransferId', + tokenStandard: TokenStandard.Erc20, + quantity: '100', + sender: MockSenderAddress, + recipient: MockRecipientAddress, + direction: TransactionDirection.Out, + asset: { + __typename: 'Token', + id: DAI.address, + name: 'DAI', + symbol: 'DAI', + address: DAI.address, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'projectId', + isSpam: false, + logo: { + __typename: 'Image', + id: 'logoId', + url: 'logoUrl', + }, + }, + }, + transactedValue: { + __typename: 'Amount', + id: 'amountId', + currency: Currency.Usd, + value: 100, + }, +} + +const mockNativeTokenTransferOutPartsFragment: TokenTransferPartsFragment = { + __typename: 'TokenTransfer', + id: 'tokenTransferId', + asset: { + __typename: 'Token', + id: 'ETH', + name: 'Ether', + symbol: 'ETH', + address: undefined, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Native, + project: { + __typename: 'TokenProject', + id: 'Ethereum', + isSpam: false, + logo: { + __typename: 'Image', + id: 'ETH_logo', + url: 'https://token-icons.s3.amazonaws.com/eth.png', + }, + }, + }, + tokenStandard: TokenStandard.Native, + quantity: '0.25', + sender: MockSenderAddress, + recipient: MockRecipientAddress, + direction: TransactionDirection.Out, + transactedValue: { + __typename: 'Amount', + id: 'ETH_amount', + currency: Currency.Usd, + value: 399.0225, + }, +} + +const mockWrappedEthTransferInPartsFragment: TokenTransferPartsFragment = { + __typename: 'TokenTransfer', + id: 'tokenTransferId', + asset: { + __typename: 'Token', + id: 'WETH', + name: 'Wrapped Ether', + symbol: 'WETH', + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'weth_project_id', + isSpam: false, + logo: { + __typename: 'Image', + id: 'weth_image', + url: 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/logo.png', + }, + }, + }, + tokenStandard: TokenStandard.Erc20, + quantity: '0.25', + sender: MockSenderAddress, + recipient: MockRecipientAddress, + direction: TransactionDirection.In, + transactedValue: { + __typename: 'Amount', + id: 'mockWethAmountId', + currency: Currency.Usd, + value: 399.1334007875, + }, +} + +export const mockTokenTransferInPartsFragment: TokenTransferPartsFragment = { + __typename: 'TokenTransfer', + id: 'tokenTransferId', + tokenStandard: TokenStandard.Erc20, + quantity: '100', + sender: MockSenderAddress, + recipient: MockRecipientAddress, + direction: TransactionDirection.In, + asset: { + __typename: 'Token', + id: WETH9[1].address, + name: 'Wrapped Ether', + symbol: 'WETH', + address: WETH9[1].address, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'projectId', + isSpam: false, + logo: { + __typename: 'Image', + id: 'logoId', + url: 'logoUrl', + }, + }, + }, + transactedValue: { + __typename: 'Amount', + id: 'amountId', + currency: Currency.Usd, + value: 100, + }, +} + +const mockSpamTokenTransferInPartsFragment: TokenTransferPartsFragment = { + ...mockTokenTransferInPartsFragment, + asset: { + ...mockTokenTransferInPartsFragment.asset, + project: { + ...mockTokenTransferInPartsFragment.asset.project, + id: WETH9[1].address, + isSpam: true, + }, + }, +} + +const mockTokenApprovalPartsFragment: TokenApprovalPartsFragment = { + __typename: 'TokenApproval', + id: 'tokenApprovalId', + tokenStandard: TokenStandard.Erc20, + approvedAddress: DAI.address, + quantity: '50', + asset: { + __typename: 'Token', + id: 'tokenId', + name: 'DAI', + symbol: 'DAI', + address: DAI.address, + decimals: 18, + chain: Chain.Ethereum, + standard: TokenStandard.Erc20, + project: { + __typename: 'TokenProject', + id: 'projectId', + isSpam: false, + logo: { + __typename: 'Image', + id: 'logoId', + url: 'logoUrl', + }, + }, + }, +} + +export const MockOpenUniswapXOrder = { + ...mockAssetActivityPartsFragment, + details: mockSwapOrderDetailsPartsFragment, +} as AssetActivityPartsFragment + +export const MockClosedUniswapXOrder = { + ...mockAssetActivityPartsFragment, + details: { + ...mockSwapOrderDetailsPartsFragment, + orderStatus: SwapOrderStatus.Expired, + }, +} as AssetActivityPartsFragment + +const commonTransactionDetailsFields = { + __typename: 'TransactionDetails', + from: MockSenderAddress, + hash: 'someHash', + id: 'transactionId', + nonce: 12345, + status: TransactionStatus.Confirmed, + to: MockRecipientAddress, +} + +export const MockNFTApproval = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Approve, + assetChanges: [mockNftApprovalPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockNFTApprovalForAll = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Approve, + assetChanges: [mockNftApproveForAllPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockMint = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Mint, + assetChanges: [mockNftTransferPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockSpamMint = { + ...MockMint, + details: { + ...MockMint.details, + assetChanges: [mockSpamNftTransferPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockSwap = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Swap, + assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockSpamSwap = { + ...MockSwap, + details: { + ...MockSwap.details, + assetChanges: [mockTokenTransferOutPartsFragment, mockSpamTokenTransferInPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockSwapOrder = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.SwapOrder, + assetChanges: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockTokenApproval = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Approve, + assetChanges: [mockTokenApprovalPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockTokenSend = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Send, + assetChanges: [mockTokenTransferOutPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockTokenReceive = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Receive, + assetChanges: [mockTokenTransferInPartsFragment], + }, +} as AssetActivityPartsFragment + +export const MockRemoveLiquidity = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + to: NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[ChainId.MAINNET], + type: TransactionType.Receive, + assetChanges: [ + mockTokenTransferInPartsFragment, + { + ...mockTokenTransferOutPartsFragment, + direction: TransactionDirection.In, + }, + ], + }, +} as AssetActivityPartsFragment + +export const MockMoonpayPurchase = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Receive, + assetChanges: [ + { + ...mockTokenTransferInPartsFragment, + sender: MOONPAY_SENDER_ADDRESSES[0], + }, + ], + }, +} as AssetActivityPartsFragment + +export const MockNFTReceive = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Receive, + assetChanges: [ + { + ...mockNftTransferPartsFragment, + direction: TransactionDirection.In, + }, + ], + }, +} as AssetActivityPartsFragment + +export const MockNFTPurchase = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Swap, + assetChanges: [ + mockTokenTransferOutPartsFragment, + { + ...mockNftTransferPartsFragment, + direction: TransactionDirection.In, + }, + ], + }, +} as AssetActivityPartsFragment + +export const MockWrap = { + ...mockAssetActivityPartsFragment, + details: { + ...commonTransactionDetailsFields, + type: TransactionType.Lend, + assetChanges: [mockNativeTokenTransferOutPartsFragment, mockWrappedEthTransferInPartsFragment], + }, +} as AssetActivityPartsFragment diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts new file mode 100644 index 0000000..f5d16ec --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/hooks.ts @@ -0,0 +1,118 @@ +import { useLocalActivities } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal' +import { TransactionStatus, useActivityQuery } from 'graphql/data/__generated__/types-and-hooks' +import { useEffect, useMemo } from 'react' +import { usePendingOrders } from 'state/signatures/hooks' +import { SignatureType } from 'state/signatures/types' +import { usePendingTransactions, useTransactionCanceller } from 'state/transactions/hooks' +import { useFormatter } from 'utils/formatNumbers' + +import { GQL_MAINNET_CHAINS } from 'graphql/data/util' +import { parseRemoteActivities } from './parseRemote' +import { Activity, ActivityMap } from './types' + +/** Detects transactions from same account with the same nonce and different hash */ +function findCancelTx(localActivity: Activity, remoteMap: ActivityMap, account: string): string | undefined { + // handles locally cached tx's that were stored before we started tracking nonces + if (!localActivity.nonce || localActivity.status !== TransactionStatus.Pending) return undefined + + for (const remoteTx of Object.values(remoteMap)) { + if (!remoteTx) continue + + // A pending tx is 'cancelled' when another tx with the same account & nonce but different hash makes it on chain + if ( + remoteTx.nonce === localActivity.nonce && + remoteTx.from.toLowerCase() === account.toLowerCase() && + remoteTx.hash.toLowerCase() !== localActivity.hash.toLowerCase() && + remoteTx.chainId === localActivity.chainId + ) { + return remoteTx.hash + } + } + + return undefined +} + +/** Deduplicates local and remote activities */ +function combineActivities(localMap: ActivityMap = {}, remoteMap: ActivityMap = {}): Array { + const txHashes = [...new Set([...Object.keys(localMap), ...Object.keys(remoteMap)])] + + return txHashes.reduce((acc: Array, hash) => { + const localActivity = (localMap?.[hash] ?? {}) as Activity + const remoteActivity = (remoteMap?.[hash] ?? {}) as Activity + + if (localActivity.cancelled) { + // Hides misleading activities caused by cross-chain nonce collisions previously being incorrectly labelled as cancelled txs in redux + if (localActivity.chainId !== remoteActivity.chainId) { + acc.push(remoteActivity) + return acc + } + // Remote data only contains data of the cancel tx, rather than the original tx, so we prefer local data here + acc.push(localActivity) + } else { + // Generally prefer remote values to local value because i.e. remote swap amounts are on-chain rather than client-estimated + acc.push({ ...localActivity, ...remoteActivity } as Activity) + } + + return acc + }, []) +} + +export function useAllActivities(account: string) { + const { formatNumberOrString } = useFormatter() + const { data, loading, refetch } = useActivityQuery({ + variables: { account, chains: GQL_MAINNET_CHAINS }, + errorPolicy: 'all', + fetchPolicy: 'cache-first', + }) + + const localMap = useLocalActivities(account) + const remoteMap = useMemo( + () => parseRemoteActivities(data?.portfolios?.[0].assetActivities, account, formatNumberOrString), + [account, data?.portfolios, formatNumberOrString] + ) + const updateCancelledTx = useTransactionCanceller() + + /* Updates locally stored pendings tx's when remote data contains a conflicting cancellation tx */ + useEffect(() => { + if (!remoteMap) return + + Object.values(localMap).forEach((localActivity) => { + if (!localActivity) return + + const cancelHash = findCancelTx(localActivity, remoteMap, account) + + if (cancelHash) updateCancelledTx(localActivity.hash, localActivity.chainId, cancelHash) + }) + }, [account, localMap, remoteMap, updateCancelledTx]) + + const combinedActivities = useMemo(() => combineActivities(localMap, remoteMap ?? {}), [localMap, remoteMap]) + + return { loading, activities: combinedActivities, refetch } +} + +export function useOpenLimitOrders(account: string) { + const { activities, loading, refetch } = useAllActivities(account) + const openLimitOrders = + activities?.filter( + (activity) => + activity.offchainOrderDetails?.type === SignatureType.SIGN_LIMIT && + activity.status === TransactionStatus.Pending + ) ?? [] + return { + openLimitOrders, + loading, + refetch, + } +} + +export function usePendingActivity() { + const pendingTransactions = usePendingTransactions() + const pendingOrders = usePendingOrders() + + const pendingOrdersWithoutLimits = pendingOrders.filter((order) => order.type !== SignatureType.SIGN_LIMIT) + + const hasPendingActivity = pendingTransactions.length > 0 || pendingOrdersWithoutLimits.length > 0 + const pendingActivityCount = pendingTransactions.length + pendingOrdersWithoutLimits.length + + return { hasPendingActivity, pendingActivityCount } +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx new file mode 100644 index 0000000..f39fe05 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/index.tsx @@ -0,0 +1,92 @@ +import { MenuState, miniPortfolioMenuStateAtom } from 'components/AccountDrawer/DefaultMenu' +import { hideSpamAtom } from 'components/AccountDrawer/SpamToggle' +import Column from 'components/Column' +import { LoadingBubble } from 'components/Tokens/loading' +import { PollingInterval } from 'graphql/data/util' +import { atom, useAtom } from 'jotai' +import { useAtomValue } from 'jotai/utils' +import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' +import { useEffect, useMemo } from 'react' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton' +import { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' +import { useAccountDrawer } from '../hooks' +import { ActivityRow } from './ActivityRow' +import { useAllActivities } from './hooks' +import { createGroups } from './utils' + +const ActivityGroupWrapper = styled(Column)` + margin-top: 16px; + gap: 8px; +` + +const lastFetchedAtom = atom(0) + +const OpenLimitOrdersActivityButton = styled(OpenLimitOrdersButton)` + width: calc(100% - 32px); + margin: 0 16px -4px; +` + +export function ActivityTab({ account }: { account: string }) { + const [drawerOpen, toggleWalletDrawer] = useAccountDrawer() + const [, setMenu] = useAtom(miniPortfolioMenuStateAtom) + + const [lastFetched, setLastFetched] = useAtom(lastFetchedAtom) + const { activities, loading, refetch } = useAllActivities(account) + + // We only refetch remote activity if the user renavigates to the activity tab by changing tabs or opening the drawer + useEffect(() => { + const currentTime = Date.now() + if (!lastFetched) { + setLastFetched(currentTime) + } else if (drawerOpen && lastFetched && currentTime - lastFetched > PollingInterval.Slow) { + refetch() + setLastFetched(currentTime) + } + }, [drawerOpen, lastFetched, refetch, setLastFetched]) + + const hideSpam = useAtomValue(hideSpamAtom) + const activityGroups = useMemo(() => createGroups(activities, hideSpam), [activities, hideSpam]) + + if (activityGroups.length === 0) { + if (loading) { + return ( + <> + + + + ) + } else { + return ( + <> + setMenu(MenuState.LIMITS)} account={account} /> + + + ) + } + } else { + return ( + <> + {/* OpenLimitOrdersActivityButton is rendered outside of the wrapper to avoid the flash on loading */} + setMenu(MenuState.LIMITS)} account={account} /> + + {activityGroups.map((activityGroup) => ( + + + {activityGroup.title} + + + {activityGroup.transactions.map( + (activity) => + !(hideSpam && activity.isSpam) && + )} + + + ))} + + + ) + } +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts new file mode 100644 index 0000000..7fe7811 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.test.ts @@ -0,0 +1,579 @@ +import { ChainId, TradeType as MockTradeType, Token } from '@uniswap/sdk-core' +import { PERMIT2_ADDRESS } from '@uniswap/universal-router-sdk' +import { DAI as MockDAI, USDC_MAINNET as MockUSDC_MAINNET, USDT as MockUSDT, nativeOnChain } from 'constants/tokens' +import { TransactionStatus as MockTxStatus } from 'graphql/data/__generated__/types-and-hooks' +import { ChainTokenMap } from 'hooks/Tokens' +import { + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + TransactionType as MockTxType, + TransactionDetails, + TransactionInfo, +} from 'state/transactions/types' +import { renderHook } from 'test-utils/render' +import { useFormatter } from 'utils/formatNumbers' + +import { UniswapXOrderStatus } from '../../../../lib/hooks/orders/types' +import { SignatureDetails, SignatureType } from '../../../../state/signatures/types' +import { signatureToActivity, transactionToActivity, useLocalActivities } from './parseLocal' + +function mockSwapInfo( + type: MockTradeType, + inputCurrency: Token, + inputCurrencyAmountRaw: string, + outputCurrency: Token, + outputCurrencyAmountRaw: string +): ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo { + if (type === MockTradeType.EXACT_INPUT) { + return { + type: MockTxType.SWAP, + tradeType: MockTradeType.EXACT_INPUT, + inputCurrencyId: inputCurrency.address, + inputCurrencyAmountRaw, + outputCurrencyId: outputCurrency.address, + expectedOutputCurrencyAmountRaw: outputCurrencyAmountRaw, + minimumOutputCurrencyAmountRaw: outputCurrencyAmountRaw, + isUniswapXOrder: false, + } + } else { + return { + type: MockTxType.SWAP, + tradeType: MockTradeType.EXACT_OUTPUT, + inputCurrencyId: inputCurrency.address, + expectedInputCurrencyAmountRaw: inputCurrencyAmountRaw, + maximumInputCurrencyAmountRaw: inputCurrencyAmountRaw, + outputCurrencyId: outputCurrency.address, + outputCurrencyAmountRaw, + isUniswapXOrder: false, + } + } +} + +const mockAccount1 = '0x000000000000000000000000000000000000000001' +const mockAccount2 = '0x000000000000000000000000000000000000000002' +const mockChainId = ChainId.MAINNET +const mockSpenderAddress = PERMIT2_ADDRESS[mockChainId] +const mockCurrencyAmountRaw = '1000000000000000000' +const mockCurrencyAmountRawUSDC = '1000000' +const mockApprovalAmountRaw = '10000000' + +function mockHash(id: string, status: MockTxStatus = MockTxStatus.Confirmed) { + return id + status +} + +function mockCommonFields(id: string, account = mockAccount2, status: MockTxStatus) { + const hash = mockHash(id, status) + return { + hash, + from: account, + receipt: + status === MockTxStatus.Pending + ? undefined + : { + transactionHash: hash, + status: status === MockTxStatus.Confirmed ? 1 : 0, + }, + addedTime: 0, + } +} + +function mockMultiStatus(info: TransactionInfo, id: string): [TransactionDetails, number][] { + // Mocks a transaction with multiple statuses + return [ + [ + { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Pending) } as unknown as TransactionDetails, + mockChainId, + ], + [ + { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Confirmed) } as unknown as TransactionDetails, + mockChainId, + ], + [ + { info, ...mockCommonFields(id, mockAccount2, MockTxStatus.Failed) } as unknown as TransactionDetails, + mockChainId, + ], + ] +} + +const mockTokenAddressMap: ChainTokenMap = { + [mockChainId]: { + [MockDAI.address]: MockDAI, + [MockUSDC_MAINNET.address]: MockUSDC_MAINNET, + [MockUSDT.address]: MockUSDT, + }, +} + +jest.mock('../../../../hooks/Tokens', () => ({ + useAllTokensMultichain: () => mockTokenAddressMap, +})) + +jest.mock('../../../../state/transactions/hooks', () => { + return { + useMultichainTransactions: (): [TransactionDetails, number][] => { + return [ + [ + { + info: mockSwapInfo( + MockTradeType.EXACT_INPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + ...mockCommonFields('0x123', mockAccount1, MockTxStatus.Confirmed), + } as TransactionDetails, + mockChainId, + ], + ...mockMultiStatus( + mockSwapInfo( + MockTradeType.EXACT_OUTPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + '0xswap_exact_input' + ), + ...mockMultiStatus( + mockSwapInfo( + MockTradeType.EXACT_INPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + '0xswap_exact_output' + ), + ...mockMultiStatus( + { + type: MockTxType.APPROVAL, + tokenAddress: MockDAI.address, + spender: mockSpenderAddress, + amount: mockApprovalAmountRaw, + }, + '0xapproval' + ), + ...mockMultiStatus( + { + type: MockTxType.APPROVAL, + tokenAddress: MockUSDT.address, + spender: mockSpenderAddress, + amount: '0', + }, + '0xrevoke_approval' + ), + ...mockMultiStatus( + { + type: MockTxType.WRAP, + unwrapped: false, + currencyAmountRaw: mockCurrencyAmountRaw, + chainId: mockChainId, + }, + '0xwrap' + ), + ...mockMultiStatus( + { + type: MockTxType.WRAP, + unwrapped: true, + currencyAmountRaw: mockCurrencyAmountRaw, + chainId: mockChainId, + }, + '0xunwrap' + ), + ...mockMultiStatus( + { + type: MockTxType.ADD_LIQUIDITY_V3_POOL, + createPool: false, + baseCurrencyId: MockUSDC_MAINNET.address, + quoteCurrencyId: MockDAI.address, + feeAmount: 500, + expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, + expectedAmountQuoteRaw: mockCurrencyAmountRaw, + }, + '0xadd_liquidity_v3' + ), + ...mockMultiStatus( + { + type: MockTxType.REMOVE_LIQUIDITY_V3, + baseCurrencyId: MockUSDC_MAINNET.address, + quoteCurrencyId: MockDAI.address, + expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, + expectedAmountQuoteRaw: mockCurrencyAmountRaw, + }, + '0xremove_liquidity_v3' + ), + ...mockMultiStatus( + { + type: MockTxType.ADD_LIQUIDITY_V2_POOL, + baseCurrencyId: MockUSDC_MAINNET.address, + quoteCurrencyId: MockDAI.address, + expectedAmountBaseRaw: mockCurrencyAmountRawUSDC, + expectedAmountQuoteRaw: mockCurrencyAmountRaw, + }, + '0xadd_liquidity_v2' + ), + ...mockMultiStatus( + { + type: MockTxType.COLLECT_FEES, + currencyId0: MockUSDC_MAINNET.address, + currencyId1: MockDAI.address, + expectedCurrencyOwed0: mockCurrencyAmountRawUSDC, + expectedCurrencyOwed1: mockCurrencyAmountRaw, + }, + '0xcollect_fees' + ), + ...mockMultiStatus( + { + type: MockTxType.MIGRATE_LIQUIDITY_V3, + baseCurrencyId: MockUSDC_MAINNET.address, + quoteCurrencyId: MockDAI.address, + isFork: false, + }, + '0xmigrate_v3_liquidity' + ), + ] + }, + } +}) + +describe('parseLocalActivity', () => { + it('returns swap activity fields with known tokens, exact input', () => { + const { formatNumber } = renderHook(() => useFormatter()).result.current + + const details = { + info: mockSwapInfo( + MockTradeType.EXACT_INPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + receipt: { + transactionHash: '0x123', + status: 1, + }, + } as TransactionDetails + const chainId = ChainId.MAINNET + expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toEqual({ + chainId: 1, + currencies: [MockUSDC_MAINNET, MockDAI], + descriptor: '1.00 USDC for 1.00 DAI', + hash: undefined, + from: undefined, + status: 'CONFIRMED', + timestamp: NaN, + title: 'Swapped', + }) + }) + + it('returns swap activity fields with known tokens, exact output', () => { + const { formatNumber } = renderHook(() => useFormatter()).result.current + + const details = { + info: mockSwapInfo( + MockTradeType.EXACT_OUTPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + receipt: { + transactionHash: '0x123', + status: 1, + }, + } as TransactionDetails + const chainId = ChainId.MAINNET + expect(transactionToActivity(details, chainId, mockTokenAddressMap, formatNumber)).toMatchObject({ + chainId: 1, + currencies: [MockUSDC_MAINNET, MockDAI], + descriptor: '1.00 USDC for 1.00 DAI', + status: 'CONFIRMED', + title: 'Swapped', + }) + }) + + it('returns swap activity fields with unknown tokens', () => { + const { formatNumber } = renderHook(() => useFormatter()).result.current + + const details = { + info: mockSwapInfo( + MockTradeType.EXACT_INPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + receipt: { + transactionHash: '0x123', + status: 1, + }, + } as TransactionDetails + const chainId = ChainId.MAINNET + const tokens = {} as ChainTokenMap + expect(transactionToActivity(details, chainId, tokens, formatNumber)).toMatchObject({ + chainId: 1, + currencies: [undefined, undefined], + descriptor: 'Unknown for Unknown', + status: 'CONFIRMED', + title: 'Swapped', + }) + }) + + it('only returns activity for the current account', () => { + const account1Activites = renderHook(() => useLocalActivities(mockAccount1)).result.current + const account2Activites = renderHook(() => useLocalActivities(mockAccount2)).result.current + + expect(Object.values(account1Activites)).toHaveLength(1) + expect(Object.values(account2Activites)).toHaveLength(33) + }) + + it('Properly uses correct tense of activity title based on tx status', () => { + const activities = renderHook(() => useLocalActivities(mockAccount2)).result.current + + expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Pending)]?.title).toEqual('Swapping') + expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Confirmed)]?.title).toEqual('Swapped') + expect(activities[mockHash('0xswap_exact_input', MockTxStatus.Failed)]?.title).toEqual('Swap failed') + }) + + it('Adapts Swap exact input to Activity type', () => { + const hash = mockHash('0xswap_exact_input') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Swapped', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts Swap exact output to Activity type', () => { + const hash = mockHash('0xswap_exact_output') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Swapped', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} for 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts Approval to Activity type', () => { + const hash = mockHash('0xapproval') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockDAI], + title: 'Approved', + descriptor: MockDAI.symbol, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts Revoke Approval to Activity type', () => { + const hash = mockHash('0xrevoke_approval') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDT], + title: 'Revoked approval', + descriptor: MockUSDT.symbol, + hash, + status: MockTxStatus.Confirmed, + }) + }) + + it('Adapts Wrap to Activity type', () => { + const hash = mockHash('0xwrap') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + const native = nativeOnChain(mockChainId) + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [native, native.wrapped], + title: 'Wrapped', + descriptor: `1.00 ${native.symbol} for 1.00 ${native.wrapped.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts Unwrap to Activity type', () => { + const hash = mockHash('0xunwrap') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + const native = nativeOnChain(mockChainId) + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [native.wrapped, native], + title: 'Unwrapped', + descriptor: `1.00 ${native.wrapped.symbol} for 1.00 ${native.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts AddLiquidityV3 to Activity type', () => { + const hash = mockHash('0xadd_liquidity_v3') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Added liquidity', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts RemoveLiquidityV3 to Activity type', () => { + const hash = mockHash('0xremove_liquidity_v3') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Removed liquidity', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts RemoveLiquidityV2 to Activity type', () => { + const hash = mockHash('0xadd_liquidity_v2') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Added V2 liquidity', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts CollectFees to Activity type', () => { + const hash = mockHash('0xcollect_fees') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Collected fees', + descriptor: `1.00 ${MockUSDC_MAINNET.symbol} and 1.00 ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Adapts MigrateLiquidityV3 to Activity type', () => { + const hash = mockHash('0xmigrate_v3_liquidity') + const activity = renderHook(() => useLocalActivities(mockAccount2)).result.current[hash] + + expect(activity).toMatchObject({ + chainId: mockChainId, + currencies: [MockUSDC_MAINNET, MockDAI], + title: 'Migrated liquidity', + descriptor: `${MockUSDC_MAINNET.symbol} and ${MockDAI.symbol}`, + hash, + status: MockTxStatus.Confirmed, + from: mockAccount2, + }) + }) + + it('Signature to activity - returns undefined if is filled onchain order', () => { + const { formatNumber } = renderHook(() => useFormatter()).result.current + + expect( + signatureToActivity( + { + type: SignatureType.SIGN_UNISWAPX_ORDER, + status: UniswapXOrderStatus.FILLED, + } as SignatureDetails, + {}, + formatNumber + ) + ).toBeUndefined() + }) + + it('Signature to activity - returns activity if is cancelled onchain order', () => { + const { formatNumber } = renderHook(() => useFormatter()).result.current + + expect( + signatureToActivity( + { + type: SignatureType.SIGN_UNISWAPX_ORDER, + status: UniswapXOrderStatus.CANCELLED, + chainId: ChainId.MAINNET, + swapInfo: mockSwapInfo( + MockTradeType.EXACT_INPUT, + MockUSDC_MAINNET, + mockCurrencyAmountRawUSDC, + MockDAI, + mockCurrencyAmountRaw + ), + } as SignatureDetails, + { + [ChainId.MAINNET]: { + [MockUSDC_MAINNET.address]: MockUSDC_MAINNET, + [MockDAI.address]: MockDAI, + }, + }, + formatNumber + ) + ).toEqual({ + chainId: 1, + currencies: [MockUSDC_MAINNET, MockDAI], + descriptor: '1.00 USDC for 1.00 DAI', + from: undefined, + hash: undefined, + offchainOrderDetails: { + addedTime: undefined, + chainId: 1, + encodedOrder: undefined, + id: undefined, + offerer: undefined, + orderHash: undefined, + status: 'cancelled', + swapInfo: { + expectedOutputCurrencyAmountRaw: '1000000000000000000', + inputCurrencyAmountRaw: '1000000', + inputCurrencyId: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + isUniswapXOrder: false, + minimumOutputCurrencyAmountRaw: '1000000000000000000', + outputCurrencyId: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + tradeType: 0, + type: 1, + }, + txHash: undefined, + type: 'signUniswapXOrder', + }, + prefixIconSrc: undefined, + status: 'FAILED', + statusMessage: undefined, + timestamp: NaN, + title: 'Swap cancelled', + }) + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts new file mode 100644 index 0000000..d9159ab --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseLocal.ts @@ -0,0 +1,336 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { t } from '@lingui/macro' +import { ChainId, Currency, CurrencyAmount, TradeType } from '@uniswap/sdk-core' +import UniswapXBolt from 'assets/svg/bolt.svg' +import { nativeOnChain } from 'constants/tokens' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { ChainTokenMap, useAllTokensMultichain } from 'hooks/Tokens' +import { useMemo } from 'react' +import { isOnChainOrder, useAllSignatures } from 'state/signatures/hooks' +import { SignatureDetails, SignatureType } from 'state/signatures/types' +import { useMultichainTransactions } from 'state/transactions/hooks' +import { + AddLiquidityV2PoolTransactionInfo, + AddLiquidityV3PoolTransactionInfo, + ApproveTransactionInfo, + CollectFeesTransactionInfo, + CreateV3PoolTransactionInfo, + ExactInputSwapTransactionInfo, + ExactOutputSwapTransactionInfo, + MigrateV2LiquidityToV3TransactionInfo, + RemoveLiquidityV3TransactionInfo, + SendTransactionInfo, + TransactionDetails, + TransactionType, + WrapTransactionInfo, +} from 'state/transactions/types' +import { isAddress } from 'utilities/src/addresses' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { CancelledTransactionTitleTable, getActivityTitle, LimitOrderTextTable, OrderTextTable } from '../constants' +import { Activity, ActivityMap } from './types' + +type FormatNumberFunctionType = ReturnType['formatNumber'] + +function getCurrency(currencyId: string, chainId: ChainId, tokens: ChainTokenMap): Currency | undefined { + return currencyId === 'ETH' ? nativeOnChain(chainId) : tokens[chainId]?.[currencyId] +} + +function buildCurrencyDescriptor( + currencyA: Currency | undefined, + amtA: string, + currencyB: Currency | undefined, + amtB: string, + formatNumber: FormatNumberFunctionType, + delimiter = t`for` +) { + const formattedA = currencyA + ? formatNumber({ + input: parseFloat(CurrencyAmount.fromRawAmount(currencyA, amtA).toSignificant()), + type: NumberType.TokenNonTx, + }) + : t`Unknown` + const symbolA = currencyA?.symbol ?? '' + const formattedB = currencyB + ? formatNumber({ + input: parseFloat(CurrencyAmount.fromRawAmount(currencyB, amtB).toSignificant()), + type: NumberType.TokenNonTx, + }) + : t`Unknown` + const symbolB = currencyB?.symbol ?? '' + return [formattedA, symbolA, delimiter, formattedB, symbolB].filter(Boolean).join(' ') +} + +function parseSwap( + swap: ExactInputSwapTransactionInfo | ExactOutputSwapTransactionInfo, + chainId: ChainId, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Partial { + const tokenIn = getCurrency(swap.inputCurrencyId, chainId, tokens) + const tokenOut = getCurrency(swap.outputCurrencyId, chainId, tokens) + const [inputRaw, outputRaw] = + swap.tradeType === TradeType.EXACT_INPUT + ? [swap.inputCurrencyAmountRaw, swap.settledOutputCurrencyAmountRaw ?? swap.expectedOutputCurrencyAmountRaw] + : [swap.expectedInputCurrencyAmountRaw, swap.outputCurrencyAmountRaw] + + return { + descriptor: buildCurrencyDescriptor(tokenIn, inputRaw, tokenOut, outputRaw, formatNumber, undefined), + currencies: [tokenIn, tokenOut], + prefixIconSrc: swap.isUniswapXOrder ? UniswapXBolt : undefined, + } +} + +function parseWrap( + wrap: WrapTransactionInfo, + chainId: ChainId, + status: TransactionStatus, + formatNumber: FormatNumberFunctionType +): Partial { + const native = nativeOnChain(chainId) + const wrapped = native.wrapped + const [input, output] = wrap.unwrapped ? [wrapped, native] : [native, wrapped] + + const descriptor = buildCurrencyDescriptor( + input, + wrap.currencyAmountRaw, + output, + wrap.currencyAmountRaw, + formatNumber + ) + const title = getActivityTitle(TransactionType.WRAP, status, wrap.unwrapped) + const currencies = wrap.unwrapped ? [wrapped, native] : [native, wrapped] + + return { title, descriptor, currencies } +} + +function parseApproval( + approval: ApproveTransactionInfo, + chainId: ChainId, + tokens: ChainTokenMap, + status: TransactionStatus +): Partial { + const currency = getCurrency(approval.tokenAddress, chainId, tokens) + const descriptor = currency?.symbol ?? currency?.name ?? t`Unknown` + return { + title: getActivityTitle( + TransactionType.APPROVAL, + status, + BigNumber.from(approval.amount).eq(0) /* use alternate if it's a revoke */ + ), + descriptor, + currencies: [currency], + } +} + +type GenericLPInfo = Omit< + AddLiquidityV3PoolTransactionInfo | RemoveLiquidityV3TransactionInfo | AddLiquidityV2PoolTransactionInfo, + 'type' +> +function parseLP( + lp: GenericLPInfo, + chainId: ChainId, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Partial { + const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens) + const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens) + const [baseRaw, quoteRaw] = [lp.expectedAmountBaseRaw, lp.expectedAmountQuoteRaw] + const descriptor = buildCurrencyDescriptor(baseCurrency, baseRaw, quoteCurrency, quoteRaw, formatNumber, t`and`) + + return { descriptor, currencies: [baseCurrency, quoteCurrency] } +} + +function parseCollectFees( + collect: CollectFeesTransactionInfo, + chainId: ChainId, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Partial { + // Adapts CollectFeesTransactionInfo to generic LP type + const { + currencyId0: baseCurrencyId, + currencyId1: quoteCurrencyId, + expectedCurrencyOwed0: expectedAmountBaseRaw, + expectedCurrencyOwed1: expectedAmountQuoteRaw, + } = collect + return parseLP( + { baseCurrencyId, quoteCurrencyId, expectedAmountBaseRaw, expectedAmountQuoteRaw }, + chainId, + tokens, + formatNumber + ) +} + +function parseMigrateCreateV3( + lp: MigrateV2LiquidityToV3TransactionInfo | CreateV3PoolTransactionInfo, + chainId: ChainId, + tokens: ChainTokenMap +): Partial { + const baseCurrency = getCurrency(lp.baseCurrencyId, chainId, tokens) + const baseSymbol = baseCurrency?.symbol ?? t`Unknown` + const quoteCurrency = getCurrency(lp.quoteCurrencyId, chainId, tokens) + const quoteSymbol = quoteCurrency?.symbol ?? t`Unknown` + const descriptor = t`${baseSymbol} and ${quoteSymbol}` + + return { descriptor, currencies: [baseCurrency, quoteCurrency] } +} + +function parseSend( + send: SendTransactionInfo, + chainId: ChainId, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Partial { + const { currencyId, amount, recipient } = send + const currency = getCurrency(currencyId, chainId, tokens) + const formattedAmount = currency + ? formatNumber({ + input: parseFloat(CurrencyAmount.fromRawAmount(currency, amount).toSignificant()), + type: NumberType.TokenNonTx, + }) + : t`Unknown` + + return { + descriptor: `${formattedAmount} ${currency?.symbol} ${t`to`} `, + otherAccount: isAddress(recipient) || undefined, + currencies: [currency], + } +} + +export function getTransactionStatus(details: TransactionDetails): TransactionStatus { + return !details.receipt + ? TransactionStatus.Pending + : details.receipt.status === 1 || details.receipt?.status === undefined + ? TransactionStatus.Confirmed + : TransactionStatus.Failed +} + +export function transactionToActivity( + details: TransactionDetails, + chainId: ChainId, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Activity | undefined { + try { + const status = getTransactionStatus(details) + + const defaultFields = { + hash: details.hash, + chainId, + title: getActivityTitle(details.info.type, status), + status, + timestamp: (details.confirmedTime ?? details.addedTime) / 1000, + from: details.from, + nonce: details.nonce, + cancelled: details.cancelled, + } + + let additionalFields: Partial = {} + const info = details.info + if (info.type === TransactionType.SWAP) { + additionalFields = parseSwap(info, chainId, tokens, formatNumber) + } else if (info.type === TransactionType.APPROVAL) { + additionalFields = parseApproval(info, chainId, tokens, status) + } else if (info.type === TransactionType.WRAP) { + additionalFields = parseWrap(info, chainId, status, formatNumber) + } else if ( + info.type === TransactionType.ADD_LIQUIDITY_V3_POOL || + info.type === TransactionType.REMOVE_LIQUIDITY_V3 || + info.type === TransactionType.ADD_LIQUIDITY_V2_POOL + ) { + additionalFields = parseLP(info, chainId, tokens, formatNumber) + } else if (info.type === TransactionType.COLLECT_FEES) { + additionalFields = parseCollectFees(info, chainId, tokens, formatNumber) + } else if (info.type === TransactionType.MIGRATE_LIQUIDITY_V3 || info.type === TransactionType.CREATE_V3_POOL) { + additionalFields = parseMigrateCreateV3(info, chainId, tokens) + } else if (info.type === TransactionType.SEND) { + additionalFields = parseSend(info, chainId, tokens, formatNumber) + } + + const activity = { ...defaultFields, ...additionalFields } + + if (details.cancelled) { + activity.title = CancelledTransactionTitleTable[details.info.type] + activity.status = TransactionStatus.Confirmed + } + + return activity + } catch (error) { + console.debug(`Failed to parse transaction ${details.hash}`, error) + return undefined + } +} + +export function signatureToActivity( + signature: SignatureDetails, + tokens: ChainTokenMap, + formatNumber: FormatNumberFunctionType +): Activity | undefined { + switch (signature.type) { + case SignatureType.SIGN_UNISWAPX_ORDER: + case SignatureType.SIGN_LIMIT: { + // Only returns Activity items for orders that don't have an on-chain counterpart + if (isOnChainOrder(signature.status)) return undefined + + const { title, statusMessage, status } = + signature.type === SignatureType.SIGN_LIMIT + ? LimitOrderTextTable[signature.status] + : OrderTextTable[signature.status] + + return { + hash: signature.orderHash, + chainId: signature.chainId, + title, + status, + offchainOrderDetails: { + orderHash: signature.orderHash, + id: signature.id, + offerer: signature.offerer, + txHash: signature.txHash, + chainId: signature.chainId, + type: + signature.type === SignatureType.SIGN_LIMIT ? SignatureType.SIGN_LIMIT : SignatureType.SIGN_UNISWAPX_ORDER, + status: signature.status, + swapInfo: signature.swapInfo, + addedTime: signature.addedTime, + encodedOrder: signature.encodedOrder, + expiry: signature.expiry, + }, + timestamp: signature.addedTime / 1000, + from: signature.offerer, + statusMessage, + prefixIconSrc: UniswapXBolt, + ...parseSwap(signature.swapInfo, signature.chainId, tokens, formatNumber), + } + } + default: + return undefined + } +} + +export function useLocalActivities(account: string): ActivityMap { + const allTransactions = useMultichainTransactions() + const allSignatures = useAllSignatures() + const tokens = useAllTokensMultichain() + const { formatNumber } = useFormatter() + + return useMemo(() => { + const activityMap: ActivityMap = {} + for (const [transaction, chainId] of allTransactions) { + if (transaction.from !== account) continue + + const activity = transactionToActivity(transaction, chainId, tokens, formatNumber) + if (activity) activityMap[transaction.hash] = activity + } + + for (const signature of Object.values(allSignatures)) { + if (signature.offerer !== account) continue + + const activity = signatureToActivity(signature, tokens, formatNumber) + if (activity) activityMap[signature.id] = activity + } + + return activityMap + }, [account, allSignatures, allTransactions, formatNumber, tokens]) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx new file mode 100644 index 0000000..cc592aa --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.test.tsx @@ -0,0 +1,230 @@ +import { act, renderHook } from '@testing-library/react' +import ms from 'ms' + +import { + MockClosedUniswapXOrder, + MockMint, + MockMoonpayPurchase, + MockNFTApproval, + MockNFTApprovalForAll, + MockNFTPurchase, + MockNFTReceive, + MockOpenUniswapXOrder, + MockRemoveLiquidity, + MockSenderAddress, + MockSpamMint, + MockSpamSwap, + MockSwap, + MockSwapOrder, + MockTokenApproval, + MockTokenReceive, + MockTokenSend, + mockTokenTransferInPartsFragment, + mockTokenTransferOutPartsFragment, + mockTransactionDetailsPartsFragment, + MockWrap, +} from './fixtures/activity' +import { + offchainOrderDetailsFromGraphQLTransactionActivity, + parseRemoteActivities, + parseSwapAmounts, + useTimeSince, +} from './parseRemote' + +const swapOrderTokenChanges = { + TokenTransfer: [mockTokenTransferOutPartsFragment, mockTokenTransferInPartsFragment], + NftTransfer: [], + TokenApproval: [], + NftApproval: [], + NftApproveForAll: [], +} + +describe('parseRemote', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + describe('parseRemoteActivities', () => { + it('should not parse open UniswapX order', () => { + const result = parseRemoteActivities([MockOpenUniswapXOrder], '', jest.fn()) + expect(result).toEqual({}) + }) + it('should parse closed UniswapX order', () => { + const result = parseRemoteActivities([MockClosedUniswapXOrder], '', jest.fn()) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse NFT approval', () => { + const result = parseRemoteActivities([MockNFTApproval], '', jest.fn()) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse NFT approval for all', () => { + const result = parseRemoteActivities([MockNFTApprovalForAll], '', jest.fn()) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse NFT Mint', () => { + const result = parseRemoteActivities([MockMint], '', jest.fn()) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should mark spam when tx does not come from user and contains spam', () => { + const resultFromExternal = parseRemoteActivities([MockSpamMint], '', jest.fn()) + expect(resultFromExternal?.['someHash'].isSpam).toBeTruthy() + const resultFromUser = parseRemoteActivities([MockSpamMint], MockSenderAddress, jest.fn()) + expect(resultFromUser?.['someHash'].isSpam).toBeFalsy() + }) + it('should parse swap', () => { + const result = parseRemoteActivities([MockSwap], '', jest.fn().mockReturnValue('100')) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should not mark swaps for spam tokens as spam', () => { + const result = parseRemoteActivities([MockSpamSwap], '', jest.fn().mockReturnValue('100')) + expect(result?.['someHash'].isSpam).toBeFalsy() + }) + it('should parse nft purchase', () => { + const result = parseRemoteActivities([MockNFTPurchase], '', jest.fn().mockReturnValue('100')) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse token approval', () => { + const result = parseRemoteActivities([MockTokenApproval], '', jest.fn()) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse send', () => { + const result = parseRemoteActivities([MockTokenSend], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse receive', () => { + const result = parseRemoteActivities([MockTokenReceive], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse NFT receive', () => { + const result = parseRemoteActivities([MockNFTReceive], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse remove liquidity', () => { + const result = parseRemoteActivities([MockRemoveLiquidity], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse moonpay purchase', () => { + const result = parseRemoteActivities([MockMoonpayPurchase], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse swap order', () => { + const result = parseRemoteActivities([MockSwapOrder], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + it('should parse eth wrap', () => { + const result = parseRemoteActivities([MockWrap], '', jest.fn().mockReturnValue(100)) + expect(result?.['someHash']).toMatchSnapshot() + }) + }) + + describe('useTimeSince', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + }) + + it('should initialize with the correct time since', () => { + const timestamp = Math.floor(Date.now() / 1000) - 60 // 60 seconds ago + const { result } = renderHook(() => useTimeSince(timestamp)) + + expect(result.current).toBe('1m') + }) + + it('should update time since every second', async () => { + const timestamp = Math.floor(Date.now() / 1000) - 50 // 50 seconds ago + const { result, rerender } = renderHook(() => useTimeSince(timestamp)) + + act(() => { + jest.advanceTimersByTime(ms('1.1s')) + }) + rerender() + + expect(result.current).toBe('51s') + }) + + it('should stop updating after 61 seconds', () => { + const timestamp = Math.floor(Date.now() / 1000) - 61 // 61 seconds ago + const { result, rerender } = renderHook(() => useTimeSince(timestamp)) + + act(() => { + jest.advanceTimersByTime(ms('121.1s')) + }) + rerender() + + // maxes out at 1m + expect(result.current).toBe('1m') + }) + }) + + describe('parseSwapAmounts', () => { + it('should correctly parse amounts when both sent and received tokens are present', () => { + const result = parseSwapAmounts(swapOrderTokenChanges, jest.fn().mockReturnValue('100')) + expect(result).toEqual({ + inputAmount: '100', + inputAmountRaw: '100000000000000000000', + inputCurrencyId: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + outputAmount: '100', + outputAmountRaw: '100000000000000000000', + outputCurrencyId: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + sent: mockTokenTransferOutPartsFragment, + received: mockTokenTransferInPartsFragment, + }) + }) + + it('should return undefined when sent token is missing', () => { + const result = parseSwapAmounts( + { + ...swapOrderTokenChanges, + TokenTransfer: [mockTokenTransferOutPartsFragment], + }, + jest.fn().mockReturnValue('100') + ) + expect(result).toEqual(undefined) + }) + }) + + describe('offchainOrderDetailsFromGraphQLTransactionActivity', () => { + it('should return undefined when the activity is not a swap order', () => { + const result = offchainOrderDetailsFromGraphQLTransactionActivity( + { ...MockSwapOrder, details: { ...mockTransactionDetailsPartsFragment, __typename: 'TransactionDetails' } }, + { + ...swapOrderTokenChanges, + TokenTransfer: [], + }, // no token changes + jest.fn().mockReturnValue('100') + ) + expect(result).toEqual(undefined) + }) + + it('should return the OffchainOrderDetails', () => { + const result = offchainOrderDetailsFromGraphQLTransactionActivity( + { ...MockSwapOrder, details: { ...mockTransactionDetailsPartsFragment, __typename: 'TransactionDetails' } }, + swapOrderTokenChanges, + jest.fn().mockReturnValue('100') + ) + expect(result).toEqual({ + chainId: 1, + status: 'filled', + id: 'tx123', + offerer: '0xSenderAddress', + orderHash: '0xHashValue', + swapInfo: { + expectedOutputCurrencyAmountRaw: '100000000000000000000', + inputCurrencyAmountRaw: '100000000000000000000', + inputCurrencyId: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + isUniswapXOrder: true, + minimumOutputCurrencyAmountRaw: '100000000000000000000', + outputCurrencyId: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + settledOutputCurrencyAmountRaw: '100000000000000000000', + tradeType: 0, + type: 1, + }, + txHash: '0xHashValue', + type: 'signUniswapXOrder', + addedTime: 10000, + }) + }) + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx new file mode 100644 index 0000000..b3ab00d --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/parseRemote.tsx @@ -0,0 +1,668 @@ +import { t } from '@lingui/macro' +import { ChainId, Currency, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES, TradeType, UNI_ADDRESSES } from '@uniswap/sdk-core' +import UniswapXBolt from 'assets/svg/bolt.svg' +import moonpayLogoSrc from 'assets/svg/moonpay.svg' +import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' +import { BigNumber } from 'ethers/lib/ethers' +import { formatUnits, parseUnits } from 'ethers/lib/utils' +import { + AssetActivityPartsFragment, + Currency as GQLCurrency, + NftApprovalPartsFragment, + NftApproveForAllPartsFragment, + NftTransferPartsFragment, + SwapOrderDetailsPartsFragment, + SwapOrderStatus, + SwapOrderType, + TokenApprovalPartsFragment, + TokenAssetPartsFragment, + TokenTransferPartsFragment, + TransactionDetailsPartsFragment, + TransactionType, +} from 'graphql/data/__generated__/types-and-hooks' +import { gqlToCurrency, logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import ms from 'ms' +import { useEffect, useState } from 'react' +import store from 'state' +import { addSignature } from 'state/signatures/reducer' +import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' +import { TransactionType as LocalTransactionType } from 'state/transactions/types' +import { isAddress, isSameAddress } from 'utilities/src/addresses' +import { currencyId } from 'utils/currencyId' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { MOONPAY_SENDER_ADDRESSES, OrderStatusTable, OrderTextTable } from '../constants' +import { Activity } from './types' + +type TransactionChanges = { + NftTransfer: NftTransferPartsFragment[] + TokenTransfer: TokenTransferPartsFragment[] + TokenApproval: TokenApprovalPartsFragment[] + NftApproval: NftApprovalPartsFragment[] + NftApproveForAll: NftApproveForAllPartsFragment[] +} + +type FormatNumberOrStringFunctionType = ReturnType['formatNumberOrString'] + +// TODO: Move common contract metadata to a backend service +const UNI_IMG = + 'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png' + +const ENS_IMG = + 'https://464911102-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/collections%2F2TjMAeHSzwlQgcOdL48E%2Ficon%2FKWP0gk2C6bdRPliWIA6o%2Fens%20transparent%20background.png?alt=media&token=bd28b063-5a75-4971-890c-97becea09076' + +const COMMON_CONTRACTS: { [key: string]: Partial | undefined } = { + [UNI_ADDRESSES[ChainId.MAINNET].toLowerCase()]: { + title: t`UNI Governance`, + descriptor: t`Contract Interaction`, + logos: [UNI_IMG], + }, + // TODO(cartcrom): Add permit2-specific logo + '0x000000000022d473030f116ddee9f6b43ac78ba3': { + title: t`Permit2`, + descriptor: t`Uniswap Protocol`, + logos: [UNI_IMG], + }, + '0x4976fb03c32e5b8cfe2b6ccb31c09ba78ebaba41': { + title: t`Ethereum Name Service`, + descriptor: t`Public Resolver`, + logos: [ENS_IMG], + }, + '0x58774bb8acd458a640af0b88238369a167546ef2': { + title: t`Ethereum Name Service`, + descriptor: t`DNS Registrar`, + logos: [ENS_IMG], + }, + '0x084b1c3c81545d370f3634392de611caabff8148': { + title: t`Ethereum Name Service`, + descriptor: t`Reverse Registrar`, + logos: [ENS_IMG], + }, + '0x283af0b28c62c092c9727f1ee09c02ca627eb7f5': { + title: t`Ethereum Name Service`, + descriptor: t`ETH Registrar Controller`, + logos: [ENS_IMG], + }, +} + +const SPAMMABLE_ACTIVITY_TYPES = [TransactionType.Receive, TransactionType.Mint, TransactionType.Unknown] +function isSpam( + { NftTransfer, TokenTransfer }: TransactionChanges, + details: TransactionDetailsPartsFragment, + account: string +): boolean { + if (!SPAMMABLE_ACTIVITY_TYPES.includes(details.type) || details.from === account) return false + return NftTransfer.some((nft) => nft.asset.isSpam) || TokenTransfer.some((t) => t.asset.project?.isSpam) +} + +function callsPositionManagerContract(assetActivity: TransactionActivity) { + const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) + if (!supportedChain) return false + return isSameAddress(assetActivity.details.to, NONFUNGIBLE_POSITION_MANAGER_ADDRESSES[supportedChain]) +} + +// Gets counts for number of NFTs in each collection present +function getCollectionCounts(nftTransfers: NftTransferPartsFragment[]): { [key: string]: number | undefined } { + return nftTransfers.reduce((acc, NFTChange) => { + const key = NFTChange.asset.collection?.name ?? NFTChange.asset.name + if (key) { + acc[key] = (acc?.[key] ?? 0) + 1 + } + return acc + }, {} as { [key: string]: number | undefined }) +} + +function getSwapTitle(sent: TokenTransferPartsFragment, received: TokenTransferPartsFragment): string | undefined { + const supportedSentChain = supportedChainIdFromGQLChain(sent.asset.chain) + const supportedReceivedChain = supportedChainIdFromGQLChain(received.asset.chain) + if (!supportedSentChain || !supportedReceivedChain) { + logSentryErrorForUnsupportedChain({ + extras: { sentAsset: sent.asset, receivedAsset: received.asset }, + errorMessage: 'Invalid activity from unsupported chain received from GQL', + }) + return undefined + } + if ( + sent.tokenStandard === NATIVE_CHAIN_ID && + isSameAddress(nativeOnChain(supportedSentChain).wrapped.address, received.asset.address) + ) + return t`Wrapped` + else if ( + received.tokenStandard === NATIVE_CHAIN_ID && + isSameAddress(nativeOnChain(supportedReceivedChain).wrapped.address, received.asset.address) + ) { + return t`Unwrapped` + } else { + return t`Swapped` + } +} + +function getSwapDescriptor({ + tokenIn, + inputAmount, + tokenOut, + outputAmount, +}: { + tokenIn: TokenAssetPartsFragment + outputAmount: string + tokenOut: TokenAssetPartsFragment + inputAmount: string +}) { + return `${inputAmount} ${tokenIn.symbol} for ${outputAmount} ${tokenOut.symbol}` +} + +/** + * + * @param transactedValue Transacted value amount from TokenTransfer API response + * @returns parsed & formatted USD value as a string if currency is of type USD + */ +function getTransactedValue(transactedValue: TokenTransferPartsFragment['transactedValue']): number | undefined { + if (!transactedValue) return undefined + const price = transactedValue?.currency === GQLCurrency.Usd ? transactedValue.value ?? undefined : undefined + return price +} + +type SwapAmounts = { + inputAmount: string + inputAmountRaw: string + inputCurrencyId: string + outputAmount: string + outputAmountRaw: string + outputCurrencyId: string + sent: TokenTransferPartsFragment + received: TokenTransferPartsFragment +} + +// exported for testing +// eslint-disable-next-line import/no-unused-modules +export function parseSwapAmounts( + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType +): SwapAmounts | undefined { + const sent = changes.TokenTransfer.find((t) => t.direction === 'OUT') + // Any leftover native token is refunded on exact_out swaps where the input token is native + const refund = changes.TokenTransfer.find( + (t) => t.direction === 'IN' && t.asset.id === sent?.asset.id && t.asset.standard === NATIVE_CHAIN_ID + ) + const received = changes.TokenTransfer.find((t) => t.direction === 'IN' && t !== refund) + if (!sent || !received) return undefined + const inputCurrencyId = sent.asset.standard === NATIVE_CHAIN_ID ? 'ETH' : sent.asset.address + const outputCurrencyId = received.asset.standard === NATIVE_CHAIN_ID ? 'ETH' : received.asset.address + if (!inputCurrencyId || !outputCurrencyId) return undefined + + const sentQuantity = parseUnits(sent.quantity, sent.asset.decimals) + const refundQuantity = refund ? parseUnits(refund.quantity, refund.asset.decimals) : BigNumber.from(0) + const receivedQuantity = parseUnits(received.quantity, received.asset.decimals) + + const adjustedInput = sentQuantity.sub(refundQuantity) + const inputAmountRaw = adjustedInput.toString() + const outputAmountRaw = receivedQuantity.toString() + const inputAmount = formatNumberOrString({ + input: formatUnits(adjustedInput, sent.asset.decimals), + type: NumberType.TokenNonTx, + }) + const outputAmount = formatNumberOrString({ input: received.quantity, type: NumberType.TokenNonTx }) + return { + sent, + received, + inputAmount, + outputAmount, + inputCurrencyId, + outputCurrencyId, + inputAmountRaw, + outputAmountRaw, + } +} + +function parseSwap(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { + if (changes.NftTransfer.length > 0 && changes.TokenTransfer.length === 1) { + const collectionCounts = getCollectionCounts(changes.NftTransfer) + + const title = changes.NftTransfer[0].direction === 'IN' ? t`Bought` : t`Sold` + const descriptor = Object.entries(collectionCounts) + .map(([collectionName, count]) => `${count} ${collectionName}`) + .join() + + return { title, descriptor } + } + // Some swaps may have more than 2 transfers, e.g. swaps with fees on tranfer + if (changes.TokenTransfer.length >= 2) { + const swapAmounts = parseSwapAmounts(changes, formatNumberOrString) + + if (swapAmounts) { + const { sent, received, inputAmount, outputAmount } = swapAmounts + return { + title: getSwapTitle(sent, received), + descriptor: getSwapDescriptor({ tokenIn: sent.asset, inputAmount, tokenOut: received.asset, outputAmount }), + currencies: [gqlToCurrency(sent.asset), gqlToCurrency(received.asset)], + } + } + } + return { title: t`Unknown Swap` } +} + +/** + * Wrap/unwrap transactions are labelled as lend transactions on the backend. + * This function parses the transaction changes to determine if the transaction is a wrap/unwrap transaction. + */ +function parseLend(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { + const native = changes.TokenTransfer.find((t) => t.tokenStandard === NATIVE_CHAIN_ID)?.asset + const erc20 = changes.TokenTransfer.find((t) => t.tokenStandard === 'ERC20')?.asset + if (native && erc20 && gqlToCurrency(native)?.wrapped.address === gqlToCurrency(erc20)?.wrapped.address) { + return parseSwap(changes, formatNumberOrString) + } + return { title: t`Unknown Lend` } +} + +function parseSwapOrder( + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType, + assetActivity: TransactionActivity +) { + const offchainOrderDetails = offchainOrderDetailsFromGraphQLTransactionActivity( + assetActivity, + changes, + formatNumberOrString + ) + return { + ...parseSwap(changes, formatNumberOrString), + prefixIconSrc: UniswapXBolt, + offchainOrderDetails, + } +} + +export function offchainOrderDetailsFromGraphQLTransactionActivity( + activity: AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment }, + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType +): UniswapXOrderDetails | undefined { + const chainId = supportedChainIdFromGQLChain(activity.chain) + if (!activity || !activity.details || !chainId) return undefined + if (changes.TokenTransfer.length < 2) return undefined + + const swapAmounts = parseSwapAmounts(changes, formatNumberOrString) + + if (!swapAmounts) return undefined + + const { inputCurrencyId, outputCurrencyId, inputAmountRaw, outputAmountRaw } = swapAmounts + + return { + orderHash: activity.details.hash, + id: activity.details.id, + offerer: activity.details.from, + txHash: activity.details.hash, + chainId, + type: SignatureType.SIGN_UNISWAPX_ORDER, + status: UniswapXOrderStatus.FILLED, + addedTime: activity.timestamp, + swapInfo: { + isUniswapXOrder: true, + type: LocalTransactionType.SWAP, + tradeType: TradeType.EXACT_INPUT, + inputCurrencyId, + outputCurrencyId, + inputCurrencyAmountRaw: inputAmountRaw, + expectedOutputCurrencyAmountRaw: outputAmountRaw, + minimumOutputCurrencyAmountRaw: outputAmountRaw, + settledOutputCurrencyAmountRaw: outputAmountRaw, + }, + } +} + +function parseApprove(changes: TransactionChanges) { + if (changes.TokenApproval.length === 1) { + const title = parseInt(changes.TokenApproval[0].quantity) === 0 ? t`Revoked Approval` : t`Approved` + const descriptor = `${changes.TokenApproval[0].asset.symbol}` + const currencies = [gqlToCurrency(changes.TokenApproval[0].asset)] + return { title, descriptor, currencies } + } + return { title: t`Unknown Approval` } +} + +function parseLPTransfers(changes: TransactionChanges, formatNumberOrString: FormatNumberOrStringFunctionType) { + const poolTokenA = changes.TokenTransfer[0] + const poolTokenB = changes.TokenTransfer[1] + + const tokenAQuanitity = formatNumberOrString({ input: poolTokenA.quantity, type: NumberType.TokenNonTx }) + const tokenBQuantity = formatNumberOrString({ input: poolTokenB.quantity, type: NumberType.TokenNonTx }) + + return { + descriptor: `${tokenAQuanitity} ${poolTokenA.asset.symbol} and ${tokenBQuantity} ${poolTokenB.asset.symbol}`, + logos: [poolTokenA.asset.project?.logo?.url, poolTokenB.asset.project?.logo?.url], + currencies: [gqlToCurrency(poolTokenA.asset), gqlToCurrency(poolTokenB.asset)], + } +} + +type TransactionActivity = AssetActivityPartsFragment & { details: TransactionDetailsPartsFragment } +type OrderActivity = AssetActivityPartsFragment & { details: SwapOrderDetailsPartsFragment } + +function parseSendReceive( + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType, + assetActivity: TransactionActivity +) { + // TODO(cartcrom): remove edge cases after backend implements + // Edge case: Receiving two token transfers in interaction w/ V3 manager === removing liquidity. These edge cases should potentially be moved to backend + if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) { + return { title: t`Removed Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) } + } + + let transfer: NftTransferPartsFragment | TokenTransferPartsFragment | undefined + let assetName: string | undefined + let amount: string | undefined + let currencies: (Currency | undefined)[] | undefined + if (changes.NftTransfer.length === 1) { + transfer = changes.NftTransfer[0] + assetName = transfer.asset.collection?.name + amount = '1' + } else if (changes.TokenTransfer.length === 1) { + transfer = changes.TokenTransfer[0] + assetName = transfer.asset.symbol + amount = formatNumberOrString({ input: transfer.quantity, type: NumberType.TokenNonTx }) + currencies = [gqlToCurrency(transfer.asset)] + } + + if (transfer && assetName && amount) { + const isMoonpayPurchase = MOONPAY_SENDER_ADDRESSES.some((address) => isSameAddress(address, transfer?.sender)) + + if (transfer.direction === 'IN') { + return isMoonpayPurchase && transfer.__typename === 'TokenTransfer' + ? { + title: t`Purchased`, + descriptor: `${amount} ${assetName} ${t`for`} ${formatNumberOrString({ + input: getTransactedValue(transfer.transactedValue), + type: NumberType.FiatTokenPrice, + })}`, + logos: [moonpayLogoSrc], + currencies, + } + : { + title: t`Received`, + descriptor: `${amount} ${assetName} ${t`from`} `, + otherAccount: isAddress(transfer.sender) || undefined, + currencies, + } + } else { + return { + title: t`Sent`, + descriptor: `${amount} ${assetName} ${t`to`} `, + otherAccount: isAddress(transfer.recipient) || undefined, + currencies, + } + } + } + return { title: t`Unknown Send` } +} + +function parseMint( + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType, + assetActivity: TransactionActivity +) { + const collectionMap = getCollectionCounts(changes.NftTransfer) + if (Object.keys(collectionMap).length === 1) { + const collectionName = Object.keys(collectionMap)[0] + + // Edge case: Minting a v3 positon represents adding liquidity + if (changes.TokenTransfer.length === 2 && callsPositionManagerContract(assetActivity)) { + return { title: t`Added Liquidity`, ...parseLPTransfers(changes, formatNumberOrString) } + } + return { title: t`Minted`, descriptor: `${collectionMap[collectionName]} ${collectionName}` } + } + return { title: t`Unknown Mint` } +} + +function parseUnknown( + _changes: TransactionChanges, + _formatNumberOrString: FormatNumberOrStringFunctionType, + assetActivity: TransactionActivity +) { + return { title: t`Contract Interaction`, ...COMMON_CONTRACTS[assetActivity.details.to.toLowerCase()] } +} + +type TransactionTypeParser = ( + changes: TransactionChanges, + formatNumberOrString: FormatNumberOrStringFunctionType, + assetActivity: TransactionActivity +) => Partial +const ActivityParserByType: { [key: string]: TransactionTypeParser | undefined } = { + [TransactionType.Swap]: parseSwap, + [TransactionType.Lend]: parseLend, + [TransactionType.SwapOrder]: parseSwapOrder, + [TransactionType.Approve]: parseApprove, + [TransactionType.Send]: parseSendReceive, + [TransactionType.Receive]: parseSendReceive, + [TransactionType.Mint]: parseMint, + [TransactionType.Unknown]: parseUnknown, +} + +function getLogoSrcs(changes: TransactionChanges): Array { + // Uses set to avoid duplicate logos (e.g. nft's w/ same image url) + const logoSet = new Set() + // Uses only NFT logos if they are present (will not combine nft image w/ token image) + if (changes.NftTransfer.length > 0) { + changes.NftTransfer.forEach((nftChange) => logoSet.add(nftChange.asset.image?.url)) + } else { + changes.TokenTransfer.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url)) + changes.TokenApproval.forEach((tokenChange) => logoSet.add(tokenChange.asset.project?.logo?.url)) + } + return Array.from(logoSet) +} + +function swapOrderTypeToSignatureType(swapOrderType: SwapOrderType): SignatureType { + switch (swapOrderType) { + case SwapOrderType.Limit: + return SignatureType.SIGN_LIMIT + case SwapOrderType.Dutch: + default: + return SignatureType.SIGN_UNISWAPX_ORDER + } +} + +function parseUniswapXOrder({ details, chain, timestamp }: OrderActivity): Activity | undefined { + const supportedChain = supportedChainIdFromGQLChain(chain) + if (!supportedChain) { + logSentryErrorForUnsupportedChain({ + extras: { details }, + errorMessage: 'Invalid activity from unsupported chain received from GQL', + }) + return undefined + } + + // If the order is open, maybe add it to our local records (if it was initiated on this device, this will be a no-op). + if (details.orderStatus === SwapOrderStatus.Open) { + const inputCurrency = gqlToCurrency(details.inputToken) + const outputCurrency = gqlToCurrency(details.outputToken) + + const inputTokenQuantity = parseUnits(details.inputTokenQuantity, details.inputToken.decimals).toString() + const outputTokenQuantity = parseUnits(details.outputTokenQuantity, details.outputToken.decimals).toString() + + if (inputTokenQuantity === '0' || outputTokenQuantity === '0') { + // TODO(WEB-3765): This is a temporary mitigation for a bug where the backend sends "0.000000" for small amounts. + throw new Error('Invalid activity received from GQL') + } + + store.dispatch( + addSignature({ + type: swapOrderTypeToSignatureType(details.swapOrderType), + offerer: details.offerer, + id: details.hash, + chainId: supportedChain, + orderHash: details.hash, + expiry: details.expiry, + encodedOrder: details.encodedOrder, + swapInfo: { + type: LocalTransactionType.SWAP, + inputCurrencyId: currencyId(inputCurrency), + outputCurrencyId: currencyId(outputCurrency), + isUniswapXOrder: true, + // This doesn't affect the display, but we don't know this value from the remote activity. + tradeType: TradeType.EXACT_INPUT, + inputCurrencyAmountRaw: inputTokenQuantity, + expectedOutputCurrencyAmountRaw: outputTokenQuantity, + minimumOutputCurrencyAmountRaw: outputTokenQuantity, + }, + status: UniswapXOrderStatus.OPEN, + addedTime: timestamp * 1000, + }) + ) + return undefined + } + + // If the order is not open, render it like any other remote activity. + const { inputToken, inputTokenQuantity, outputToken, outputTokenQuantity, orderStatus } = details + const uniswapXOrderStatus = OrderStatusTable[orderStatus] + const { status, statusMessage, title } = OrderTextTable[uniswapXOrderStatus] + const descriptor = getSwapDescriptor({ + tokenIn: inputToken, + inputAmount: inputTokenQuantity, + tokenOut: outputToken, + outputAmount: outputTokenQuantity, + }) + + return { + hash: details.hash, + chainId: supportedChain, + status, + statusMessage, + offchainOrderDetails: { + id: details.id, + // TODO(limits): check type from backend and use SignatureType.SIGN_LIMIT here if necessary + type: SignatureType.SIGN_UNISWAPX_ORDER, + txHash: details.hash, + orderHash: details.hash, + offerer: details.offerer, + chainId: supportedChain, + status: uniswapXOrderStatus, + addedTime: timestamp, + swapInfo: { + isUniswapXOrder: true, + type: LocalTransactionType.SWAP, + tradeType: TradeType.EXACT_INPUT, + inputCurrencyId: inputToken.address ?? '', + outputCurrencyId: outputToken.address ?? '', + inputCurrencyAmountRaw: parseUnits(inputTokenQuantity, inputToken.decimals).toString(), + expectedOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(), + minimumOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(), + settledOutputCurrencyAmountRaw: parseUnits(outputTokenQuantity, outputToken.decimals).toString(), + }, + }, + timestamp, + logos: [inputToken.project?.logo?.url, outputToken.project?.logo?.url], + currencies: [gqlToCurrency(inputToken), gqlToCurrency(outputToken)], + title, + descriptor, + from: details.offerer, + prefixIconSrc: UniswapXBolt, + } +} + +function parseRemoteActivity( + assetActivity: AssetActivityPartsFragment, + account: string, + formatNumberOrString: FormatNumberOrStringFunctionType +): Activity | undefined { + try { + if (assetActivity.details.__typename === 'SwapOrderDetails') { + return parseUniswapXOrder(assetActivity as OrderActivity) + } + + const changes = assetActivity.details.assetChanges.reduce( + (acc: TransactionChanges, assetChange) => { + if (assetChange.__typename === 'NftApproval') acc.NftApproval.push(assetChange) + else if (assetChange.__typename === 'NftApproveForAll') acc.NftApproveForAll.push(assetChange) + else if (assetChange.__typename === 'NftTransfer') acc.NftTransfer.push(assetChange) + else if (assetChange.__typename === 'TokenTransfer') acc.TokenTransfer.push(assetChange) + else if (assetChange.__typename === 'TokenApproval') acc.TokenApproval.push(assetChange) + + return acc + }, + { NftTransfer: [], TokenTransfer: [], TokenApproval: [], NftApproval: [], NftApproveForAll: [] } + ) + + const supportedChain = supportedChainIdFromGQLChain(assetActivity.chain) + if (!supportedChain) { + logSentryErrorForUnsupportedChain({ + extras: { assetActivity }, + errorMessage: 'Invalid activity from unsupported chain received from GQL', + }) + return undefined + } + + const defaultFields = { + hash: assetActivity.details.hash, + chainId: supportedChain, + status: assetActivity.details.status, + timestamp: assetActivity.timestamp, + logos: getLogoSrcs(changes), + title: assetActivity.details.type, + descriptor: assetActivity.details.to, + from: assetActivity.details.from, + nonce: assetActivity.details.nonce, + isSpam: isSpam(changes, assetActivity.details, account), + } + + const parsedFields = ActivityParserByType[assetActivity.details.type]?.( + changes, + formatNumberOrString, + assetActivity as TransactionActivity + ) + return { ...defaultFields, ...parsedFields } + } catch (e) { + console.error('Failed to parse activity', e, assetActivity) + return undefined + } +} + +export function parseRemoteActivities( + assetActivities: readonly AssetActivityPartsFragment[] | undefined, + account: string, + formatNumberOrString: FormatNumberOrStringFunctionType +) { + return assetActivities?.reduce((acc: { [hash: string]: Activity }, assetActivity) => { + const activity = parseRemoteActivity(assetActivity, account, formatNumberOrString) + if (activity) acc[activity.hash] = activity + return acc + }, {}) +} + +const getTimeSince = (timestamp: number) => { + const seconds = Math.floor(Date.now() - timestamp * 1000) + + let interval + // TODO(cartcrom): use locale to determine date shorthands to use for non-english + if ((interval = seconds / ms(`1y`)) > 1) return Math.floor(interval) + 'y' + if ((interval = seconds / ms(`30d`)) > 1) return Math.floor(interval) + 'mo' + if ((interval = seconds / ms(`1d`)) > 1) return Math.floor(interval) + 'd' + if ((interval = seconds / ms(`1h`)) > 1) return Math.floor(interval) + 'h' + if ((interval = seconds / ms(`1m`)) > 1) return Math.floor(interval) + 'm' + else return Math.floor(seconds / ms(`1s`)) + 's' +} + +/** + * Keeps track of the time since a given timestamp, keeping it up to date every second when necessary + * @param timestamp + * @returns + */ +export function useTimeSince(timestamp: number) { + const [timeSince, setTimeSince] = useState(getTimeSince(timestamp)) + + useEffect(() => { + const refreshTime = () => + setTimeout(() => { + if (Math.floor(Date.now() - timestamp * 1000) / ms(`61s`) <= 1) { + setTimeSince(getTimeSince(timestamp)) + timeout = refreshTime() + } + }, ms(`1s`)) + + let timeout = refreshTime() + + return () => { + timeout && clearTimeout(timeout) + } + }, [timestamp]) + + return timeSince +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts new file mode 100644 index 0000000..5197b00 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/types.ts @@ -0,0 +1,28 @@ +import { ChainId, Currency } from '@uniswap/sdk-core' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { UniswapXOrderDetails } from 'state/signatures/types' + +/** + * TODO: refactor parsing / Activity so that all Activity Types can have a detail sheet. + */ + +export type Activity = { + hash: string + chainId: ChainId + status: TransactionStatus + offchainOrderDetails?: UniswapXOrderDetails + statusMessage?: string + timestamp: number + title: string + descriptor?: string + logos?: Array + currencies?: Array + otherAccount?: string + from: string + nonce?: number | null + prefixIconSrc?: string + cancelled?: boolean + isSpam?: boolean +} + +export type ActivityMap = { [id: string]: Activity | undefined } diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts new file mode 100644 index 0000000..00b264a --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.test.ts @@ -0,0 +1,58 @@ +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' // Replace with the actual import if this is incorrect + +import { Activity } from './types' +import { createGroups } from './utils' + +describe('createGroups', () => { + it('should return an empty array if activities is undefined', () => { + expect(createGroups(undefined)).toEqual([]) + }) + + it('should return an empty array if activities is empty', () => { + expect(createGroups([])).toEqual([]) + }) + + it('should hide spam if requested', () => { + const mockActivities = [ + { timestamp: Date.now() / 1000 - 300, status: TransactionStatus.Confirmed, isSpam: true }, + ] as Activity[] + + expect(createGroups(mockActivities, false)).toContainEqual( + expect.objectContaining({ + title: 'Today', + transactions: expect.arrayContaining([ + expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }), + ]), + }) + ) + expect(createGroups(mockActivities, true)).toEqual([]) + }) + + it('should sort and group activities based on status and time', () => { + const mockActivities = [ + { timestamp: 1700000000, status: TransactionStatus.Pending }, + { timestamp: 1650000000, status: TransactionStatus.Confirmed }, + { timestamp: Date.now() / 1000 - 300, status: TransactionStatus.Confirmed }, + ] as Activity[] + + const result = createGroups(mockActivities) + + expect(result).toContainEqual( + expect.objectContaining({ + title: 'Pending', + transactions: expect.arrayContaining([ + expect.objectContaining({ timestamp: 1700000000, status: TransactionStatus.Pending }), + ]), + }) + ) + + expect(result).toContainEqual( + expect.objectContaining({ + title: 'Today', + transactions: expect.arrayContaining([ + expect.objectContaining({ timestamp: expect.any(Number), status: TransactionStatus.Confirmed }), + ]), + }) + ) + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts new file mode 100644 index 0000000..fe101eb --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Activity/utils.ts @@ -0,0 +1,191 @@ +import { TransactionRequest } from '@ethersproject/abstract-provider' +import { Web3Provider } from '@ethersproject/providers' +import { t } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { DutchOrder } from '@uniswap/uniswapx-sdk' +import { getYear, isSameDay, isSameMonth, isSameWeek, isSameYear } from 'date-fns' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types' +import { didUserReject } from 'utils/swapErrorToUserReadableMessage' + +import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { BigNumber, ContractTransaction } from 'ethers/lib/ethers' +import { useContract } from 'hooks/useContract' +import { useCallback } from 'react' +import { SignatureType } from 'state/signatures/types' +import { useAsyncData } from 'utilities/src/react/hooks' +import { Activity } from './types' + +interface ActivityGroup { + title: string + transactions: Array +} + +const sortActivities = (a: Activity, b: Activity) => b.timestamp - a.timestamp + +export const createGroups = (activities: Array = [], hideSpam = false) => { + if (activities.length === 0) return [] + const now = Date.now() + + const pending: Array = [] + const today: Array = [] + const currentWeek: Array = [] + const last30Days: Array = [] + const currentYear: Array = [] + const yearMap: { [key: string]: Array } = {} + + // TODO(cartcrom): create different time bucket system for activities to fall in based on design wants + activities.forEach((activity) => { + if (hideSpam && activity.isSpam) { + return + } + + const addedTime = activity.timestamp * 1000 + if (activity.status === TransactionStatus.Pending) { + switch (activity.offchainOrderDetails?.type) { + case SignatureType.SIGN_LIMIT: + // limit orders are only displayed in their own pane + break + default: + pending.push(activity) + } + } else if (isSameDay(now, addedTime)) { + today.push(activity) + } else if (isSameWeek(addedTime, now)) { + currentWeek.push(activity) + } else if (isSameMonth(addedTime, now)) { + last30Days.push(activity) + } else if (isSameYear(addedTime, now)) { + currentYear.push(activity) + } else { + const year = getYear(addedTime) + + if (!yearMap[year]) { + yearMap[year] = [activity] + } else { + yearMap[year].push(activity) + } + } + }) + const sortedYears = Object.keys(yearMap) + .sort((a, b) => parseInt(b) - parseInt(a)) + .map((year) => ({ title: year, transactions: yearMap[year] })) + + const transactionGroups: Array = [ + { title: t`Pending`, transactions: pending.sort(sortActivities) }, + { title: t`Today`, transactions: today.sort(sortActivities) }, + { title: t`This week`, transactions: currentWeek.sort(sortActivities) }, + { title: t`This month`, transactions: last30Days.sort(sortActivities) }, + { title: t`This year`, transactions: currentYear.sort(sortActivities) }, + ...sortedYears, + ] + + return transactionGroups.filter(({ transactions }) => transactions.length > 0) +} + +// TODO(WEB-3594): just use the uniswapx-sdk when getCancelMultipleParams is available + +interface SplitNonce { + word: BigNumber + bitPos: BigNumber +} + +function splitNonce(nonce: BigNumber): SplitNonce { + const word = nonce.div(256) + const bitPos = nonce.mod(256) + return { word, bitPos } +} + +// Get parameters to cancel multiple nonces +// source: https://github.com/Uniswap/uniswapx-sdk/pull/112 +function getCancelMultipleParams(noncesToCancel: BigNumber[]): { + word: BigNumber + mask: BigNumber +}[] { + const splitNonces = noncesToCancel.map(splitNonce) + const splitNoncesByWord: { [word: string]: SplitNonce[] } = {} + splitNonces.forEach((splitNonce) => { + const word = splitNonce.word.toString() + if (!splitNoncesByWord[word]) { + splitNoncesByWord[word] = [] + } + splitNoncesByWord[word].push(splitNonce) + }) + return Object.entries(splitNoncesByWord).map(([word, splitNonce]) => { + let mask = BigNumber.from(0) + splitNonce.forEach((splitNonce) => { + mask = mask.or(BigNumber.from(2).pow(splitNonce.bitPos)) + }) + return { word: BigNumber.from(word), mask } + }) +} + +function getCancelMultipleUniswapXOrdersParams(encodedOrders: string[], chainId: ChainId) { + const nonces = encodedOrders + .map((encodedOrder) => DutchOrder.parse(encodedOrder, chainId)) + .map((order) => order.info.nonce) + return getCancelMultipleParams(nonces) +} + +export async function cancelMultipleUniswapXOrders({ + encodedOrders, + chainId, + permit2, + provider, +}: { + encodedOrders: string[] + chainId: ChainId + permit2: Permit2 | null + provider?: Web3Provider +}) { + const cancelParams = getCancelMultipleUniswapXOrdersParams(encodedOrders, chainId) + if (!permit2 || !provider) return + try { + const transactions: ContractTransaction[] = [] + for (const params of cancelParams) { + const tx = await permit2.invalidateUnorderedNonces(params.word, params.mask) + transactions.push(tx) + } + return transactions + } catch (error) { + if (!didUserReject(error)) console.error(error) + return undefined + } +} + +async function getCancelMultipleUniswapXOrdersTransaction( + encodedOrders: string[], + chainId: ChainId, + permit2: Permit2 +): Promise { + const cancelParams = getCancelMultipleUniswapXOrdersParams(encodedOrders, chainId) + if (!permit2 || cancelParams.length === 0) return + try { + const tx = await permit2.populateTransaction.invalidateUnorderedNonces(cancelParams[0].word, cancelParams[0].mask) + return { + ...tx, + chainId, + } + } catch (error) { + console.error('could not populate cancel transaction') + return undefined + } +} + +export function useCreateCancelTransactionRequest( + params: + | { + encodedOrders?: string[] + chainId: ChainId + } + | undefined +): TransactionRequest | undefined { + const permit2 = useContract(PERMIT2_ADDRESS, PERMIT2_ABI, true) + const transactionFetcher = useCallback(() => { + if (!params || !params.encodedOrders || params.encodedOrders.filter(Boolean).length === 0 || !permit2) return + return getCancelMultipleUniswapXOrdersTransaction(params.encodedOrders, params.chainId, permit2) + }, [params, permit2]) + + return useAsyncData(transactionFetcher).data +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx new file mode 100644 index 0000000..3e1737b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/ExpandoRow.tsx @@ -0,0 +1,55 @@ +import { t } from '@lingui/macro' +import Column from 'components/Column' +import Row from 'components/Row' +import { PropsWithChildren } from 'react' +import { ChevronDown } from 'react-feather' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const ExpandIcon = styled(ChevronDown)<{ $expanded: boolean }>` + color: ${({ theme }) => theme.neutral2}; + transform: ${({ $expanded }) => ($expanded ? 'rotate(180deg)' : 'rotate(0deg)')}; + transition: transform ${({ theme }) => theme.transition.duration.medium}; +` + +const ToggleButton = styled(Row)` + background-color: ${({ theme }) => theme.surface3}; + border-radius: 12px; + padding: 4px 8px 4px 12px; + height: 100%; + width: fit-content; + cursor: pointer; + :hover { + opacity: 0.8; + } +` + +const Wrapper = styled(Column)<{ numItems: number; isExpanded: boolean }>` + height: ${({ numItems, isExpanded }) => (isExpanded ? numItems * 68 + 'px' : 0)}; + transition: ${({ theme }) => `height ${theme.transition.duration.medium} ease-in-out`}; + overflow: hidden; +` + +// TODO(WEB-1982): Replace this component to use `components/Expand` under the hood +type ExpandoRowProps = PropsWithChildren<{ title?: string; numItems: number; isExpanded: boolean; toggle: () => void }> +export function ExpandoRow({ title = t`Hidden`, numItems, isExpanded, toggle, children }: ExpandoRowProps) { + if (numItems === 0) return null + return ( + <> + + + {`${title} (${numItems})`} + + + + {isExpanded ? t`Hide` : t`Show`} + + + + + + {children} + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx new file mode 100644 index 0000000..7621435 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.test.tsx @@ -0,0 +1,95 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types' +import { LimitDetailActivityRow } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow' +import { DAI } from 'constants/tokens' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' +import { render, screen } from 'test-utils/render' + +jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => { + return { + ...jest.requireActual('components/AccountDrawer/MiniPortfolio/formatTimestamp'), + formatTimestamp: () => 'Expires January 1, 1970 at 12:00 AM', + } +}) + +jest.mock('hooks/Tokens', () => { + return { + useCurrency: (address?: string) => { + if (address?.toLowerCase() === DAI.address.toLowerCase()) { + return DAI + } + if (address?.toLowerCase() === WETH9[ChainId.MAINNET].address.toLowerCase()) { + return WETH9[ChainId.MAINNET] + } + return undefined + }, + } +}) + +const mockOrderDetails: UniswapXOrderDetails = { + type: SignatureType.SIGN_LIMIT, + orderHash: '0x1234', + status: UniswapXOrderStatus.OPEN, + swapInfo: { + isUniswapXOrder: true, + type: 1, + tradeType: 0, + inputCurrencyId: DAI.address, + outputCurrencyId: WETH9[ChainId.MAINNET].address, + inputCurrencyAmountRaw: '252074033564766400000', + expectedOutputCurrencyAmountRaw: '106841079134757921', + minimumOutputCurrencyAmountRaw: '106841079134757921', + settledOutputCurrencyAmountRaw: '106841079134757921', + }, + txHash: '0x1234', + encodedOrder: '0xencodedOrder', + id: '0x1234', + addedTime: 3, + chainId: ChainId.MAINNET, + expiry: 4, + offerer: '0x1234', +} + +const mockOrder: Activity = { + hash: '0x123', + chainId: ChainId.MAINNET, + status: TransactionStatus.Pending, + timestamp: 1, + title: 'Limit pending', + from: '0x456', + offchainOrderDetails: mockOrderDetails, +} + +describe('LimitDetailActivityRow', () => { + it('should not render with invalid details', () => { + const { container } = render( + + ) + expect(container.firstChild?.firstChild?.firstChild).toBeNull() + }) + + it('should not render with invalid amounts', () => { + const { container } = render( + + ) + expect(container.firstChild?.firstChild?.firstChild).toBeNull() + }) + + it('should render with valid details', () => { + const { container } = render( + + ) + expect(container.firstChild).toMatchSnapshot() + expect(screen.getByText('when 0.00042 WETH/DAI')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx new file mode 100644 index 0000000..0895058 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow.tsx @@ -0,0 +1,137 @@ +import { Trans } from '@lingui/macro' +import { CurrencyAmount, Price } from '@uniswap/sdk-core' +import { + useOpenOffchainActivityModal, + useOrderAmounts, +} from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal' +import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types' +import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow' +import { FormatType, formatTimestamp } from 'components/AccountDrawer/MiniPortfolio/formatTimestamp' +import Column from 'components/Column' +import Row from 'components/Row' +import { parseUnits } from 'ethers/lib/utils' +import useTokenLogoSource from 'hooks/useAssetLogoSource' +import { useScreenSize } from 'hooks/useScreenSize' +import { Checkbox } from 'nft/components/layout/Checkbox' +import { useMemo, useState } from 'react' +import { ArrowRight } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +const StyledPortfolioRow = styled(PortfolioRow)` + padding: 8px 0; + height: unset; + ${EllipsisStyle} +` + +interface LimitDetailActivityRowProps { + order: Activity + onToggleSelect: (order: Activity) => void + selected: boolean +} + +const StyledCheckbox = styled(Checkbox)<{ $visible?: boolean }>` + opacity: ${({ $visible }) => ($visible ? 1 : 0)}; +` + +const TradeSummaryContainer = styled(Row)` + * { + max-width: 40%; + ${EllipsisStyle} + } +` + +const CircleLogoImage = styled.img<{ size: string }>` + width: ${({ size }) => size}; + height: ${({ size }) => size}; + border-radius: 50%; +` + +export function LimitDetailActivityRow({ order, onToggleSelect, selected }: LimitDetailActivityRowProps) { + const theme = useTheme() + const { chainId, logos, currencies, offchainOrderDetails } = order + const openOffchainActivityModal = useOpenOffchainActivityModal() + const { formatReviewSwapCurrencyAmount } = useFormatter() + const [hovered, setHovered] = useState(false) + const isSmallScreen = !useScreenSize()['sm'] + + const amounts = useOrderAmounts(order.offchainOrderDetails) + const amountsDefined = !!amounts?.inputAmount?.currency && !!amounts?.outputAmount?.currency + + const displayPrice = useMemo(() => { + if (!amountsDefined) return undefined + const tradePrice = new Price({ baseAmount: amounts?.inputAmount, quoteAmount: amounts?.outputAmount }) + return tradePrice.quote( + CurrencyAmount.fromRawAmount( + amounts.inputAmount.currency, + parseUnits('1', amounts.inputAmount.currency.decimals).toString() + ) + ) + }, [amounts?.inputAmount, amounts?.outputAmount, amountsDefined]) + + const [inputLogoSrc, nextInputLogoSrc] = useTokenLogoSource({ + address: currencies?.[0]?.wrapped.address, + chainId, + isNative: currencies?.[0]?.isNative, + }) + const [outputLogoSrc, nextOutputLogoSrc2] = useTokenLogoSource({ + address: currencies?.[1]?.wrapped.address, + chainId, + isNative: currencies?.[1]?.isNative, + }) + + if (!offchainOrderDetails || !amountsDefined) return null + + return ( + setHovered(true)} onMouseLeave={() => setHovered(false)}> + + Expires {formatTimestamp(offchainOrderDetails.expiry * 1000, true, FormatType.Short)} + + ) : undefined + } + descriptor={ + + + + + {formatReviewSwapCurrencyAmount(amounts.inputAmount)} {amounts.inputAmount.currency.symbol} + + + + + {formatReviewSwapCurrencyAmount(amounts.outputAmount)} {amounts.outputAmount.currency.symbol} + + + {displayPrice && ( + + + when {formatReviewSwapCurrencyAmount(displayPrice)} {amounts.outputAmount.currency.symbol}/ + {amounts.inputAmount.currency.symbol} + + + )} + + } + right={undefined} + onClick={() => { + openOffchainActivityModal(offchainOrderDetails, { + inputLogo: order?.logos?.[0], + outputLogo: order?.logos?.[1], + }) + }} + /> + onToggleSelect(order)} + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.test.tsx new file mode 100644 index 0000000..fa54985 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.test.tsx @@ -0,0 +1,114 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' +import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types' +import { LimitsMenu } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu' +import { DAI } from 'constants/tokens' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { SignatureType, UniswapXOrderDetails } from 'state/signatures/types' +import { mocked } from 'test-utils/mocked' +import { act, fireEvent, render, screen } from 'test-utils/render' + +jest.mock('components/AccountDrawer/MiniPortfolio/Activity/hooks', () => ({ + useOpenLimitOrders: jest.fn(), +})) + +jest.mock('components/AccountDrawer/MiniPortfolio/formatTimestamp', () => ({ + ...jest.requireActual('components/AccountDrawer/MiniPortfolio/formatTimestamp'), + formatTimestamp: () => 'January 26, 2024 at 1:52PM', +})) + +jest.mock('hooks/Tokens', () => { + return { + useCurrency: (address?: string) => { + if (address?.toLowerCase() === DAI.address.toLowerCase()) { + return DAI + } + if (address?.toLowerCase() === WETH9[ChainId.MAINNET].address.toLowerCase()) { + return WETH9[ChainId.MAINNET] + } + return undefined + }, + } +}) + +const mockOrderDetails: UniswapXOrderDetails = { + type: SignatureType.SIGN_LIMIT, + orderHash: '0x1234', + status: UniswapXOrderStatus.OPEN, + swapInfo: { + isUniswapXOrder: true, + type: 1, + tradeType: 0, + inputCurrencyId: DAI.address, + outputCurrencyId: WETH9[ChainId.MAINNET].address, + inputCurrencyAmountRaw: '252074033564766400000', + expectedOutputCurrencyAmountRaw: '106841079134757921', + minimumOutputCurrencyAmountRaw: '106841079134757921', + settledOutputCurrencyAmountRaw: '106841079134757921', + }, + txHash: '0x1234', + encodedOrder: '0xencodedOrder', + id: '0x1234', + addedTime: 3, + chainId: ChainId.MAINNET, + expiry: 4, + offerer: '0x1234', +} + +const mockLimitActivity: Activity = { + hash: '0x123', + chainId: ChainId.MAINNET, + status: TransactionStatus.Pending, + timestamp: 1, + title: 'Limit pending', + from: '0x456', + offchainOrderDetails: mockOrderDetails, +} + +describe('LimitsMenu', () => { + it('should render when there is one open order', async () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: [mockLimitActivity], + loading: false, + refetch: jest.fn(), + }) + + const { container } = await act(async () => { + return render() + }) + expect(container).toMatchSnapshot() + expect(screen.getByText('Open limits')).toBeInTheDocument() + expect(screen.getByTestId('LimitsMenuContainer').children.length).toEqual(1) // one order + }) + + it('should render when there are two open orders', async () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: [mockLimitActivity, { ...mockLimitActivity, hash: '0x456' }], + loading: false, + refetch: jest.fn(), + }) + const { container } = await act(async () => { + return render() + }) + expect(container).toMatchSnapshot() + expect(screen.getByText('Open limits')).toBeInTheDocument() + expect(screen.getByTestId('LimitsMenuContainer').children.length).toEqual(2) // two orders + }) + + it('should call the close callback', async () => { + const onClose = jest.fn() + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: [mockLimitActivity], + loading: false, + refetch: jest.fn(), + }) + await act(async () => { + render() + }) + act(() => { + fireEvent.click(screen.getByTestId('wallet-back')) + }) + expect(onClose).toHaveBeenCalled() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx new file mode 100644 index 0000000..6899463 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/LimitsMenu.tsx @@ -0,0 +1,130 @@ +import { Plural, Trans } from '@lingui/macro' +import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { useWeb3React } from '@web3-react/core' +import { sendAnalyticsEvent } from 'analytics' +import { + CancelLimitsDialog, + CancellationState, +} from 'components/AccountDrawer/MiniPortfolio/Activity/CancelLimitsDialog' +import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' +import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types' +import { cancelMultipleUniswapXOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/utils' +import { LimitDetailActivityRow } from 'components/AccountDrawer/MiniPortfolio/Limits/LimitDetailActivityRow' +import { SlideOutMenu } from 'components/AccountDrawer/SlideOutMenu' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Column from 'components/Column' +import { LimitDisclaimer } from 'components/swap/LimitDisclaimer' +import { ContractTransaction } from 'ethers/lib/ethers' +import { useContract } from 'hooks/useContract' +import { useCallback, useMemo, useState } from 'react' +import { UniswapXOrderDetails } from 'state/signatures/types' +import styled from 'styled-components' +import PERMIT2_ABI from 'uniswap/src/abis/permit2.json' +import { Permit2 } from 'uniswap/src/abis/types/Permit2' + +const Container = styled(Column)` + height: 100%; + position: relative; +` + +const StyledCancelButton = styled(ThemeButton)` + bottom: 0; + width: 100%; + margin: 24px 0 0; +` + +const StyledLimitsDisclaimer = styled(LimitDisclaimer)` + margin-bottom: 24px; +` + +function useCancelMultipleOrders(orders?: UniswapXOrderDetails[]): () => Promise { + const { provider } = useWeb3React() + const permit2 = useContract(PERMIT2_ADDRESS, PERMIT2_ABI, true) + return useCallback(async () => { + if (!orders || orders.length === 0) return undefined + + sendAnalyticsEvent('UniswapX Order Cancel Initiated', { + orders: orders.map((order) => order.orderHash), + }) + + return cancelMultipleUniswapXOrders({ + encodedOrders: orders.map((order) => order.encodedOrder as string), + permit2, + provider, + chainId: orders?.[0].chainId, + }) + }, [orders, permit2, provider]) +} + +export function LimitsMenu({ onClose, account }: { account: string; onClose: () => void }) { + const { openLimitOrders } = useOpenLimitOrders(account) + const [selectedOrdersByHash, setSelectedOrdersByHash] = useState>({}) + const [cancelState, setCancelState] = useState(CancellationState.NOT_STARTED) + const [cancelTxHash, setCancelTxHash] = useState() + + const selectedOrders = useMemo(() => { + return Object.values(selectedOrdersByHash) + }, [selectedOrdersByHash]) + + const cancelOrders = useCancelMultipleOrders(selectedOrders) + + const toggleOrderSelection = (order: Activity) => { + const newSelectedOrders = { ...selectedOrdersByHash } + if (order.hash in selectedOrdersByHash) { + delete newSelectedOrders[order.hash] + } else if (order.offchainOrderDetails) { + newSelectedOrders[order.hash] = order.offchainOrderDetails + } + setSelectedOrdersByHash(newSelectedOrders) + } + + return ( + Open limits} onClose={onClose}> + + + {openLimitOrders.map((order) => ( + + ))} + {Boolean(Object.keys(selectedOrdersByHash).length) && ( + setCancelState(CancellationState.REVIEWING_CANCELLATION)} + size={ButtonSize.medium} + disabled={cancelState !== CancellationState.NOT_STARTED || selectedOrders.length === 0} + > + + + )} + + setCancelState(CancellationState.NOT_STARTED)} + onConfirm={async () => { + setCancelState(CancellationState.PENDING_SIGNATURE) + const transactions = await cancelOrders() + if (transactions && transactions.length > 0) { + setCancelState(CancellationState.PENDING_CONFIRMATION) + setCancelTxHash(transactions[0].hash) + await transactions[0].wait(1) + setCancelState(CancellationState.CANCELLED) + } else { + setCancelState(CancellationState.REVIEWING_CANCELLATION) + } + }} + cancelState={cancelState} + cancelTxHash={cancelTxHash} + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.test.tsx new file mode 100644 index 0000000..9a4a197 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.test.tsx @@ -0,0 +1,69 @@ +import { ChainId } from '@uniswap/sdk-core' +import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' +import { OpenLimitOrdersButton } from 'components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { mocked } from 'test-utils/mocked' +import { act, fireEvent, render, screen } from 'test-utils/render' + +jest.mock('components/AccountDrawer/MiniPortfolio/Activity/hooks', () => ({ + useOpenLimitOrders: jest.fn(), +})) + +const mockLimitActivity = { + hash: '0x123', + chainId: ChainId.MAINNET, + status: TransactionStatus.Pending, + timestamp: 1, + title: 'Limit pending', + from: '0x456', +} + +describe('OpenLimitOrdersButton', () => { + it('should not render if there are no open limit orders', () => { + mocked(useOpenLimitOrders).mockReturnValue({ openLimitOrders: [], loading: false, refetch: jest.fn() }) + const { container } = render() + expect(container.firstChild?.firstChild?.firstChild).toBeNull() + }) + it('should render if there are open limit orders', () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: [mockLimitActivity], + loading: false, + refetch: jest.fn(), + }) + const { container } = render() + expect(container).toMatchSnapshot() + expect(screen.getByText('1 open limit')).toBeInTheDocument() + }) + it('should call the callback when clicked', () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: [mockLimitActivity], + loading: false, + refetch: jest.fn(), + }) + const clickCallback = jest.fn() + const { container } = render() + act(() => { + fireEvent.click(container.firstChild?.firstChild?.firstChild as HTMLElement) + }) + expect(clickCallback).toHaveBeenCalled() + }) + it('should have a warning when the limit is reached', () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: Array(100).fill(mockLimitActivity), + loading: false, + refetch: jest.fn(), + }) + render() + expect(screen.getByText('Cancel limits to proceed')).toBeInTheDocument() + }) + + it('should have a warning when the limit is almost reached', () => { + mocked(useOpenLimitOrders).mockReturnValue({ + openLimitOrders: Array(90).fill(mockLimitActivity), + loading: false, + refetch: jest.fn(), + }) + render() + expect(screen.getByText('Approaching 100 limit maximum')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx new file mode 100644 index 0000000..259d8a8 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/OpenLimitOrdersButton.tsx @@ -0,0 +1,60 @@ +import { Plural, t, Trans } from '@lingui/macro' +import { useOpenLimitOrders } from 'components/AccountDrawer/MiniPortfolio/Activity/hooks' +import Column from 'components/Column' +import { TimeForwardIcon } from 'components/Icons/TimeForward' +import Row from 'components/Row' +import { ChevronRight } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { ClickableStyle, ThemedText } from 'theme/components' + +const Container = styled.button` + border-radius: 16px; + border: none; + background: ${({ theme }) => theme.surface2}; + padding: 12px 16px; + margin-top: 12px; + ${ClickableStyle} +` + +function getExtraWarning(openLimitOrders: any[]) { + if (openLimitOrders.length >= 100) return Cancel limits to proceed + if (openLimitOrders.length >= 90) return Approaching 100 limit maximum + return undefined +} + +export function OpenLimitOrdersButton({ + openLimitsMenu, + account, + disabled, + className, +}: { + account: string + openLimitsMenu: () => void + disabled?: boolean + className?: string +}) { + const { openLimitOrders } = useOpenLimitOrders(account) + const theme = useTheme() + const extraWarning = getExtraWarning(openLimitOrders) + if (!openLimitOrders || openLimitOrders.length < 1) return null + return ( + + + + + + + + + {extraWarning && {extraWarning}} + + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap new file mode 100644 index 0000000..3c863c2 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitDetailActivityRow.test.tsx.snap @@ -0,0 +1,304 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LimitDetailActivityRow should render with valid details 1`] = ` +.c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c5 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c10 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c11 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c4 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c2 { + gap: 12px; + height: 68px; + padding: 0 16px; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + cursor: pointer; +} + +.c2:hover { + cursor: pointer; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + overflow: hidden; + cursor: pointer; + line-height: 1; +} + +.c14 { + border-color: #22222212; + display: inline-block; + margin-right: 1px; + border-radius: 4px; + height: 18px; + width: 18px; + border-style: solid; + border-width: 1.5px; + position: relative; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c14:hover { + opacity: 0.6; +} + +.c14:active { + opacity: 0.4; +} + +.c15 { + position: absolute; + top: -24px; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.c16 { + display: none; + height: 18px; + width: 18px; + color: white; + position: absolute; + right: 1px; +} + +.c3 { + padding: 8px 0; + height: unset; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c13 { + opacity: 0; +} + +.c8 * { + max-width: 40%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c9 { + width: 16px; + height: 16px; + border-radius: 50%; +} + + + +
+
+
+
+ Expires Expires January 1, 1970 at 12:00 AM +
+
+
+ +
+ 252.074 + + DAI +
+ + + + + +
+ 0.10684 + + WETH +
+
+
+ when 0.00042 WETH/DAI +
+
+
+
+ +
+
+
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap new file mode 100644 index 0000000..35991f4 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/LimitsMenu.test.tsx.snap @@ -0,0 +1,1107 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LimitsMenu should render when there are two open orders 1`] = ` +.c13 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c14 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c5 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c9 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c21 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c11 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c11:hover { + opacity: 0.6; +} + +.c11:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c17 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c10 { + line-height: 16px; +} + +.c15 { + gap: 12px; + height: 68px; + padding: 0 16px; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + cursor: pointer; +} + +.c15:hover { + cursor: pointer; +} + +.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + overflow: hidden; + cursor: pointer; + line-height: 1; +} + +.c24 { + border-color: #22222212; + display: inline-block; + margin-right: 1px; + border-radius: 4px; + height: 18px; + width: 18px; + border-style: solid; + border-width: 1.5px; + position: relative; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c24:hover { + opacity: 0.6; +} + +.c24:active { + opacity: 0.4; +} + +.c25 { + position: absolute; + top: -24px; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.c26 { + display: none; + height: 18px; + width: 18px; + color: white; + position: absolute; + right: 1px; +} + +.c16 { + padding: 8px 0; + height: unset; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c23 { + opacity: 0; +} + +.c19 * { + max-width: 40%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c20 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c1 { + width: 100%; + overflow: auto; + margin-top: 4px; + padding: 14px 16px 16px; + -webkit-scrollbar-width: thin; + -moz-scrollbar-width: thin; + -ms-scrollbar-width: thin; + scrollbar-width: thin; + -webkit-scrollbar-color: #22222212 transparent; + -moz-scrollbar-color: #22222212 transparent; + -ms-scrollbar-color: #22222212 transparent; + scrollbar-color: #22222212 transparent; + height: 100%; +} + +.c1::-webkit-scrollbar { + background: transparent; + width: 4px; + overflow-y: scroll; +} + +.c1::-webkit-scrollbar-thumb { + background: #22222212; + border-radius: 8px; +} + +.c1::-webkit-scrollbar-track { + margin-top: 40px; +} + +.c4 { + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); +} + +.c3 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c3:hover { + opacity: 0.6; +} + +.c3:active { + opacity: 0.4; +} + +.c2 { + color: #222222; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: relative; + width: 100%; + margin-bottom: 20px; +} + +.c12 { + height: 100%; + position: relative; +} + +.c8 { + margin-bottom: 24px; +} + +
+ + +
+
+ + + + + +
+ Open limits +
+
+
+
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
+
+
+
+
+ Expires January 26, 2024 at 1:52PM +
+
+
+ +
+ 252.074 + + DAI +
+ + + + + +
+ 0.10684 + + WETH +
+
+
+ when 0.00042 WETH/DAI +
+
+
+
+ +
+
+
+
+
+ Expires January 26, 2024 at 1:52PM +
+
+
+ +
+ 252.074 + + DAI +
+ + + + + +
+ 0.10684 + + WETH +
+
+
+ when 0.00042 WETH/DAI +
+
+
+
+ +
+
+
+
+
+
+`; + +exports[`LimitsMenu should render when there is one open order 1`] = ` +.c13 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c14 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c5 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c9 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c21 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c11 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c11:hover { + opacity: 0.6; +} + +.c11:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c17 { + display: grid; + grid-auto-rows: auto; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7 { + background-color: #F9F9F9; + border-radius: 12px; + padding: 12px; + margin-top: 12px; +} + +.c10 { + line-height: 16px; +} + +.c15 { + gap: 12px; + height: 68px; + padding: 0 16px; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + cursor: pointer; +} + +.c15:hover { + cursor: pointer; +} + +.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + overflow: hidden; + cursor: pointer; + line-height: 1; +} + +.c24 { + border-color: #22222212; + display: inline-block; + margin-right: 1px; + border-radius: 4px; + height: 18px; + width: 18px; + border-style: solid; + border-width: 1.5px; + position: relative; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c24:hover { + opacity: 0.6; +} + +.c24:active { + opacity: 0.4; +} + +.c25 { + position: absolute; + top: -24px; + -webkit-clip: rect(0 0 0 0); + clip: rect(0 0 0 0); + -webkit-clip-path: inset(50%); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; +} + +.c26 { + display: none; + height: 18px; + width: 18px; + color: white; + position: absolute; + right: 1px; +} + +.c16 { + padding: 8px 0; + height: unset; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c23 { + opacity: 0; +} + +.c19 * { + max-width: 40%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c20 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c1 { + width: 100%; + overflow: auto; + margin-top: 4px; + padding: 14px 16px 16px; + -webkit-scrollbar-width: thin; + -moz-scrollbar-width: thin; + -ms-scrollbar-width: thin; + scrollbar-width: thin; + -webkit-scrollbar-color: #22222212 transparent; + -moz-scrollbar-color: #22222212 transparent; + -ms-scrollbar-color: #22222212 transparent; + scrollbar-color: #22222212 transparent; + height: 100%; +} + +.c1::-webkit-scrollbar { + background: transparent; + width: 4px; + overflow-y: scroll; +} + +.c1::-webkit-scrollbar-thumb { + background: #22222212; + border-radius: 8px; +} + +.c1::-webkit-scrollbar-track { + margin-top: 40px; +} + +.c4 { + position: absolute; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); +} + +.c3 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c3:hover { + opacity: 0.6; +} + +.c3:active { + opacity: 0.4; +} + +.c2 { + color: #222222; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + position: relative; + width: 100%; + margin-bottom: 20px; +} + +.c12 { + height: 100%; + position: relative; +} + +.c8 { + margin-bottom: 24px; +} + +
+ + +
+
+ + + + + +
+ Open limits +
+
+
+
+
+ Please be aware that the execution for limits may vary based on real-time market fluctuations and Ethereum network congestion. Limits may not execute exactly when tokens reach the specified price. +
+
+ Canceling a limit has a network cost. +
+ +
+
+
+
+
+
+ Expires January 26, 2024 at 1:52PM +
+
+
+ +
+ 252.074 + + DAI +
+ + + + + +
+ 0.10684 + + WETH +
+
+
+ when 0.00042 WETH/DAI +
+
+
+
+ +
+
+
+
+
+
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap new file mode 100644 index 0000000..2980208 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/__snapshots__/OpenLimitOrdersButton.test.tsx.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`OpenLimitOrdersButton should render if there are open limit orders 1`] = ` +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c3 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c5 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + border-radius: 16px; + border: none; + background: #F9F9F9; + padding: 12px 16px; + margin-top: 12px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c0:hover { + opacity: 0.6; +} + +.c0:active { + opacity: 0.4; +} + +
+ + + + + +
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/hooks/useCancelLimitsGasEstimate.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/hooks/useCancelLimitsGasEstimate.ts new file mode 100644 index 0000000..daadcff --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Limits/hooks/useCancelLimitsGasEstimate.ts @@ -0,0 +1,20 @@ +import { useCreateCancelTransactionRequest } from 'components/AccountDrawer/MiniPortfolio/Activity/utils' +import { GasFeeResult, GasSpeed, useTransactionGasFee } from 'hooks/useTransactionGasFee' +import { useMemo } from 'react' +import { UniswapXOrderDetails } from 'state/signatures/types' + +export function useCancelLimitsGasEstimate(orders?: UniswapXOrderDetails[]): GasFeeResult { + const cancelTransactionParams = useMemo( + () => + orders && orders.length > 0 + ? { + encodedOrders: orders.map((order) => order.encodedOrder as string), + chainId: orders[0].chainId, + } + : undefined, + [orders] + ) + const cancelTransaction = useCreateCancelTransactionRequest(cancelTransactionParams) + const gasEstimate = useTransactionGasFee(cancelTransaction, GasSpeed.Fast) + return gasEstimate +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx new file mode 100644 index 0000000..b0625ad --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/NFTItem.tsx @@ -0,0 +1,113 @@ +import { InterfaceElementName, SharedEventName } from '@uniswap/analytics-events' +import { sendAnalyticsEvent, useTrace } from 'analytics' +import Column from 'components/Column' +import Row from 'components/Row' +import { Box } from 'nft/components/Box' +import { NftCard } from 'nft/components/card' +import { detailsHref } from 'nft/components/card/utils' +import { VerifiedIcon } from 'nft/components/icons' +import { WalletAsset } from 'nft/types' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { useToggleAccountDrawer } from '../hooks' + +const FloorPrice = styled(Row)` + opacity: 0; + + // prevent empty whitespace from collapsing line height to maintain + // consistent spacing below rows + white-space: pre; +` + +const NFTContainer = styled(Column)` + gap: 8px; + min-height: 150px; + + &:hover { + ${FloorPrice} { + opacity: 1; + } + } +` +const NFTCollectionName = styled(ThemedText.BodySmall)` + white-space: pre; + text-overflow: ellipsis; + overflow: hidden; +` + +export function NFT({ + asset, + mediaShouldBePlaying, + setCurrentTokenPlayingMedia, +}: { + asset: WalletAsset + mediaShouldBePlaying: boolean + setCurrentTokenPlayingMedia: (tokenId: string | undefined) => void +}) { + const toggleWalletDrawer = useToggleAccountDrawer() + const navigate = useNavigate() + const trace = useTrace() + + const navigateToNFTDetails = () => { + toggleWalletDrawer() + navigate(detailsHref(asset)) + } + + return ( + + + sendAnalyticsEvent(SharedEventName.ELEMENT_CLICKED, { + element: InterfaceElementName.MINI_PORTFOLIO_NFT_ITEM, + collection_name: asset.collection?.name, + collection_address: asset.collection?.address, + token_id: asset.tokenId, + ...trace, + }) + } + mediaShouldBePlaying={mediaShouldBePlaying} + setCurrentTokenPlayingMedia={setCurrentTokenPlayingMedia} + testId="mini-portfolio-nft" + /> + + + ) +} + +function NFTDetails({ asset }: { asset: WalletAsset }) { + const { formatNumberOrString } = useFormatter() + + return ( + + + {asset.asset_contract.name} + {asset.collectionIsVerified && } + + + + {asset.floorPrice + ? `${formatNumberOrString({ input: asset.floorPrice, type: NumberType.NFTTokenFloorPrice })} ETH` + : ' '} + + + + ) +} + +const BADGE_SIZE = '18px' +function Verified() { + return ( + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx new file mode 100644 index 0000000..e156943 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/NFTs/index.tsx @@ -0,0 +1,78 @@ +import { useNftBalance } from 'graphql/data/nft/NftBalance' +import { LoadingAssets } from 'nft/components/collection/CollectionAssetLoading' +import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' +import { useState } from 'react' +import InfiniteScroll from 'react-infinite-scroll-component' +import styled from 'styled-components' + +import { DEFAULT_NFT_QUERY_AMOUNT } from '../constants' +import { useAccountDrawer } from '../hooks' +import { NFT } from './NFTItem' + +export default function NFTs({ account }: { account: string }) { + const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer() + const { walletAssets, loading, hasNext, loadMore } = useNftBalance( + account, + [], + [], + DEFAULT_NFT_QUERY_AMOUNT, + undefined, + undefined, + undefined, + !walletDrawerOpen + ) + + const [currentTokenPlayingMedia, setCurrentTokenPlayingMedia] = useState() + + if (loading && !walletAssets) + return ( + + + + ) + + if (!walletAssets || walletAssets?.length === 0) { + return + } + + return ( + + + + ) + } + dataLength={walletAssets?.length ?? 0} + style={{ overflow: 'unset' }} + scrollableTarget="wallet-dropdown-scroll-wrapper" + > + + {walletAssets?.length + ? walletAssets.map((asset, index) => { + return ( + + ) + }) + : null} + + + ) +} + +const AssetsContainer = styled.div` + display: grid; + gap: 12px; + + // use minmax to not let grid items escape the parent container + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + margin: 16px; +` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts new file mode 100644 index 0000000..fa7bdbf --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/cache.ts @@ -0,0 +1,139 @@ +import { ChainId, Token } from '@uniswap/sdk-core' +import { Pool, Position } from '@uniswap/v3-sdk' +import { useAllTokensMultichain } from 'hooks/Tokens' +import { atom, useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' +import ms from 'ms' +import { useCallback } from 'react' +import { deserializeToken, serializeToken } from 'state/user/hooks' +import { SerializedToken } from 'state/user/types' +import { PositionDetails } from 'types/position' +import { buildCurrencyKey, currencyKey } from 'utils/currencyKey' + +import { getTokensAsync } from './getTokensAsync' +import { useInterfaceMulticallContracts } from './hooks' + +export type PositionInfo = { + owner: string + chainId: ChainId + position: Position + pool: Pool + details: PositionDetails + inRange: boolean + closed: boolean + fees?: [number?, number?] + prices?: [number?, number?] +} + +const POSITION_CACHE_EXPIRY = ms(`1m`) // 1 minute is arbitrary here +// Allows reusing recently fetched positions between component mounts +type CachedPositionsEntry = { result: PositionInfo[]; stale: boolean } +const cachedPositionsAtom = atom<{ [address: string]: CachedPositionsEntry | undefined }>({}) +type UseCachedPositionsReturnType = [CachedPositionsEntry | undefined, (positions: PositionInfo[]) => void] +/** + * Caches positions to allow reusing between component mounts + * @param account address to cache positions for + * @returns cached positions for the account, whether the cache is stale, and a function to update the positions and cache + */ +export function useCachedPositions(account: string): UseCachedPositionsReturnType { + const [cachedPositions, setCachedPositions] = useAtom(cachedPositionsAtom) + const setPositionsAndStaleTimeout = useCallback( + (positions: PositionInfo[]) => { + setCachedPositions((cache) => ({ ...cache, [account]: { result: positions, stale: false } })) + setTimeout( + () => + setCachedPositions((cache) => { + // sets stale to true if the positions haven't been updated since the timeout + if (positions === cache[account]?.result) { + return { ...cache, [account]: { result: positions, stale: true } } + } else { + return cache + } + }), + POSITION_CACHE_EXPIRY + ) + }, + [account, setCachedPositions] + ) + return [cachedPositions[account], setPositionsAndStaleTimeout] +} + +const poolAddressKey = (details: PositionDetails, chainId: ChainId) => + `${chainId}-${details.token0}-${details.token1}-${details.fee}` + +type PoolAddressMap = { [key: string]: string | undefined } +const poolAddressCacheAtom = atomWithStorage('poolCache', {}) +/** + * Caches pool addresses to prevent components from having to re-compute them + * @returns get and set functions for the cache + */ +export function usePoolAddressCache() { + const [cache, updateCache] = useAtom(poolAddressCacheAtom) + const get = useCallback( + (details: PositionDetails, chainId: ChainId) => cache[poolAddressKey(details, chainId)], + [cache] + ) + const set = useCallback( + (details: PositionDetails, chainId: ChainId, address: string) => + updateCache((c) => ({ ...c, [poolAddressKey(details, chainId)]: address })), + [updateCache] + ) + return { get, set } +} + +// These values are static, so we can persist them across sessions using `WithStorage` +const tokenCacheAtom = atomWithStorage<{ [key: string]: SerializedToken | undefined }>('cachedAsyncTokens', {}) +function useTokenCache() { + const [cache, setCache] = useAtom(tokenCacheAtom) + const get = useCallback( + (chainId: number, address: string) => { + const entry = cache[buildCurrencyKey(chainId, address)] + return entry ? deserializeToken(entry) : undefined + }, + [cache] + ) + const set = useCallback( + (token?: Token) => { + if (token) { + setCache((cache) => ({ ...cache, [currencyKey(token)]: serializeToken(token) })) + } + }, + [setCache] + ) + return { get, set } +} + +type TokenGetterFn = (addresses: string[], chainId: ChainId) => Promise<{ [key: string]: Token | undefined }> +export function useGetCachedTokens(chains: ChainId[]): TokenGetterFn { + const allTokens = useAllTokensMultichain() + const multicallContracts = useInterfaceMulticallContracts(chains) + const tokenCache = useTokenCache() + + // Used to fetch tokens not available in local state + const fetchRemoteTokens: TokenGetterFn = useCallback( + async (addresses, chainId) => { + const fetched = await getTokensAsync(addresses, chainId, multicallContracts[chainId]) + Object.values(fetched).forEach(tokenCache.set) + return fetched + }, + [multicallContracts, tokenCache] + ) + + // Uses tokens from local state if available, otherwise fetches them + const getTokens: TokenGetterFn = useCallback( + async (addresses, chainId) => { + const local: { [address: string]: Token | undefined } = {} + const missing = new Set() + addresses.forEach((address) => { + const cached = tokenCache.get(chainId, address) ?? allTokens[chainId]?.[address] + cached ? (local[address] = cached) : missing.add(address) + }) + + const fetched = await fetchRemoteTokens([...missing], chainId) + return { ...local, ...fetched } + }, + [allTokens, fetchRemoteTokens, tokenCache] + ) + + return getTokens +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts new file mode 100644 index 0000000..0d372ca --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/getTokensAsync.ts @@ -0,0 +1,127 @@ +import { ChainId, Token } from '@uniswap/sdk-core' +import { Interface } from 'ethers/lib/utils' +import ERC20_ABI from 'uniswap/src/abis/erc20.json' +import { Erc20Interface } from 'uniswap/src/abis/types/Erc20' +import { Erc20Bytes32Interface } from 'uniswap/src/abis/types/Erc20Bytes32' +import { UniswapInterfaceMulticall } from 'uniswap/src/abis/types/v3' +import { isAddress } from 'utilities/src/addresses' +import { DEFAULT_ERC20_DECIMALS } from 'utilities/src/tokens/constants' +import { arrayToSlices } from 'utils/arrays' +import { CurrencyKey, buildCurrencyKey, currencyKey } from 'utils/currencyKey' + +type TokenMap = { [address: string]: Token | undefined } +export type Call = { target: string; callData: string; gasLimit: number } +type CallResult = { success: boolean; returnData: string } +export const DEFAULT_GAS_LIMIT = 1_000_000 + +const Erc20 = new Interface(ERC20_ABI) as Erc20Interface +const Erc20Bytes32 = new Interface(ERC20_ABI) as Erc20Bytes32Interface // Used for tokens that return bytes32 for name/symbol rather than string + +// TODO(WEB-1760): cartcrom - adapt support for multi-function multi-interface multicalls into redux-multicall to remove than this custom cache/chunking logic +// Infura rejects calls with gas costs > 10x the current block gas limit; in such case we split the call into 2 chunks +async function fetchChunk(multicall: UniswapInterfaceMulticall, chunk: Call[]): Promise { + try { + return (await multicall.callStatic.multicall(chunk)).returnData + } catch (error) { + if (error.code === -32603 || error.message?.indexOf('execution ran out of gas') !== -1) { + if (chunk.length > 1) { + const half = Math.floor(chunk.length / 2) + return Promise.all([ + fetchChunk(multicall, chunk.slice(0, half)), + fetchChunk(multicall, chunk.slice(half, chunk.length)), + ]).then(([c0, c1]) => [...c0, ...c1]) + } + } + console.error('Failed to fetch chunk', error) + throw error + } +} + +function tryParseToken(address: string, chainId: ChainId, data: CallResult[]) { + try { + const [nameData, symbolData, decimalsData, nameDataBytes32, symbolDataBytes32] = data + + const name = nameData.success + ? (Erc20.decodeFunctionResult('name', nameData.returnData)[0] as string) + : nameDataBytes32.success + ? (Erc20Bytes32.decodeFunctionResult('name', nameDataBytes32.returnData)[0] as string) + : undefined + const symbol = symbolData.success + ? (Erc20.decodeFunctionResult('symbol', symbolData.returnData)[0] as string) + : symbolDataBytes32.success + ? (Erc20Bytes32.decodeFunctionResult('symbol', symbolDataBytes32.returnData)[0] as string) + : undefined + const decimals = decimalsData.success ? parseInt(decimalsData.returnData) : DEFAULT_ERC20_DECIMALS + + return new Token(chainId, address, decimals, symbol, name) + } catch (error) { + console.error(`Failed to fetch token at address ${address} on chain ${chainId}`) + return undefined + } +} + +function parseTokens(addresses: string[], chainId: ChainId, returnData: CallResult[]) { + const tokenDataSlices = arrayToSlices(returnData, 5) + + return tokenDataSlices.reduce((acc: TokenMap, slice, index) => { + const parsedToken = tryParseToken(addresses[index], chainId, slice) + if (parsedToken) acc[parsedToken.address] = parsedToken + return acc + }, {}) +} + +const createCalls = (target: string, callData: string[]): Call[] => + callData.map((callData) => ({ target, callData, gasLimit: DEFAULT_GAS_LIMIT })) + +function createCallsForToken(address: string) { + return createCalls(address, [ + Erc20.encodeFunctionData('name'), + Erc20.encodeFunctionData('symbol'), + Erc20.encodeFunctionData('decimals'), + Erc20Bytes32.encodeFunctionData('name'), + Erc20Bytes32.encodeFunctionData('symbol'), + ]) +} + +// Prevents tokens from being fetched multiple times +const TokenPromiseCache: { [key: CurrencyKey]: Promise | undefined } = {} + +// Returns tokens using a single RPC call to the multicall contract +export async function getTokensAsync( + addresses: string[], + chainId: ChainId, + multicall: UniswapInterfaceMulticall +): Promise { + if (addresses.length === 0) return {} + const formattedAddresses: string[] = [] + const calls: Call[] = [] + const previouslyCalledTokens: Promise[] = [] + + addresses.forEach((tokenAddress) => { + const key = buildCurrencyKey(chainId, tokenAddress) + const previousCall = TokenPromiseCache[key] + if (previousCall !== undefined) { + previouslyCalledTokens.push(previousCall) + } else { + const formattedAddress = isAddress(tokenAddress) + if (!formattedAddress) return + formattedAddresses.push(formattedAddress) + calls.push(...createCallsForToken(formattedAddress)) + } + }) + + const calledTokens = fetchChunk(multicall, calls).then((returnData) => parseTokens(addresses, chainId, returnData)) + + // Caches tokens currently being fetched for further calls to use + formattedAddresses.forEach( + (address) => + (TokenPromiseCache[buildCurrencyKey(chainId, address)] = calledTokens.then((tokenMap) => tokenMap[address])) + ) + + const tokenMap = await calledTokens + // Add tokens from previous calls to the map of tokens fetched in this call + const resolvedPreviousTokens = await Promise.all(previouslyCalledTokens) + resolvedPreviousTokens.forEach((token) => token && (tokenMap[currencyKey(token)] = token)) + + return tokenMap +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts new file mode 100644 index 0000000..ac71db3 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/hooks.ts @@ -0,0 +1,110 @@ +import { + ChainId, + MULTICALL_ADDRESSES, + Token, + NONFUNGIBLE_POSITION_MANAGER_ADDRESSES as V3NFT_ADDRESSES, +} from '@uniswap/sdk-core' +import type { AddressMap } from '@uniswap/smart-order-router' +import NFTPositionManagerJSON from '@uniswap/v3-periphery/artifacts/contracts/NonfungiblePositionManager.sol/NonfungiblePositionManager.json' +import MulticallJSON from '@uniswap/v3-periphery/artifacts/contracts/lens/UniswapInterfaceMulticall.sol/UniswapInterfaceMulticall.json' +import { useWeb3React } from '@web3-react/core' +import { isSupportedChain } from 'constants/chains' +import { DEPRECATED_RPC_PROVIDERS, RPC_PROVIDERS } from 'constants/providers' +import { BaseContract } from 'ethers/lib/ethers' +import { useFallbackProviderEnabled } from 'featureFlags/flags/fallbackProvider' +import { ContractInput, useUniswapPricesQuery } from 'graphql/data/__generated__/types-and-hooks' +import { toContractInput } from 'graphql/data/util' +import useStablecoinPrice from 'hooks/useStablecoinPrice' +import { useMemo } from 'react' +import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'uniswap/src/abis/types/v3' +import { getContract } from 'utilities/src/contracts/getContract' +import { CurrencyKey, currencyKey, currencyKeyFromGraphQL } from 'utils/currencyKey' + +import { PositionInfo } from './cache' + +type ContractMap = { [key: number]: T } + +// Constructs a chain-to-contract map, using the wallet's provider when available +export function useContractMultichain( + addressMap: AddressMap, + ABI: any, + chainIds?: ChainId[] +): ContractMap { + const { chainId: walletChainId, provider: walletProvider } = useWeb3React() + + const networkProviders = useFallbackProviderEnabled() ? RPC_PROVIDERS : DEPRECATED_RPC_PROVIDERS + + return useMemo(() => { + const relevantChains = + chainIds ?? + Object.keys(addressMap) + .map((chainId) => parseInt(chainId)) + .filter((chainId) => isSupportedChain(chainId)) + + return relevantChains.reduce((acc: ContractMap, chainId) => { + const provider = + walletProvider && walletChainId === chainId + ? walletProvider + : isSupportedChain(chainId) + ? networkProviders[chainId] + : undefined + if (provider) { + acc[chainId] = getContract(addressMap[chainId] ?? '', ABI, provider) as T + } + return acc + }, {}) + }, [ABI, addressMap, chainIds, networkProviders, walletChainId, walletProvider]) +} + +export function useV3ManagerContracts(chainIds: ChainId[]): ContractMap { + return useContractMultichain(V3NFT_ADDRESSES, NFTPositionManagerJSON.abi, chainIds) +} + +export function useInterfaceMulticallContracts(chainIds: ChainId[]): ContractMap { + return useContractMultichain(MULTICALL_ADDRESSES, MulticallJSON.abi, chainIds) +} + +type PriceMap = { [key: CurrencyKey]: number | undefined } +export function usePoolPriceMap(positions: PositionInfo[] | undefined) { + const contracts = useMemo(() => { + if (!positions || !positions.length) return [] + // Avoids fetching duplicate tokens by placing in map + const contractMap = positions.reduce((acc: { [key: string]: ContractInput }, { pool: { token0, token1 } }) => { + acc[currencyKey(token0)] = toContractInput(token0) + acc[currencyKey(token1)] = toContractInput(token1) + return acc + }, {}) + return Object.values(contractMap) + }, [positions]) + + const { data, loading } = useUniswapPricesQuery({ variables: { contracts }, skip: !contracts.length }) + + const priceMap = useMemo( + () => + data?.tokens?.reduce((acc: PriceMap, current) => { + if (current) acc[currencyKeyFromGraphQL(current)] = current.project?.markets?.[0]?.price?.value + return acc + }, {}) ?? {}, + [data?.tokens] + ) + + return { priceMap, pricesLoading: loading && !data } +} + +function useFeeValue(token: Token, fee: number | undefined, queriedPrice: number | undefined) { + const stablecoinPrice = useStablecoinPrice(!queriedPrice ? token : undefined) + return useMemo(() => { + // Prefers gql price, as fetching stablecoinPrice will trigger multiple infura calls for each pool position + const price = queriedPrice ?? (stablecoinPrice ? parseFloat(stablecoinPrice.toSignificant()) : undefined) + const feeValue = fee && price ? fee * price : undefined + + return [price, feeValue] + }, [fee, queriedPrice, stablecoinPrice]) +} + +export function useFeeValues(position: PositionInfo) { + const [priceA, feeValueA] = useFeeValue(position.pool.token0, position.fees?.[0], position.prices?.[0]) + const [priceB, feeValueB] = useFeeValue(position.pool.token1, position.fees?.[1], position.prices?.[1]) + + return { priceA, priceB, fees: (feeValueA || 0) + (feeValueB || 0) } +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx new file mode 100644 index 0000000..324295d --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.test.tsx @@ -0,0 +1,19 @@ +import { mocked } from 'test-utils/mocked' +import { owner, useMultiChainPositionsReturnValue } from 'test-utils/pools/fixtures' +import { render } from 'test-utils/render' + +import Pools from '.' +import useMultiChainPositions from './useMultiChainPositions' + +jest.mock('./useMultiChainPositions') + +jest.spyOn(console, 'warn').mockImplementation() + +beforeEach(() => { + mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) +}) +test('Pools should render LP positions', () => { + const props = { account: owner } + const { container } = render() + expect(container).not.toBeEmptyDOMElement() +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx new file mode 100644 index 0000000..b96d186 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/index.tsx @@ -0,0 +1,202 @@ +import { t } from '@lingui/macro' +import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events' +import { Position } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' +import { TraceEvent } from 'analytics' +import Row from 'components/Row' +import { MouseoverTooltip } from 'components/Tooltip' +import { BIPS_BASE } from 'constants/misc' +import { useFilterPossiblyMaliciousPositions } from 'hooks/useFilterPossiblyMaliciousPositions' +import { useSwitchChain } from 'hooks/useSwitchChain' +import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' +import { useCallback, useMemo, useReducer } from 'react' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { ExpandoRow } from '../ExpandoRow' +import { useToggleAccountDrawer } from '../hooks' +import { PortfolioLogo } from '../PortfolioLogo' +import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' +import { PositionInfo } from './cache' +import { useFeeValues } from './hooks' +import useMultiChainPositions from './useMultiChainPositions' + +/** + * Takes an array of PositionInfo objects (format used by the Uniswap Labs gql API). + * The hook access PositionInfo.details (format used by the NFT position contract), + * filters the PositionDetails data for malicious content, + * and then returns the original data in its original format. + */ +function useFilterPossiblyMaliciousPositionInfo(positions: PositionInfo[] | undefined): PositionInfo[] { + const tokenIdsToPositionInfo: Record = useMemo( + () => + positions + ? positions.reduce((acc, position) => ({ ...acc, [position.details.tokenId.toString()]: position }), {}) + : {}, + [positions] + ) + const positionDetails = useMemo(() => positions?.map((position) => position.details) ?? [], [positions]) + const filteredPositionDetails = useFilterPossiblyMaliciousPositions(positionDetails) + + return useMemo( + () => filteredPositionDetails.map((positionDetails) => tokenIdsToPositionInfo[positionDetails.tokenId.toString()]), + [filteredPositionDetails, tokenIdsToPositionInfo] + ) +} + +export default function Pools({ account }: { account: string }) { + const { positions, loading } = useMultiChainPositions(account) + const filteredPositions = useFilterPossiblyMaliciousPositionInfo(positions) + const [showClosed, toggleShowClosed] = useReducer((showClosed) => !showClosed, false) + + const [openPositions, closedPositions] = useMemo(() => { + const openPositions: PositionInfo[] = [] + const closedPositions: PositionInfo[] = [] + for (let i = 0; i < filteredPositions.length; i++) { + const position = filteredPositions[i] + if (position.closed) { + closedPositions.push(position) + } else { + openPositions.push(position) + } + } + return [openPositions, closedPositions] + }, [filteredPositions]) + + const toggleWalletDrawer = useToggleAccountDrawer() + + if (!filteredPositions || loading) { + return + } + + if (filteredPositions.length === 0) { + return + } + + return ( + + {openPositions.map((positionInfo) => ( + + ))} + + {closedPositions.map((positionInfo) => ( + + ))} + + + ) +} + +const ActiveDot = styled.span<{ closed: boolean; outOfRange: boolean }>` + background-color: ${({ theme, closed, outOfRange }) => + closed ? theme.neutral2 : outOfRange ? theme.deprecated_accentWarning : theme.success}; + border-radius: 50%; + height: 8px; + width: 8px; + margin-left: 4px; + margin-top: 1px; +` + +function calculateLiquidityValue(price0: number | undefined, price1: number | undefined, position: Position) { + if (!price0 || !price1) return undefined + + const value0 = parseFloat(position.amount0.toExact()) * price0 + const value1 = parseFloat(position.amount1.toExact()) * price1 + return value0 + value1 +} + +function PositionListItem({ positionInfo }: { positionInfo: PositionInfo }) { + const { formatNumber } = useFormatter() + + const { chainId, position, pool, details, inRange, closed } = positionInfo + + const { priceA, priceB, fees: feeValue } = useFeeValues(positionInfo) + const liquidityValue = calculateLiquidityValue(priceA, priceB, position) + + const navigate = useNavigate() + const toggleWalletDrawer = useToggleAccountDrawer() + const { chainId: walletChainId, connector } = useWeb3React() + const switchChain = useSwitchChain() + const onClick = useCallback(async () => { + if (walletChainId !== chainId) await switchChain(connector, chainId) + toggleWalletDrawer() + navigate('/pool/' + details.tokenId) + }, [walletChainId, chainId, switchChain, connector, toggleWalletDrawer, navigate, details.tokenId]) + const analyticsEventProperties = useMemo( + () => ({ + chain_id: chainId, + pool_token_0_symbol: pool.token0.symbol, + pool_token_1_symbol: pool.token1.symbol, + pool_token_0_address: pool.token0.address, + pool_token_1_address: pool.token1.address, + }), + [chainId, pool.token0.address, pool.token0.symbol, pool.token1.address, pool.token1.symbol] + ) + + return ( + + } + title={ + + + {pool.token0.symbol} / {pool.token1?.symbol} + + + } + descriptor={{`${pool.fee / BIPS_BASE}%`}} + right={ + <> + + {`${formatNumber({ + input: liquidityValue, + type: NumberType.PortfolioBalance, + })} (liquidity) + ${formatNumber({ + input: feeValue, + type: NumberType.PortfolioBalance, + })} (fees)`} +
+ } + > + + {formatNumber({ + input: (liquidityValue ?? 0) + (feeValue ?? 0), + type: NumberType.PortfolioBalance, + })} + + + + + + {closed ? t`Closed` : inRange ? t`In range` : t`Out of range`} + + + + + } + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx new file mode 100644 index 0000000..55cb5a2 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions.tsx @@ -0,0 +1,230 @@ +import { ChainId, CurrencyAmount, Token, V3_CORE_FACTORY_ADDRESSES } from '@uniswap/sdk-core' +import IUniswapV3PoolStateJSON from '@uniswap/v3-core/artifacts/contracts/interfaces/pool/IUniswapV3PoolState.sol/IUniswapV3PoolState.json' +import { Pool, Position, computePoolAddress } from '@uniswap/v3-sdk' +import { BigNumber } from 'ethers/lib/ethers' +import { Interface } from 'ethers/lib/utils' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { PositionDetails } from 'types/position' +import { NonfungiblePositionManager, UniswapInterfaceMulticall } from 'uniswap/src/abis/types/v3' +import { UniswapV3PoolInterface } from 'uniswap/src/abis/types/v3/UniswapV3Pool' +import { DEFAULT_ERC20_DECIMALS } from 'utilities/src/tokens/constants' +import { currencyKey } from 'utils/currencyKey' + +import { PositionInfo, useCachedPositions, useGetCachedTokens, usePoolAddressCache } from './cache' +import { Call, DEFAULT_GAS_LIMIT } from './getTokensAsync' +import { useInterfaceMulticallContracts, usePoolPriceMap, useV3ManagerContracts } from './hooks' + +function createPositionInfo( + owner: string, + chainId: ChainId, + details: PositionDetails, + slot0: any, + tokenA: Token, + tokenB: Token +): PositionInfo { + /* Instantiates a Pool with a hardcoded 0 liqudity value since the sdk only uses this value for swap state and this avoids an RPC fetch */ + const pool = new Pool(tokenA, tokenB, details.fee, slot0.sqrtPriceX96.toString(), 0, slot0.tick) + const position = new Position({ + pool, + liquidity: details.liquidity.toString(), + tickLower: details.tickLower, + tickUpper: details.tickUpper, + }) + const inRange = slot0.tick >= details.tickLower && slot0.tick < details.tickUpper + const closed = details.liquidity.eq(0) + return { owner, chainId, pool, position, details, inRange, closed } +} + +type FeeAmounts = [BigNumber, BigNumber] + +const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1) + +const DEFAULT_CHAINS = [ + ChainId.MAINNET, + ChainId.ARBITRUM_ONE, + ChainId.OPTIMISM, + ChainId.POLYGON, + ChainId.CELO, + ChainId.BNB, + ChainId.AVALANCHE, + ChainId.BASE, +] + +type UseMultiChainPositionsData = { positions?: PositionInfo[]; loading: boolean } + +/** + * Returns all positions for a given account on multiple chains. + * + * This hook doesn't use the redux-multicall library to avoid having to manually fetching blocknumbers for each chain. + * + * @param account - account to fetch positions for + * @param chains - chains to fetch positions from + * @returns positions, fees + */ +export default function useMultiChainPositions(account: string, chains = DEFAULT_CHAINS): UseMultiChainPositionsData { + const pms = useV3ManagerContracts(chains) + const multicalls = useInterfaceMulticallContracts(chains) + + const getTokens = useGetCachedTokens(chains) + const poolAddressCache = usePoolAddressCache() + + const [cachedPositions, setPositions] = useCachedPositions(account) + const positions = cachedPositions?.result + const positionsFetching = useRef(false) + const positionsLoading = !cachedPositions?.result && positionsFetching.current + + const [feeMap, setFeeMap] = useState<{ [key: string]: FeeAmounts }>({}) + + const { priceMap, pricesLoading } = usePoolPriceMap(positions) + + const fetchPositionFees = useCallback( + async (pm: NonfungiblePositionManager, positionIds: BigNumber[], chainId: number) => { + const callData = positionIds.map((id) => + pm.interface.encodeFunctionData('collect', [ + { tokenId: id, recipient: account, amount0Max: MAX_UINT128, amount1Max: MAX_UINT128 }, + ]) + ) + const fees = (await pm.callStatic.multicall(callData)).reduce((acc, feeBytes, index) => { + const key = chainId.toString() + positionIds[index] + acc[key] = pm.interface.decodeFunctionResult('collect', feeBytes) as FeeAmounts + return acc + }, {} as { [key: string]: FeeAmounts }) + + setFeeMap((prev) => ({ ...prev, ...fees })) + }, + [account] + ) + + const fetchPositionIds = useCallback( + async (pm: NonfungiblePositionManager, balance: BigNumber) => { + const callData = Array.from({ length: balance.toNumber() }, (_, i) => + pm.interface.encodeFunctionData('tokenOfOwnerByIndex', [account, i]) + ) + return (await pm.callStatic.multicall(callData)).map((idByte) => BigNumber.from(idByte)) + }, + [account] + ) + + const fetchPositionDetails = useCallback(async (pm: NonfungiblePositionManager, positionIds: BigNumber[]) => { + const callData = positionIds.map((id) => pm.interface.encodeFunctionData('positions', [id])) + return (await pm.callStatic.multicall(callData)).map( + (positionBytes, index) => + ({ + ...pm.interface.decodeFunctionResult('positions', positionBytes), + tokenId: positionIds[index], + } as unknown as PositionDetails) + ) + }, []) + + // Combines PositionDetails with Pool data to build our return type + const fetchPositionInfo = useCallback( + async (positionDetails: PositionDetails[], chainId: ChainId, multicall: UniswapInterfaceMulticall) => { + const poolInterface = new Interface(IUniswapV3PoolStateJSON.abi) as UniswapV3PoolInterface + const tokens = await getTokens( + positionDetails.flatMap((details) => [details.token0, details.token1]), + chainId + ) + + const calls: Call[] = [] + const poolPairs: [Token, Token][] = [] + positionDetails.forEach((details) => { + const tokenA = tokens[details.token0] ?? new Token(chainId, details.token0, DEFAULT_ERC20_DECIMALS) + const tokenB = tokens[details.token1] ?? new Token(chainId, details.token1, DEFAULT_ERC20_DECIMALS) + + let poolAddress = poolAddressCache.get(details, chainId) + if (!poolAddress) { + const factoryAddress = V3_CORE_FACTORY_ADDRESSES[chainId] + poolAddress = computePoolAddress({ factoryAddress, tokenA, tokenB, fee: details.fee }) + poolAddressCache.set(details, chainId, poolAddress) + } + poolPairs.push([tokenA, tokenB]) + calls.push({ + target: poolAddress, + callData: poolInterface.encodeFunctionData('slot0'), + gasLimit: DEFAULT_GAS_LIMIT, + }) + }, []) + + return (await multicall.callStatic.multicall(calls)).returnData.reduce((acc: PositionInfo[], result, i) => { + if (result.success) { + const slot0 = poolInterface.decodeFunctionResult('slot0', result.returnData) + acc.push(createPositionInfo(account, chainId, positionDetails[i], slot0, ...poolPairs[i])) + } else { + console.debug('slot0 fetch errored', result) + } + return acc + }, []) + }, + [account, poolAddressCache, getTokens] + ) + + const fetchPositionsForChain = useCallback( + async (chainId: ChainId): Promise => { + if (!account || account.length === 0) { + return [] + } + try { + const pm = pms[chainId] + const multicall = multicalls[chainId] + const balance = await pm?.balanceOf(account) + if (!pm || !multicall || balance.lt(1)) return [] + + const positionIds = await fetchPositionIds(pm, balance) + // Fetches fees in the background and stores them separetely from the results of this function + fetchPositionFees(pm, positionIds, chainId) + + const postionDetails = await fetchPositionDetails(pm, positionIds) + return fetchPositionInfo(postionDetails, chainId, multicall) + } catch (error) { + console.error(`Failed to fetch positions for chain ${chainId}`, error) + return [] + } + }, + [account, fetchPositionDetails, fetchPositionFees, fetchPositionIds, fetchPositionInfo, pms, multicalls] + ) + + const fetchAllPositions = useCallback(async () => { + positionsFetching.current = true + const positions = (await Promise.all(chains.map(fetchPositionsForChain))).flat() + positionsFetching.current = false + setPositions(positions) + }, [chains, fetchPositionsForChain, setPositions]) + + // Fetches positions when existing positions are stale and the document has focus + useEffect(() => { + if (positionsFetching.current || cachedPositions?.stale === false) return + else if (document.hasFocus()) { + fetchAllPositions() + } else { + // Avoids refetching positions until the user returns to Interface to avoid polling unnused rpc data + const onFocus = () => { + fetchAllPositions() + window.removeEventListener('focus', onFocus) + } + window.addEventListener('focus', onFocus) + return () => { + window.removeEventListener('focus', onFocus) + } + } + return + }, [fetchAllPositions, positionsFetching, cachedPositions?.stale]) + + const positionsWithFeesAndPrices: PositionInfo[] | undefined = useMemo( + () => + positions?.map((position) => { + const key = position.chainId.toString() + position.details.tokenId + const fees = feeMap[key] + ? [ + // We parse away from SDK/ethers types so fees can be multiplied by primitive number prices + parseFloat(CurrencyAmount.fromRawAmount(position.pool.token0, feeMap[key]?.[0].toString()).toExact()), + parseFloat(CurrencyAmount.fromRawAmount(position.pool.token1, feeMap[key]?.[1].toString()).toExact()), + ] + : undefined + const prices = [priceMap[currencyKey(position.pool.token0)], priceMap[currencyKey(position.pool.token1)]] + return { ...position, fees, prices } as PositionInfo + }), + [feeMap, positions, priceMap] + ) + + return { positions: positionsWithFeesAndPrices, loading: pricesLoading || positionsLoading } +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx new file mode 100644 index 0000000..b9c3bd8 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.test.tsx @@ -0,0 +1,19 @@ +import { ChainId } from '@uniswap/sdk-core' +import { DAI, DAI_ARBITRUM_ONE, USDC_ARBITRUM, USDC_MAINNET } from 'constants/tokens' +import { render } from 'test-utils/render' + +import { PortfolioLogo } from './PortfolioLogo' + +describe('PortfolioLogo', () => { + it('renders without L2 icon', () => { + const { container } = render() + expect(container).toMatchSnapshot() + }) + + it('renders with L2 icon', () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx new file mode 100644 index 0000000..c9e4715 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioLogo.tsx @@ -0,0 +1,192 @@ +import { ChainId, Currency } from '@uniswap/sdk-core' +import blankTokenUrl from 'assets/svg/blank_token.svg' +import { ReactComponent as UnknownStatus } from 'assets/svg/contract-interaction.svg' +import { MissingImageLogo } from 'components/Logo/AssetLogo' +import { ChainLogo, getDefaultBorderRadius } from 'components/Logo/ChainLogo' +import { Unicon } from 'components/Unicon' +import { useUniconV2Flag } from 'featureFlags/flags/uniconV2' +import useTokenLogoSource from 'hooks/useAssetLogoSource' +import useENSAvatar from 'hooks/useENSAvatar' +import React from 'react' +import { Loader } from 'react-feather' +import styled from 'styled-components' +import { UniconV2 } from 'ui/src' + +const UnknownContract = styled(UnknownStatus)` + color: ${({ theme }) => theme.neutral2}; +` + +const DoubleLogoContainer = styled.div` + display: flex; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; + img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; + } + img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; + } + img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; + } +` + +const LogoContainer = styled.div` + display: flex; + align-items: center; + position: relative; + top: 0; + left: 0; +` + +const ENSAvatarImg = styled.img` + border-radius: 8px; + height: 40px; + width: 40px; +` + +const CircleLogoImage = styled.img<{ size: string }>` + width: ${({ size }) => size}; + height: ${({ size }) => size}; + border-radius: 50%; +` + +const L2LogoContainer = styled.div` + position: absolute; + border-radius: ${getDefaultBorderRadius(16)}px; + left: 65%; + top: 65%; + outline: 1.5px solid ${({ theme }) => theme.surface1}; + width: 40%; + height: 40%; + display: flex; + align-items: center; + justify-content: center; +` + +interface DoubleLogoProps { + logo1?: string + logo2?: string + size: string + onError1?: () => void + onError2?: () => void +} + +function DoubleLogo({ logo1, onError1, logo2, onError2, size }: DoubleLogoProps) { + return ( + + + + + ) +} + +interface DoubleCurrencyLogoProps { + chainId: ChainId + currencies: Array + images?: Array + size: string +} + +function DoubleCurrencyLogo({ chainId, currencies, images, size }: DoubleCurrencyLogoProps) { + const [src, nextSrc] = useTokenLogoSource({ + address: currencies?.[0]?.wrapped.address, + chainId, + isNative: currencies?.[0]?.isNative, + primaryImg: images?.[0], + }) + const [src2, nextSrc2] = useTokenLogoSource({ + address: currencies?.[1]?.wrapped.address, + chainId, + isNative: currencies?.[1]?.isNative, + primaryImg: images?.[1], + }) + + if (currencies.length === 1 && src) { + return + } + if (currencies.length > 1) { + return + } + return ( + + {currencies[0]?.symbol?.toUpperCase().replace('$', '').replace(/\s+/g, '').slice(0, 3)} + + ) +} + +function PortfolioAvatar({ accountAddress, size }: { accountAddress: string; size: string }) { + const { avatar, loading } = useENSAvatar(accountAddress, false) + const uniconV2Enabled = useUniconV2Flag() + + if (loading) { + return + } + if (avatar) { + return + } + return ( + <> + {uniconV2Enabled ? ( + + ) : ( + + )} + + ) +} + +interface PortfolioLogoProps { + chainId: ChainId + accountAddress?: string + currencies?: Array + images?: Array + size?: string + style?: React.CSSProperties +} + +function SquareL2Logo({ chainId }: { chainId: ChainId }) { + if (chainId === ChainId.MAINNET) return null + + return ( + + + + ) +} + +// TODO(WEB-2983) +/** + * Renders an image by prioritizing a list of sources, and then eventually a fallback contract icon + */ +export function PortfolioLogo(props: PortfolioLogoProps) { + return ( + + {getLogo(props)} + + + ) +} + +function getLogo({ chainId, accountAddress, currencies, images, size = '40px' }: PortfolioLogoProps) { + if (accountAddress) { + return + } + if (currencies && currencies.length) { + return + } + if (images?.length === 1) { + return + } + if (images && images?.length >= 2) { + return + } + return +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx new file mode 100644 index 0000000..7e55431 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/PortfolioRow.tsx @@ -0,0 +1,96 @@ +import Column, { AutoColumn } from 'components/Column' +import Row from 'components/Row' +import { LoadingBubble } from 'components/Tokens/loading' +import styled, { css, keyframes } from 'styled-components' + +export const PortfolioRowWrapper = styled(Row)<{ onClick?: any }>` + gap: 12px; + height: 68px; + padding: 0 16px; + + transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color`}; + + ${({ onClick }) => onClick && 'cursor: pointer'}; + + &:hover { + cursor: pointer; + } +` + +const EndColumn = styled(Column)` + align-items: flex-end; +` + +export default function PortfolioRow({ + ['data-testid']: testId, + left, + title, + descriptor, + right, + onClick, + className, +}: { + 'data-testid'?: string + left: React.ReactNode + title: React.ReactNode + descriptor?: React.ReactNode + right?: React.ReactNode + setIsHover?: (b: boolean) => void + onClick?: () => void + className?: string +}) { + return ( + + {left} + + {title} + {descriptor} + + {right && {right}} + + ) +} + +function PortfolioSkeletonRow({ shrinkRight }: { shrinkRight?: boolean }) { + return ( + + + + + + + + {shrinkRight ? ( + + ) : ( + <> + + + + )} + + + ) +} + +export function PortfolioSkeleton({ shrinkRight = false }: { shrinkRight?: boolean }) { + return ( + <> + {Array.from({ length: 5 }).map((_, i) => ( + + ))} + + ) +} + +const fadeIn = keyframes` + from { opacity: .25 } + to { opacity: 1 } +` +export const portfolioFadeInAnimation = css` + animation: ${fadeIn} ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.in}`}; +` + +export const PortfolioTabWrapper = styled.div` + ${portfolioFadeInAnimation} +` diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx new file mode 100644 index 0000000..aeb3bae --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/Tokens/index.tsx @@ -0,0 +1,146 @@ +import { BrowserEvent, InterfaceElementName, SharedEventName } from '@uniswap/analytics-events' +import { TraceEvent } from 'analytics' +import { hideSpamAtom } from 'components/AccountDrawer/SpamToggle' +import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' +import Row from 'components/Row' +import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' +import { PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks' +import { PortfolioToken } from 'graphql/data/portfolios' +import { getTokenDetailsURL, gqlToCurrency, logSentryErrorForUnsupportedChain } from 'graphql/data/util' +import { useAtomValue } from 'jotai/utils' +import { EmptyWalletModule } from 'nft/components/profile/view/EmptyWalletContent' +import { useCallback, useMemo, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { splitHiddenTokens } from 'utils/splitHiddenTokens' + +import { hideSmallBalancesAtom } from '../../SmallBalanceToggle' +import { ExpandoRow } from '../ExpandoRow' +import { useToggleAccountDrawer } from '../hooks' +import { PortfolioLogo } from '../PortfolioLogo' +import PortfolioRow, { PortfolioSkeleton, PortfolioTabWrapper } from '../PortfolioRow' + +export default function Tokens({ account }: { account: string }) { + const toggleWalletDrawer = useToggleAccountDrawer() + const hideSmallBalances = useAtomValue(hideSmallBalancesAtom) + const hideSpam = useAtomValue(hideSpamAtom) + const [showHiddenTokens, setShowHiddenTokens] = useState(false) + + const { data } = useCachedPortfolioBalancesQuery({ account }) + + const tokenBalances = data?.portfolios?.[0].tokenBalances + + const { visibleTokens, hiddenTokens } = useMemo( + () => splitHiddenTokens(tokenBalances ?? [], { hideSmallBalances, hideSpam }), + [hideSmallBalances, tokenBalances, hideSpam] + ) + + if (!data) { + return + } + + if (tokenBalances?.length === 0) { + // TODO: consider launching moonpay here instead of just closing the drawer + return + } + + const toggleHiddenTokens = () => setShowHiddenTokens((showHiddenTokens) => !showHiddenTokens) + + return ( + + {visibleTokens.map( + (tokenBalance) => + tokenBalance.token && + )} + + {hiddenTokens.map( + (tokenBalance) => + tokenBalance.token && + )} + + + ) +} + +const TokenBalanceText = styled(ThemedText.BodySecondary)` + ${EllipsisStyle} +` +const TokenNameText = styled(ThemedText.SubHeader)` + ${EllipsisStyle} +` + +function TokenRow({ + token, + quantity, + denominatedValue, + tokenProjectMarket, +}: PortfolioTokenBalancePartsFragment & { token: PortfolioToken }) { + const { formatDelta } = useFormatter() + const percentChange = tokenProjectMarket?.pricePercentChange?.value ?? 0 + + const navigate = useNavigate() + const toggleWalletDrawer = useToggleAccountDrawer() + + const navigateToTokenDetails = useCallback(async () => { + navigate(getTokenDetailsURL({ ...token })) + toggleWalletDrawer() + }, [navigate, token, toggleWalletDrawer]) + const { formatNumber } = useFormatter() + + const currency = gqlToCurrency(token) + if (!currency) { + logSentryErrorForUnsupportedChain({ + extras: { token }, + errorMessage: 'Token from unsupported chain received from Mini Portfolio Token Balance Query', + }) + return null + } + return ( + + + } + title={{token?.name}} + descriptor={ + + {formatNumber({ + input: quantity, + type: NumberType.TokenNonTx, + })}{' '} + {token?.symbol} + + } + onClick={navigateToTokenDetails} + right={ + denominatedValue && ( + <> + + {formatNumber({ + input: denominatedValue?.value, + type: NumberType.PortfolioBalance, + })} + + + + {formatDelta(percentChange)} + + + ) + } + /> + + ) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap b/apps/web/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap new file mode 100644 index 0000000..275a321 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/__snapshots__/PortfolioLogo.test.tsx.snap @@ -0,0 +1,215 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PortfolioLogo renders with L2 icon 1`] = ` +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c1 img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; +} + +.c1 img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; +} + +.c1 img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c2 { + width: 40px; + height: 40px; + border-radius: 50%; +} + +.c3 { + position: absolute; + border-radius: 4px; + left: 65%; + top: 65%; + outline: 1.5px solid #FFFFFF; + width: 40%; + height: 40%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +
+ + +
+
+ + +
+
+ + + Arbitrum logo + + + + + arbitrum.svg + + +
+
+
+
+
+`; + +exports[`PortfolioLogo renders without L2 icon 1`] = ` +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c1 img:nth-child(n) { + width: 19px; + height: 40px; + object-fit: cover; +} + +.c1 img:nth-child(1) { + border-radius: 20px 0 0 20px; + object-position: 0 0; +} + +.c1 img:nth-child(2) { + border-radius: 0 20px 20px 0; + object-position: 100% 0; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c2 { + width: 40px; + height: 40px; + border-radius: 50%; +} + +
+ + +
+
+ + +
+
+
+
+
+`; diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/constants.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/constants.tsx new file mode 100644 index 0000000..c50ffdb --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/constants.tsx @@ -0,0 +1,277 @@ +import { t } from '@lingui/macro' +import { SwapOrderStatus, TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { UniswapXOrderStatus } from 'lib/hooks/orders/types' +import { TransactionType } from 'state/transactions/types' + +// use even number because rows are in groups of 2 +export const DEFAULT_NFT_QUERY_AMOUNT = 26 + +const TransactionTitleTable: { [key in TransactionType]: { [state in TransactionStatus]: string } } = { + [TransactionType.SWAP]: { + [TransactionStatus.Pending]: t`Swapping`, + [TransactionStatus.Confirmed]: t`Swapped`, + [TransactionStatus.Failed]: t`Swap failed`, + }, + [TransactionType.WRAP]: { + [TransactionStatus.Pending]: t`Wrapping`, + [TransactionStatus.Confirmed]: t`Wrapped`, + [TransactionStatus.Failed]: t`Wrap failed`, + }, + [TransactionType.ADD_LIQUIDITY_V3_POOL]: { + [TransactionStatus.Pending]: t`Adding liquidity`, + [TransactionStatus.Confirmed]: t`Added liquidity`, + [TransactionStatus.Failed]: t`Add liquidity failed`, + }, + [TransactionType.REMOVE_LIQUIDITY_V3]: { + [TransactionStatus.Pending]: t`Removing liquidity`, + [TransactionStatus.Confirmed]: t`Removed liquidity`, + [TransactionStatus.Failed]: t`Remove liquidity failed`, + }, + [TransactionType.CREATE_V3_POOL]: { + [TransactionStatus.Pending]: t`Creating pool`, + [TransactionStatus.Confirmed]: t`Created pool`, + [TransactionStatus.Failed]: t`Create pool failed`, + }, + [TransactionType.COLLECT_FEES]: { + [TransactionStatus.Pending]: t`Collecting fees`, + [TransactionStatus.Confirmed]: t`Collected fees`, + [TransactionStatus.Failed]: t`Collect fees failed`, + }, + [TransactionType.APPROVAL]: { + [TransactionStatus.Pending]: t`Approving`, + [TransactionStatus.Confirmed]: t`Approved`, + [TransactionStatus.Failed]: t`Approval failed`, + }, + [TransactionType.CLAIM]: { + [TransactionStatus.Pending]: t`Claiming`, + [TransactionStatus.Confirmed]: t`Claimed`, + [TransactionStatus.Failed]: t`Claim failed`, + }, + [TransactionType.BUY]: { + [TransactionStatus.Pending]: t`Buying`, + [TransactionStatus.Confirmed]: t`Bought`, + [TransactionStatus.Failed]: t`Buy failed`, + }, + [TransactionType.SEND]: { + [TransactionStatus.Pending]: t`Sending`, + [TransactionStatus.Confirmed]: t`Sent`, + [TransactionStatus.Failed]: t`Send failed`, + }, + [TransactionType.RECEIVE]: { + [TransactionStatus.Pending]: t`Receiving`, + [TransactionStatus.Confirmed]: t`Received`, + [TransactionStatus.Failed]: t`Receive failed`, + }, + [TransactionType.MINT]: { + [TransactionStatus.Pending]: t`Minting`, + [TransactionStatus.Confirmed]: t`Minted`, + [TransactionStatus.Failed]: t`Mint failed`, + }, + [TransactionType.BURN]: { + [TransactionStatus.Pending]: t`Burning`, + [TransactionStatus.Confirmed]: t`Burned`, + [TransactionStatus.Failed]: t`Burn failed`, + }, + [TransactionType.VOTE]: { + [TransactionStatus.Pending]: t`Voting`, + [TransactionStatus.Confirmed]: t`Voted`, + [TransactionStatus.Failed]: t`Vote failed`, + }, + [TransactionType.QUEUE]: { + [TransactionStatus.Pending]: t`Queuing`, + [TransactionStatus.Confirmed]: t`Queued`, + [TransactionStatus.Failed]: t`Queue failed`, + }, + [TransactionType.EXECUTE]: { + [TransactionStatus.Pending]: t`Executing`, + [TransactionStatus.Confirmed]: t`Executed`, + [TransactionStatus.Failed]: t`Execute failed`, + }, + [TransactionType.BORROW]: { + [TransactionStatus.Pending]: t`Borrowing`, + [TransactionStatus.Confirmed]: t`Borrowed`, + [TransactionStatus.Failed]: t`Borrow failed`, + }, + [TransactionType.REPAY]: { + [TransactionStatus.Pending]: t`Repaying`, + [TransactionStatus.Confirmed]: t`Repaid`, + [TransactionStatus.Failed]: t`Repay failed`, + }, + [TransactionType.DEPLOY]: { + [TransactionStatus.Pending]: t`Deploying`, + [TransactionStatus.Confirmed]: t`Deployed`, + [TransactionStatus.Failed]: t`Deploy failed`, + }, + [TransactionType.CANCEL]: { + [TransactionStatus.Pending]: t`Cancelling`, + [TransactionStatus.Confirmed]: t`Cancelled`, + [TransactionStatus.Failed]: t`Cancel failed`, + }, + [TransactionType.DELEGATE]: { + [TransactionStatus.Pending]: t`Delegating`, + [TransactionStatus.Confirmed]: t`Delegated`, + [TransactionStatus.Failed]: t`Delegate failed`, + }, + [TransactionType.DEPOSIT_LIQUIDITY_STAKING]: { + [TransactionStatus.Pending]: t`Depositing`, + [TransactionStatus.Confirmed]: t`Deposited`, + [TransactionStatus.Failed]: t`Deposit failed`, + }, + [TransactionType.WITHDRAW_LIQUIDITY_STAKING]: { + [TransactionStatus.Pending]: t`Withdrawing`, + [TransactionStatus.Confirmed]: t`Withdrew`, + [TransactionStatus.Failed]: t`Withdraw failed`, + }, + [TransactionType.ADD_LIQUIDITY_V2_POOL]: { + [TransactionStatus.Pending]: t`Adding V2 liquidity`, + [TransactionStatus.Confirmed]: t`Added V2 liquidity`, + [TransactionStatus.Failed]: t`Add V2 liquidity failed`, + }, + [TransactionType.MIGRATE_LIQUIDITY_V3]: { + [TransactionStatus.Pending]: t`Migrating liquidity`, + [TransactionStatus.Confirmed]: t`Migrated liquidity`, + [TransactionStatus.Failed]: t`Migrate liquidity failed`, + }, + [TransactionType.SUBMIT_PROPOSAL]: { + [TransactionStatus.Pending]: t`Submitting proposal`, + [TransactionStatus.Confirmed]: t`Submitted proposal`, + [TransactionStatus.Failed]: t`Submit proposal failed`, + }, + [TransactionType.LIMIT]: { + [TransactionStatus.Pending]: t`Limit opened`, + [TransactionStatus.Confirmed]: t`Limit executed`, + [TransactionStatus.Failed]: t`Limit failed`, + }, +} + +export const CancelledTransactionTitleTable: { [key in TransactionType]: string } = { + [TransactionType.SWAP]: t`Swap cancelled`, + [TransactionType.WRAP]: t`Wrap cancelled`, + [TransactionType.ADD_LIQUIDITY_V3_POOL]: t`Add liquidity cancelled`, + [TransactionType.REMOVE_LIQUIDITY_V3]: t`Remove liquidity cancelled`, + [TransactionType.CREATE_V3_POOL]: t`Create pool cancelled`, + [TransactionType.COLLECT_FEES]: t`Collect fees cancelled`, + [TransactionType.APPROVAL]: t`Approval cancelled`, + [TransactionType.CLAIM]: t`Claim cancelled`, + [TransactionType.BUY]: t`Buy cancelled`, + [TransactionType.SEND]: t`Send cancelled`, + [TransactionType.RECEIVE]: t`Receive cancelled`, + [TransactionType.MINT]: t`Mint cancelled`, + [TransactionType.BURN]: t`Burn cancelled`, + [TransactionType.VOTE]: t`Vote cancelled`, + [TransactionType.QUEUE]: t`Queue cancelled`, + [TransactionType.EXECUTE]: t`Execute cancelled`, + [TransactionType.BORROW]: t`Borrow cancelled`, + [TransactionType.REPAY]: t`Repay cancelled`, + [TransactionType.DEPLOY]: t`Deploy cancelled`, + [TransactionType.CANCEL]: t`Cancellation cancelled`, + [TransactionType.DELEGATE]: t`Delegate cancelled`, + [TransactionType.DEPOSIT_LIQUIDITY_STAKING]: t`Deposit cancelled`, + [TransactionType.WITHDRAW_LIQUIDITY_STAKING]: t`Withdrawal cancelled`, + [TransactionType.ADD_LIQUIDITY_V2_POOL]: t`Add V2 liquidity cancelled`, + [TransactionType.MIGRATE_LIQUIDITY_V3]: t`Migrate liquidity cancelled`, + [TransactionType.SUBMIT_PROPOSAL]: t`Submit proposal cancelled`, + [TransactionType.LIMIT]: t`Limit cancelled`, +} + +const AlternateTransactionTitleTable: { [key in TransactionType]?: { [state in TransactionStatus]: string } } = { + [TransactionType.WRAP]: { + [TransactionStatus.Pending]: t`Unwrapping`, + [TransactionStatus.Confirmed]: t`Unwrapped`, + [TransactionStatus.Failed]: t`Unwrap failed`, + }, + [TransactionType.APPROVAL]: { + [TransactionStatus.Pending]: t`Revoking approval`, + [TransactionStatus.Confirmed]: t`Revoked approval`, + [TransactionStatus.Failed]: t`Revoke approval failed`, + }, +} + +export function getActivityTitle(type: TransactionType, status: TransactionStatus, alternate?: boolean) { + if (alternate) { + const alternateTitle = AlternateTransactionTitleTable[type] + if (alternateTitle !== undefined) return alternateTitle[status] + } + return TransactionTitleTable[type][status] +} + +const SwapTitleTable = TransactionTitleTable[TransactionType.SWAP] +export const OrderTextTable: { + [status in UniswapXOrderStatus]: { title: string; status: TransactionStatus; statusMessage?: string } +} = { + [UniswapXOrderStatus.OPEN]: { + title: SwapTitleTable.PENDING, + status: TransactionStatus.Pending, + }, + [UniswapXOrderStatus.FILLED]: { + title: SwapTitleTable.CONFIRMED, + status: TransactionStatus.Confirmed, + }, + [UniswapXOrderStatus.EXPIRED]: { + title: t`Swap expired`, + statusMessage: t`Your swap could not be fulfilled at this time. Please try again.`, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.ERROR]: { + title: SwapTitleTable.FAILED, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.INSUFFICIENT_FUNDS]: { + title: t`Insufficient funds`, + statusMessage: t`Your account had insufficent funds to complete this swap.`, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.CANCELLED]: { + title: t`Swap cancelled`, + status: TransactionStatus.Failed, + }, +} + +const LimitTitleTable = TransactionTitleTable[TransactionType.LIMIT] +export const LimitOrderTextTable: { + [status in UniswapXOrderStatus]: { title: string; status: TransactionStatus; statusMessage?: string } +} = { + [UniswapXOrderStatus.OPEN]: { + title: LimitTitleTable.PENDING, + status: TransactionStatus.Pending, + }, + [UniswapXOrderStatus.FILLED]: { + title: LimitTitleTable.CONFIRMED, + status: TransactionStatus.Confirmed, + }, + [UniswapXOrderStatus.EXPIRED]: { + title: t`Limit expired`, + statusMessage: t`Your limit could not be fulfilled at this time. Please try again.`, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.ERROR]: { + title: LimitTitleTable.FAILED, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.INSUFFICIENT_FUNDS]: { + title: LimitTitleTable.FAILED, + statusMessage: t`Your account had insufficent funds to complete this swap.`, + status: TransactionStatus.Failed, + }, + [UniswapXOrderStatus.CANCELLED]: { + title: t`Limit cancelled`, + status: TransactionStatus.Failed, + }, +} + +// Non-exhaustive list of addresses Moonpay uses when sending purchased tokens +export const MOONPAY_SENDER_ADDRESSES = [ + '0x8216874887415e2650d12d53ff53516f04a74fd7', + '0x151b381058f91cf871e7ea1ee83c45326f61e96d', + '0xb287eac48ab21c5fb1d3723830d60b4c797555b0', + '0xd108fd0e8c8e71552a167e7a44ff1d345d233ba6', +] + +// Converts GQL backend orderStatus enum to the enum used by the frontend and UniswapX backend +export const OrderStatusTable: { [key in SwapOrderStatus]: UniswapXOrderStatus } = { + [SwapOrderStatus.Open]: UniswapXOrderStatus.OPEN, + [SwapOrderStatus.Expired]: UniswapXOrderStatus.EXPIRED, + [SwapOrderStatus.Error]: UniswapXOrderStatus.ERROR, + [SwapOrderStatus.InsufficientFunds]: UniswapXOrderStatus.INSUFFICIENT_FUNDS, + [SwapOrderStatus.Filled]: UniswapXOrderStatus.FILLED, +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/formatTimestamp.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/formatTimestamp.ts new file mode 100644 index 0000000..bdcae15 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/formatTimestamp.ts @@ -0,0 +1,27 @@ +export enum FormatType { + Short = 'short', + Long = 'long', +} + +export function formatTimestamp(timestamp: number | undefined, includeYear?: boolean, type = FormatType.Long): string { + const options: Intl.DateTimeFormatOptions = + type === FormatType.Long + ? { + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + hour12: true, + year: includeYear ? 'numeric' : undefined, + } + : { + year: includeYear ? '2-digit' : undefined, + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + hour12: true, + } + + return new Intl.DateTimeFormat('en-US', options).format(timestamp) +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/hooks.ts b/apps/web/src/components/AccountDrawer/MiniPortfolio/hooks.ts new file mode 100644 index 0000000..dbbe33c --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/hooks.ts @@ -0,0 +1,44 @@ +import { atom, useAtom } from 'jotai' +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import { useCallback } from 'react' + +const accountDrawerOpenAtom = atom(false) +const showMoonpayTextAtom = atom(false) + +export function useToggleAccountDrawer() { + const [open, updateAccountDrawerOpen] = useAtom(accountDrawerOpenAtom) + const setShowMoonpayTextInDrawer = useSetShowMoonpayText() + + return useCallback(() => { + updateAccountDrawerOpen(!open) + if (open) { + setShowMoonpayTextInDrawer(false) + } + }, [open, setShowMoonpayTextInDrawer, updateAccountDrawerOpen]) +} + +export function useCloseAccountDrawer() { + const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom) + return useCallback(() => updateAccountDrawerOpen(false), [updateAccountDrawerOpen]) +} + +export function useOpenAccountDrawer() { + const updateAccountDrawerOpen = useUpdateAtom(accountDrawerOpenAtom) + return useCallback(() => updateAccountDrawerOpen(true), [updateAccountDrawerOpen]) +} + +export function useAccountDrawer(): [boolean, () => void] { + const accountDrawerOpen = useAtomValue(accountDrawerOpenAtom) + return [accountDrawerOpen, useToggleAccountDrawer()] +} + +// Only show Moonpay text if the user opens the Account Drawer by clicking 'Buy' +export function useSetShowMoonpayText() { + const updateShowMoonpayText = useUpdateAtom(showMoonpayTextAtom) + return useCallback((newValue: boolean) => updateShowMoonpayText(newValue), [updateShowMoonpayText]) +} + +export function useShowMoonpayText(): boolean { + const showMoonpayText = useAtomValue(showMoonpayTextAtom) + return showMoonpayText +} diff --git a/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx b/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx new file mode 100644 index 0000000..d16b0cd --- /dev/null +++ b/apps/web/src/components/AccountDrawer/MiniPortfolio/index.tsx @@ -0,0 +1,164 @@ +import { Trans } from '@lingui/macro' +import { BrowserEvent, InterfaceElementName, InterfaceSectionName, SharedEventName } from '@uniswap/analytics-events' +import { Trace, TraceEvent } from 'analytics' +import Column from 'components/Column' +import { LoaderV2 } from 'components/Icons/LoadingSpinner' +import { AutoRow } from 'components/Row' +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { useIsNftPage } from 'hooks/useIsNftPage' +import { useEffect, useState } from 'react' +import styled, { useTheme } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ThemedText } from 'theme/components' + +import { atom, useAtom } from 'jotai' +import { ActivityTab } from './Activity' +import { usePendingActivity } from './Activity/hooks' +import NFTs from './NFTs' +import Pools from './Pools' +import { PortfolioRowWrapper } from './PortfolioRow' +import Tokens from './Tokens' + +const lastPageAtom = atom(0) + +const Wrapper = styled(Column)` + margin-top: 28px; + display: flex; + flex-direction: column; + height: 100%; + gap: 12px; + + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + margin-bottom: 48px; + } + + ${PortfolioRowWrapper} { + &:hover { + background: ${({ theme }) => theme.deprecated_hoverDefault}; + } + } +` + +const Nav = styled(AutoRow)` + gap: 20px; +` + +const NavItem = styled(ThemedText.SubHeader)<{ active?: boolean }>` + align-items: center; + color: ${({ theme, active }) => (active ? theme.neutral1 : theme.neutral2)}; + cursor: pointer; + display: flex; + justify-content: space-between; + transition: ${({ theme }) => `${theme.transition.duration.medium} ${theme.transition.timing.ease} color`}; + + &:hover { + ${({ theme, active }) => !active && `color: ${theme.neutral2}`}; + } +` + +const PageWrapper = styled.div` + border-radius: 12px; + margin-right: -16px; + margin-left: -16px; + width: calc(100% + 32px); + flex: 1; +` + +interface Page { + title: React.ReactNode + key: string + component: ({ account }: { account: string }) => JSX.Element + loggingElementName: string +} + +const Pages: Array = [ + { + title: Tokens, + key: 'tokens', + component: Tokens, + loggingElementName: InterfaceElementName.MINI_PORTFOLIO_TOKENS_TAB, + }, + { + title: NFTs, + key: 'nfts', + component: NFTs, + loggingElementName: InterfaceElementName.MINI_PORTFOLIO_NFT_TAB, + }, + { + title: Pools, + key: 'pools', + component: Pools, + loggingElementName: InterfaceElementName.MINI_PORTFOLIO_POOLS_TAB, + }, + { + title: Activity, + key: 'activity', + component: ActivityTab, + loggingElementName: InterfaceElementName.MINI_PORTFOLIO_ACTIVITY_TAB, + }, +] + +export default function MiniPortfolio({ account }: { account: string }) { + const theme = useTheme() + const isNftPage = useIsNftPage() + const [lastPage, setLastPage] = useAtom(lastPageAtom) + // Resumes at the last viewed page, unless you are on an NFT page + const [currentPage, setCurrentPage] = useState(isNftPage ? 1 : lastPage) + useEffect(() => void setLastPage(currentPage), [currentPage, setLastPage]) + + const shouldDisableNFTRoutes = useDisableNFTRoutes() + const [activityUnread, setActivityUnread] = useState(false) + + const { component: Page, key: currentKey } = Pages[currentPage] + + const { hasPendingActivity } = usePendingActivity() + + useEffect(() => { + if (hasPendingActivity && currentKey !== 'activity') setActivityUnread(true) + }, [currentKey, hasPendingActivity]) + + return ( + + + + + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/SettingsMenu.tsx b/apps/web/src/components/AccountDrawer/SettingsMenu.tsx new file mode 100644 index 0000000..ce14745 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SettingsMenu.tsx @@ -0,0 +1,129 @@ +import { Trans } from '@lingui/macro' +import Column from 'components/Column' +import Row from 'components/Row' +import { LOCALE_LABEL } from 'constants/locales' +import { useCurrencyConversionFlagEnabled } from 'featureFlags/flags/currencyConversion' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { useActiveLocale } from 'hooks/useActiveLocale' +import { ReactNode } from 'react' +import { ChevronRight } from 'react-feather' +import styled from 'styled-components' +import { ClickableStyle, ThemedText } from 'theme/components' +import ThemeToggle from 'theme/components/ThemeToggle' + +import { AnalyticsToggle } from './AnalyticsToggle' +import { GitVersionRow } from './GitVersionRow' +import { LanguageMenuItems } from './LanguageMenu' +import { SlideOutMenu } from './SlideOutMenu' +import { SmallBalanceToggle } from './SmallBalanceToggle' +import { SpamToggle } from './SpamToggle' +import { TestnetsToggle } from './TestnetsToggle' + +const Container = styled(Column)` + height: 100%; + justify-content: space-between; +` + +const SectionTitle = styled(ThemedText.SubHeader)` + color: ${({ theme }) => theme.neutral2}; + padding-bottom: 24px; +` + +const ToggleWrapper = styled.div<{ currencyConversionEnabled?: boolean }>` + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: ${({ currencyConversionEnabled }) => (currencyConversionEnabled ? '10px' : '24px')}; +` + +const SettingsButtonWrapper = styled(Row)` + ${ClickableStyle} + padding: 16px 0px; +` + +const StyledChevron = styled(ChevronRight)` + color: ${({ theme }) => theme.neutral2}; +` + +const LanguageLabel = styled(Row)` + white-space: nowrap; +` + +const SettingsButton = ({ + title, + currentState, + onClick, + testId, +}: { + title: ReactNode + currentState: ReactNode + onClick: () => void + testId?: string +}) => ( + + {title} + + {currentState} + + + +) + +export default function SettingsMenu({ + onClose, + openLanguageSettings, + openLocalCurrencySettings, +}: { + onClose: () => void + openLanguageSettings: () => void + openLocalCurrencySettings: () => void +}) { + const currencyConversionEnabled = useCurrencyConversionFlagEnabled() + const activeLocale = useActiveLocale() + const activeLocalCurrency = useActiveLocalCurrency() + + return ( + Settings} onClose={onClose}> + +
+ + Preferences + + + + + + + + + {!currencyConversionEnabled && ( + <> + + Language + + + + )} + + {currencyConversionEnabled && ( + + Language} + currentState={LOCALE_LABEL[activeLocale]} + onClick={openLanguageSettings} + testId="language-settings-button" + /> + Currency} + currentState={activeLocalCurrency} + onClick={openLocalCurrencySettings} + testId="local-currency-settings-button" + /> + + )} +
+ +
+
+ ) +} diff --git a/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx b/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx new file mode 100644 index 0000000..054ae2f --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SettingsToggle.test.tsx @@ -0,0 +1,31 @@ +import { act, render } from 'test-utils/render' + +import { SettingsToggle } from './SettingsToggle' + +describe('SettingsToggle', () => { + it('Updates value on click', () => { + let mockActive = false + const mockToggle = jest.fn().mockImplementation(() => (mockActive = !mockActive)) + const component = render( + + ) + + expect(mockActive).toBeFalsy() + expect(component.container).toHaveTextContent('Test toggle') + expect(component.container).toHaveTextContent('Test description') + + act(() => component.getByTestId('testId').click()) + expect(mockToggle).toHaveBeenCalledTimes(1) + expect(mockActive).toBeTruthy() + + act(() => component.getByTestId('testId').click()) + expect(mockToggle).toHaveBeenCalledTimes(2) + expect(mockActive).toBeFalsy() + }) +}) diff --git a/apps/web/src/components/AccountDrawer/SettingsToggle.tsx b/apps/web/src/components/AccountDrawer/SettingsToggle.tsx new file mode 100644 index 0000000..39117ff --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SettingsToggle.tsx @@ -0,0 +1,39 @@ +import Column from 'components/Column' +import Row from 'components/Row' +import Toggle from 'components/Toggle' +import { ReactNode } from 'react' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const StyledColumn = styled(Column)` + width: 100%; + margin-right: 10px; +` + +interface SettingsToggleProps { + title: ReactNode + description?: string + dataid?: string + isActive: boolean + toggle: () => void +} + +export function SettingsToggle({ title, description, dataid, isActive, toggle }: SettingsToggleProps) { + return ( + + + + {title} + + {description && ( + + + {description} + + + )} + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx b/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx new file mode 100644 index 0000000..e8ce58b --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SlideOutMenu.tsx @@ -0,0 +1,59 @@ +import Column from 'components/Column' +import { ScrollBarStyles } from 'components/Common' +import { ArrowLeft } from 'react-feather' +import styled from 'styled-components' +import { ClickableStyle, ThemedText } from 'theme/components' + +const Menu = styled(Column)` + width: 100%; + overflow: auto; + margin-top: 4px; + padding: 14px 16px 16px; + ${ScrollBarStyles} + ::-webkit-scrollbar-track { + margin-top: 40px; + } +` + +const Title = styled.span` + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); +` + +const StyledArrow = styled(ArrowLeft)` + ${ClickableStyle} +` + +const Header = styled.div` + color: ${({ theme }) => theme.neutral1}; + + display: flex; + justify-content: space-between; + position: relative; + width: 100%; + margin-bottom: 20px; +` + +export const SlideOutMenu = ({ + children, + onClose, + title, +}: { + onClose: () => void + title: React.ReactNode + children: React.ReactNode + onClear?: () => void +}) => ( + +
+ + + <ThemedText.SubHeader>{title}</ThemedText.SubHeader> + +
+ + {children} +
+) diff --git a/apps/web/src/components/AccountDrawer/SmallBalanceToggle.tsx b/apps/web/src/components/AccountDrawer/SmallBalanceToggle.tsx new file mode 100644 index 0000000..bc6eb6c --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SmallBalanceToggle.tsx @@ -0,0 +1,19 @@ +import { t } from '@lingui/macro' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { SettingsToggle } from './SettingsToggle' + +export const hideSmallBalancesAtom = atomWithStorage('hideSmallBalances', true) + +export function SmallBalanceToggle() { + const [hideSmallBalances, updateHideSmallBalances] = useAtom(hideSmallBalancesAtom) + + return ( + void updateHideSmallBalances((value) => !value)} + /> + ) +} diff --git a/apps/web/src/components/AccountDrawer/SpamToggle.tsx b/apps/web/src/components/AccountDrawer/SpamToggle.tsx new file mode 100644 index 0000000..198592c --- /dev/null +++ b/apps/web/src/components/AccountDrawer/SpamToggle.tsx @@ -0,0 +1,19 @@ +import { Trans } from '@lingui/macro' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { SettingsToggle } from './SettingsToggle' + +export const hideSpamAtom = atomWithStorage('hideSmallBalances', true) + +export function SpamToggle() { + const [hideSpam, updateHideSpam] = useAtom(hideSpamAtom) + + return ( + Hide unknown tokens & NFTs} + isActive={hideSpam} + toggle={() => void updateHideSpam((value) => !value)} + /> + ) +} diff --git a/apps/web/src/components/AccountDrawer/Status.tsx b/apps/web/src/components/AccountDrawer/Status.tsx new file mode 100644 index 0000000..8029a4c --- /dev/null +++ b/apps/web/src/components/AccountDrawer/Status.tsx @@ -0,0 +1,155 @@ +import Column from 'components/Column' +import { ENS } from 'components/Icons/ENS' +import { EthMini } from 'components/Icons/EthMini' +import StatusIcon from 'components/Identicon/StatusIcon' +import Row from 'components/Row' +import { Connection } from 'connection/types' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { useRef, useState } from 'react' +import { MoreHorizontal } from 'react-feather' +import styled from 'styled-components' +import { ClickableStyle, CopyHelper, ThemedText } from 'theme/components' +import { Icons } from 'ui/src' +import { shortenAddress } from 'utilities/src/addresses' + +const Container = styled.div` + display: inline-block; + width: 70%; + max-width: 70%; + padding-right: 8px; + display: inline-flex; +` +const Identifiers = styled.div` + white-space: nowrap; + display: flex; + width: 100%; + flex-direction: column; + justify-content: center; + margin-left: 8px; + user-select: none; +` +const SecondaryIdentifiersContainer = styled(Row)` + position: relative; + user-select: none; + :hover > div > #more-identifiers-icon { + display: inline-block; + } +` +const MoreIcon = styled(MoreHorizontal)<{ $isActive: boolean }>` + height: 16px; + width: 16px; + color: ${({ theme }) => theme.neutral2}; + cursor: pointer; + display: ${({ $isActive }) => !$isActive && 'none'}; + ${ClickableStyle} +` +const Dropdown = styled(Column)` + width: 240px; + position: absolute; + top: 20px; + gap: 2px; + padding: 8px; + border-radius: 20px; + border: 1px solid ${({ theme }) => theme.surface3}; + background: ${({ theme }) => theme.surface1}; +` +const EnsIcon = styled(ENS)` + height: 20px; + width: 20px; + padding: 2px 3px; +` + +function SecondaryIdentifier({ + Icon, + displayValue, + copyValue, +}: { + Icon: React.ComponentType + displayValue: string + copyValue: string +}) { + return ( + + + + {displayValue} + + + ) +} + +function SecondaryIdentifiers({ + account, + uniswapUsername, + ensUsername, +}: { + account: string + ensUsername: string | null + uniswapUsername?: string +}) { + const [isDropdownOpen, setIsDropdownOpen] = useState(false) + const ref = useRef(null) + useOnClickOutside(ref, () => setIsDropdownOpen(false)) + + // Dropdown is present if more than one secondary identifier is available + if (uniswapUsername && ensUsername) { + return ( + + setIsDropdownOpen(!isDropdownOpen)} gap="8px"> + {shortenAddress(account)} + + + {isDropdownOpen && ( + + + + + )} + + ) + } + + // Dropdown is not present if only one secondary identifier is available + return ( + + + {shortenAddress(account)} + + + ) +} + +export function Status({ + account, + ensUsername, + uniswapUsername, + connection, +}: { + account: string + ensUsername: string | null + uniswapUsername?: string + connection: Connection +}) { + return ( + + + + + + + {uniswapUsername ?? ensUsername ?? shortenAddress(account)} + {uniswapUsername && } + + + + {(uniswapUsername || ensUsername) && ( + + )} + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/TestnetsToggle.tsx b/apps/web/src/components/AccountDrawer/TestnetsToggle.tsx new file mode 100644 index 0000000..c914aea --- /dev/null +++ b/apps/web/src/components/AccountDrawer/TestnetsToggle.tsx @@ -0,0 +1,20 @@ +import { t } from '@lingui/macro' +import { useAtom } from 'jotai' +import { atomWithStorage } from 'jotai/utils' + +import { SettingsToggle } from './SettingsToggle' + +export const showTestnetsAtom = atomWithStorage('showTestnets', false) + +export function TestnetsToggle() { + const [showTestnets, updateShowTestnets] = useAtom(showTestnetsAtom) + + return ( + void updateShowTestnets((value) => !value)} + /> + ) +} diff --git a/apps/web/src/components/AccountDrawer/UniwalletModal.tsx b/apps/web/src/components/AccountDrawer/UniwalletModal.tsx new file mode 100644 index 0000000..5784537 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/UniwalletModal.tsx @@ -0,0 +1,121 @@ +import { Trans } from '@lingui/macro' +import { InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events' +import { WalletConnect as WalletConnectv2 } from '@web3-react/walletconnect-v2' +import { sendAnalyticsEvent } from 'analytics' +import Column, { AutoColumn } from 'components/Column' +import Modal from 'components/Modal' +import { RowBetween } from 'components/Row' +import { uniwalletWCV2ConnectConnection } from 'connection' +import { ActivationStatus, useActivationState } from 'connection/activate' +import { ConnectionType } from 'connection/types' +import { UniwalletConnect as UniwalletConnectV2 } from 'connection/WalletConnectV2' +import { QRCodeSVG } from 'qrcode.react' +import { useEffect, useState } from 'react' +import styled, { useTheme } from 'styled-components' +import { CloseIcon, ThemedText } from 'theme/components' +import { isAndroid, isIOS } from 'uniswap/src/utils/platform' + +import uniPng from '../../assets/images/uniwallet_modal_icon.png' +import { DownloadButton } from './DownloadButton' + +const UniwalletConnectWrapper = styled(RowBetween)` + display: flex; + flex-direction: column; + padding: 20px 16px 16px; +` +const HeaderRow = styled(RowBetween)` + display: flex; +` +const QRCodeWrapper = styled(RowBetween)` + aspect-ratio: 1; + border-radius: 12px; + background-color: ${({ theme }) => theme.white}; + margin: 24px 32px 20px; + padding: 10px; +` +const Divider = styled.div` + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + width: 100%; +` + +export default function UniwalletModal() { + const { activationState, cancelActivation } = useActivationState() + const [uri, setUri] = useState() + + // Displays the modal if not on iOS/Android, a Uniswap Wallet Connection is pending, & qrcode URI is available + const onLaunchedMobilePlatform = isIOS || isAndroid + const open = + !onLaunchedMobilePlatform && + activationState.status === ActivationStatus.PENDING && + activationState.connection.type === ConnectionType.UNISWAP_WALLET_V2 && + !!uri + + useEffect(() => { + const connectorV2 = uniwalletWCV2ConnectConnection.connector as WalletConnectv2 + connectorV2.events.addListener(UniwalletConnectV2.UNI_URI_AVAILABLE, (uri: string) => { + uri && setUri(uri) + }) + }, []) + + useEffect(() => { + if (open) sendAnalyticsEvent(InterfaceEventName.UNIWALLET_CONNECT_MODAL_OPENED) + }, [open]) + + const theme = useTheme() + return ( + + + + + Scan with Uniswap Wallet + + + + + {uri && ( + + )} + + + + + + ) +} + +const InfoSectionWrapper = styled(RowBetween)` + display: flex; + flex-direction: row; + padding-top: 20px; + gap: 20px; +` + +function InfoSection() { + return ( + + + + Don't have a Uniswap wallet? + + + Safely store and swap tokens with the Uniswap app. Available on iOS and Android. + + + + + + + ) +} diff --git a/apps/web/src/components/AccountDrawer/index.tsx b/apps/web/src/components/AccountDrawer/index.tsx new file mode 100644 index 0000000..850f1a7 --- /dev/null +++ b/apps/web/src/components/AccountDrawer/index.tsx @@ -0,0 +1,255 @@ +import { BrowserEvent, InterfaceEventName } from '@uniswap/analytics-events' +import { TraceEvent } from 'analytics' +import { ScrollBarStyles } from 'components/Common' +import useDisableScrolling from 'hooks/useDisableScrolling' +import usePrevious from 'hooks/usePrevious' +import { useWindowSize } from 'hooks/useWindowSize' +import { useEffect, useRef, useState } from 'react' +import { ChevronsRight } from 'react-feather' +import { useGesture } from 'react-use-gesture' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' +import { isMobile } from 'uniswap/src/utils/platform' + +import DefaultMenu from './DefaultMenu' +import { useAccountDrawer } from './MiniPortfolio/hooks' + +const DRAWER_WIDTH_XL = '390px' +const DRAWER_WIDTH = '320px' +const DRAWER_MARGIN = '8px' +const DRAWER_OFFSET = '10px' +const DRAWER_TOP_MARGIN_MOBILE_WEB = '24px' + +const ScrimBackground = styled.div<{ $open: boolean; $maxWidth?: number; $zIndex?: number }>` + z-index: ${({ $zIndex }) => $zIndex ?? Z_INDEX.modalBackdrop}; + overflow: hidden; + top: 0; + left: 0; + position: fixed; + width: 100%; + height: 100%; + background-color: ${({ theme }) => theme.scrim}; + + opacity: 0; + pointer-events: none; + @media only screen and (max-width: ${({ theme, $maxWidth }) => `${$maxWidth ?? theme.breakpoint.sm}px`}) { + opacity: ${({ $open }) => ($open ? 1 : 0)}; + pointer-events: ${({ $open }) => ($open ? 'auto' : 'none')}; + transition: opacity ${({ theme }) => theme.transition.duration.medium} ease-in-out; + } +` + +interface ScrimBackgroundProps extends React.ComponentPropsWithRef<'div'> { + $open: boolean + $maxWidth?: number + $zIndex?: number +} + +export const Scrim = (props: ScrimBackgroundProps) => { + const { width } = useWindowSize() + + useEffect(() => { + if (width && width < BREAKPOINTS.sm && props.$open) document.body.style.overflow = 'hidden' + return () => { + document.body.style.overflow = 'visible' + } + }, [props.$open, width]) + + return +} + +const AccountDrawerScrollWrapper = styled.div` + overflow-y: auto; + + ${ScrollBarStyles} + + scrollbar-gutter: stable; + overscroll-behavior: contain; + border-radius: 12px; +` + +const Container = styled.div` + display: flex; + flex-direction: row; + height: calc(100% - 2 * ${DRAWER_MARGIN}); + overflow: hidden; + position: fixed; + right: ${DRAWER_MARGIN}; + top: ${DRAWER_MARGIN}; + z-index: ${Z_INDEX.fixed}; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + top: 100vh; + left: 0; + right: 0; + width: 100%; + overflow: visible; + } +` + +const AccountDrawerWrapper = styled.div<{ open: boolean }>` + margin-right: ${({ open }) => (open ? 0 : '-' + DRAWER_WIDTH)}; + height: 100%; + overflow: hidden; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + z-index: ${Z_INDEX.modal}; + position: absolute; + margin-right: 0; + top: ${({ open }) => (open ? `calc(-1 * (100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}))` : 0)}; + height: calc(100% - ${DRAWER_TOP_MARGIN_MOBILE_WEB}); + + width: 100%; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + box-shadow: unset; + transition: top ${({ theme }) => theme.transition.duration.medium}; + } + + @media screen and (min-width: 1440px) { + margin-right: ${({ open }) => (open ? 0 : `-${DRAWER_WIDTH_XL}`)}; + width: ${DRAWER_WIDTH_XL}; + } + + border-radius: 12px; + width: ${DRAWER_WIDTH}; + font-size: 16px; + background-color: ${({ theme }) => theme.surface1}; + border: ${({ theme }) => `1px solid ${theme.surface3}`}; + + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + transition: margin-right ${({ theme }) => theme.transition.duration.medium}; +` + +const CloseIcon = styled(ChevronsRight).attrs({ size: 24 })` + stroke: ${({ theme }) => theme.neutral2}; +` + +const CloseDrawer = styled.div` + ${ClickableStyle} + cursor: pointer; + height: 100%; + // When the drawer is not hovered, the icon should be 18px from the edge of the sidebar. + padding: 24px calc(18px + ${DRAWER_OFFSET}) 24px 14px; + border-radius: 20px 0 0 20px; + transition: ${({ theme }) => + `${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`}; + &:hover { + z-index: -1; + margin: 0 -8px 0 0; + background-color: ${({ theme }) => theme.deprecated_stateOverlayHover}; + } + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + display: none; + } +` + +function AccountDrawer() { + const [walletDrawerOpen, toggleWalletDrawer] = useAccountDrawer() + const wasWalletDrawerOpen = usePrevious(walletDrawerOpen) + const scrollRef = useRef(null) + useEffect(() => { + if (wasWalletDrawerOpen && !walletDrawerOpen) { + scrollRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) + } + }, [walletDrawerOpen, wasWalletDrawerOpen]) + + // close on escape keypress + useEffect(() => { + const escapeKeyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'Escape' && walletDrawerOpen) { + event.preventDefault() + toggleWalletDrawer() + } + } + + document.addEventListener('keydown', escapeKeyDownHandler) + + return () => { + document.removeEventListener('keydown', escapeKeyDownHandler) + } + }, [walletDrawerOpen, toggleWalletDrawer]) + + // useStates for detecting swipe gestures + const [yPosition, setYPosition] = useState(0) + const [dragStartTop, setDragStartTop] = useState(true) + useDisableScrolling(walletDrawerOpen) + + // useGesture hook for detecting swipe gestures + const bind = useGesture({ + // if the drawer is open and the user is dragging down, close the drawer + onDrag: (state) => { + // if the user is dragging up, set dragStartTop to false + if (state.movement[1] < 0) { + setDragStartTop(false) + if (scrollRef.current) { + scrollRef.current.style.overflowY = 'auto' + } + } else if ( + (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) && + walletDrawerOpen && + dragStartTop + ) { + toggleWalletDrawer() + } else if (walletDrawerOpen && dragStartTop && state.movement[1] > 0) { + setYPosition(state.movement[1]) + if (scrollRef.current) { + scrollRef.current.style.overflowY = 'hidden' + } + } + }, + // reset the yPosition when the user stops dragging + onDragEnd: () => { + setYPosition(0) + if (scrollRef.current) { + scrollRef.current.style.overflowY = 'auto' + } + }, + // set dragStartTop to true if the user starts dragging from the top of the drawer + onDragStart: () => { + if (!scrollRef.current?.scrollTop || scrollRef.current?.scrollTop < 30) { + setDragStartTop(true) + } else { + setDragStartTop(false) + if (scrollRef.current) { + scrollRef.current.style.overflowY = 'auto' + } + } + }, + }) + + return ( + + {walletDrawerOpen && ( + + + + + + )} + + + {/* id used for child InfiniteScrolls to reference when it has reached the bottom of the component */} + + + + + + ) +} + +export default AccountDrawer diff --git a/apps/web/src/components/AccountDrawer/shared.tsx b/apps/web/src/components/AccountDrawer/shared.tsx new file mode 100644 index 0000000..d28ecba --- /dev/null +++ b/apps/web/src/components/AccountDrawer/shared.tsx @@ -0,0 +1,58 @@ +import Column from 'components/Column' +import Row from 'components/Row' +import { ReactNode } from 'react' +import { Check } from 'react-feather' +import type { To } from 'react-router-dom' +import { Link } from 'react-router-dom' +import styled, { useTheme } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle, ThemedText } from 'theme/components' + +const InternalLinkMenuItem = styled(Link)` + ${ClickableStyle} + + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + padding: 12px 0; + justify-content: space-between; + text-decoration: none; + color: ${({ theme }) => theme.neutral1}; +` + +export const MenuColumn = styled(Column)` + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + padding-bottom: 14px; + } +` + +export function MenuItem({ + label, + logo, + to, + onClick, + isActive, + testId, +}: { + label: ReactNode + logo?: ReactNode + to?: To + onClick?: () => void + isActive: boolean + testId?: string +}) { + const theme = useTheme() + + if (!to) return null + + return ( + + + {logo && logo} + {label} + + {isActive && } + + ) +} diff --git a/apps/web/src/components/AddressInputPanel/index.tsx b/apps/web/src/components/AddressInputPanel/index.tsx new file mode 100644 index 0000000..6d0a943 --- /dev/null +++ b/apps/web/src/components/AddressInputPanel/index.tsx @@ -0,0 +1,142 @@ +import { Trans } from '@lingui/macro' +// eslint-disable-next-line no-restricted-imports +import { t } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { ChangeEvent, ReactNode, useCallback } from 'react' +import styled, { useTheme } from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { flexColumnNoWrap } from 'theme/styles' + +import useENS from '../../hooks/useENS' +import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink' +import { AutoColumn } from '../Column' +import { RowBetween } from '../Row' + +const InputPanel = styled.div` + ${flexColumnNoWrap}; + position: relative; + border-radius: 1.25rem; + background-color: ${({ theme }) => theme.surface1}; + z-index: 1; + width: 100%; +` + +const ContainerRow = styled.div<{ error: boolean }>` + display: flex; + justify-content: center; + align-items: center; + border-radius: 1.25rem; + border: 1px solid ${({ error, theme }) => (error ? theme.critical : theme.surface3)}; + transition: border-color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}, + color 500ms ${({ error }) => (error ? 'step-end' : 'step-start')}; + background-color: ${({ theme }) => theme.surface1}; +` + +const InputContainer = styled.div` + flex: 1; + padding: 1rem; +` + +const Input = styled.input<{ error?: boolean }>` + font-size: 1.25rem; + outline: none; + border: none; + flex: 1 1 auto; + width: 0; + background-color: ${({ theme }) => theme.surface1}; + transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}; + color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 535; + width: 100%; + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } + padding: 0px; + -webkit-appearance: textfield; + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-outer-spin-button, + ::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } +` + +export default function AddressInputPanel({ + id, + className = 'recipient-address-input', + label, + placeholder, + value, + onChange, +}: { + id?: string + className?: string + label?: ReactNode + placeholder?: string + // the typed string value + value: string + // triggers whenever the typed value changes + onChange: (value: string) => void +}) { + const { chainId } = useWeb3React() + const theme = useTheme() + + const { address, loading, name } = useENS(value) + + const handleInput = useCallback( + (event: ChangeEvent) => { + const input = event.target.value + const withoutSpaces = input.replace(/\s+/g, '') + onChange(withoutSpaces) + }, + [onChange] + ) + + const error = Boolean(value.length > 0 && !loading && !address) + + return ( + + + + + + + {label ?? Recipient} + + {address && chainId && ( + + (View on Explorer) + + )} + + + + + + + ) +} diff --git a/apps/web/src/components/AnimatedDropdown/index.test.tsx b/apps/web/src/components/AnimatedDropdown/index.test.tsx new file mode 100644 index 0000000..e7c53ca --- /dev/null +++ b/apps/web/src/components/AnimatedDropdown/index.test.tsx @@ -0,0 +1,31 @@ +import { render, screen, waitFor } from 'test-utils/render' + +import AnimatedDropdown from './index' + +describe('AnimatedDropdown', () => { + it('does not render children when closed', () => { + render(Body) + expect(screen.getByText('Body')).not.toBeVisible() + }) + + it('renders children when open', () => { + render(Body) + expect(screen.getByText('Body')).toBeVisible() + }) + + it('animates when open changes', async () => { + const { rerender } = render(Body) + + const body = screen.getByText('Body') + + expect(body).not.toBeVisible() + + rerender(Body) + expect(body).not.toBeVisible() + + // wait for React Spring animation to finish + await waitFor(() => { + expect(body).toBeVisible() + }) + }) +}) diff --git a/apps/web/src/components/AnimatedDropdown/index.tsx b/apps/web/src/components/AnimatedDropdown/index.tsx new file mode 100644 index 0000000..02bc23b --- /dev/null +++ b/apps/web/src/components/AnimatedDropdown/index.tsx @@ -0,0 +1,34 @@ +import { animated, useSpring, UseSpringProps } from 'react-spring' +import useResizeObserver from 'use-resize-observer' + +type AnimatedDropdownProps = React.PropsWithChildren<{ open: boolean; springProps?: UseSpringProps }> +/** + * @param open conditional to show content or hide + * @param springProps additional props to include in spring animation + * @returns Wrapper to smoothly hide and expand content + */ +export default function AnimatedDropdown({ open, springProps, children }: AnimatedDropdownProps) { + const { ref, height } = useResizeObserver() + + const props = useSpring({ + // On initial render, `height` will be undefined as ref has not been set yet. + // If the dropdown should be open, we fallback to `auto` to avoid flickering. + // Otherwise, we just animate between actual height (when open) and 0 (when closed). + height: open ? height ?? 'auto' : 0, + config: { + mass: 1.2, + tension: 300, + friction: 20, + clamp: true, + velocity: 0.01, + }, + ...springProps, + }) + return ( + +
{children}
+
+ ) +} diff --git a/apps/web/src/components/Badge/RangeBadge.tsx b/apps/web/src/components/Badge/RangeBadge.tsx new file mode 100644 index 0000000..26ab23c --- /dev/null +++ b/apps/web/src/components/Badge/RangeBadge.tsx @@ -0,0 +1,81 @@ +import { Trans } from '@lingui/macro' +import { AlertTriangle, Slash } from 'react-feather' +import styled, { useTheme } from 'styled-components' + +import { MouseoverTooltip } from '../../components/Tooltip' + +const BadgeWrapper = styled.div` + font-size: 14px; + display: flex; + justify-content: flex-end; +` + +const BadgeText = styled.div` + font-weight: 535; + font-size: 12px; + line-height: 14px; + margin-right: 8px; +` + +const ActiveDot = styled.span` + background-color: ${({ theme }) => theme.success}; + border-radius: 50%; + height: 8px; + width: 8px; +` + +const LabelText = styled.div<{ color: string }>` + align-items: center; + color: ${({ color }) => color}; + display: flex; + flex-direction: row; + justify-content: flex-end; +` + +export default function RangeBadge({ removed, inRange }: { removed?: boolean; inRange?: boolean }) { + const theme = useTheme() + return ( + + {removed ? ( + Your position has 0 liquidity, and is not earning fees.}> + + + Closed + + + + + ) : inRange ? ( + + The price of this pool is within your selected range. Your position is currently earning fees. + + } + > + + + In range + + + + + ) : ( + + The price of this pool is outside of your selected range. Your position is not currently earning fees. + + } + > + + + Out of range + + + + + )} + + ) +} diff --git a/apps/web/src/components/Badge/index.tsx b/apps/web/src/components/Badge/index.tsx new file mode 100644 index 0000000..50ebce1 --- /dev/null +++ b/apps/web/src/components/Badge/index.tsx @@ -0,0 +1,85 @@ +import { readableColor } from 'polished' +import { PropsWithChildren } from 'react' +import styled, { DefaultTheme } from 'styled-components' + +export enum BadgeVariant { + DEFAULT = 'DEFAULT', + NEGATIVE = 'NEGATIVE', + POSITIVE = 'POSITIVE', + PRIMARY = 'PRIMARY', + WARNING = 'WARNING', + PROMOTIONAL = 'PROMOTIONAL', + BRANDED = 'BRANDED', + SOFT = 'SOFT', + + WARNING_OUTLINE = 'WARNING_OUTLINE', +} + +interface BadgeProps { + variant?: BadgeVariant +} + +function pickBackgroundColor(variant: BadgeVariant | undefined, theme: DefaultTheme): string { + switch (variant) { + case BadgeVariant.BRANDED: + return theme.brandedGradient + case BadgeVariant.PROMOTIONAL: + return theme.promotionalGradient + case BadgeVariant.NEGATIVE: + return theme.critical + case BadgeVariant.POSITIVE: + return theme.success + case BadgeVariant.SOFT: + return theme.accent2 + case BadgeVariant.PRIMARY: + return theme.accent1 + case BadgeVariant.WARNING: + return theme.deprecated_accentWarning + case BadgeVariant.WARNING_OUTLINE: + return 'transparent' + default: + return theme.surface2 + } +} + +function pickBorder(variant: BadgeVariant | undefined, theme: DefaultTheme): string { + switch (variant) { + case BadgeVariant.WARNING_OUTLINE: + return `1px solid ${theme.deprecated_accentWarning}` + default: + return 'unset' + } +} + +function pickFontColor(variant: BadgeVariant | undefined, theme: DefaultTheme): string { + switch (variant) { + case BadgeVariant.BRANDED: + return theme.darkMode ? theme.neutral1 : theme.white + case BadgeVariant.NEGATIVE: + return readableColor(theme.critical) + case BadgeVariant.POSITIVE: + return readableColor(theme.success) + case BadgeVariant.SOFT: + return theme.accent1 + case BadgeVariant.WARNING: + return readableColor(theme.deprecated_accentWarning) + case BadgeVariant.WARNING_OUTLINE: + return theme.deprecated_accentWarning + default: + return theme.neutral2 + } +} + +const Badge = styled.div>` + align-items: center; + background: ${({ theme, variant }) => pickBackgroundColor(variant, theme)}; + border: ${({ theme, variant }) => pickBorder(variant, theme)}; + border-radius: 0.5rem; + color: ${({ theme, variant }) => pickFontColor(variant, theme)}; + display: inline-flex; + padding: 4px 6px; + justify-content: center; + font-weight: 535; +` + +export default Badge diff --git a/apps/web/src/components/Banner/Outage/OutageBanner.tsx b/apps/web/src/components/Banner/Outage/OutageBanner.tsx new file mode 100644 index 0000000..33bfb89 --- /dev/null +++ b/apps/web/src/components/Banner/Outage/OutageBanner.tsx @@ -0,0 +1,80 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { Container, PopupContainer, StyledXButton, TextContainer } from 'components/Banner/shared/styled' +import { chainIdToBackendName } from 'graphql/data/util' +import { useState } from 'react' +import { Globe } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { capitalize } from 'tsafe' + +const IconContainer = styled.div` + height: 100%; + margin: 12px 0 0 12px; + align-self: flex-start; +` + +const IconBackground = styled.div` + background-color: #1f1e02; + padding: 10px; + border-radius: 12px; +` + +const StyledPopupContainer = styled(PopupContainer)` + height: unset; +` + +const OutageTextContainer = styled(TextContainer)` + padding: 10px 10px 10px 0; +` + +const HelpCenterLink = styled(ExternalLink)` + font-size: 14px; + margin-top: 4px; +` + +export function getOutageBannerSessionStorageKey(chainId: ChainId) { + return `hideOutageBanner-${chainId}` +} + +export function OutageBanner({ chainId }: { chainId: ChainId }) { + const [hidden, setHidden] = useState(false) + const theme = useTheme() + + return ( + + + + + + + + + + Data will be back soon + + + + {capitalize(chainIdToBackendName(chainId).toLowerCase())} data is unavailable right now, but we expect the + issue to be resolved shortly. + + + + You can still swap and provide liquidity for this token without issue. + + + Learn more + + + { + setHidden(true) + sessionStorage.setItem(getOutageBannerSessionStorageKey(chainId), 'true') + }} + /> + + + ) +} diff --git a/apps/web/src/components/Banner/shared/Banners.tsx b/apps/web/src/components/Banner/shared/Banners.tsx new file mode 100644 index 0000000..7cd40a3 --- /dev/null +++ b/apps/web/src/components/Banner/shared/Banners.tsx @@ -0,0 +1,46 @@ +import { InterfacePageName } from '@uniswap/analytics-events' +import { ChainId } from '@uniswap/sdk-core' +import { OutageBanner, getOutageBannerSessionStorageKey } from 'components/Banner/Outage/OutageBanner' +import { useOutageBanners } from 'featureFlags/flags/outageBanner' +import { getValidUrlChainId } from 'graphql/data/util' +import { useMemo } from 'react' +import { useLocation } from 'react-router-dom' +import { getCurrentPageFromLocation } from 'utils/urlRoutes' + +export function Banners() { + const { pathname } = useLocation() + const currentPage = getCurrentPageFromLocation(pathname) + + const outageBanners = useOutageBanners() + + // Calculate the chainId for the current page's contextual chain (e.g. /tokens/ethereum or /tokens/arbitrum), if it exists. + const pageChainId = useMemo(() => { + const chainName = pathname.split('/').find((maybeChainName) => { + const validatedChainId = getValidUrlChainId(maybeChainName) + return validatedChainId !== undefined + }) + return chainName ? getValidUrlChainId(chainName) : undefined + }, [pathname]) + + const showOutageBanner = useMemo(() => { + return ( + currentPage && + pageChainId && + outageBanners[pageChainId as ChainId] && + !sessionStorage.getItem(getOutageBannerSessionStorageKey(pageChainId)) && + [ + InterfacePageName.EXPLORE_PAGE, + InterfacePageName.TOKEN_DETAILS_PAGE, + InterfacePageName.POOL_DETAILS_PAGE, + InterfacePageName.TOKENS_PAGE, + ].includes(currentPage) + ) + }, [currentPage, outageBanners, pageChainId]) + + // Outage Banners should take precedence over other promotional banners + if (pageChainId && showOutageBanner) { + return + } + + return null +} diff --git a/apps/web/src/components/Banner/shared/styled.tsx b/apps/web/src/components/Banner/shared/styled.tsx new file mode 100644 index 0000000..a963354 --- /dev/null +++ b/apps/web/src/components/Banner/shared/styled.tsx @@ -0,0 +1,73 @@ +import { OpacityHoverState } from 'components/Common' +import { X } from 'react-feather' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { Z_INDEX } from 'theme/zIndex' + +export const PopupContainer = styled.div<{ show: boolean }>` + ${({ show }) => !show && 'display: none'}; + background-color: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral1}; + position: fixed; + z-index: ${Z_INDEX.sticky}; + bottom: 40px; + right: 20px; + width: 360px; + + user-select: none; + border-radius: 20px; + height: 92px; + border: 1.3px solid ${({ theme }) => theme.surface3}; + + @media only screen and (max-width: ${BREAKPOINTS.md}px) { + bottom: 62px; + } + + @media only screen and (max-width: ${BREAKPOINTS.xs}px) { + width: unset; + right: 10px; + left: 10px; + } +` +export const StyledXButton = styled(X)` + cursor: pointer; + position: absolute; + top: -30px; + right: 0px; + padding: 4px; + border-radius: 50%; + + background-color: ${({ theme }) => theme.surface5}; + color: ${({ theme }) => theme.neutral2}; + ${OpacityHoverState}; + + @media only screen and (max-width: ${BREAKPOINTS.xs}px) { + top: 8px; + right: 8px; + } +` + +export const Container = styled.div` + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + height: 100%; + overflow: hidden; + border-radius: 20px; + gap: 16px; +` + +export const TextContainer = styled.div` + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + color: ${({ theme }) => theme.neutral2}; + padding: 10px 0px 10px; + line-height: 16px; + + @media only screen and (max-width: ${BREAKPOINTS.xs}px) { + width: 220px; + } +` diff --git a/apps/web/src/components/BreadcrumbNav/__snapshots__/index.test.tsx.snap b/apps/web/src/components/BreadcrumbNav/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000..db8214b --- /dev/null +++ b/apps/web/src/components/BreadcrumbNav/__snapshots__/index.test.tsx.snap @@ -0,0 +1,144 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BreadcrumbNav does not display address hover for native tokens 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + gap: 6px; +} + +.c3 { + font-weight: inherit; + font-size: inherit; + line-height: inherit; + color: #222222; + white-space: nowrap; + margin: 0; +} + + + +
+

+ ETH +

+ +
+
+
+
+`; + +exports[`BreadcrumbNav renders hover components correctly 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c5 { + display: inline-block; + height: inherit; +} + +.c2 { + gap: 6px; +} + +.c3 { + font-weight: inherit; + font-size: inherit; + line-height: inherit; + color: #222222; + white-space: nowrap; + margin: 0; +} + +.c4 { + cursor: pointer; + gap: 10px; + white-space: nowrap; +} + + + +
+

+ WBTC +

+ +
+
+ 0x2260...C599 +
+
+
+
+
+
+`; diff --git a/apps/web/src/components/BreadcrumbNav/index.test.tsx b/apps/web/src/components/BreadcrumbNav/index.test.tsx new file mode 100644 index 0000000..9760967 --- /dev/null +++ b/apps/web/src/components/BreadcrumbNav/index.test.tsx @@ -0,0 +1,37 @@ +import userEvent from '@testing-library/user-event' +import { ChainId } from '@uniswap/sdk-core' +import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' +import { TokenFromList } from 'state/lists/tokenFromList' +import { act, render, screen } from 'test-utils/render' + +import { CurrentPageBreadcrumb } from '.' + +describe('BreadcrumbNav', () => { + it('renders hover components correctly', async () => { + const currency = new TokenFromList({ + chainId: 1, + address: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + name: 'Wrapped BTC', + decimals: 18, + symbol: 'WBTC', + }) + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + + await act(() => userEvent.hover(screen.getByTestId('current-breadcrumb'))) + expect(screen.getByTestId('breadcrumb-hover-copy')).toBeInTheDocument() + await act(() => userEvent.unhover(screen.getByTestId('current-breadcrumb'))) + expect(screen.queryByTestId('breadcrumb-hover-copy')).not.toBeInTheDocument() + }) + + it('does not display address hover for native tokens', async () => { + const ETH = nativeOnChain(ChainId.MAINNET) + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + await act(() => userEvent.hover(screen.getByTestId('current-breadcrumb'))) + expect(screen.queryByTestId('breadcrumb-hover-copy')).not.toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/BreadcrumbNav/index.tsx b/apps/web/src/components/BreadcrumbNav/index.tsx new file mode 100644 index 0000000..3458307 --- /dev/null +++ b/apps/web/src/components/BreadcrumbNav/index.tsx @@ -0,0 +1,111 @@ +import { t, Trans } from '@lingui/macro' +import { Currency } from '@uniswap/sdk-core' +import Row from 'components/Row' +import Tooltip, { TooltipSize } from 'components/Tooltip' +import useCopyClipboard from 'hooks/useCopyClipboard' +import { useScreenSize } from 'hooks/useScreenSize' +import { useCallback, useState } from 'react' +import { Copy } from 'react-feather' +import { Link } from 'react-router-dom' +import styled, { useTheme } from 'styled-components' +import { ClickableStyle } from 'theme/components' +import { shortenAddress } from 'utilities/src/addresses' + +export const BreadcrumbNavContainer = styled.nav` + display: flex; + color: ${({ theme }) => theme.neutral2}; + font-size: 16px; + line-height: 24px; + align-items: center; + gap: 4px; + margin-bottom: 20px; + width: fit-content; +` + +export const BreadcrumbNavLink = styled(Link)` + display: flex; + align-items: center; + color: ${({ theme }) => theme.neutral2}; + transition-duration: ${({ theme }) => theme.transition.duration.fast}; + text-decoration: none; + + &:hover { + color: ${({ theme }) => theme.neutral3}; + } +` + +const CurrentPageBreadcrumbContainer = styled(Row)` + gap: 6px; +` + +// This must be an h1 to match the SEO title, and must be the first heading tag in code. +const PageTitleText = styled.h1` + font-weight: inherit; + font-size: inherit; + line-height: inherit; + color: ${({ theme }) => theme.neutral1}; + white-space: nowrap; + margin: 0; +` + +const TokenAddressHoverContainer = styled(Row)<{ isDisabled?: boolean }>` + cursor: ${({ isDisabled }) => (isDisabled ? 'default' : 'pointer')}; + gap: 10px; + white-space: nowrap; +` + +const CopyIcon = styled(Copy)` + ${ClickableStyle} +` + +// Used in both TDP & PDP. +// On TDP, currency is defined & poolName is undefined. On PDP, currency is undefined & poolName is defined. +export const CurrentPageBreadcrumb = ({ + address, + currency, + poolName, +}: { + address: string + currency?: Currency + poolName?: string +}) => { + const { neutral2 } = useTheme() + const screenSize = useScreenSize() + const [hover, setHover] = useState(false) + + const [isCopied, setCopied] = useCopyClipboard() + const copy = useCallback(() => { + setCopied(address) + }, [address, setCopied]) + + const isNative = currency?.isNative + const tokenSymbolName = currency?.symbol ?? Symbol not found + + const shouldEnableCopy = screenSize['sm'] + const shouldShowActions = shouldEnableCopy && hover && !isCopied + + return ( + setHover(true)} + onMouseLeave={() => setHover(false)} + > + {currency ? tokenSymbolName : poolName}{' '} + {(!currency || (currency && !isNative)) && ( + + + {shortenAddress(address)} + + {shouldShowActions && ( + + )} + + )} + + ) +} diff --git a/apps/web/src/components/Button/GetHelp.tsx b/apps/web/src/components/Button/GetHelp.tsx new file mode 100644 index 0000000..737b9f2 --- /dev/null +++ b/apps/web/src/components/Button/GetHelp.tsx @@ -0,0 +1,36 @@ +import { Trans } from '@lingui/macro' +import { EnvelopeHeartIcon } from 'components/Icons/EnvelopeHeart' +import Row from 'components/Row' +import { SupportArticleURL } from 'constants/supportArticles' +import styled from 'styled-components' +import { ExternalLink } from 'theme/components' + +const StyledExternalLink = styled(ExternalLink)` + width: fit-content; + border-radius: 16px; + padding: 4px 6px; + font-size: 14px; + font-weight: 485; + line-height: 20px; + background: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral2}; + :hover { + background: ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral1}; + path { + fill: ${({ theme }) => theme.neutral1}; + } + opacity: unset; + } + stroke: none; +` +export default function GetHelp() { + return ( + + + + Get help + + + ) +} diff --git a/apps/web/src/components/Button/LoadingButtonSpinner.tsx b/apps/web/src/components/Button/LoadingButtonSpinner.tsx new file mode 100644 index 0000000..9cb659d --- /dev/null +++ b/apps/web/src/components/Button/LoadingButtonSpinner.tsx @@ -0,0 +1,13 @@ +import { SpinnerSVG } from 'theme/components' + +const ButtonLoadingSpinner = (props: React.ComponentPropsWithoutRef<'svg'>) => ( + + + + +) + +export default ButtonLoadingSpinner diff --git a/apps/web/src/components/Button/index.tsx b/apps/web/src/components/Button/index.tsx new file mode 100644 index 0000000..d3f3f53 --- /dev/null +++ b/apps/web/src/components/Button/index.tsx @@ -0,0 +1,550 @@ +import { darken } from 'polished' +import { forwardRef } from 'react' +import { Check, ChevronDown } from 'react-feather' +import { ButtonProps as ButtonPropsOriginal, Button as RebassButton } from 'rebass/styled-components' +import styled, { DefaultTheme, useTheme } from 'styled-components' + +import { RowBetween } from '../Row' + +export { default as LoadingButtonSpinner } from './LoadingButtonSpinner' + +type ButtonProps = Omit + +const ButtonOverlay = styled.div` + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + transition: 150ms ease background-color; + width: 100%; +` + +type BaseButtonProps = { + padding?: string + width?: string + $borderRadius?: string + altDisabledStyle?: boolean +} & ButtonProps + +export const BaseButton = styled(RebassButton)` + padding: ${({ padding }) => padding ?? '16px'}; + width: ${({ width }) => width ?? '100%'}; + line-height: 24px; + font-weight: 535; + text-align: center; + border-radius: ${({ $borderRadius }) => $borderRadius ?? '16px'}; + outline: none; + border: 1px solid transparent; + color: ${({ theme }) => theme.neutral1}; + text-decoration: none; + display: flex; + justify-content: center; + flex-wrap: nowrap; + align-items: center; + cursor: pointer; + position: relative; + z-index: 1; + &:disabled { + opacity: 50%; + cursor: auto; + pointer-events: none; + } + + will-change: transform; + transition: transform 450ms ease; + transform: perspective(1px) translateZ(0); + + > * { + user-select: none; + } + + > a { + text-decoration: none; + } +` + +export const ButtonPrimary = styled(BaseButton)` + background-color: ${({ theme }) => theme.accent1}; + font-size: 20px; + font-weight: 535; + padding: 16px; + color: ${({ theme }) => theme.neutralContrast}; + &:focus { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.accent1)}; + background-color: ${({ theme }) => darken(0.05, theme.accent1)}; + } + &:hover { + background-color: ${({ theme }) => darken(0.05, theme.accent1)}; + } + &:active { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.accent1)}; + background-color: ${({ theme }) => darken(0.1, theme.accent1)}; + } + &:disabled { + background-color: ${({ theme, altDisabledStyle, disabled }) => + altDisabledStyle ? (disabled ? theme.accent1 : theme.surface3) : theme.surface3}; + color: ${({ altDisabledStyle, disabled, theme }) => + altDisabledStyle ? (disabled ? theme.neutralContrast : theme.neutral2) : theme.neutral2}; + cursor: auto; + box-shadow: none; + border: 1px solid transparent; + outline: none; + } +` + +export const SmallButtonPrimary = styled(ButtonPrimary)` + width: auto; + font-size: 16px; + padding: ${({ padding }) => padding ?? '8px 12px'}; + + border-radius: 12px; +` + +const BaseButtonLight = styled(BaseButton)` + background-color: ${({ theme }) => theme.accent2}; + color: ${({ theme }) => theme.accent1}; + font-size: 20px; + font-weight: 535; + + &:focus { + box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accent2}; + background-color: ${({ theme, disabled }) => !disabled && theme.accent2}; + } + &:hover { + background-color: ${({ theme, disabled }) => !disabled && theme.accent2}; + } + &:active { + box-shadow: 0 0 0 1pt ${({ theme, disabled }) => !disabled && theme.accent2}; + background-color: ${({ theme, disabled }) => !disabled && theme.accent2}; + } + + :hover { + ${ButtonOverlay} { + background-color: ${({ theme }) => theme.deprecated_stateOverlayHover}; + } + } + + :active { + ${ButtonOverlay} { + background-color: ${({ theme }) => theme.deprecated_stateOverlayPressed}; + } + } + + :disabled { + opacity: 0.4; + :hover { + cursor: auto; + background-color: transparent; + box-shadow: none; + border: 1px solid transparent; + outline: none; + } + } +` + +export const ButtonGray = styled(BaseButton)` + background-color: ${({ theme }) => theme.surface1}; + color: ${({ theme }) => theme.neutral2}; + border: 1px solid ${({ theme }) => theme.surface3}; + font-size: 16px; + font-weight: 535; + + &:hover { + background-color: ${({ theme, disabled }) => !disabled && darken(0.05, theme.surface2)}; + } + &:active { + background-color: ${({ theme, disabled }) => !disabled && darken(0.1, theme.surface2)}; + } +` + +export const ButtonSecondary = styled(BaseButton)` + border: 1px solid ${({ theme }) => theme.accent2}; + color: ${({ theme }) => theme.accent1}; + background-color: transparent; + font-size: 16px; + border-radius: 12px; + padding: ${({ padding }) => (padding ? padding : '10px')}; + + &:focus { + box-shadow: 0 0 0 1pt ${({ theme }) => theme.accent2}; + border: 1px solid ${({ theme }) => theme.accent1}; + } + &:hover { + border: 1px solid ${({ theme }) => theme.accent1}; + } + &:active { + box-shadow: 0 0 0 1pt ${({ theme }) => theme.accent2}; + border: 1px solid ${({ theme }) => theme.accent1}; + } + &:disabled { + opacity: 50%; + cursor: auto; + } + a:hover { + text-decoration: none; + } +` + +export const ButtonOutlined = styled(BaseButton)` + border: 1px solid ${({ theme }) => theme.surface3}; + background-color: transparent; + color: ${({ theme }) => theme.neutral1}; + &:focus { + box-shadow: 0 0 0 1px ${({ theme }) => theme.surface3}; + } + &:hover { + box-shadow: 0 0 0 1px ${({ theme }) => theme.neutral3}; + } + &:active { + box-shadow: 0 0 0 1px ${({ theme }) => theme.surface3}; + } + &:disabled { + opacity: 50%; + cursor: auto; + } +` + +export const ButtonEmpty = styled(BaseButton)` + background-color: transparent; + color: ${({ theme }) => theme.accent1}; + display: flex; + justify-content: center; + align-items: center; + + &:focus { + text-decoration: underline; + } + &:hover { + text-decoration: none; + } + &:active { + text-decoration: none; + } + &:disabled { + opacity: 50%; + cursor: auto; + } +` + +export const ButtonText = styled(BaseButton)` + padding: 0; + width: fit-content; + background: none; + text-decoration: none; + &:focus { + text-decoration: underline; + } + &:hover { + opacity: 0.9; + } + &:active { + text-decoration: underline; + } + &:disabled { + opacity: 50%; + cursor: auto; + } +` + +const ButtonConfirmedStyle = styled(BaseButton)` + background-color: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral1}; + /* border: 1px solid ${({ theme }) => theme.success}; */ + + &:disabled { + opacity: 50%; + background-color: ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral2}; + cursor: auto; + } +` + +const ButtonErrorStyle = styled(BaseButton)` + background-color: ${({ theme }) => theme.critical}; + border: 1px solid ${({ theme }) => theme.critical}; + + &:focus { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.05, theme.critical)}; + background-color: ${({ theme }) => darken(0.05, theme.critical)}; + } + &:hover { + background-color: ${({ theme }) => darken(0.05, theme.critical)}; + } + &:active { + box-shadow: 0 0 0 1pt ${({ theme }) => darken(0.1, theme.critical)}; + background-color: ${({ theme }) => darken(0.1, theme.critical)}; + } + &:disabled { + opacity: 50%; + cursor: auto; + box-shadow: none; + background-color: ${({ theme }) => theme.critical}; + border: 1px solid ${({ theme }) => theme.critical}; + } +` + +export function ButtonConfirmed({ + confirmed, + altDisabledStyle, + ...rest +}: { confirmed?: boolean; altDisabledStyle?: boolean } & ButtonProps) { + if (confirmed) { + return + } else { + return + } +} + +export function ButtonError({ error, ...rest }: { error?: boolean } & BaseButtonProps) { + if (error) { + return + } else { + return + } +} + +export function ButtonDropdown({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { + return ( + + +
{children}
+ +
+
+ ) +} + +export function ButtonDropdownLight({ disabled = false, children, ...rest }: { disabled?: boolean } & ButtonProps) { + return ( + + +
{children}
+ +
+
+ ) +} + +const ActiveOutlined = styled(ButtonOutlined)` + border: 1px solid; + border-color: ${({ theme }) => theme.accent1}; +` + +const Circle = styled.div` + height: 17px; + width: 17px; + border-radius: 50%; + background-color: ${({ theme }) => theme.accent1}; + display: flex; + align-items: center; + justify-content: center; +` + +const CheckboxWrapper = styled.div` + width: 20px; + padding: 0 10px; + position: absolute; + top: 11px; + right: 15px; +` + +const ResponsiveCheck = styled(Check)` + size: 13px; +` + +export function ButtonRadioChecked({ active = false, children, ...rest }: { active?: boolean } & ButtonProps) { + const theme = useTheme() + + if (!active) { + return ( + + {children} + + ) + } else { + return ( + + + {children} + + + + + + + + ) + } +} + +export enum ButtonSize { + small, + medium, + large, +} +export enum ButtonEmphasis { + high, + promotional, + highSoft, + medium, + low, + warning, + destructive, + failure, +} +interface BaseThemeButtonProps { + size: ButtonSize + emphasis: ButtonEmphasis +} + +function pickThemeButtonBackgroundColor({ theme, emphasis }: { theme: DefaultTheme; emphasis: ButtonEmphasis }) { + switch (emphasis) { + case ButtonEmphasis.high: + return theme.accent1 + case ButtonEmphasis.promotional: + case ButtonEmphasis.highSoft: + return theme.accent2 + case ButtonEmphasis.low: + return 'transparent' + case ButtonEmphasis.warning: + return theme.deprecated_accentWarningSoft + case ButtonEmphasis.destructive: + return theme.critical + case ButtonEmphasis.failure: + return theme.deprecated_accentFailureSoft + case ButtonEmphasis.medium: + default: + return theme.surface3 + } +} +function pickThemeButtonFontSize({ size }: { size: ButtonSize }) { + switch (size) { + case ButtonSize.large: + return '20px' + case ButtonSize.medium: + return '16px' + case ButtonSize.small: + return '14px' + default: + return '16px' + } +} +function pickThemeButtonLineHeight({ size }: { size: ButtonSize }) { + switch (size) { + case ButtonSize.large: + return '24px' + case ButtonSize.medium: + return '20px' + case ButtonSize.small: + return '16px' + default: + return '20px' + } +} +function pickThemeButtonPadding({ size }: { size: ButtonSize }) { + switch (size) { + case ButtonSize.large: + return '16px' + case ButtonSize.medium: + return '10px 12px' + case ButtonSize.small: + return '8px' + default: + return '10px 12px' + } +} +function pickThemeButtonTextColor({ theme, emphasis }: { theme: DefaultTheme; emphasis: ButtonEmphasis }) { + switch (emphasis) { + case ButtonEmphasis.high: + case ButtonEmphasis.promotional: + return theme.accent1 + case ButtonEmphasis.highSoft: + return theme.accent1 + case ButtonEmphasis.low: + return theme.neutral2 + case ButtonEmphasis.warning: + return theme.deprecated_accentWarning + case ButtonEmphasis.destructive: + return theme.neutral1 + case ButtonEmphasis.failure: + return theme.critical + case ButtonEmphasis.medium: + default: + return theme.neutral1 + } +} + +const BaseThemeButton = styled.button` + align-items: center; + background-color: ${pickThemeButtonBackgroundColor}; + border-radius: 16px; + border: 0; + color: ${pickThemeButtonTextColor}; + cursor: pointer; + display: flex; + flex-direction: row; + font-size: ${pickThemeButtonFontSize}; + font-weight: 535; + gap: 12px; + justify-content: center; + line-height: ${pickThemeButtonLineHeight}; + padding: ${pickThemeButtonPadding}; + position: relative; + transition: 150ms ease opacity; + user-select: none; + + :active { + ${ButtonOverlay} { + background-color: ${({ theme }) => theme.deprecated_stateOverlayPressed}; + } + } + :focus { + ${ButtonOverlay} { + background-color: ${({ theme }) => theme.deprecated_stateOverlayPressed}; + } + } + :hover { + ${ButtonOverlay} { + background-color: ${({ theme }) => theme.deprecated_stateOverlayHover}; + } + } + :disabled { + cursor: default; + opacity: 0.6; + } + :disabled:active, + :disabled:focus, + :disabled:hover { + ${ButtonOverlay} { + background-color: transparent; + } + } +` + +interface ThemeButtonProps extends React.ComponentPropsWithoutRef<'button'>, BaseThemeButtonProps {} +type ThemeButtonRef = HTMLButtonElement + +export const ThemeButton = forwardRef(function ThemeButton( + { children, ...rest }, + ref +) { + return ( + + + {children} + + ) +}) + +export const ButtonLight = ({ children, ...rest }: BaseButtonProps) => { + return ( + + + {children} + + ) +} diff --git a/apps/web/src/components/Card/index.tsx b/apps/web/src/components/Card/index.tsx new file mode 100644 index 0000000..9fb4767 --- /dev/null +++ b/apps/web/src/components/Card/index.tsx @@ -0,0 +1,45 @@ +import { Box } from 'rebass/styled-components' +import styled from 'styled-components' + +const Card = styled(Box)<{ width?: string; padding?: string; border?: string; $borderRadius?: string }>` + width: ${({ width }) => width ?? '100%'}; + padding: ${({ padding }) => padding ?? '1rem'}; + border-radius: ${({ $borderRadius }) => $borderRadius ?? '16px'}; + border: ${({ border }) => border}; +` +export default Card + +export const LightCard = styled(Card)` + border: 1px solid ${({ theme }) => theme.surface3}; + background-color: ${({ theme }) => theme.surface2}; +` + +export const GrayCard = styled(Card)` + background-color: ${({ theme }) => theme.surface2}; +` + +export const DarkGrayCard = styled(Card)` + background-color: ${({ theme }) => theme.surface3}; +` + +export const DarkCard = styled(Card)` + background-color: ${({ theme }) => theme.surface1}; + border: 1px solid ${({ theme }) => theme.surface3}; +` + +export const OutlineCard = styled(Card)` + border: 1px solid ${({ theme }) => theme.surface3}; + background-color: ${({ theme }) => theme.surface2}; +` + +export const YellowCard = styled(Card)` + background-color: rgba(243, 132, 30, 0.05); + color: ${({ theme }) => theme.deprecated_yellow3}; + font-weight: 535; +` + +export const BlueCard = styled(Card)` + background-color: ${({ theme }) => theme.accent2}; + color: ${({ theme }) => theme.accent1}; + border-radius: 12px; +` diff --git a/apps/web/src/components/Charts/ChartHeader.tsx b/apps/web/src/components/Charts/ChartHeader.tsx new file mode 100644 index 0000000..a34bfa2 --- /dev/null +++ b/apps/web/src/components/Charts/ChartHeader.tsx @@ -0,0 +1,138 @@ +import { useHeaderDateFormatter } from 'components/Charts/hooks' +import Column from 'components/Column' +import Row from 'components/Row' +import { PriceSource } from 'graphql/data/__generated__/types-and-hooks' +import { getProtocolColor, getProtocolName } from 'graphql/data/util' +import { UTCTimestamp } from 'lightweight-charts' +import { ReactElement, ReactNode } from 'react' +import styled, { useTheme } from 'styled-components' +import { EllipsisStyle } from 'theme/components' +import { ThemedText } from 'theme/components/text' +import { textFadeIn } from 'theme/styles' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +export type ChartHeaderProtocolInfo = { protocol: PriceSource; value?: number } + +const ChartHeaderWrapper = styled(Row)` + ${textFadeIn}; + position: absolute; + width: 100%; + gap: 8px; + align-items: flex-start; +` +const ChartHeaderLeftDisplay = styled.div` + position: absolute; + display: flex; + flex-direction: column; + gap: 4px; + padding-bottom: 14px; + text-align: left; + pointer-events: none; + width: 70%; + + * { + ${EllipsisStyle} + } +` +const ProtocolLegendWrapper = styled(Column)` + position: absolute; + right: 0px; + padding: 4px 12px; + gap: 12px; + text-align: left; + pointer-events: none; +` +const ProtocolBlip = styled.div<{ color: string }>` + background-color: ${({ color }) => color}; + border-radius: 4px; + width: 12px; + height: 12px; +` + +const ProtocolText = styled(ThemedText.Caption)` + width: 80px; + text-align: right; + ${EllipsisStyle} +` + +function ProtocolLegend({ protocolData }: { protocolData?: ChartHeaderProtocolInfo[] }) { + const { formatFiatPrice } = useFormatter() + const theme = useTheme() + + return ( + + {protocolData + ?.map(({ value, protocol }) => { + const display = value + ? formatFiatPrice({ price: value, type: NumberType.ChartFiatValue }) + : getProtocolName(protocol) + return ( + + {display} + + + ) + }) + .reverse()} + + ) +} + +interface HeaderValueDisplayProps { + /** The number to be formatted and displayed, or the ReactElement to be displayed */ + value?: number | ReactElement + /** Used to override default format NumberType (ChartFiatValue) */ + valueFormatterType?: NumberType +} + +function HeaderValueDisplay({ value, valueFormatterType = NumberType.ChartFiatValue }: HeaderValueDisplayProps) { + const { formatFiatPrice } = useFormatter() + + if (typeof value !== 'number' && typeof value !== 'undefined') { + return <>{value} + } + + return ( + {formatFiatPrice({ price: value, type: valueFormatterType })} + ) +} + +interface HeaderTimeDisplayProps { + time?: UTCTimestamp + /** Optional string to display when time is undefined */ + timePlaceholder?: string +} + +function HeaderTimeDisplay({ time, timePlaceholder }: HeaderTimeDisplayProps) { + const headerDateFormatter = useHeaderDateFormatter() + return ( + {time ? headerDateFormatter(time) : timePlaceholder} + ) +} + +interface ChartHeaderProps extends HeaderValueDisplayProps, HeaderTimeDisplayProps { + protocolData?: ChartHeaderProtocolInfo[] + additionalFields?: ReactNode +} + +export function ChartHeader({ + value, + valueFormatterType, + time, + timePlaceholder, + protocolData, + additionalFields, +}: ChartHeaderProps) { + return ( + + + + + {additionalFields} + + + + + + ) +} diff --git a/apps/web/src/components/Charts/ChartModel.tsx b/apps/web/src/components/Charts/ChartModel.tsx new file mode 100644 index 0000000..308f92d --- /dev/null +++ b/apps/web/src/components/Charts/ChartModel.tsx @@ -0,0 +1,339 @@ +import { Trans } from '@lingui/macro' +import { formatTickMarks } from 'components/Charts/utils' +import Row from 'components/Row' +import { MissingDataBars } from 'components/Table/icons' +import { useActiveLocale } from 'hooks/useActiveLocale' +import { useScreenSize } from 'hooks/useScreenSize' +import { useUpdateAtom } from 'jotai/utils' +import { + BarPrice, + CrosshairMode, + DeepPartial, + IChartApi, + ISeriesApi, + LineStyle, + Logical, + TimeChartOptions, + createChart, +} from 'lightweight-charts' +import { ReactElement, useEffect, useMemo, useRef, useState } from 'react' +import styled, { DefaultTheme, useTheme } from 'styled-components' +import { ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { Z_INDEX } from 'theme/zIndex' +import { useFormatter } from 'utils/formatNumbers' +import { v4 as uuidv4 } from 'uuid' +import { refitChartContentAtom } from './TimeSelector' +import { SeriesDataItemType } from './types' + +interface ChartUtilParams { + locale: string + theme: DefaultTheme + format: ReturnType + isLargeScreen: boolean + onCrosshairMove?: (data: TDataType | undefined) => void +} + +interface ChartDataParams { + color?: string + data: TDataType[] + /** Repesents whether `data` is stale. If true, stale UI will appear */ + stale?: boolean +} + +export type ChartModelParams = ChartUtilParams & + ChartDataParams + +type ChartTooltipBodyComponent = React.FunctionComponent<{ + data: TDataType +}> + +export type ChartHoverData = { + item: TDataType + x: number + y: number + logicalIndex: Logical +} + +/** Util for managing lightweight-charts' state outside of the React Lifecycle. */ +export abstract class ChartModel { + protected api: IChartApi + protected abstract series: ISeriesApi + protected data: TDataType[] + protected chartDiv: HTMLDivElement + protected onCrosshairMove?: (data: TDataType | undefined, index: number | undefined) => void + private _hoverData?: ChartHoverData | undefined + private _lastTooltipWidth: number | null = null + + public tooltipId = `chart-tooltip-${uuidv4()}` + + constructor(chartDiv: HTMLDivElement, params: ChartModelParams) { + this.chartDiv = chartDiv + this.onCrosshairMove = params.onCrosshairMove + this.data = params.data + + this.api = createChart(chartDiv) + + this.api.subscribeCrosshairMove((param) => { + let newHoverData: ChartHoverData | undefined = undefined + const logical = param.logical + const x = param.point?.x + const y = param.point?.y + + if ( + x !== undefined && + isBetween(x, 0, this.chartDiv.clientWidth) && + y !== undefined && + isBetween(y, 0, this.chartDiv.clientHeight) && + logical !== undefined + ) { + const item = param.seriesData.get(this.series) as TDataType | undefined + if (item) newHoverData = { item, x, y, logicalIndex: logical } + } + + const prevHoverData = this._hoverData + if ( + newHoverData?.item.time !== prevHoverData?.item.time || + newHoverData?.logicalIndex !== prevHoverData?.logicalIndex || + newHoverData?.x !== prevHoverData?.x || + newHoverData?.y !== prevHoverData?.y + ) { + this._hoverData = newHoverData + // Dynamically accesses this.onCrosshairMove rather than params.onCrosshairMove so we only ever have to make one subscribeCrosshairMove call + this.onSeriesHover?.(newHoverData) + } + }) + } + + /** + * Updates React state with the current crosshair data. + * This method should be overridden in subclasses to provide specific hover functionality. + * When overriding, call `super.onSeriesHover(data)` to maintain base functionality. + */ + protected onSeriesHover(hoverData?: ChartHoverData) { + this.onCrosshairMove?.(hoverData?.item, hoverData?.logicalIndex) + + if (!hoverData) return + + // Tooltip positioning modified from https://github.com/tradingview/lightweight-charts/blob/master/plugin-examples/src/plugins/tooltip/tooltip.ts + const x = hoverData.x + this.api.priceScale('left').width() + 10 + const deadzoneWidth = this._lastTooltipWidth ? Math.ceil(this._lastTooltipWidth) : 45 + const xAdjusted = Math.min(x, this.api.paneSize().width - deadzoneWidth) + + const transformX = `calc(${xAdjusted}px)` + + const y = hoverData.y + const flip = y <= 20 + 100 + const yPx = y + (flip ? 1 : -1) * 20 + const yPct = flip ? '' : ' - 100%' + const transformY = `calc(${yPx}px${yPct})` + + const tooltip = document.getElementById(this.tooltipId) + + if (tooltip) { + tooltip.style.transform = `translate(${transformX}, ${transformY})` + + const tooltipMeasurement = tooltip.getBoundingClientRect() + this._lastTooltipWidth = tooltipMeasurement?.width || null + } + } + + /** Updates the chart without re-creating it or resetting pan/zoom. */ + public updateOptions( + { locale, theme, format, isLargeScreen, onCrosshairMove }: ChartModelParams, + nonDefaultChartOptions?: DeepPartial + ) { + this.onCrosshairMove = onCrosshairMove + + // Below are default options that will apply to all Chart models that extend this class and call super.updateOptions(). + // Subclasses can override / extend these options by passing in nonDefaultChartOptions. + const defaultOptions: DeepPartial = { + localization: { + locale, + priceFormatter: (price: BarPrice) => format.formatFiatPrice({ price }), + }, + autoSize: true, + layout: { textColor: theme.neutral2, background: { color: 'transparent' } }, + timeScale: { + tickMarkFormatter: formatTickMarks, + borderVisible: false, + ticksVisible: false, + timeVisible: true, + fixLeftEdge: true, + fixRightEdge: true, + }, + rightPriceScale: { + visible: isLargeScreen, + borderVisible: false, + scaleMargins: { + top: 0.32, + bottom: 0.15, + }, + autoScale: true, + }, + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, + crosshair: { + horzLine: { + visible: true, + style: LineStyle.Solid, + width: 1, + color: theme.surface3, + labelVisible: false, + }, + mode: CrosshairMode.Magnet, + vertLine: { + visible: true, + style: LineStyle.Solid, + width: 1, + color: theme.surface3, + labelVisible: false, + }, + }, + } + + this.api.applyOptions({ ...defaultOptions, ...nonDefaultChartOptions }) + } + + /** Updates visible range to fit all data from all series. */ + public fitContent() { + this.api.timeScale().fitContent() + } + + /** Removes the injected canvas from the chartDiv. */ + public remove() { + this.api.remove() + } +} + +const isBetween = (num: number, lower: number, upper: number) => num > lower && num < upper + +const ChartDiv = styled.div<{ height?: number }>` + ${({ height }) => height && `height: ${height}px`}; + width: 100%; + position: relative; + ${textFadeIn}; +` + +/** Returns a div injected with a lightweight-chart, corresponding to the given Model and params */ +export function Chart, TDataType extends SeriesDataItemType>({ + Model, + TooltipBody, + params, + height, + children, + className, +}: { + Model: new (chartDiv: HTMLDivElement, params: TParamType & ChartUtilParams) => ChartModel + TooltipBody?: ChartTooltipBodyComponent + params: TParamType + height?: number + children?: (crosshair?: TDataType) => ReactElement + className?: string +}) { + const setRefitChartContent = useUpdateAtom(refitChartContentAtom) + // Lightweight-charts injects a canvas into the page through the div referenced below + // It is stored in state to cause a re-render upon div mount, avoiding delay in chart creation + const [chartDivElement, setChartDivElement] = useState(null) + const [crosshairData, setCrosshairData] = useState(undefined) + const format = useFormatter() + const theme = useTheme() + const locale = useActiveLocale() + const { md: isLargeScreen } = useScreenSize() + const modelParams = useMemo( + () => ({ ...params, format, theme, locale, isLargeScreen, onCrosshairMove: setCrosshairData }), + [format, isLargeScreen, locale, params, theme] + ) + + // Chart model state should not affect React render cycles since the chart canvas is drawn outside of React, so we store via ref + const chartModelRef = useRef>() + + // Creates the chart as soon as the chart div ref is defined + useEffect(() => { + if (chartDivElement && chartModelRef.current === undefined) { + chartModelRef.current = new Model(chartDivElement, modelParams) + // Providers the time period selector with a handle to refit the chart + setRefitChartContent(() => () => chartModelRef.current?.fitContent()) + } + }, [Model, chartDivElement, modelParams, setRefitChartContent]) + + // Keeps the chart up-to-date with latest data/params, without re-creating the entire chart + useEffect(() => { + chartModelRef.current?.updateOptions(modelParams) + }, [modelParams]) + + // Handles chart removal on unmount + useEffect(() => { + return () => { + chartModelRef.current?.remove() + // This ref's value will persist when being initially remounted in React.StrictMode. + // The persisted IChartApi would err if utilized after calling remove(), so we manually clear the ref here. + chartModelRef.current = undefined + setRefitChartContent(undefined) + } + }, [setRefitChartContent]) + + return ( + e.stopPropagation()} + > + {children && children(crosshairData)} + {TooltipBody && crosshairData && ( + + + + )} + {params.stale && } + + ) +} + +const ChartTooltip = styled.div` + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + left: 0%; + top: 0; + z-index: ${Z_INDEX.tooltip}; + background: ${({ theme }) => theme.surface5}; + backdrop-filter: ${({ theme }) => theme.blur.light}; + border-radius: 8px; + border: 1px solid ${({ theme }) => theme.surface3}; + padding: 8px; +` + +const StaleBannerWrapper = styled(ChartTooltip)` + border-radius: 16px; + left: unset; + top: unset; + right: 12px; + bottom: 40px; + padding: 12px; + background: ${({ theme }) => theme.surface4}; +` + +function StaleBanner() { + const theme = useTheme() + // TODO(WEB-3739): Update Chart UI to grayscale when data is stale + return ( + + + + + Data may be outdated + + + + ) +} diff --git a/apps/web/src/components/Charts/LiquidityChart/index.tsx b/apps/web/src/components/Charts/LiquidityChart/index.tsx new file mode 100644 index 0000000..1cf8b2f --- /dev/null +++ b/apps/web/src/components/Charts/LiquidityChart/index.tsx @@ -0,0 +1,315 @@ +import { ChartHoverData, ChartModel, ChartModelParams } from 'components/Charts/ChartModel' +import { ISeriesApi, UTCTimestamp } from 'lightweight-charts' + +import { ChainId, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { FeeAmount, Pool, TICK_SPACINGS, TickMath, tickToPrice } from '@uniswap/v3-sdk' +import { BigNumber } from 'ethers/lib/ethers' +import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData' +import JSBI from 'jsbi' +import { useEffect, useState } from 'react' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { LiquidityBarSeries } from './liquidity-bar-series' +import { LiquidityBarData, LiquidityBarProps, LiquidityBarSeriesOptions } from './renderer' + +interface LiquidityBarChartModelParams extends ChartModelParams, LiquidityBarProps {} + +export class LiquidityBarChartModel extends ChartModel { + protected series: ISeriesApi<'Custom'> + private activeTick?: number + + constructor(chartDiv: HTMLDivElement, params: LiquidityBarChartModelParams) { + super(chartDiv, params) + this.series = this.api.addCustomSeries(new LiquidityBarSeries(params)) + + this.series.setData(this.data) + + this.updateOptions(params) + this.fitContent() + } + + updateOptions(params: LiquidityBarChartModelParams) { + super.updateOptions(params, { + localization: { + locale: params.locale, + }, + rightPriceScale: { + visible: false, + borderVisible: false, + scaleMargins: { + top: 0.35, + bottom: 0, + }, + autoScale: true, + }, + timeScale: { + visible: false, + fixLeftEdge: true, + fixRightEdge: true, + borderVisible: false, + }, + crosshair: { + horzLine: { + visible: false, + labelVisible: false, + }, + vertLine: { + visible: false, + labelVisible: false, + }, + }, + grid: { + vertLines: { + visible: false, + }, + horzLines: { + visible: false, + }, + }, + }) + const { data, activeTick } = params + + this.activeTick = activeTick + + if (this.data !== data) { + this.data = data + this.series.setData(data) + this.fitContent() + } + + this.series.applyOptions({ + priceFormat: { + type: 'volume', + }, + priceLineVisible: false, + lastValueVisible: false, + }) + + this.series.applyOptions(params) + } + + override onSeriesHover(hoverData?: ChartHoverData) { + super.onSeriesHover(hoverData) + const updatedOptions: Partial = { hoveredTick: hoverData?.item.tick ?? this.activeTick } + this.series.applyOptions(updatedOptions) + } + + activeTickIndex() { + return this.data.findIndex((bar) => bar.tick === this.activeTick) + } + + fitContent() { + const length = this.data.length + const activeTickIndex = this.data.findIndex((bar) => bar.tick === this.activeTick) + const midPoint = activeTickIndex !== -1 ? activeTickIndex : length / 2 + + this.api + .timeScale() + .setVisibleLogicalRange({ from: Math.max(midPoint - 50, 0), to: Math.min(midPoint + 50, this.data.length) }) + } +} + +const MAX_UINT128 = BigNumber.from(2).pow(128).sub(1) + +function maxAmount(token: Token) { + return CurrencyAmount.fromRawAmount(token, MAX_UINT128.toString()) +} + +/** Calculates tokens locked in the active tick range based on the current tick */ +async function calculateActiveRangeTokensLocked( + token0: Token, + token1: Token, + feeTier: FeeAmount, + tick: TickProcessed, + poolData: { + sqrtPriceX96?: JSBI + currentTick?: number + liquidity?: JSBI + } +): Promise<{ amount0Locked: number; amount1Locked: number } | undefined> { + if (!poolData.currentTick || !poolData.sqrtPriceX96 || !poolData.liquidity) { + return undefined + } + + try { + const liqGross = JSBI.greaterThan(tick.liquidityNet, JSBI.BigInt(0)) + ? tick.liquidityNet + : JSBI.multiply(tick.liquidityNet, JSBI.BigInt('-1')) + + const mockTicks = [ + { + index: tick.tick, + liquidityGross: liqGross, + liquidityNet: JSBI.multiply(tick.liquidityNet, JSBI.BigInt('-1')), + }, + { + index: tick.tick + TICK_SPACINGS[feeTier], + liquidityGross: liqGross, + liquidityNet: tick.liquidityNet, + }, + ] + // Initialize pool containing only the active range + const pool1 = new Pool( + token0, + token1, + feeTier, + poolData.sqrtPriceX96, + tick.liquidityActive, + poolData.currentTick, + mockTicks + ) + // Calculate amount of token0 that would need to be swapped to reach the bottom of the range + const bottomOfRangePrice = TickMath.getSqrtRatioAtTick(mockTicks[0].index) + const token1Amount = (await pool1.getOutputAmount(maxAmount(token0), bottomOfRangePrice))[0] + const amount0Locked = parseFloat(tick.sdkPrice.invert().quote(token1Amount).toExact()) + + // Calculate amount of token1 that would need to be swapped to reach the top of the range + const topOfRangePrice = TickMath.getSqrtRatioAtTick(mockTicks[1].index) + const token0Amount = (await pool1.getOutputAmount(maxAmount(token1), topOfRangePrice))[0] + const amount1Locked = parseFloat(tick.sdkPrice.quote(token0Amount).toExact()) + + return { amount0Locked, amount1Locked } + } catch { + return { amount0Locked: 0, amount1Locked: 0 } + } +} + +/** Returns amounts of tokens locked in the given tick. Reference: https://docs.uniswap.org/sdk/v3/guides/advanced/active-liquidity */ +async function calculateTokensLocked( + token0: Token, + token1: Token, + feeTier: FeeAmount, + tick: TickProcessed +): Promise<{ amount0Locked: number; amount1Locked: number }> { + try { + const tickSpacing = TICK_SPACINGS[feeTier] + const liqGross = JSBI.greaterThan(tick.liquidityNet, JSBI.BigInt(0)) + ? tick.liquidityNet + : JSBI.multiply(tick.liquidityNet, JSBI.BigInt('-1')) + + const sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick.tick) + const mockTicks = [ + { + index: tick.tick, + liquidityGross: liqGross, + liquidityNet: JSBI.multiply(tick.liquidityNet, JSBI.BigInt('-1')), + }, + { + index: tick.tick + TICK_SPACINGS[feeTier], + liquidityGross: liqGross, + liquidityNet: tick.liquidityNet, + }, + ] + + // Initialize pool containing only the current range + const pool = new Pool(token0, token1, Number(feeTier), sqrtPriceX96, tick.liquidityActive, tick.tick, mockTicks) + + // Calculate token amounts that would need to be swapped to reach the next range + const nextSqrtX96 = TickMath.getSqrtRatioAtTick(tick.tick - tickSpacing) + const maxAmountToken0 = CurrencyAmount.fromRawAmount(token0, MAX_UINT128.toString()) + const token1Amount = (await pool.getOutputAmount(maxAmountToken0, nextSqrtX96))[0] + const amount0Locked = parseFloat(tick.sdkPrice.invert().quote(token1Amount).toExact()) + const amount1Locked = parseFloat(token1Amount.toExact()) + + return { amount0Locked, amount1Locked } + } catch { + return { amount0Locked: 0, amount1Locked: 0 } + } +} + +export function useLiquidityBarData({ + tokenA, + tokenB, + feeTier, + isReversed, + chainId, +}: { + tokenA: Token + tokenB: Token + feeTier: FeeAmount + isReversed: boolean + chainId: ChainId +}) { + const { formatNumber, formatPrice } = useFormatter() + const activePoolData = usePoolActiveLiquidity(tokenA, tokenB, feeTier, chainId) + + const [tickData, setTickData] = useState<{ + barData: LiquidityBarData[] + activeRangeData?: LiquidityBarData + activeRangePercentage?: number + }>() + + useEffect(() => { + async function formatData() { + const ticksProcessed = activePoolData.data + if (!ticksProcessed) return + + let activeRangePercentage: number | undefined = undefined + let activeRangeIndex: number | undefined = undefined + + const barData: LiquidityBarData[] = [] + for (let index = 0; index < ticksProcessed.length; index++) { + const t = ticksProcessed[index] + + // Lightweight-charts require the x-axis to be time; a fake time base on index is provided + const fakeTime = (isReversed ? index * 1000 : (ticksProcessed.length - index) * 1000) as UTCTimestamp + const isActive = activePoolData.activeTick === t.tick + + let price0 = t.sdkPrice + let price1 = t.sdkPrice.invert() + + if (isActive && activePoolData.activeTick && activePoolData.currentTick) { + activeRangeIndex = index + activeRangePercentage = (activePoolData.currentTick - t.tick) / TICK_SPACINGS[feeTier] + + price0 = tickToPrice(tokenA, tokenB, t.tick) + price1 = price0.invert() + } + + const { amount0Locked, amount1Locked } = await calculateTokensLocked(tokenA, tokenB, feeTier, t) + + barData.push({ + tick: t.tick, + liquidity: parseFloat(t.liquidityActive.toString()), + price0: formatPrice({ price: price0, type: NumberType.SwapDetailsAmount }), + price1: formatPrice({ price: price1, type: NumberType.SwapDetailsAmount }), + time: fakeTime, + amount0Locked, + amount1Locked, + }) + } + + // offset the values to line off bars with TVL used to swap across bar + barData?.map((entry, i) => { + if (i > 0) { + barData[i - 1].amount0Locked = entry.amount0Locked + barData[i - 1].amount1Locked = entry.amount1Locked + } + }) + + const activeRangeData = activeRangeIndex !== undefined ? barData[activeRangeIndex] : undefined + // For active range, adjust amounts locked to adjust for where current tick/price is within the range + if (activeRangeIndex !== undefined && activeRangeData) { + const activeTickTvl = await calculateActiveRangeTokensLocked( + tokenA, + tokenB, + feeTier, + ticksProcessed[activeRangeIndex], + activePoolData + ) + barData[activeRangeIndex] = { ...activeRangeData, ...activeTickTvl } + } + + // Reverse data so that token0 is on the left by default + if (!isReversed) { + barData.reverse() + } + + // TODO(WEB-3672): investigate why negative/inaccurate liquidity values that are appearing from computeSurroundingTicks + setTickData({ barData: barData.filter((t) => t.liquidity > 0), activeRangeData, activeRangePercentage }) + } + + formatData() + }, [activePoolData, tokenA, tokenB, formatNumber, formatPrice, isReversed, feeTier]) + + return { tickData, activeTick: activePoolData.activeTick, loading: activePoolData.isLoading || !tickData } +} diff --git a/apps/web/src/components/Charts/LiquidityChart/liquidity-bar-series.tsx b/apps/web/src/components/Charts/LiquidityChart/liquidity-bar-series.tsx new file mode 100644 index 0000000..4543cea --- /dev/null +++ b/apps/web/src/components/Charts/LiquidityChart/liquidity-bar-series.tsx @@ -0,0 +1,51 @@ +import { + customSeriesDefaultOptions, + CustomSeriesPricePlotValues, + ICustomSeriesPaneView, + PaneRendererCustomData, + Time, + WhitespaceData, +} from 'lightweight-charts' + +import { LiquidityBarData, LiquidityBarProps, LiquidityBarSeriesOptions, LiquidityBarSeriesRenderer } from './renderer' + +export class LiquidityBarSeries + implements ICustomSeriesPaneView +{ + _renderer: LiquidityBarSeriesRenderer + _tokenAColor: string + _tokenBColor: string + _highlightColor: string + + constructor(props: LiquidityBarProps) { + this._tokenAColor = props.tokenAColor + this._renderer = new LiquidityBarSeriesRenderer(props) + this._tokenBColor = props.tokenBColor + this._highlightColor = props.highlightColor + } + + priceValueBuilder(plotRow: TData): CustomSeriesPricePlotValues { + return [0, plotRow.liquidity] + } + + isWhitespace(data: TData | WhitespaceData): data is WhitespaceData { + return !(data as TData).liquidity + } + + renderer(): LiquidityBarSeriesRenderer { + return this._renderer + } + + update(data: PaneRendererCustomData, options: LiquidityBarSeriesOptions): void { + this._renderer.update(data, options) + } + + defaultOptions() { + return { + ...customSeriesDefaultOptions, + tokenAColor: this._tokenAColor, + tokenBColor: this._tokenBColor, + highlightColor: this._highlightColor, + } + } +} diff --git a/apps/web/src/components/Charts/LiquidityChart/renderer.tsx b/apps/web/src/components/Charts/LiquidityChart/renderer.tsx new file mode 100644 index 0000000..65636af --- /dev/null +++ b/apps/web/src/components/Charts/LiquidityChart/renderer.tsx @@ -0,0 +1,156 @@ +import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas' +import { + CustomData, + CustomSeriesOptions, + ICustomSeriesPaneRenderer, + PaneRendererCustomData, + PriceToCoordinateConverter, + Time, + UTCTimestamp, +} from 'lightweight-charts' +import { ColumnPosition, calculateColumnPositionsInPlace, positionsBox } from '../VolumeChart/utils' + +export interface LiquidityBarData extends CustomData { + time: UTCTimestamp + tick: number + price0: string + price1: string + liquidity: number + amount0Locked: number + amount1Locked: number +} + +interface LiquidityBarItem { + x: number + y: number + column?: ColumnPosition + tick: number +} + +export interface LiquidityBarProps { + tokenAColor: string + tokenBColor: string + highlightColor: string + activeTick?: number + activeTickProgress?: number +} + +export interface LiquidityBarSeriesOptions extends CustomSeriesOptions, LiquidityBarProps { + hoveredTick?: number +} + +export class LiquidityBarSeriesRenderer implements ICustomSeriesPaneRenderer { + _data: PaneRendererCustomData | null = null + _options: LiquidityBarProps & Partial + + constructor(props: LiquidityBarProps) { + this._options = props + this._options.hoveredTick = props.activeTick + } + + draw(target: CanvasRenderingTarget2D, priceConverter: PriceToCoordinateConverter): void { + target.useBitmapCoordinateSpace((scope) => this._drawImpl(scope, priceConverter)) + } + + update(data: PaneRendererCustomData, options: LiquidityBarSeriesOptions): void { + this._data = data + this._options = { ...this._options, ...options } + } + + _drawImpl(renderingScope: BitmapCoordinatesRenderingScope, priceToCoordinate: PriceToCoordinateConverter): void { + if ( + this._data === null || + this._data.bars.length === 0 || + this._data.visibleRange === null || + this._options === null + ) { + return + } + const ctx = renderingScope.context + const bars: LiquidityBarItem[] = this._data.bars.map((bar) => { + return { + x: bar.x, + y: priceToCoordinate(bar.originalData.liquidity) ?? 0, + tick: bar.originalData.tick, + } + }) + calculateColumnPositionsInPlace( + bars, + this._data.barSpacing, + renderingScope.horizontalPixelRatio, + this._data.visibleRange.from, + this._data.visibleRange.to + ) + const zeroY = priceToCoordinate(0) ?? 0 + ctx.fillStyle = this._options.tokenAColor + + for (let i = this._data.visibleRange.from; i < this._data.visibleRange.to; i++) { + const stack = bars[i] + const column = stack.column + const isCurrentTick = this._options.activeTick === stack.tick + const isHoveredTick = this._options.hoveredTick === stack.tick + + if (!column) return + const width = Math.min( + Math.max(renderingScope.horizontalPixelRatio, column.right - column.left), + this._data.barSpacing * renderingScope.horizontalPixelRatio + ) + + // Create margin to make visual bars thin + const margin = 0.3 * width + const widthWithMargin = width - margin * 2 + const totalBox = positionsBox(zeroY, stack.y, renderingScope.verticalPixelRatio) + + if (isHoveredTick) { + const highlightOffset = 0.3 * ctx.canvas.height + const highlightLength = ctx.canvas.height - highlightOffset + + // Draw background highlight bar + ctx.fillStyle = this._options.highlightColor + ctx.beginPath() + ctx.roundRect(column.left + margin, highlightOffset, widthWithMargin, highlightLength, 8) + ctx.fill() + + ctx.globalAlpha = 1 + } else { + // Reduce opacity of non-hovered bars + ctx.globalAlpha = 0.4 + } + + // Select color based on where current bar is in relation to activeTick + if (isCurrentTick) { + ctx.fillStyle = this._options.tokenBColor + } else if (this._options.activeTick === undefined) { + ctx.fillStyle = this._options.color ?? 'white' + } else if (this._options.activeTick > stack.tick) { + ctx.fillStyle = this._options.tokenBColor + } else if (this._options.activeTick < stack.tick) { + ctx.fillStyle = this._options.tokenAColor + } + + // Draw bar + ctx.beginPath() + ctx.roundRect(column.left + margin, totalBox.position, widthWithMargin, totalBox.length, 8) + ctx.fill() + + // Reset opacity + ctx.globalAlpha = 1 + + if (isCurrentTick && this._options.activeTickProgress) { + // Draw token A color on top of token B color for active range + ctx.globalCompositeOperation = 'source-atop' + + // Token A color takes up activeTickProgress % of the bar height + const activeRangeAdjustedHeight = totalBox.length * this._options.activeTickProgress + const activeRangeAdjustedPosition = totalBox.position + (totalBox.length - activeRangeAdjustedHeight) + + ctx.fillStyle = this._options.tokenAColor + ctx.beginPath() + ctx.fillRect(column.left, activeRangeAdjustedPosition, width, activeRangeAdjustedHeight) + + // Reset composite operation to default + ctx.globalCompositeOperation = 'source-over' + } + } + } +} diff --git a/apps/web/src/components/Charts/LoadingState.tsx b/apps/web/src/components/Charts/LoadingState.tsx new file mode 100644 index 0000000..e1c2dfa --- /dev/null +++ b/apps/web/src/components/Charts/LoadingState.tsx @@ -0,0 +1,185 @@ +import { Trans } from '@lingui/macro' +import Column from 'components/Column' +import Row from 'components/Row' +import { MissingDataIcon } from 'components/Table/icons' +import { lighten } from 'polished' +import { PropsWithChildren, ReactNode } from 'react' +import styled, { useTheme } from 'styled-components' +import { ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { opacify } from 'theme/utils' +import { ChartType } from './utils' + +const ChartErrorContainer = styled(Row)` + position: absolute; + width: max-content; + align-items: flex-start; + max-width: 320px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + border-radius: 20px; + border: 1.3px solid ${({ theme }) => theme.surface3}; + background-color: ${({ theme }) => theme.surface1}; + padding: 12px 20px 12px 12px; + gap: 12px; + ${textFadeIn}; +` +const ErrorTextColumn = styled(Column)` + white-space: normal; +` + +function ChartErrorView({ children }: PropsWithChildren) { + return ( + +
+ +
+ + + Missing chart data + + {children} + +
+ ) +} + +function ChartSkeletonAxes({ + height, + fillColor, + tickColor, + hideYAxis, +}: { + height: number + fillColor: string + tickColor: string + hideYAxis?: boolean +}) { + return ( + + + + + + + + + + + {!hideYAxis && ( + + + + + + + + )} + + ) +} + +function ChartLoadingStateMask({ type, height, id }: { type: ChartType; height: number; id: string }) { + const theme = useTheme() + + switch (type) { + case ChartType.TVL: + case ChartType.PRICE: + return ( + <> + + + + + + + + + + + + + + + + + + ) + case ChartType.VOLUME: + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) + default: + return null + } +} + +export function ChartSkeleton({ + errorText, + height, + type, + dim, + hideYAxis, +}: { + height: number + errorText?: ReactNode + type: ChartType + dim?: boolean + hideYAxis?: boolean +}) { + const theme = useTheme() + const neutral3Opacified = opacify(25, theme.neutral3) + + const fillColor = errorText || dim ? neutral3Opacified : theme.neutral3 + const tickColor = errorText ? opacify(12.5, theme.neutral3) : neutral3Opacified + + const maskId = `mask-${type}-${height}` + + return ( + + + + + + + + + {errorText && {errorText}} + + ) +} diff --git a/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts new file mode 100644 index 0000000..b324a4e --- /dev/null +++ b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/renderer.ts @@ -0,0 +1,197 @@ +/** + * Copied from https://github.com/tradingview/lightweight-charts/blob/master/plugin-examples/src/plugins/rounded-candle-series/renderer.ts + */ +import { positionsLine } from 'components/Charts/VolumeChart/CrosshairHighlightPrimitive' +import { positionsBox } from 'components/Charts/VolumeChart/utils' +import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas' +import { + CandlestickData, + ICustomSeriesPaneRenderer, + PaneRendererCustomData, + PriceToCoordinateConverter, + Range, + Time, + UTCTimestamp, +} from 'lightweight-charts' +import { RoundedCandleSeriesOptions } from './rounded-candles-series' + +interface BarItem { + openY: number + highY: number + lowY: number + closeY: number + x: number + isUp: boolean +} + +export class RoundedCandleSeriesRenderer> + implements ICustomSeriesPaneRenderer +{ + _data: PaneRendererCustomData | null = null + _options: RoundedCandleSeriesOptions | null = null + + draw(target: CanvasRenderingTarget2D, priceConverter: PriceToCoordinateConverter): void { + target.useBitmapCoordinateSpace((scope) => this._drawImpl(scope, priceConverter)) + } + + update(data: PaneRendererCustomData, options: RoundedCandleSeriesOptions): void { + this._data = data + this._options = options + } + + _drawImpl(renderingScope: BitmapCoordinatesRenderingScope, priceToCoordinate: PriceToCoordinateConverter): void { + if ( + this._data === null || + this._data.bars.length === 0 || + this._data.visibleRange === null || + this._options === null + ) { + return + } + + let lastClose = -Infinity + const bars: BarItem[] = this._data.bars.map((bar) => { + const isUp = bar.originalData.close >= lastClose + lastClose = bar.originalData.close ?? lastClose + const openY = priceToCoordinate(bar.originalData.open as number) ?? 0 + const highY = priceToCoordinate(bar.originalData.high as number) ?? 0 + const lowY = priceToCoordinate(bar.originalData.low as number) ?? 0 + const closeY = priceToCoordinate(bar.originalData.close as number) ?? 0 + return { + openY, + highY, + lowY, + closeY, + x: bar.x, + isUp, + } + }) + + const radius = this._options.radius(this._data.barSpacing) + this._drawWicks(renderingScope, bars, this._data.visibleRange) + this._drawCandles(renderingScope, bars, this._data.visibleRange, radius) + } + + private _drawWicks( + renderingScope: BitmapCoordinatesRenderingScope, + bars: readonly BarItem[], + visibleRange: Range + ): void { + if (this._data === null || this._options === null) { + return + } + + const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope + + const wickWidth = gridAndCrosshairMediaWidth(horizontalPixelRatio) + + for (let i = visibleRange.from; i < visibleRange.to; i++) { + const bar = bars[i] + ctx.fillStyle = bar.isUp ? this._options.wickUpColor : this._options.wickDownColor + + const verticalPositions = positionsBox(bar.lowY, bar.highY, verticalPixelRatio) + const linePositions = positionsLine(bar.x, horizontalPixelRatio, wickWidth) + ctx.fillRect(linePositions.position, verticalPositions.position, linePositions.length, verticalPositions.length) + } + } + + private _drawCandles( + renderingScope: BitmapCoordinatesRenderingScope, + bars: readonly BarItem[], + visibleRange: Range, + radius: number + ): void { + if (this._data === null || this._options === null) { + return + } + + const { context: ctx, horizontalPixelRatio, verticalPixelRatio } = renderingScope + + // we want this in media width therefore using 1 + // positionsLine will adjust for pixelRatio + const candleBodyWidth = candlestickWidth(this._data.barSpacing, 1) + + for (let i = visibleRange.from; i < visibleRange.to; i++) { + const bar = bars[i] + + const verticalPositions = positionsBox( + Math.min(bar.openY, bar.closeY), + Math.max(bar.openY, bar.closeY), + verticalPixelRatio + ) + const linePositions = positionsLine(bar.x, horizontalPixelRatio, candleBodyWidth) + + ctx.fillStyle = bar.isUp ? this._options.upColor : this._options.downColor + + // roundRect might need to polyfilled for older browsers + if (ctx.roundRect) { + ctx.beginPath() + ctx.roundRect( + linePositions.position, + verticalPositions.position, + linePositions.length, + Math.max(verticalPositions.length, 1), + radius + ) + ctx.fill() + } else { + ctx.fillRect(linePositions.position, verticalPositions.position, linePositions.length, verticalPositions.length) + } + } + } +} + +function optimalCandlestickWidth(barSpacing: number, pixelRatio: number): number { + const barSpacingSpecialCaseFrom = 2.5 + const barSpacingSpecialCaseTo = 4 + const barSpacingSpecialCaseCoeff = 3 + if (barSpacing >= barSpacingSpecialCaseFrom && barSpacing <= barSpacingSpecialCaseTo) { + return Math.floor(barSpacingSpecialCaseCoeff * pixelRatio) + } + // coeff should be 1 on small barspacing and go to 0.8 while groing bar spacing + const barSpacingReducingCoeff = 0.2 + const coeff = + 1 - + (barSpacingReducingCoeff * Math.atan(Math.max(barSpacingSpecialCaseTo, barSpacing) - barSpacingSpecialCaseTo)) / + (Math.PI * 0.5) + const res = Math.floor(barSpacing * coeff * pixelRatio) + const scaledBarSpacing = Math.floor(barSpacing * pixelRatio) + const optimal = Math.min(res, scaledBarSpacing) + return Math.max(Math.floor(pixelRatio), optimal) +} + +/** + * Calculates the candlestick width that the library would use for the current + * bar spacing. + * @param barSpacing bar spacing in media coordinates + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns The width (in bitmap coordinates) that the chart would use to draw a candle body + */ +function candlestickWidth(barSpacing: number, horizontalPixelRatio: number): number { + let width = optimalCandlestickWidth(barSpacing, horizontalPixelRatio) + if (width >= 2) { + const wickWidth = Math.floor(horizontalPixelRatio) + if (wickWidth % 2 !== width % 2) { + width-- + } + } + return width +} + +/** + * Default grid / crosshair line width in Bitmap sizing + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns default grid / crosshair line width in Bitmap sizing + */ +function gridAndCrosshairBitmapWidth(horizontalPixelRatio: number): number { + return Math.max(1, Math.floor(horizontalPixelRatio)) +} + +/** + * Default grid / crosshair line width in Media sizing + * @param horizontalPixelRatio - horizontal pixel ratio + * @returns default grid / crosshair line width in Media sizing + */ +function gridAndCrosshairMediaWidth(horizontalPixelRatio: number): number { + return gridAndCrosshairBitmapWidth(horizontalPixelRatio) / horizontalPixelRatio +} diff --git a/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/rounded-candles-series.ts b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/rounded-candles-series.ts new file mode 100644 index 0000000..8307103 --- /dev/null +++ b/apps/web/src/components/Charts/PriceChart/RoundedCandlestickSeries/rounded-candles-series.ts @@ -0,0 +1,72 @@ +/** + * Copied from https://github.com/tradingview/lightweight-charts/blob/master/plugin-examples/src/plugins/rounded-candle-series/rounded-candle-series.ts + */ +import { + CandlestickData, + CandlestickSeriesOptions, + CustomSeriesOptions, + CustomSeriesPricePlotValues, + ICustomSeriesPaneView, + PaneRendererCustomData, + Time, + UTCTimestamp, + WhitespaceData, + customSeriesDefaultOptions, +} from 'lightweight-charts' +import { RoundedCandleSeriesRenderer } from './renderer' + +export interface RoundedCandleSeriesOptions + extends CustomSeriesOptions, + Exclude { + radius: (barSpacing: number) => number + neutralColor: string +} + +const defaultOptions: RoundedCandleSeriesOptions = { + ...customSeriesDefaultOptions, + upColor: '#26a69a', + downColor: '#ef5350', + neutralColor: '#26a69a', + wickVisible: true, + borderVisible: true, + borderColor: '#378658', + borderUpColor: '#26a69a', + borderDownColor: '#ef5350', + wickColor: '#737375', + wickUpColor: '#26a69a', + wickDownColor: '#ef5350', + radius(bs: number) { + if (bs < 4) return 0 + return bs / 3 + }, +} as const + +export class RoundedCandleSeries> + implements ICustomSeriesPaneView +{ + _renderer: RoundedCandleSeriesRenderer + + constructor() { + this._renderer = new RoundedCandleSeriesRenderer() + } + + priceValueBuilder(plotRow: TData): CustomSeriesPricePlotValues { + return [plotRow.high, plotRow.low, plotRow.close] + } + + renderer(): RoundedCandleSeriesRenderer { + return this._renderer + } + + isWhitespace(data: TData | WhitespaceData): data is WhitespaceData { + return (data as Partial).close === undefined + } + + update(data: PaneRendererCustomData, options: RoundedCandleSeriesOptions): void { + this._renderer.update(data, options) + } + + defaultOptions() { + return defaultOptions + } +} diff --git a/apps/web/src/components/Charts/PriceChart/index.tsx b/apps/web/src/components/Charts/PriceChart/index.tsx new file mode 100644 index 0000000..3be793e --- /dev/null +++ b/apps/web/src/components/Charts/PriceChart/index.tsx @@ -0,0 +1,277 @@ +import { Trans } from '@lingui/macro' +import { ChartHeader } from 'components/Charts/ChartHeader' +import { Chart, ChartHoverData, ChartModel, ChartModelParams } from 'components/Charts/ChartModel' +import { getCandlestickPriceBounds } from 'components/Charts/PriceChart/utils' +import { PriceChartType } from 'components/Charts/utils' +import { RowBetween } from 'components/Row' +import { DeltaArrow, DeltaText, calculateDelta } from 'components/Tokens/TokenDetails/Delta' +import { + AreaData, + AreaSeriesPartialOptions, + BarPrice, + CandlestickData, + IPriceLine, + ISeriesApi, + LineStyle, + LineType, + PriceLineOptions, + UTCTimestamp, +} from 'lightweight-charts' +import { useMemo } from 'react' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { opacify } from 'theme/utils' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { RoundedCandleSeries, RoundedCandleSeriesOptions } from './RoundedCandlestickSeries/rounded-candles-series' + +export type PriceChartData = CandlestickData & AreaData + +interface PriceChartModelParams extends ChartModelParams { + type: PriceChartType +} + +const LOW_PRICE_RANGE_THRESHOLD = 0.2 +const LOW_PRICE_RANGE_SCALE_FACTOR = 1000000000 + +export class PriceChartModel extends ChartModel { + protected series: ISeriesApi<'Area'> | ISeriesApi<'Custom'> + private originalData: PriceChartData[] + private lowPriceRangeScaleFactor = 1 + private type: PriceChartType + private minPriceLine: IPriceLine | undefined + private maxPriceLine: IPriceLine | undefined + private priceLineOptions: Partial | undefined + private min: number + private max: number + + constructor(chartDiv: HTMLDivElement, params: PriceChartModelParams) { + super(chartDiv, params) + this.originalData = this.data + + const { adjustedData, lowPriceRangeScaleFactor, min, max } = PriceChartModel.getAdjustedPrices(params.data) + this.data = adjustedData + this.lowPriceRangeScaleFactor = lowPriceRangeScaleFactor + this.min = min + this.max = max + + this.type = params.type + this.series = + this.type === PriceChartType.LINE ? this.api.addAreaSeries() : this.api.addCustomSeries(new RoundedCandleSeries()) + this.series.setData(this.data) + this.updateOptions(params) + this.fitContent() + } + + private static applyPriceScaleFactor(data: PriceChartData, scaleFactor: number): PriceChartData { + return { + time: data.time, + value: (data.value || data.close) * scaleFactor, + open: data.open * scaleFactor, + close: data.close * scaleFactor, + high: data.high * scaleFactor, + low: data.low * scaleFactor, + } + } + + private static getAdjustedPrices(data: PriceChartData[]) { + let lowPriceRangeScaleFactor = 1 + let adjustedData = data + let { min, max } = getCandlestickPriceBounds(data) + + // Lightweight-charts shows few price-axis points for low-value/volatility tokens, + // so we workaround by "scaling" the prices, causing more price-axis points to be shown + if (max - min < LOW_PRICE_RANGE_THRESHOLD) { + lowPriceRangeScaleFactor = LOW_PRICE_RANGE_SCALE_FACTOR + adjustedData = data.map((point) => this.applyPriceScaleFactor(point, lowPriceRangeScaleFactor)) + min = min * lowPriceRangeScaleFactor + max = max * lowPriceRangeScaleFactor + } + + return { adjustedData, lowPriceRangeScaleFactor, min, max } + } + + updateOptions(params: PriceChartModelParams) { + const { data, theme, type, locale, format } = params + super.updateOptions(params, { + localization: { + locale, + priceFormatter: (price: BarPrice) => { + return format.formatFiatPrice({ + // Transform price back to original value if it was scaled + price: Number(price) / this.lowPriceRangeScaleFactor, + type: NumberType.ChartFiatValue, + }) + }, + }, + grid: { + vertLines: { style: LineStyle.CustomDotGrid, color: theme.neutral3 }, + horzLines: { style: LineStyle.CustomDotGrid, color: theme.neutral3 }, + }, + }) + + // Handles changing between line/candlestick view + if (this.type !== type) { + this.type = params.type + this.api.removeSeries(this.series) + if (this.type === PriceChartType.CANDLESTICK) { + this.series = this.api.addCustomSeries(new RoundedCandleSeries()) + } else { + this.series = this.api.addAreaSeries() + } + this.series.setData(this.data) + } + // Handles changes in data, e.g. time period selection + if (this.originalData !== data) { + this.originalData = data + const { adjustedData, lowPriceRangeScaleFactor, min, max } = PriceChartModel.getAdjustedPrices(data) + this.data = adjustedData + this.lowPriceRangeScaleFactor = lowPriceRangeScaleFactor + this.min = min + this.max = max + + this.series.setData(this.data) + this.fitContent() + } + + this.series.applyOptions({ + priceLineVisible: false, + lastValueVisible: false, + + // Line-specific options: + lineType: data.length < 20 ? LineType.WithSteps : LineType.Curved, // Stepped line is visually preferred for smaller datasets + lineWidth: 2, + lineColor: theme.accent1, + topColor: opacify(12, theme.accent1), + bottomColor: opacify(12, theme.accent1), + crosshairMarkerRadius: 5, + crosshairMarkerBorderColor: opacify(30, theme.accent1), + crosshairMarkerBorderWidth: 3, + + // Candlestick-specific options: + upColor: theme.success, + wickUpColor: theme.success, + downColor: theme.critical, + wickDownColor: theme.critical, + borderVisible: false, + } as Partial & AreaSeriesPartialOptions) + + this.priceLineOptions = { + color: theme.surface3, + lineWidth: 2, + lineStyle: LineStyle.Dashed, + axisLabelColor: theme.neutral3, + axisLabelTextColor: theme.neutral2, + } + this.minPriceLine?.applyOptions({ price: this.min, ...this.priceLineOptions }) + this.maxPriceLine?.applyOptions({ price: this.max, ...this.priceLineOptions }) + } + + override onSeriesHover(hoverData?: ChartHoverData) { + if (hoverData) { + // Use original data point for hover functionality rather than data that has been scaled by lowPriceRangeScaleFactor + const originalItem = this.originalData[hoverData.logicalIndex] + const updatedHoverData = { ...hoverData, item: originalItem } + super.onSeriesHover(updatedHoverData) + } else { + super.onSeriesHover(undefined) + } + + // Hide/display price lines based on hover + if (hoverData === undefined) { + if (this.minPriceLine && this.maxPriceLine) { + this.series.removePriceLine(this.minPriceLine) + this.series.removePriceLine(this.maxPriceLine) + this.minPriceLine = undefined + this.maxPriceLine = undefined + } + } else if (!this.minPriceLine && !this.maxPriceLine && this.min && this.max) { + this.minPriceLine = this.series.createPriceLine({ price: this.min, ...this.priceLineOptions }) + this.maxPriceLine = this.series.createPriceLine({ price: this.max, ...this.priceLineOptions }) + } + } +} + +const DeltaContainer = styled.div` + font-size: 16px; + line-height: 24px; + display: flex; + align-items: center; + gap: 4px; +` + +interface PriceChartDeltaProps { + startingPrice: PriceChartData + endingPrice: PriceChartData + noColor?: boolean +} + +export function PriceChartDelta({ startingPrice, endingPrice, noColor }: PriceChartDeltaProps) { + const delta = calculateDelta(startingPrice.close ?? startingPrice.value, endingPrice.close ?? endingPrice.value) + const { formatDelta } = useFormatter() + + return ( + + + {formatDelta(delta)} + + ) +} + +interface PriceChartProps { + type: PriceChartType + height: number + data: PriceChartData[] + stale: boolean +} + +const TooltipText = styled(ThemedText.LabelSmall)` + color: ${({ theme }) => theme.neutral1}; + line-height: 20px; +` + +function CandlestickTooltip({ data }: { data: PriceChartData }) { + const { formatFiatPrice } = useFormatter() + return ( + <> + + + Open +
{formatFiatPrice({ price: data.open })}
+
+ + High +
{formatFiatPrice({ price: data.high })}
+
+ + Low +
{formatFiatPrice({ price: data.low })}
+
+ + Close +
{formatFiatPrice({ price: data.close })}
+
+
+ + ) +} + +export function PriceChart({ data, height, type, stale }: PriceChartProps) { + const lastPrice = data[data.length - 1] + return ( + ({ data, type, stale }), [data, stale, type])} + height={height} + TooltipBody={type === PriceChartType.CANDLESTICK ? CandlestickTooltip : undefined} + > + {(crosshairData) => ( + } + valueFormatterType={NumberType.FiatTokenPrice} + time={crosshairData?.time} + /> + )} + + ) +} diff --git a/apps/web/src/components/Charts/PriceChart/utils.ts b/apps/web/src/components/Charts/PriceChart/utils.ts new file mode 100644 index 0000000..e598538 --- /dev/null +++ b/apps/web/src/components/Charts/PriceChart/utils.ts @@ -0,0 +1,44 @@ +import { PricePoint } from 'graphql/data/util' +import { CandlestickData } from 'lightweight-charts' + +/** + * Returns the minimum and maximum values in the given array of PricePoints. + */ +export function getPriceBounds(prices: PricePoint[]): { min: number; max: number } { + if (!prices.length) return { min: 0, max: 0 } + + let min = prices[0].value + let max = prices[0].value + + for (const pricePoint of prices) { + if (pricePoint.value < min) { + min = pricePoint.value + } + if (pricePoint.value > max) { + max = pricePoint.value + } + } + + return { min, max } +} + +/** + * Returns the minimum and maximum values in the given array of candlestick data. + */ +export function getCandlestickPriceBounds(data: CandlestickData[]): { min: number; max: number } { + if (!data.length) return { min: 0, max: 0 } + + let min = data[0].low + let max = data[0].high + + for (const dataPoint of data) { + if (dataPoint.low < min) { + min = dataPoint.low + } + if (dataPoint.high > max) { + max = dataPoint.high + } + } + + return { min, max } +} diff --git a/apps/web/src/components/Charts/SparklineChart/LineChart.tsx b/apps/web/src/components/Charts/SparklineChart/LineChart.tsx new file mode 100644 index 0000000..3cb3fd3 --- /dev/null +++ b/apps/web/src/components/Charts/SparklineChart/LineChart.tsx @@ -0,0 +1,50 @@ +import { Group } from '@visx/group' +import { LinePath } from '@visx/shape' +import { CurveFactory } from 'd3' +import React, { ReactNode } from 'react' +import { useTheme } from 'styled-components' + +interface LineChartProps { + data: T[] + getX: (t: T) => number + getY: (t: T) => number + marginTop?: number + curve: CurveFactory + color?: string + strokeWidth: number + children?: ReactNode + width: number + height: number +} + +function LineChart({ + data, + getX, + getY, + marginTop, + curve, + color, + strokeWidth, + width, + height, + children, +}: LineChartProps) { + const theme = useTheme() + return ( + + + + + {children} + + ) +} + +export default React.memo(LineChart) as typeof LineChart diff --git a/apps/web/src/components/Charts/SparklineChart/index.tsx b/apps/web/src/components/Charts/SparklineChart/index.tsx new file mode 100644 index 0000000..e3dc42c --- /dev/null +++ b/apps/web/src/components/Charts/SparklineChart/index.tsx @@ -0,0 +1,79 @@ +import { curveCardinal, scaleLinear } from 'd3' +import { SparklineMap, TopToken } from 'graphql/data/TopTokens' +import { PricePoint } from 'graphql/data/util' +import { memo } from 'react' +import styled, { useTheme } from 'styled-components' + +import { LoadingBubble } from 'components/Tokens/loading' +import { getPriceBounds } from '../PriceChart/utils' +import LineChart from './LineChart' + +const LoadingContainer = styled.div` + height: 100%; + display: flex; + justify-content: center; + align-items: center; +` + +const LongLoadingBubble = styled(LoadingBubble)` + width: 90%; +` + +const SparkLineLoadingBubble = styled(LongLoadingBubble)` + height: 4px; +` + +interface SparklineChartProps { + width: number + height: number + tokenData: TopToken + pricePercentChange?: number | null + sparklineMap: SparklineMap +} + +function _SparklineChart({ width, height, tokenData, pricePercentChange, sparklineMap }: SparklineChartProps) { + const theme = useTheme() + // for sparkline + const pricePoints = tokenData?.address ? sparklineMap[tokenData.address] : null + + // Don't display if there's one or less pricepoints + if (!pricePoints || pricePoints.length <= 1) { + return ( + + + + ) + } + + const startingPrice = pricePoints[0] + const endingPrice = pricePoints[pricePoints.length - 1] + const widthScale = scaleLinear() + .domain( + // the range of possible input values + [startingPrice.timestamp, endingPrice.timestamp] + ) + .range( + // the range of possible output values that the inputs should be transformed to (see https://www.d3indepth.com/scales/ for details) + [0, 110] + ) + + const { min, max } = getPriceBounds(pricePoints) + const rdScale = scaleLinear().domain([min, max]).range([30, 0]) + const curveTension = 0.9 + + return ( + widthScale(p.timestamp)} + getY={(p: PricePoint) => rdScale(p.value)} + curve={curveCardinal.tension(curveTension)} + marginTop={5} + color={pricePercentChange && pricePercentChange < 0 ? theme.critical : theme.success} + strokeWidth={1.5} + width={width} + height={height} + /> + ) +} + +export default memo(_SparklineChart) diff --git a/apps/web/src/components/Charts/StackedLineChart/index.tsx b/apps/web/src/components/Charts/StackedLineChart/index.tsx new file mode 100644 index 0000000..39f481a --- /dev/null +++ b/apps/web/src/components/Charts/StackedLineChart/index.tsx @@ -0,0 +1,122 @@ +import { ChartHeader } from 'components/Charts/ChartHeader' +import { Chart, ChartModel, ChartModelParams } from 'components/Charts/ChartModel' +import { StackedAreaSeriesOptions } from 'components/Charts/StackedLineChart/stacked-area-series/options' +import { StackedAreaSeries } from 'components/Charts/StackedLineChart/stacked-area-series/stacked-area-series' +import { PriceSource } from 'graphql/data/__generated__/types-and-hooks' +import { getProtocolColor } from 'graphql/data/util' +import { + CustomStyleOptions, + DeepPartial, + ISeriesApi, + LineStyle, + Logical, + UTCTimestamp, + WhitespaceData, +} from 'lightweight-charts' +import { useMemo } from 'react' +import { useTheme } from 'styled-components' + +export interface StackedLineData extends WhitespaceData { + values: number[] +} + +interface TVLChartParams extends ChartModelParams { + colors: string[] +} + +export class TVLChartModel extends ChartModel { + protected series: ISeriesApi<'Custom'> + + private hoveredLogicalIndex: Logical | null | undefined + + constructor(chartDiv: HTMLDivElement, params: TVLChartParams) { + super(chartDiv, params) + + this.series = this.api.addCustomSeries(new StackedAreaSeries(), {} as DeepPartial) + + this.series.setData(this.data) + this.updateOptions(params) + this.fitContent() + + this.api.subscribeCrosshairMove((param) => { + if (param?.logical !== this.hoveredLogicalIndex) { + this.hoveredLogicalIndex = param?.logical + this.series.applyOptions({ + hoveredLogicalIndex: this.hoveredLogicalIndex ?? (-1 as Logical), // -1 is used because series will use prev value if undefined is passed + } as DeepPartial) + } + }) + } + + updateOptions(params: TVLChartParams) { + const isSingleLineChart = params.colors.length === 1 + + const gridSettings = isSingleLineChart + ? { + grid: { + vertLines: { style: LineStyle.CustomDotGrid, color: params.theme.neutral3 }, + horzLines: { style: LineStyle.CustomDotGrid, color: params.theme.neutral3 }, + }, + } + : {} + + super.updateOptions(params, { + handleScale: false, + handleScroll: false, + rightPriceScale: { + visible: isSingleLineChart, // Hide pricescale on multi-line charts + borderVisible: false, + scaleMargins: { + top: 0.25, + bottom: 0, + }, + autoScale: true, + }, + ...gridSettings, + }) + const { data, colors } = params + + // Handles changes in data, e.g. time period selection + if (this.data !== data) { + this.data = data + this.series.setData(data) + this.fitContent() + } + + this.series.applyOptions({ + priceLineVisible: false, + lastValueVisible: false, + colors, + lineWidth: 2.5, + } as DeepPartial) + } +} + +interface LineChartProps { + height: number + sources?: PriceSource[] + data: StackedLineData[] + stale: boolean +} + +export function LineChart({ height, data, sources, stale }: LineChartProps) { + const theme = useTheme() + + const params = useMemo(() => { + const colors = sources?.map((source) => getProtocolColor(source, theme)) ?? [theme.accent1] + return { data, colors, stale } + }, [data, theme, sources, stale]) + + const lastEntry = data[data.length - 1] + return ( + + {(crosshairData: StackedLineData | undefined) => ( + (sum += v), 0)} + time={crosshairData?.time} + protocolData={sources?.map((source, index) => ({ protocol: source, value: crosshairData?.values[index] }))} + /> + )} + + ) +} diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/data.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/data.ts new file mode 100644 index 0000000..40ccec8 --- /dev/null +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/data.ts @@ -0,0 +1,9 @@ +/* Copied from: https://github.com/tradingview/lightweight-charts/blob/f13a3c1f3fefcace9d4da5b97c1638009298b3c8/plugin-examples/src/plugins/stacked-area-series */ +import { CustomData } from 'lightweight-charts' + +/** + * StackedArea Series Data + */ +export interface StackedAreaData extends CustomData { + values: number[] +} diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/options.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/options.ts new file mode 100644 index 0000000..c1f1630 --- /dev/null +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/options.ts @@ -0,0 +1,19 @@ +/** + * Copied and modified from: https://github.com/tradingview/lightweight-charts/blob/f13a3c1f3fefcace9d4da5b97c1638009298b3c8/plugin-examples/src/plugins/stacked-area-series + * Modifications are called out with comments. + */ + +import { customSeriesDefaultOptions, CustomSeriesOptions, Logical } from 'lightweight-charts' + +export interface StackedAreaSeriesOptions extends CustomSeriesOptions { + colors: readonly string[] + lineWidth: number + // Modification: tracks the hovered data point, used for rendering crosshair + hoveredLogicalIndex?: Logical +} + +export const defaultOptions: StackedAreaSeriesOptions = { + ...customSeriesDefaultOptions, + colors: [], + lineWidth: 2, +} as const diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts new file mode 100644 index 0000000..05f315d --- /dev/null +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/renderer.ts @@ -0,0 +1,267 @@ +/** + * Copied and modified from: https://github.com/tradingview/lightweight-charts/blob/f13a3c1f3fefcace9d4da5b97c1638009298b3c8/plugin-examples/src/plugins/stacked-area-series + * Modifications are called out with comments. + */ + +import { BitmapCoordinatesRenderingScope, CanvasRenderingTarget2D } from 'fancy-canvas' +import { + ICustomSeriesPaneRenderer, + PaneRendererCustomData, + PriceToCoordinateConverter, + Range, + Time, +} from 'lightweight-charts' + +import { StackedAreaData } from './data' +import { StackedAreaSeriesOptions } from './options' + +interface Position { + x: number + y: number +} + +interface LinePathData { + path: Path2D + first: Position + last: Position +} + +interface StackedAreaBarItem { + x: number + ys: number[] +} + +function cumulativeBuildUp(arr: number[]): number[] { + let sum = 0 + return arr.map((value) => { + const newValue = sum + value + sum = newValue + return newValue + }) +} + +export class StackedAreaSeriesRenderer implements ICustomSeriesPaneRenderer { + _data: PaneRendererCustomData | null = null + _options: StackedAreaSeriesOptions | null = null + + draw(target: CanvasRenderingTarget2D, priceConverter: PriceToCoordinateConverter): void { + target.useBitmapCoordinateSpace((scope) => this._drawImpl(scope, priceConverter)) + } + + update(data: PaneRendererCustomData, options: StackedAreaSeriesOptions): void { + this._data = data + this._options = options + } + + _drawImpl(renderingScope: BitmapCoordinatesRenderingScope, priceToCoordinate: PriceToCoordinateConverter): void { + if ( + this._data === null || + this._data.bars.length === 0 || + this._data.visibleRange === null || + this._options === null + ) { + return + } + + const ctx = renderingScope.context + + const options = this._options + const bars: StackedAreaBarItem[] = this._data.bars.map((bar) => { + return { + x: bar.x, + ys: cumulativeBuildUp(bar.originalData.values).map((value) => priceToCoordinate(value) ?? 0), + } + }) + const zeroY = priceToCoordinate(0) ?? 0 + const { linesMeshed, hoverInfo } = this._createLinePaths( + bars, + this._data.visibleRange, + renderingScope, + zeroY * renderingScope.verticalPixelRatio, + options.hoveredLogicalIndex + ) + + const areaPaths = this._createAreas(linesMeshed) + + const colorsCount = options.colors.length + areaPaths.forEach((areaPath, index) => { + // Modification: determine area fill opacity based on number of lines and hover state + if (areaPaths.length === 1) { + ctx.globalAlpha = 0.12 // single-line charts have low opacity fill + } else { + const hasHoveredIndex = options.hoveredLogicalIndex !== undefined && options.hoveredLogicalIndex !== -1 + ctx.globalAlpha = hasHoveredIndex ? 0.24 : 1 // multi-line charts have lower opacity on hover, otherwise full opacity + } + + ctx.fillStyle = options.colors[index % colorsCount] + ctx.fill(areaPath) + }) + + ctx.globalAlpha = 1 + + ctx.lineWidth = options.lineWidth * renderingScope.verticalPixelRatio + ctx.lineJoin = 'round' + + linesMeshed.toReversed().forEach((linePath, index) => { + const unreversedIndex = linesMeshed.length - index + const color = options.colors[unreversedIndex % colorsCount] + ctx.strokeStyle = color + ctx.fillStyle = color + + // Bottom line is just the x-axis, which should not be drawn + if (index !== linesMeshed.length - 1) { + // Line rendering: + ctx.beginPath() + ctx.strokeStyle = color + ctx.stroke(linePath.path) + } + + // Modification: Draws a glyph where lines intersect with the crosshair + const hoverY = hoverInfo.points[index - 1] + + // Glyph rendering: + ctx.globalCompositeOperation = 'destination-out' // This mode allows removing a portion of the drawn line from the canvas + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 5 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Cuts a hole out of the line where part of the glyph should be rendered + + ctx.globalCompositeOperation = 'source-over' // Resets to default mode + + ctx.beginPath() + + ctx.arc(hoverInfo.x, hoverY, 3 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws innermost portion of glyph + + ctx.globalAlpha = 0.2 + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 8 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws middle portion of glyph + + ctx.globalAlpha = 0.3 + ctx.beginPath() + ctx.arc(hoverInfo.x, hoverY, 12 * renderingScope.verticalPixelRatio, 0, 2 * Math.PI) + ctx.fill() // Draws outer portion of glyph + + ctx.globalAlpha = 1 + }) + } + + /** Builds canvas line paths based on input data */ + _createLinePaths( + bars: StackedAreaBarItem[], + visibleRange: Range, + renderingScope: BitmapCoordinatesRenderingScope, + zeroY: number, + hoveredIndex?: number | null + ) { + const { horizontalPixelRatio, verticalPixelRatio } = renderingScope + const oddLines: LinePathData[] = [] + const evenLines: LinePathData[] = [] + let firstBar = true + + // Modification: tracks and returns coordinates of where a glyph should be rendered for each line when a crosshair is drawn + const hoverInfo = { points: new Array(), x: 0 } + + // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart + for (let i = visibleRange.from - 1; i < visibleRange.to + 1; i++) { + if (i >= bars.length || i < 0) continue + + const stack = bars[i] + let lineIndex = 0 + stack.ys.forEach((yMedia, index) => { + if (index % 2 !== 0) { + return // only doing odd at the moment + } + + const x = stack.x * horizontalPixelRatio + const y = yMedia * verticalPixelRatio + + if (i === hoveredIndex) { + hoverInfo.points[index] = y + hoverInfo.x = x + } + + if (firstBar) { + oddLines[lineIndex] = { + path: new Path2D(), + first: { x, y }, + last: { x, y }, + } + oddLines[lineIndex].path.moveTo(x, y) + } else { + oddLines[lineIndex].path.lineTo(x, y) + oddLines[lineIndex].last.x = x + oddLines[lineIndex].last.y = y + } + lineIndex += 1 + }) + firstBar = false + } + firstBar = true + // Modification: updated loop to include one point above and below the visible range to ensure the line is drawn to edges of chart + for (let i = visibleRange.to + 1; i >= visibleRange.from - 1; i--) { + if (i >= bars.length || i < 0) continue + const stack = bars[i] + let lineIndex = 0 + stack.ys.forEach((yMedia, index) => { + if (index % 2 === 0) { + return // only doing even at the moment + } + + const x = stack.x * horizontalPixelRatio + const y = yMedia * verticalPixelRatio + + if (i === hoveredIndex) { + hoverInfo.points[index] = y + hoverInfo.x = x + } + + if (firstBar) { + evenLines[lineIndex] = { + path: new Path2D(), + first: { x, y }, + last: { x, y }, + } + evenLines[lineIndex].path.moveTo(x, y) + } else { + evenLines[lineIndex].path.lineTo(x, y) + evenLines[lineIndex].last.x = x + evenLines[lineIndex].last.y = y + } + lineIndex += 1 + }) + firstBar = false + } + + const baseLine = { + path: new Path2D(), + first: { x: oddLines[0].last.x, y: zeroY }, + last: { x: oddLines[0].first.x, y: zeroY }, + } + baseLine.path.moveTo(oddLines[0].last.x, zeroY) + baseLine.path.lineTo(oddLines[0].first.x, zeroY) + const linesMeshed: LinePathData[] = [baseLine] + for (let i = 0; i < oddLines.length; i++) { + linesMeshed.push(oddLines[i]) + if (i < evenLines.length) { + linesMeshed.push(evenLines[i]) + } + } + + return { linesMeshed, hoverInfo } + } + + /** Builds canvas area paths to fill under lines */ + _createAreas(linesMeshed: LinePathData[]): Path2D[] { + const areas: Path2D[] = [] + for (let i = 1; i < linesMeshed.length; i++) { + const areaPath = new Path2D(linesMeshed[i - 1].path) + areaPath.lineTo(linesMeshed[i].first.x, linesMeshed[i].first.y) + areaPath.addPath(linesMeshed[i].path) + areaPath.lineTo(linesMeshed[i - 1].first.x, linesMeshed[i - 1].first.y) + areaPath.closePath() + areas.push(areaPath) + } + return areas + } +} diff --git a/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/stacked-area-series.ts b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/stacked-area-series.ts new file mode 100644 index 0000000..85d09c2 --- /dev/null +++ b/apps/web/src/components/Charts/StackedLineChart/stacked-area-series/stacked-area-series.ts @@ -0,0 +1,42 @@ +/* Copied from: https://github.com/tradingview/lightweight-charts/blob/f13a3c1f3fefcace9d4da5b97c1638009298b3c8/plugin-examples/src/plugins/stacked-area-series */ +import { + CustomSeriesPricePlotValues, + ICustomSeriesPaneView, + PaneRendererCustomData, + Time, + WhitespaceData, +} from 'lightweight-charts' + +import { StackedAreaData } from './data' +import { defaultOptions, StackedAreaSeriesOptions } from './options' +import { StackedAreaSeriesRenderer } from './renderer' + +export class StackedAreaSeries + implements ICustomSeriesPaneView +{ + _renderer: StackedAreaSeriesRenderer + + constructor() { + this._renderer = new StackedAreaSeriesRenderer() + } + + priceValueBuilder(plotRow: TData): CustomSeriesPricePlotValues { + return [0, plotRow.values.reduce((previousValue, currentValue) => previousValue + currentValue, 0)] + } + + isWhitespace(data: TData | WhitespaceData): data is WhitespaceData { + return !(data as Partial).values?.length + } + + renderer(): StackedAreaSeriesRenderer { + return this._renderer + } + + update(data: PaneRendererCustomData, options: StackedAreaSeriesOptions): void { + this._renderer.update(data, options) + } + + defaultOptions() { + return defaultOptions + } +} diff --git a/apps/web/src/components/Charts/TimeSelector.tsx b/apps/web/src/components/Charts/TimeSelector.tsx new file mode 100644 index 0000000..618d97a --- /dev/null +++ b/apps/web/src/components/Charts/TimeSelector.tsx @@ -0,0 +1,93 @@ +import { DISPLAYS, ORDERED_TIMES } from 'components/Tokens/TokenTable/TimeSelector' +import { TimePeriod } from 'graphql/data/util' +import { atom } from 'jotai' +import { useAtomValue } from 'jotai/utils' +import styled from 'styled-components' + +import { MEDIUM_MEDIA_BREAKPOINT } from '../Tokens/constants' + +export const refitChartContentAtom = atom<(() => void) | undefined>(undefined) +const DEFAULT_TIME_SELECTOR_OPTIONS = ORDERED_TIMES.map((time: TimePeriod) => ({ time, display: DISPLAYS[time] })) + +const TimeOptionsWrapper = styled.div` + display: flex; + width: 100%; + justify-content: flex-end; +` + +const TimeOptionsContainer = styled.div` + display: flex; + justify-content: flex-end; + margin-top: 4px; + gap: 4px; + border-radius: 16px; + height: 40px; + padding: 4px; + width: fit-content; + + @media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) { + width: 100%; + justify-content: space-between; + border: none; + } +` +const TimeButton = styled.button<{ active: boolean }>` + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: ${({ theme, active }) => (active ? theme.surface3 : 'transparent')}; + font-weight: 535; + font-size: 14px; + padding: 6px 12px; + border-radius: 12px; + line-height: 16px; + border: none; + cursor: pointer; + color: ${({ theme, active }) => (active ? theme.neutral1 : theme.neutral2)}; + transition-duration: ${({ theme }) => theme.transition.duration.fast}; + :hover { + ${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`} + } +` + +interface TimePeriodSelectorOption { + time: TimePeriod // Value to be selected/stored, used as default display value + display: string // Value to be displayed +} + +export default function TimePeriodSelector({ + options = DEFAULT_TIME_SELECTOR_OPTIONS, + timePeriod, + onChangeTimePeriod, + className, +}: { + options?: TimePeriodSelectorOption[] + timePeriod: TimePeriod + onChangeTimePeriod: (t: TimePeriod) => void + className?: string +}) { + const refitChartContent = useAtomValue(refitChartContentAtom) + + return ( + + + {options.map(({ time, display }) => ( + { + if (timePeriod === time) { + refitChartContent?.() + } else { + onChangeTimePeriod(time) + } + }} + > + {display} + + ))} + + + ) +} diff --git a/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx b/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx new file mode 100644 index 0000000..d864e1d --- /dev/null +++ b/apps/web/src/components/Charts/VolumeChart/CrosshairHighlightPrimitive.tsx @@ -0,0 +1,235 @@ +/** + * Copied from https://github.com/tradingview/lightweight-charts/blob/master/plugin-examples/src/plugins/highlight-bar-crosshair/highlight-bar-crosshair.ts. + * Modifications are called out with comments. + */ +import { CanvasRenderingTarget2D } from 'fancy-canvas' +import { + CrosshairMode, + ISeriesPrimitive, + ISeriesPrimitivePaneRenderer, + ISeriesPrimitivePaneView, + MouseEventParams, + SeriesAttachedParameter, + Time, +} from 'lightweight-charts' + +interface BitmapPositionLength { + /** coordinate for use with a bitmap rendering scope */ + position: number + /** length for use with a bitmap rendering scope */ + length: number +} + +function centreOffset(lineBitmapWidth: number): number { + return Math.floor(lineBitmapWidth * 0.5) +} + +/** + * Calculates the bitmap position for an item with a desired length (height or width), and centred according to + * an position coordinate defined in media sizing. + * @param positionMedia - position coordinate for the bar (in media coordinates) + * @param pixelRatio - pixel ratio. Either horizontal for x positions, or vertical for y positions + * @param desiredWidthMedia - desired width (in media coordinates) + * @returns Position of of the start point and length dimension. + */ +export function positionsLine( + positionMedia: number, + pixelRatio: number, + desiredWidthMedia = 1, + widthIsBitmap?: boolean +): BitmapPositionLength { + const scaledPosition = Math.round(pixelRatio * positionMedia) + const lineBitmapWidth = widthIsBitmap ? desiredWidthMedia : Math.round(desiredWidthMedia * pixelRatio) + const offset = centreOffset(lineBitmapWidth) + const position = scaledPosition - offset + return { position, length: lineBitmapWidth } +} + +interface CrosshairHighlightData { + x: number + visible: boolean + barSpacing: number +} + +interface HighlightBarCrosshairOptions { + color: string + crosshairYPosition: number +} + +class CrosshairHighlightPaneRenderer implements ISeriesPrimitivePaneRenderer { + _data: CrosshairHighlightData & HighlightBarCrosshairOptions + + constructor(data: CrosshairHighlightData & HighlightBarCrosshairOptions) { + this._data = data + } + + draw(target: CanvasRenderingTarget2D) { + if (!this._data.visible) return + target.useBitmapCoordinateSpace((scope) => { + const ctx = scope.context + const crosshairPos = positionsLine(this._data.x, scope.horizontalPixelRatio, Math.max(1, this._data.barSpacing)) + ctx.fillStyle = this._data.color + const crosshairYPosition = this._data.crosshairYPosition * scope.verticalPixelRatio + + // Modification: increase space between bars + const margin = + Math.min( + Math.max(scope.horizontalPixelRatio, crosshairPos.length), + this._data.barSpacing * scope.horizontalPixelRatio + ) * 0.035 + const crosshairXPosition = crosshairPos.position + margin + + // Modification: use rounded rectangle + ctx.beginPath() + ctx.roundRect( + crosshairXPosition, + crosshairYPosition, + crosshairPos.length, + scope.bitmapSize.height - crosshairYPosition, + 9 + ) + ctx.fill() + + // Modification: lower opacity of all content outside the highlight bar + ctx.globalCompositeOperation = 'destination-out' + ctx.globalAlpha = 0.76 // results in existing items being left with 0.24 opacity + ctx.fillStyle = 'black' + + // lower opacity to left of highlight bar + ctx.fillRect(0, crosshairYPosition, crosshairXPosition, scope.bitmapSize.height - crosshairYPosition) + // lower opacity to right of highlight bar + ctx.fillRect( + crosshairXPosition + crosshairPos.length, + crosshairYPosition, + scope.bitmapSize.width - (crosshairXPosition + crosshairPos.length), + scope.bitmapSize.height - crosshairYPosition + ) + // reset global settings + ctx.globalAlpha = 1 + ctx.globalCompositeOperation = 'source-over' + }) + } +} + +class CrosshairHighlightPaneView implements ISeriesPrimitivePaneView { + _data: CrosshairHighlightData + _options: HighlightBarCrosshairOptions + constructor(data: CrosshairHighlightData, options: HighlightBarCrosshairOptions) { + this._data = data + this._options = options + } + + update(data: CrosshairHighlightData, options: HighlightBarCrosshairOptions): void { + this._data = data + this._options = options + } + + renderer(): ISeriesPrimitivePaneRenderer | null { + return new CrosshairHighlightPaneRenderer({ ...this._data, ...this._options }) + } +} + +export class CrosshairHighlightPrimitive implements ISeriesPrimitive
} isOpen={false} onToggle={noop} button={Button}> + Body + + ) + expect(screen.queryByText('Body')).not.toBeVisible() + }) + + it('renders children when open', () => { + render( + Header
} isOpen={true} onToggle={noop} button={Button}> + Body + + ) + expect(screen.queryByText('Body')).toBeVisible() + }) + + it('calls `onToggle` when button is pressed', () => { + const onToggle = jest.fn() + render( + Header} isOpen={false} onToggle={onToggle} button={Button}> + Body + + ) + + const button = screen.getByText('Button') + + fireEvent.click(button) + expect(onToggle).toHaveBeenCalled() + }) +}) diff --git a/apps/web/src/components/Expand/index.tsx b/apps/web/src/components/Expand/index.tsx new file mode 100644 index 0000000..0ab4e25 --- /dev/null +++ b/apps/web/src/components/Expand/index.tsx @@ -0,0 +1,53 @@ +import AnimatedDropdown from 'components/AnimatedDropdown' +import Column from 'components/Column' +import { PropsWithChildren, ReactElement } from 'react' +import { ChevronDown } from 'react-feather' +import styled from 'styled-components' + +import Row, { RowBetween } from '../Row' + +const ButtonContainer = styled(Row)` + cursor: pointer; + justify-content: flex-end; + width: unset; +` + +const ExpandIcon = styled(ChevronDown)<{ $isOpen: boolean }>` + color: ${({ theme }) => theme.neutral2}; + transform: ${({ $isOpen }) => ($isOpen ? 'rotate(180deg)' : 'rotate(0deg)')}; + transition: transform ${({ theme }) => theme.transition.duration.medium}; +` + +const Content = styled(Column)` + padding-top: ${({ theme }) => theme.grids.md}; +` + +export default function Expand({ + header, + button, + children, + testId, + isOpen, + onToggle, +}: PropsWithChildren<{ + header: ReactElement + button: ReactElement + testId?: string + isOpen: boolean + onToggle: () => void +}>) { + return ( + + + {header} + + {button} + + + + + {children} + + + ) +} diff --git a/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx new file mode 100644 index 0000000..0018860 --- /dev/null +++ b/apps/web/src/components/FeatureFlagModal/FeatureFlagModal.tsx @@ -0,0 +1,417 @@ +import { ChainId } from '@uniswap/sdk-core' +import Column from 'components/Column' +import { BaseVariant, FeatureFlag, featureFlagSettings, useUpdateConfig, useUpdateFlag } from 'featureFlags' +import { DynamicConfigName } from 'featureFlags/dynamicConfig' +import { useQuickRouteChains } from 'featureFlags/dynamicConfig/quickRouteChains' +import { useCurrencyConversionFlag } from 'featureFlags/flags/currencyConversion' +import { useEip6963EnabledFlag } from 'featureFlags/flags/eip6963' +import { useFallbackProviderEnabledFlag } from 'featureFlags/flags/fallbackProvider' +import { useGatewayDNSUpdateAllEnabledFlag, useGatewayDNSUpdateEnabledFlag } from 'featureFlags/flags/gatewayDNSUpdate' +import { useGqlTokenListsEnabledFlag } from 'featureFlags/flags/gqlTokenLists' +import { useExitAnimationFlag } from 'featureFlags/flags/landingPageV2' +import { useLimitsEnabledFlag } from 'featureFlags/flags/limits' +import { useLimitsFeeesEnabledFlag } from 'featureFlags/flags/limitsFees' +import { useMultichainUXFlag } from 'featureFlags/flags/multichainUx' +import { + useOutageBannerArbitrum, + useOutageBannerOptimism, + useOutageBannerPolygon, +} from 'featureFlags/flags/outageBanner' +import { useQuickRouteMainnetFlag } from 'featureFlags/flags/quickRouteMainnet' +import { useRealtimeFlag } from 'featureFlags/flags/realtime' +import { useSendEnabledFlag } from 'featureFlags/flags/send' +import { TraceJsonRpcVariant, useTraceJsonRpcFlag } from 'featureFlags/flags/traceJsonRpc' +import { useUniconV2Flag } from 'featureFlags/flags/uniconV2' +import { useUniswapXSyntheticQuoteFlag } from 'featureFlags/flags/uniswapXUseSyntheticQuote' +import { useV2EverywhereFlag } from 'featureFlags/flags/v2Everywhere' +import { useUpdateAtom } from 'jotai/utils' +import { Children, PropsWithChildren, ReactElement, ReactNode, useCallback, useState } from 'react' +import { X } from 'react-feather' +import { useModalIsOpen, useToggleFeatureFlags } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { Z_INDEX } from 'theme/zIndex' + +const StyledModal = styled.div` + position: fixed; + display: flex; + left: 50%; + top: 50vh; + transform: translate(-50%, -50%); + width: 400px; + height: fit-content; + color: ${({ theme }) => theme.neutral1}; + font-size: 18px; + padding: 20px 0px; + background-color: ${({ theme }) => theme.surface2}; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + z-index: ${Z_INDEX.modal}; + flex-direction: column; + gap: 8px; + border: 1px solid ${({ theme }) => theme.surface3}; + + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + max-height: 80vh; + } +` + +function Modal({ open, children }: { open: boolean; children: ReactNode }) { + return open ? {children} : null +} + +const FlagsColumn = styled(Column)` + max-height: 600px; + overflow-y: auto; + padding: 0px 20px; + + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + max-height: unset; + } +` + +const Row = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 0px; +` + +const CloseButton = styled.button` + cursor: pointer; + background: transparent; + border: none; + color: ${({ theme }) => theme.neutral1}; +` + +const ToggleButton = styled.button` + cursor: pointer; + background: transparent; + border: none; + color: ${({ theme }) => theme.neutral1}; +` + +const Header = styled(Row)` + padding: 0px 16px 8px; + font-weight: 535; + font-size: 16px; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; +` +const FlagName = styled.span` + font-size: 16px; + line-height: 20px; + color: ${({ theme }) => theme.neutral1}; +` +const FlagGroupName = styled.span` + font-size: 20px; + line-height: 24px; + color: ${({ theme }) => theme.neutral1}; + font-weight: 535; +` +const FlagDescription = styled.span` + font-size: 12px; + line-height: 16px; + color: ${({ theme }) => theme.neutral2}; + display: flex; + align-items: center; +` +const FlagVariantSelection = styled.select` + border-radius: 12px; + padding: 8px; + background: ${({ theme }) => theme.surface3}; + font-weight: 535; + font-size: 16px; + border: none; + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + + :hover { + background: ${({ theme }) => theme.surface3}; + } +` + +const FlagInfo = styled.div` + display: flex; + flex-direction: column; + padding-left: 8px; +` + +const SaveButton = styled.button` + border-radius: 12px; + padding: 8px; + margin: 0px 20px; + background: ${({ theme }) => theme.surface3}; + font-weight: 535; + font-size: 16px; + border: none; + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + + :hover { + background: ${({ theme }) => theme.surface3}; + } +` + +function Variant({ option }: { option: string }) { + return +} + +interface FeatureFlagProps { + variant: Record + featureFlag: FeatureFlag + value: string + label: string +} + +function FeatureFlagGroup({ name, children }: PropsWithChildren<{ name: string }>) { + // type FeatureFlagOption = { props: FeatureFlagProps } + const togglableOptions = Children.toArray(children) + .filter>( + (child): child is ReactElement => + child instanceof Object && 'type' in child && child.type === FeatureFlagOption + ) + .map(({ props }) => props) + .filter(({ variant }) => { + const values = Object.values(variant) + return values.includes(BaseVariant.Control) && values.includes(BaseVariant.Enabled) + }) + + const setFeatureFlags = useUpdateAtom(featureFlagSettings) + const allEnabled = togglableOptions.every(({ value }) => value === BaseVariant.Enabled) + const onToggle = useCallback(() => { + setFeatureFlags((flags) => ({ + ...flags, + ...togglableOptions.reduce( + (flags, { featureFlag }) => ({ + ...flags, + [featureFlag]: allEnabled ? BaseVariant.Control : BaseVariant.Enabled, + }), + {} + ), + })) + }, [allEnabled, setFeatureFlags, togglableOptions]) + + return ( + <> + + {name} + {allEnabled ? 'Disable' : 'Enable'} group + + {children} + + ) +} + +function FeatureFlagOption({ value, variant, featureFlag, label }: FeatureFlagProps) { + const updateFlag = useUpdateFlag() + const [count, setCount] = useState(0) + + return ( + + + {featureFlag} + {label} + + { + updateFlag(featureFlag, e.target.value) + setCount(count + 1) + }} + value={value} + > + {Object.values(variant).map((variant) => ( + + ))} + + + ) +} + +interface DynamicConfigDropdownProps { + configName: DynamicConfigName + label: string + options: any[] + selected: any[] + parser: (opt: string) => any +} + +function DynamicConfigDropdown({ configName, label, options, selected, parser }: DynamicConfigDropdownProps) { + const updateConfig = useUpdateConfig() + const handleSelectChange = (e: React.ChangeEvent) => { + const selectedValues = Array.from(e.target.selectedOptions, (opt) => parser(opt.value)) + // Saved to atom as { [configName]: { [configName]: values } } to match Statsig return format + updateConfig(configName, { [configName]: selectedValues }) + } + return ( + + + {configName} + {label} + + + + ) +} + +export default function FeatureFlagModal() { + const open = useModalIsOpen(ApplicationModal.FEATURE_FLAGS) + const toggleModal = useToggleFeatureFlags() + + return ( + +
+ Feature Flag Settings + + + +
+ + + + + + + + + + + + + + + + + + + !isNaN(Number(v))) as ChainId[]} + parser={Number.parseInt} + configName={DynamicConfigName.quickRouteChains} + label="Enable quick routes for these chains" + /> + + + + + + + + + + + + + + window.location.reload()}>Reload +
+ ) +} diff --git a/apps/web/src/components/FeeSelector/FeeOption.tsx b/apps/web/src/components/FeeSelector/FeeOption.tsx new file mode 100644 index 0000000..42842e4 --- /dev/null +++ b/apps/web/src/components/FeeSelector/FeeOption.tsx @@ -0,0 +1,53 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import { ButtonRadioChecked } from 'components/Button' +import { AutoColumn } from 'components/Column' +import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' +import { PoolState } from 'hooks/usePools' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +import { FeeTierPercentageBadge } from './FeeTierPercentageBadge' +import { FEE_AMOUNT_DETAIL } from './shared' + +const ResponsiveText = styled(ThemedText.DeprecatedLabel)` + line-height: 16px; + font-size: 14px; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + font-size: 12px; + line-height: 12px; + `}; +` + +interface FeeOptionProps { + feeAmount: FeeAmount + active: boolean + distributions: ReturnType['distributions'] + poolState: PoolState + onClick: () => void +} + +export function FeeOption({ feeAmount, active, poolState, distributions, onClick }: FeeOptionProps) { + const { formatDelta } = useFormatter() + + return ( + + + + + {formatDelta(parseFloat(FEE_AMOUNT_DETAIL[feeAmount].label))} + + + {FEE_AMOUNT_DETAIL[feeAmount].description} + + + + {distributions && ( + + )} + + + ) +} diff --git a/apps/web/src/components/FeeSelector/FeeTierPercentageBadge.tsx b/apps/web/src/components/FeeSelector/FeeTierPercentageBadge.tsx new file mode 100644 index 0000000..0f06e04 --- /dev/null +++ b/apps/web/src/components/FeeSelector/FeeTierPercentageBadge.tsx @@ -0,0 +1,30 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import Badge from 'components/Badge' +import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' +import { PoolState } from 'hooks/usePools' +import { ThemedText } from 'theme/components' + +export function FeeTierPercentageBadge({ + feeAmount, + distributions, + poolState, +}: { + feeAmount: FeeAmount + distributions: ReturnType['distributions'] + poolState: PoolState +}) { + return ( + + + {!distributions || poolState === PoolState.NOT_EXISTS || poolState === PoolState.INVALID ? ( + Not created + ) : distributions[feeAmount] !== undefined ? ( + {distributions[feeAmount]?.toFixed(0)}% select + ) : ( + No data + )} + + + ) +} diff --git a/apps/web/src/components/FeeSelector/index.tsx b/apps/web/src/components/FeeSelector/index.tsx new file mode 100644 index 0000000..367784a --- /dev/null +++ b/apps/web/src/components/FeeSelector/index.tsx @@ -0,0 +1,210 @@ +import { Trans } from '@lingui/macro' +import { FeePoolSelectAction, LiquidityEventName } from '@uniswap/analytics-events' +import { Currency } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' +import { sendAnalyticsEvent, useTrace } from 'analytics' +import { ButtonGray } from 'components/Button' +import Card from 'components/Card' +import { AutoColumn } from 'components/Column' +import { RowBetween } from 'components/Row' +import { useFeeTierDistribution } from 'hooks/useFeeTierDistribution' +import { PoolState, usePools } from 'hooks/usePools' +import usePrevious from 'hooks/usePrevious' +import { DynamicSection } from 'pages/AddLiquidity/styled' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Box } from 'rebass' +import styled, { keyframes } from 'styled-components' +import { ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +import { FeeOption } from './FeeOption' +import { FeeTierPercentageBadge } from './FeeTierPercentageBadge' +import { FEE_AMOUNT_DETAIL } from './shared' + +const pulse = (color: string) => keyframes` + 0% { + box-shadow: 0 0 0 0 ${color}; + } + + 70% { + box-shadow: 0 0 0 2px ${color}; + } + + 100% { + box-shadow: 0 0 0 0 ${color}; + } +` +const FocusedOutlineCard = styled(Card)<{ pulsing: boolean }>` + border: 1px solid ${({ theme }) => theme.surface3}; + animation: ${({ pulsing, theme }) => pulsing && pulse(theme.accent1)} 0.6s linear; + align-self: center; +` + +const Select = styled.div` + align-items: flex-start; + display: grid; + grid-auto-flow: column; + grid-gap: 8px; +` + +export default function FeeSelector({ + disabled = false, + feeAmount, + handleFeePoolSelect, + currencyA, + currencyB, +}: { + disabled?: boolean + feeAmount?: FeeAmount + handleFeePoolSelect: (feeAmount: FeeAmount) => void + currencyA?: Currency + currencyB?: Currency +}) { + const { chainId } = useWeb3React() + const trace = useTrace() + const { formatDelta } = useFormatter() + + const { isLoading, isError, largestUsageFeeTier, distributions } = useFeeTierDistribution(currencyA, currencyB) + + // get pool data on-chain for latest states + const pools = usePools([ + [currencyA, currencyB, FeeAmount.LOWEST], + [currencyA, currencyB, FeeAmount.LOW], + [currencyA, currencyB, FeeAmount.MEDIUM], + [currencyA, currencyB, FeeAmount.HIGH], + ]) + + const poolsByFeeTier: Record = useMemo( + () => + pools.reduce( + (acc, [curPoolState, curPool]) => { + acc = { + ...acc, + ...{ [curPool?.fee as FeeAmount]: curPoolState }, + } + return acc + }, + { + // default all states to NOT_EXISTS + [FeeAmount.LOWEST]: PoolState.NOT_EXISTS, + [FeeAmount.LOW]: PoolState.NOT_EXISTS, + [FeeAmount.MEDIUM]: PoolState.NOT_EXISTS, + [FeeAmount.HIGH]: PoolState.NOT_EXISTS, + } + ), + [pools] + ) + + const [showOptions, setShowOptions] = useState(false) + const [pulsing, setPulsing] = useState(false) + + const previousFeeAmount = usePrevious(feeAmount) + + const recommended = useRef(false) + + const handleFeePoolSelectWithEvent = useCallback( + (fee: FeeAmount) => { + sendAnalyticsEvent(LiquidityEventName.SELECT_LIQUIDITY_POOL_FEE_TIER, { + action: FeePoolSelectAction.MANUAL, + ...trace, + }) + handleFeePoolSelect(fee) + }, + [handleFeePoolSelect, trace] + ) + + useEffect(() => { + if (feeAmount || isLoading || isError) { + return + } + + if (!largestUsageFeeTier) { + // cannot recommend, open options + setShowOptions(true) + } else { + setShowOptions(false) + + recommended.current = true + sendAnalyticsEvent(LiquidityEventName.SELECT_LIQUIDITY_POOL_FEE_TIER, { + action: FeePoolSelectAction.RECOMMENDED, + ...trace, + }) + + handleFeePoolSelect(largestUsageFeeTier) + } + }, [feeAmount, isLoading, isError, largestUsageFeeTier, handleFeePoolSelect, trace]) + + useEffect(() => { + setShowOptions(isError) + }, [isError]) + + useEffect(() => { + if (feeAmount && previousFeeAmount !== feeAmount) { + setPulsing(true) + } + }, [previousFeeAmount, feeAmount]) + + return ( + + + setPulsing(false)}> + + + {!feeAmount ? ( + <> + + Fee tier + + + The % you will earn in fees. + + + ) : ( + <> + + {formatDelta(parseFloat(FEE_AMOUNT_DETAIL[feeAmount].label))} fee tier + + {distributions && ( + + + + )} + + )} + + + setShowOptions(!showOptions)} width="auto" padding="4px" $borderRadius="6px"> + {showOptions ? Hide : Edit} + + + + + {chainId && showOptions && ( + + )} + + + ) +} diff --git a/apps/web/src/components/FeeSelector/shared.tsx b/apps/web/src/components/FeeSelector/shared.tsx new file mode 100644 index 0000000..b2ca763 --- /dev/null +++ b/apps/web/src/components/FeeSelector/shared.tsx @@ -0,0 +1,41 @@ +import { Trans } from '@lingui/macro' +import { ChainId, SUPPORTED_CHAINS } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import type { ReactNode } from 'react' + +export const FEE_AMOUNT_DETAIL: Record< + FeeAmount, + { label: string; description: ReactNode; supportedChains: readonly ChainId[] } +> = { + [FeeAmount.LOWEST]: { + label: '0.01', + description: Best for very stable pairs., + supportedChains: [ + ChainId.ARBITRUM_ONE, + ChainId.BNB, + ChainId.CELO, + ChainId.CELO_ALFAJORES, + ChainId.MAINNET, + ChainId.OPTIMISM, + ChainId.POLYGON, + ChainId.POLYGON_MUMBAI, + ChainId.AVALANCHE, + ChainId.BASE, + ], + }, + [FeeAmount.LOW]: { + label: '0.05', + description: Best for stable pairs., + supportedChains: SUPPORTED_CHAINS, + }, + [FeeAmount.MEDIUM]: { + label: '0.3', + description: Best for most pairs., + supportedChains: SUPPORTED_CHAINS, + }, + [FeeAmount.HIGH]: { + label: '1', + description: Best for exotic pairs., + supportedChains: SUPPORTED_CHAINS, + }, +} diff --git a/apps/web/src/components/FiatOnrampModal/constants.ts b/apps/web/src/components/FiatOnrampModal/constants.ts new file mode 100644 index 0000000..db0ea4b --- /dev/null +++ b/apps/web/src/components/FiatOnrampModal/constants.ts @@ -0,0 +1,17 @@ +export const MOONPAY_SUPPORTED_CURRENCY_CODES = [ + 'eth', + 'eth_arbitrum', + 'eth_optimism', + 'eth_polygon', + 'weth', + 'wbtc', + 'matic_polygon', + 'polygon', + 'usdc_arbitrum', + 'usdc_optimism', + 'usdc_polygon', + 'usdc', + 'usdt', +] as const + +export type MoonpaySupportedCurrencyCode = (typeof MOONPAY_SUPPORTED_CURRENCY_CODES)[number] diff --git a/apps/web/src/components/FiatOnrampModal/index.tsx b/apps/web/src/components/FiatOnrampModal/index.tsx new file mode 100644 index 0000000..126f34b --- /dev/null +++ b/apps/web/src/components/FiatOnrampModal/index.tsx @@ -0,0 +1,164 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { useCallback, useEffect, useState } from 'react' +import { useHref } from 'react-router-dom' +import { useCloseModal, useModalIsOpen } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled, { useTheme } from 'styled-components' +import { CustomLightSpinner, ThemedText } from 'theme/components' +import { useIsDarkMode } from 'theme/components/ThemeToggle' + +import Circle from '../../assets/images/blue-loader.svg' +import Modal from '../Modal' +import { MOONPAY_SUPPORTED_CURRENCY_CODES } from './constants' +import { getDefaultCurrencyCode, parsePathParts } from './utils' + +const MOONPAY_DARK_BACKGROUND = '#1c1c1e' +const Wrapper = styled.div<{ isDarkMode: boolean }>` + // #1c1c1e is the background color for the darkmode moonpay iframe as of 2/16/2023 + background-color: ${({ isDarkMode, theme }) => (isDarkMode ? MOONPAY_DARK_BACKGROUND : theme.white)}; + border-radius: 20px; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + display: flex; + flex-flow: column nowrap; + margin: 0; + flex: 1 1; + min-width: 375px; + position: relative; + width: 100%; +` + +const ErrorText = styled(ThemedText.BodyPrimary)` + color: ${({ theme }) => theme.critical}; + margin: auto !important; + text-align: center; + width: 90%; +` +const StyledIframe = styled.iframe<{ isDarkMode: boolean }>` + // #1c1c1e is the background color for the darkmode moonpay iframe as of 2/16/2023 + background-color: ${({ isDarkMode, theme }) => (isDarkMode ? MOONPAY_DARK_BACKGROUND : theme.white)}; + border-radius: 12px; + bottom: 0; + left: 0; + height: calc(100% - 16px); + margin: 8px; + padding: 0; + position: absolute; + right: 0; + top: 0; + width: calc(100% - 16px); +` +const StyledSpinner = styled(CustomLightSpinner)` + bottom: 0; + left: 0; + margin: auto; + position: absolute; + right: 0; + top: 0; +` + +const MoonpayTextWrapper = styled.div` + position: absolute; + bottom: 20px; + margin: auto; + left: 0; + right: 0; + width: 100%; + text-align: center; +` + +export default function FiatOnrampModal() { + const { account } = useWeb3React() + const theme = useTheme() + const isDarkMode = useIsDarkMode() + const closeModal = useCloseModal() + const fiatOnrampModalOpen = useModalIsOpen(ApplicationModal.FIAT_ONRAMP) + + const { network, tokenAddress } = parsePathParts(location.pathname) + + const [signedIframeUrl, setSignedIframeUrl] = useState(null) + const [error, setError] = useState(null) + const [loading, setLoading] = useState(false) + + const swapUrl = useHref('/swap') + + const fetchSignedIframeUrl = useCallback(async () => { + if (!account) { + setError('Please connect an account before making a purchase.') + return + } + setLoading(true) + setError(null) + try { + const signedIframeUrlFetchEndpoint = process.env.REACT_APP_MOONPAY_LINK as string + const res = await fetch(signedIframeUrlFetchEndpoint, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ + theme: isDarkMode ? 'dark' : 'light', + colorCode: theme.accent1, + defaultCurrencyCode: getDefaultCurrencyCode(tokenAddress, network), + redirectUrl: swapUrl, + walletAddresses: JSON.stringify( + MOONPAY_SUPPORTED_CURRENCY_CODES.reduce( + (acc, currencyCode) => ({ + ...acc, + [currencyCode]: account, + }), + {} + ) + ), + }), + }) + const { url } = await res.json() + setSignedIframeUrl(url) + } catch (e) { + console.log('there was an error fetching the link', e) + setError(e.toString()) + } finally { + setLoading(false) + } + }, [account, isDarkMode, network, swapUrl, theme.accent1, tokenAddress]) + + useEffect(() => { + fetchSignedIframeUrl() + }, [fetchSignedIframeUrl]) + + return ( + <> + closeModal()} height={80 /* vh */}> + + {error ? ( + <> + + MoonPay fiat on-ramp iframe + + + Something went wrong! +
+ {error} +
+ + ) : loading ? ( + + ) : ( + + )} +
+ + + Powered by MoonPay USA LLC + + +
+ + ) +} diff --git a/apps/web/src/components/FiatOnrampModal/utils.test.ts b/apps/web/src/components/FiatOnrampModal/utils.test.ts new file mode 100644 index 0000000..deddced --- /dev/null +++ b/apps/web/src/components/FiatOnrampModal/utils.test.ts @@ -0,0 +1,79 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { + MATIC_MAINNET, + NATIVE_CHAIN_ID, + USDC_ARBITRUM, + USDC_MAINNET, + USDC_OPTIMISM, + USDC_POLYGON, + USDT, + WBTC, + WETH_POLYGON, +} from 'constants/tokens' + +import { getDefaultCurrencyCode, parsePathParts } from './utils' + +describe('getDefaultCurrencyCode', () => { + it('NATIVE/arbitrum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, 'arbitrum')).toBe('eth_arbitrum') + }) + it('NATIVE/optimism should return the correct currency code', () => { + expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, 'optimism')).toBe('eth_optimism') + }) + it('WETH/polygon should return the correct currency code', () => { + expect(getDefaultCurrencyCode(WETH_POLYGON.address, 'polygon')).toBe('eth_polygon') + }) + it('WETH/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(WETH9[ChainId.MAINNET].address, 'ethereum')).toBe('weth') + }) + it('WBTC/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(WBTC.address, 'ethereum')).toBe('wbtc') + }) + it('NATIVE/polygon should return the correct currency code', () => { + expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, 'polygon')).toBe('matic_polygon') + }) + it('MATIC/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(MATIC_MAINNET.address, 'ethereum')).toBe('polygon') + }) + it('USDC/arbitrum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(USDC_ARBITRUM.address, 'arbitrum')).toBe('usdc_arbitrum') + }) + it('USDC/optimism should return the correct currency code', () => { + expect(getDefaultCurrencyCode(USDC_OPTIMISM.address, 'optimism')).toBe('usdc_optimism') + }) + it('USDC/polygon should return the correct currency code', () => { + expect(getDefaultCurrencyCode(USDC_POLYGON.address, 'polygon')).toBe('usdc_polygon') + }) + it('native/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(NATIVE_CHAIN_ID, 'ethereum')).toBe('eth') + }) + it('usdc/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(USDC_MAINNET.address, 'ethereum')).toBe('usdc') + }) + it('usdt/ethereum should return the correct currency code', () => { + expect(getDefaultCurrencyCode(USDT.address, 'ethereum')).toBe('usdt') + }) + it('chain/token mismatch should default to eth', () => { + expect(getDefaultCurrencyCode(USDC_ARBITRUM.address, 'ethereum')).toBe('eth') + expect(getDefaultCurrencyCode(USDC_OPTIMISM.address, 'ethereum')).toBe('eth') + expect(getDefaultCurrencyCode(USDC_POLYGON.address, 'ethereum')).toBe('eth') + expect(getDefaultCurrencyCode(MATIC_MAINNET.address, 'arbitrum')).toBe('eth') + }) +}) + +describe('parseLocation', () => { + it('should parse the URL correctly', () => { + expect(parsePathParts('/tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({ + network: 'ethereum', + tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + }) + expect(parsePathParts('tokens/ethereum/0x2260fac5e5542a773aa44fbcfedf7c193bc2c599')).toEqual({ + network: 'ethereum', + tokenAddress: '0x2260fac5e5542a773aa44fbcfedf7c193bc2c599', + }) + expect(parsePathParts('/swap')).toEqual({ + network: undefined, + tokenAddress: undefined, + }) + }) +}) diff --git a/apps/web/src/components/FiatOnrampModal/utils.ts b/apps/web/src/components/FiatOnrampModal/utils.ts new file mode 100644 index 0000000..953ccb9 --- /dev/null +++ b/apps/web/src/components/FiatOnrampModal/utils.ts @@ -0,0 +1,72 @@ +import { ChainId, WETH9 } from '@uniswap/sdk-core' +import { + MATIC_MAINNET, + USDC_ARBITRUM, + USDC_MAINNET, + USDC_OPTIMISM, + USDC_POLYGON, + USDT, + WBTC, + WETH_POLYGON, +} from 'constants/tokens' +import { Chain } from 'graphql/data/__generated__/types-and-hooks' +import { validateUrlChainParam } from 'graphql/data/util' + +import { MoonpaySupportedCurrencyCode } from './constants' + +type MoonpaySupportedChain = Chain.Ethereum | Chain.Polygon | Chain.Arbitrum | Chain.Optimism +const moonPaySupportedChains = [Chain.Ethereum, Chain.Polygon, Chain.Arbitrum, Chain.Optimism] + +const CURRENCY_CODES: { + [K in MoonpaySupportedChain]: { + [key: string]: MoonpaySupportedCurrencyCode + native: MoonpaySupportedCurrencyCode + } +} = { + [Chain.Ethereum]: { + [WETH9[ChainId.MAINNET]?.address.toLowerCase()]: 'weth', + [USDC_MAINNET.address.toLowerCase()]: 'usdc', + [USDT.address.toLowerCase()]: 'usdt', + [WBTC.address.toLowerCase()]: 'wbtc', + [MATIC_MAINNET.address.toLowerCase()]: 'polygon', + native: 'eth', + }, + [Chain.Arbitrum]: { + [USDC_ARBITRUM.address.toLowerCase()]: 'usdc_arbitrum', + native: 'eth_arbitrum', + }, + [Chain.Optimism]: { + [USDC_OPTIMISM.address.toLowerCase()]: 'usdc_optimism', + native: 'eth_optimism', + }, + [Chain.Polygon]: { + [USDC_POLYGON.address.toLowerCase()]: 'usdc_polygon', + [WETH_POLYGON.address.toLowerCase()]: 'eth_polygon', + native: 'matic_polygon', + }, +} + +export function getDefaultCurrencyCode( + address: string | undefined, + chainName: string | undefined +): MoonpaySupportedCurrencyCode { + const chain = validateUrlChainParam(chainName) + if (!address || !chain) return 'eth' + if (moonPaySupportedChains.includes(chain)) { + const code = CURRENCY_CODES[chain as MoonpaySupportedChain]?.[address.toLowerCase()] + return code ?? 'eth' + } + return 'eth' +} + +/** + * You should use useParams() from react-router-dom instead of this function if possible. + * This function is only used in the case where we need to parse the path outside the scope of the router. + */ +export function parsePathParts(pathname: string): { network?: string; tokenAddress?: string } { + const pathParts = pathname.split('/') + // Matches the /tokens// path. + const network = pathParts.length > 2 ? pathParts[pathParts.length - 2] : undefined + const tokenAddress = pathParts.length > 2 ? pathParts[pathParts.length - 1] : undefined + return { network, tokenAddress } +} diff --git a/apps/web/src/components/FormattedCurrencyAmount/index.tsx b/apps/web/src/components/FormattedCurrencyAmount/index.tsx new file mode 100644 index 0000000..31f2be5 --- /dev/null +++ b/apps/web/src/components/FormattedCurrencyAmount/index.tsx @@ -0,0 +1,22 @@ +import { Currency, CurrencyAmount, Fraction } from '@uniswap/sdk-core' +import JSBI from 'jsbi' + +const CURRENCY_AMOUNT_MIN = new Fraction(JSBI.BigInt(1), JSBI.BigInt(1000000)) + +export default function FormattedCurrencyAmount({ + currencyAmount, + significantDigits = 4, +}: { + currencyAmount: CurrencyAmount + significantDigits?: number +}) { + return ( + <> + {currencyAmount.equalTo(JSBI.BigInt(0)) + ? '0' + : currencyAmount.greaterThan(CURRENCY_AMOUNT_MIN) + ? currencyAmount.toSignificant(significantDigits) + : `<${CURRENCY_AMOUNT_MIN.toSignificant(1)}`} + + ) +} diff --git a/apps/web/src/components/HoverInlineText/index.tsx b/apps/web/src/components/HoverInlineText/index.tsx new file mode 100644 index 0000000..68c28ce --- /dev/null +++ b/apps/web/src/components/HoverInlineText/index.tsx @@ -0,0 +1,77 @@ +import Tooltip from 'components/Tooltip' +import { useState } from 'react' +import styled from 'styled-components' + +const TextWrapper = styled.span<{ + margin: boolean + link?: boolean + fontSize?: string + adjustSize?: boolean + textColor?: string +}>` + margin-left: ${({ margin }) => margin && '4px'}; + font-size: ${({ fontSize }) => fontSize ?? 'inherit'}; + + @media screen and (max-width: 600px) { + font-size: ${({ adjustSize }) => adjustSize && '12px'}; + } +` + +const HoverInlineText = ({ + text, + maxCharacters = 20, + margin = false, + adjustSize = false, + fontSize, + textColor, + link, + ...rest +}: { + text?: string + maxCharacters?: number + margin?: boolean + adjustSize?: boolean + fontSize?: string + textColor?: string + link?: boolean +}) => { + const [showHover, setShowHover] = useState(false) + + if (!text) { + return + } + + if (text.length > maxCharacters) { + return ( + + setShowHover(true)} + onMouseLeave={() => setShowHover(false)} + margin={margin} + adjustSize={adjustSize} + textColor={textColor} + link={link} + fontSize={fontSize} + {...rest} + > + {' ' + text.slice(0, maxCharacters - 1) + '...'} + + + ) + } + + return ( + + {text} + + ) +} + +export default HoverInlineText diff --git a/apps/web/src/components/Icons/AlertTriangleFilled.tsx b/apps/web/src/components/Icons/AlertTriangleFilled.tsx new file mode 100644 index 0000000..b3329ea --- /dev/null +++ b/apps/web/src/components/Icons/AlertTriangleFilled.tsx @@ -0,0 +1,18 @@ +import { useTheme } from 'styled-components' + +import { StyledSVG } from './shared' + +export default function AlertTriangleFilled({ size = '16px', ...rest }: { size?: string; [k: string]: any }) { + const theme = useTheme() + return ( + + + + ) +} diff --git a/apps/web/src/components/Icons/AppleLogo.tsx b/apps/web/src/components/Icons/AppleLogo.tsx new file mode 100644 index 0000000..e984e2a --- /dev/null +++ b/apps/web/src/components/Icons/AppleLogo.tsx @@ -0,0 +1,23 @@ +import { ComponentProps } from 'react' + +export function AppleLogo(props: ComponentProps<'svg'>) { + return ( + + + + + ) +} diff --git a/apps/web/src/components/Icons/ArrowChangeDown.tsx b/apps/web/src/components/Icons/ArrowChangeDown.tsx new file mode 100644 index 0000000..1871e0e --- /dev/null +++ b/apps/web/src/components/Icons/ArrowChangeDown.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const ArrowChangeDown = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/ArrowChangeUp.tsx b/apps/web/src/components/Icons/ArrowChangeUp.tsx new file mode 100644 index 0000000..f7487e9 --- /dev/null +++ b/apps/web/src/components/Icons/ArrowChangeUp.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const ArrowChangeUp = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/CheckMark.tsx b/apps/web/src/components/Icons/CheckMark.tsx new file mode 100644 index 0000000..6c36930 --- /dev/null +++ b/apps/web/src/components/Icons/CheckMark.tsx @@ -0,0 +1,22 @@ +type SVGProps = React.SVGProps & { + fill?: string + height?: string | number + width?: string | number +} + +export const CheckMark = (props: SVGProps) => ( + + + +) diff --git a/apps/web/src/components/Icons/CreditCard.tsx b/apps/web/src/components/Icons/CreditCard.tsx new file mode 100644 index 0000000..67e8081 --- /dev/null +++ b/apps/web/src/components/Icons/CreditCard.tsx @@ -0,0 +1,10 @@ +export function CreditCardIcon() { + return ( + + + + ) +} diff --git a/apps/web/src/components/Icons/ENS.tsx b/apps/web/src/components/Icons/ENS.tsx new file mode 100644 index 0000000..67b8ac0 --- /dev/null +++ b/apps/web/src/components/Icons/ENS.tsx @@ -0,0 +1,17 @@ +import { ComponentProps } from 'react' + +export function ENS(props: ComponentProps<'svg'>) { + return ( + + + + + + + + + ) +} diff --git a/apps/web/src/components/Icons/EnvelopeHeart.tsx b/apps/web/src/components/Icons/EnvelopeHeart.tsx new file mode 100644 index 0000000..face36b --- /dev/null +++ b/apps/web/src/components/Icons/EnvelopeHeart.tsx @@ -0,0 +1,10 @@ +export function EnvelopeHeartIcon() { + return ( + + + + ) +} diff --git a/apps/web/src/components/Icons/EthMini.tsx b/apps/web/src/components/Icons/EthMini.tsx new file mode 100644 index 0000000..796a1d0 --- /dev/null +++ b/apps/web/src/components/Icons/EthMini.tsx @@ -0,0 +1,14 @@ +export function EthMini() { + return ( + + + + + ) +} diff --git a/apps/web/src/components/Icons/Etherscan.tsx b/apps/web/src/components/Icons/Etherscan.tsx new file mode 100644 index 0000000..f6d7136 --- /dev/null +++ b/apps/web/src/components/Icons/Etherscan.tsx @@ -0,0 +1,22 @@ +import { ComponentProps } from 'react' + +export const EtherscanLogo = (props: ComponentProps<'svg'>) => ( + + + + +) diff --git a/apps/web/src/components/Icons/ExplorerIcon.tsx b/apps/web/src/components/Icons/ExplorerIcon.tsx new file mode 100644 index 0000000..8480a71 --- /dev/null +++ b/apps/web/src/components/Icons/ExplorerIcon.tsx @@ -0,0 +1,15 @@ +import { ComponentProps } from 'react' + +export const ExplorerIcon = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Gas.tsx b/apps/web/src/components/Icons/Gas.tsx new file mode 100644 index 0000000..512df53 --- /dev/null +++ b/apps/web/src/components/Icons/Gas.tsx @@ -0,0 +1,12 @@ +import { ComponentProps } from 'react' + +export const Gas = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Globe.tsx b/apps/web/src/components/Icons/Globe.tsx new file mode 100644 index 0000000..b17778a --- /dev/null +++ b/apps/web/src/components/Icons/Globe.tsx @@ -0,0 +1,18 @@ +import { ComponentProps } from 'react' + +export const Globe = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/GooglePlayStoreLogo.tsx b/apps/web/src/components/Icons/GooglePlayStoreLogo.tsx new file mode 100644 index 0000000..e2f20c0 --- /dev/null +++ b/apps/web/src/components/Icons/GooglePlayStoreLogo.tsx @@ -0,0 +1,29 @@ +export function GooglePlayStoreLogo() { + return ( + + + + + + + ) +} diff --git a/apps/web/src/components/Icons/Images.tsx b/apps/web/src/components/Icons/Images.tsx new file mode 100644 index 0000000..8fdc1f5 --- /dev/null +++ b/apps/web/src/components/Icons/Images.tsx @@ -0,0 +1,10 @@ +export function ImagesIcon() { + return ( + + + + ) +} diff --git a/apps/web/src/components/Icons/LoadingSpinner.tsx b/apps/web/src/components/Icons/LoadingSpinner.tsx new file mode 100644 index 0000000..de47cc5 --- /dev/null +++ b/apps/web/src/components/Icons/LoadingSpinner.tsx @@ -0,0 +1,84 @@ +import { useTheme } from 'styled-components' + +import { StyledRotatingSVG, StyledSVG } from './shared' + +/** + * Takes in custom size and stroke for circle color, default to primary color as fill, + * need ...rest for layered styles on top + */ +export default function Loader({ + size = '16px', + stroke, + strokeWidth, + ...rest +}: { + size?: string + stroke?: string + strokeWidth?: number + [k: string]: any +}) { + const theme = useTheme() + return ( + + + + ) +} + +export function LoaderV2() { + const theme = useTheme() + return ( + + + + + + + + + ) +} + +export function LoaderV3({ size = '16px', color, ...rest }: { size?: string; color?: string; [k: string]: any }) { + const theme = useTheme() + return ( + + + + + ) +} diff --git a/apps/web/src/components/Icons/Pool.tsx b/apps/web/src/components/Icons/Pool.tsx new file mode 100644 index 0000000..0cf0930 --- /dev/null +++ b/apps/web/src/components/Icons/Pool.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const Pool = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Power.tsx b/apps/web/src/components/Icons/Power.tsx new file mode 100644 index 0000000..a698869 --- /dev/null +++ b/apps/web/src/components/Icons/Power.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const Power = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/ReverseArrow.tsx b/apps/web/src/components/Icons/ReverseArrow.tsx new file mode 100644 index 0000000..2915a14 --- /dev/null +++ b/apps/web/src/components/Icons/ReverseArrow.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const ReverseArrow = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Search.tsx b/apps/web/src/components/Icons/Search.tsx new file mode 100644 index 0000000..4f53922 --- /dev/null +++ b/apps/web/src/components/Icons/Search.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const Search = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Settings.tsx b/apps/web/src/components/Icons/Settings.tsx new file mode 100644 index 0000000..afc25d6 --- /dev/null +++ b/apps/web/src/components/Icons/Settings.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const Settings = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Share.tsx b/apps/web/src/components/Icons/Share.tsx new file mode 100644 index 0000000..935815a --- /dev/null +++ b/apps/web/src/components/Icons/Share.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const Share = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/Sign.tsx b/apps/web/src/components/Icons/Sign.tsx new file mode 100644 index 0000000..b77741a --- /dev/null +++ b/apps/web/src/components/Icons/Sign.tsx @@ -0,0 +1,10 @@ +export const Sign = () => ( + + + + + +) diff --git a/apps/web/src/components/Icons/Swap.tsx b/apps/web/src/components/Icons/Swap.tsx new file mode 100644 index 0000000..f26fd97 --- /dev/null +++ b/apps/web/src/components/Icons/Swap.tsx @@ -0,0 +1,12 @@ +export const Swap = () => ( + + + + +) diff --git a/apps/web/src/components/Icons/TimeForward.tsx b/apps/web/src/components/Icons/TimeForward.tsx new file mode 100644 index 0000000..d07c867 --- /dev/null +++ b/apps/web/src/components/Icons/TimeForward.tsx @@ -0,0 +1,13 @@ +import { useTheme } from 'styled-components' + +export const TimeForwardIcon = () => { + const theme = useTheme() + return ( + + + + ) +} diff --git a/apps/web/src/components/Icons/TwitterX.tsx b/apps/web/src/components/Icons/TwitterX.tsx new file mode 100644 index 0000000..c87b09f --- /dev/null +++ b/apps/web/src/components/Icons/TwitterX.tsx @@ -0,0 +1,18 @@ +import { ComponentProps } from 'react' + +export const TwitterXLogo = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/UserIcon.tsx b/apps/web/src/components/Icons/UserIcon.tsx new file mode 100644 index 0000000..985b6ca --- /dev/null +++ b/apps/web/src/components/Icons/UserIcon.tsx @@ -0,0 +1,10 @@ +import { ComponentProps } from 'react' + +export const UserIcon = (props: ComponentProps<'svg'>) => ( + + + +) diff --git a/apps/web/src/components/Icons/shared.tsx b/apps/web/src/components/Icons/shared.tsx new file mode 100644 index 0000000..f8df59b --- /dev/null +++ b/apps/web/src/components/Icons/shared.tsx @@ -0,0 +1,28 @@ +import styled, { css, keyframes } from 'styled-components' + +const rotateAnimation = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +` + +const RotationStyle = css` + animation: 2s ${rotateAnimation} linear infinite; +` + +export const StyledSVG = styled.svg<{ size: string; stroke?: string; fill?: string }>` + height: ${({ size }) => size}; + width: ${({ size }) => size}; + path { + stroke: ${({ stroke }) => stroke}; + background: ${({ theme }) => theme.neutral2}; + fill: ${({ fill }) => fill}; + } +` + +export const StyledRotatingSVG = styled(StyledSVG)` + ${RotationStyle} +` diff --git a/apps/web/src/components/Identicon/StatusIcon.test.tsx b/apps/web/src/components/Identicon/StatusIcon.test.tsx new file mode 100644 index 0000000..9c4ef1a --- /dev/null +++ b/apps/web/src/components/Identicon/StatusIcon.test.tsx @@ -0,0 +1,49 @@ +import { useWeb3React } from '@web3-react/core' +import { deprecatedInjectedConnection } from 'connection' +import { mocked } from 'test-utils/mocked' +import { render } from 'test-utils/render' + +import StatusIcon from './StatusIcon' + +const ACCOUNT = '0x0' + +jest.mock('../../hooks/useSocksBalance', () => ({ + useHasSocks: () => true, +})) + +describe('StatusIcon', () => { + describe('with no account', () => { + it('renders children in correct order', () => { + const component = render() + expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() + }) + + it('renders without mini icons', () => { + const component = render( + + ) + expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0) + }) + }) + + describe('with account', () => { + beforeEach(() => { + mocked(useWeb3React).mockReturnValue({ + account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db', + isActive: true, + } as ReturnType) + }) + + it('renders children in correct order', () => { + const component = render() + expect(component.getByTestId('StatusIconRoot')).toMatchSnapshot() + }) + + it('renders without mini icons', () => { + const component = render( + + ) + expect(component.getByTestId('StatusIconRoot').children.length).toEqual(0) + }) + }) +}) diff --git a/apps/web/src/components/Identicon/StatusIcon.tsx b/apps/web/src/components/Identicon/StatusIcon.tsx new file mode 100644 index 0000000..13c3519 --- /dev/null +++ b/apps/web/src/components/Identicon/StatusIcon.tsx @@ -0,0 +1,144 @@ +import { useWeb3React } from '@web3-react/core' +import { Unicon } from 'components/Unicon' +import { Connection } from 'connection/types' +import { useUniconV2Flag } from 'featureFlags/flags/uniconV2' +import useENSAvatar from 'hooks/useENSAvatar' +import styled from 'styled-components' +import { useIsDarkMode } from 'theme/components/ThemeToggle' +import { flexColumnNoWrap } from 'theme/styles' +import { UniconV2 } from 'ui/src' +import { getWalletMeta } from 'utils/walletMeta' + +import { navSearchInputVisibleSize } from 'hooks/useScreenSize' +import { useUnitagByAddressWithoutFlag } from 'uniswap/src/features/unitags/hooksWithoutFlags' +import sockImg from '../../assets/svg/socks.svg' +import { useHasSocks } from '../../hooks/useSocksBalance' +import Identicon from '../Identicon' + +export const IconWrapper = styled.div<{ size?: number }>` + position: relative; + ${flexColumnNoWrap}; + align-items: center; + justify-content: center; + @media only screen and (min-width: ${navSearchInputVisibleSize}px) { + margin-right: 4px; + } + & > img, + span { + height: ${({ size }) => (size ? size + 'px' : '32px')}; + width: ${({ size }) => (size ? size + 'px' : '32px')}; + } + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium` + align-items: flex-end; + `}; +` + +const MiniIconContainer = styled.div<{ side: 'left' | 'right' }>` + position: absolute; + display: flex; + justify-content: center; + align-items: center; + width: 16px; + height: 16px; + bottom: -4px; + ${({ side }) => `${side === 'left' ? 'left' : 'right'}: -4px;`} + border-radius: 50%; + outline: 2px solid ${({ theme }) => theme.surface1}; + outline-offset: -0.1px; + background-color: ${({ theme }) => theme.surface1}; + overflow: hidden; + @supports (overflow: clip) { + overflow: clip; + } +` + +const UnigramContainer = styled.div<{ $iconSize: number }>` + height: ${({ $iconSize: iconSize }) => `${iconSize}px`}; + width: ${({ $iconSize: iconSize }) => `${iconSize}px`}; + border-radius: 50%; + background-color: ${({ theme }) => theme.surface3}; + font-size: initial; +` + +const Unigram = styled.img` + height: inherit; + width: inherit; + border-radius: inherit; + object-fit: cover; +` + +const MiniImg = styled.img` + width: 16px; + height: 16px; +` + +const Socks = () => { + return ( + + + + ) +} + +const MiniWalletIcon = ({ connection, side }: { connection: Connection; side: 'left' | 'right' }) => { + const isDarkMode = useIsDarkMode() + const { provider } = useWeb3React() + + const providerInfo = connection.getProviderInfo(isDarkMode) + + // Uses icon from wallet meta when available, otherwise uses icon from connection + const icon = (provider && getWalletMeta(provider)?.icons?.[0]) ?? providerInfo.icon + + return ( + + + + ) +} + +const MainWalletIcon = ({ account, connection, size }: { account: string; connection: Connection; size: number }) => { + const { unitag } = useUnitagByAddressWithoutFlag(account, Boolean(account)) + const { avatar } = useENSAvatar(account ?? undefined) + const uniconV2Enabled = useUniconV2Flag() + + if (!account) return null + + if (unitag && unitag.metadata?.avatar) { + return ( + + + + ) + } + + const hasIdenticon = avatar || connection.getProviderInfo().name === 'MetaMask' + return hasIdenticon ? ( + + ) : uniconV2Enabled ? ( + + ) : ( + + ) +} + +export default function StatusIcon({ + account, + connection, + size = 16, + showMiniIcons = true, +}: { + account: string + connection: Connection + size?: number + showMiniIcons?: boolean +}) { + const hasSocks = useHasSocks() + + return ( + + + {showMiniIcons && } + {hasSocks && showMiniIcons && } + + ) +} diff --git a/apps/web/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap b/apps/web/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap new file mode 100644 index 0000000..0321590 --- /dev/null +++ b/apps/web/src/components/Identicon/__snapshots__/StatusIcon.test.tsx.snap @@ -0,0 +1,267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StatusIcon with account renders children in correct order 1`] = ` +.c0 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c0 > img, +.c0 span { + height: 16px; + width: 16px; +} + +.c1 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 16px; + height: 16px; + bottom: -4px; + right: -4px; + border-radius: 50%; + outline: 2px solid #FFFFFF; + outline-offset: -0.1px; + background-color: #FFFFFF; + overflow: hidden; +} + +.c3 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 16px; + height: 16px; + bottom: -4px; + left: -4px; + border-radius: 50%; + outline: 2px solid #FFFFFF; + outline-offset: -0.1px; + background-color: #FFFFFF; + overflow: hidden; +} + +.c2 { + width: 16px; + height: 16px; +} + +@media only screen and (min-width:1100px) { + .c0 { + margin-right: 4px; + } +} + +@media (max-width:960px) { + .c0 { + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + } +} + +@supports (overflow:clip) { + .c1 { + overflow: clip; + } +} + +@supports (overflow:clip) { + .c3 { + overflow: clip; + } +} + +
+
+ Install MetaMask icon +
+
+ +
+
+`; + +exports[`StatusIcon with no account renders children in correct order 1`] = ` +.c0 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c0 > img, +.c0 span { + height: 16px; + width: 16px; +} + +.c1 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 16px; + height: 16px; + bottom: -4px; + right: -4px; + border-radius: 50%; + outline: 2px solid #FFFFFF; + outline-offset: -0.1px; + background-color: #FFFFFF; + overflow: hidden; +} + +.c3 { + position: absolute; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 16px; + height: 16px; + bottom: -4px; + left: -4px; + border-radius: 50%; + outline: 2px solid #FFFFFF; + outline-offset: -0.1px; + background-color: #FFFFFF; + overflow: hidden; +} + +.c2 { + width: 16px; + height: 16px; +} + +@media only screen and (min-width:1100px) { + .c0 { + margin-right: 4px; + } +} + +@media (max-width:960px) { + .c0 { + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; + } +} + +@supports (overflow:clip) { + .c1 { + overflow: clip; + } +} + +@supports (overflow:clip) { + .c3 { + overflow: clip; + } +} + +
+
+ Install MetaMask icon +
+
+ +
+
+`; diff --git a/apps/web/src/components/Identicon/index.tsx b/apps/web/src/components/Identicon/index.tsx new file mode 100644 index 0000000..5ff4271 --- /dev/null +++ b/apps/web/src/components/Identicon/index.tsx @@ -0,0 +1,53 @@ +import jazzicon from '@metamask/jazzicon' +import useENSAvatar from 'hooks/useENSAvatar' +import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react' +import styled from 'styled-components' + +const StyledIdenticon = styled.div<{ iconSize: number }>` + height: ${({ iconSize }) => `${iconSize}px`}; + width: ${({ iconSize }) => `${iconSize}px`}; + border-radius: 50%; + background-color: ${({ theme }) => theme.surface3}; + font-size: initial; +` + +const StyledAvatar = styled.img` + height: inherit; + width: inherit; + border-radius: inherit; +` + +export default function Identicon({ account, size }: { account: string; size?: number }) { + const { avatar } = useENSAvatar(account ?? undefined) + const [fetchable, setFetchable] = useState(true) + const iconSize = size ?? 24 + + const icon = useMemo(() => account && jazzicon(iconSize, parseInt(account.slice(2, 10), 16)), [account, iconSize]) + const iconRef = useRef(null) + useLayoutEffect(() => { + const current = iconRef.current + if (icon) { + current?.appendChild(icon) + return () => { + try { + current?.removeChild(icon) + } catch (e) { + console.error('Avatar icon not found') + } + } + } + return + }, [icon, iconRef]) + + const handleError = useCallback(() => setFetchable(false), []) + + return ( + + {avatar && fetchable ? ( + + ) : ( + + )} + + ) +} diff --git a/apps/web/src/components/InputStepCounter/InputStepCounter.tsx b/apps/web/src/components/InputStepCounter/InputStepCounter.tsx new file mode 100644 index 0000000..1699491 --- /dev/null +++ b/apps/web/src/components/InputStepCounter/InputStepCounter.tsx @@ -0,0 +1,185 @@ +import { Trans } from '@lingui/macro' +import { FeeAmount } from '@uniswap/v3-sdk' +import { ButtonGray } from 'components/Button' +import { OutlineCard } from 'components/Card' +import { AutoColumn } from 'components/Column' +import { ReactNode, useCallback, useEffect, useState } from 'react' +import { Minus, Plus } from 'react-feather' +import styled, { keyframes } from 'styled-components' +import { ThemedText } from 'theme/components' + +import { Input as NumericalInput } from '../NumericalInput' + +const pulse = (color: string) => keyframes` + 0% { + box-shadow: 0 0 0 0 ${color}; + } + + 70% { + box-shadow: 0 0 0 2px ${color}; + } + + 100% { + box-shadow: 0 0 0 0 ${color}; + } +` + +const InputRow = styled.div` + display: flex; +` + +const SmallButton = styled(ButtonGray)` + border-radius: 8px; + padding: 4px; +` + +const FocusedOutlineCard = styled(OutlineCard)<{ active?: boolean; pulsing?: boolean }>` + border-color: ${({ active, theme }) => active && theme.deprecated_stateOverlayPressed}; + padding: 12px; + animation: ${({ pulsing, theme }) => pulsing && pulse(theme.accent1)} 0.8s linear; +` + +const StyledInput = styled(NumericalInput)<{ usePercent?: boolean }>` + background-color: transparent; + font-weight: 535; + text-align: left; + width: 100%; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + font-size: 16px; + `}; +` + +const InputColumn = styled(AutoColumn)` + width: 100%; +` + +const InputTitle = styled(ThemedText.DeprecatedSmall)` + color: ${({ theme }) => theme.neutral2}; + font-size: 12px; + font-weight: 535; +` + +const ButtonLabel = styled(ThemedText.DeprecatedWhite)<{ disabled: boolean }>` + color: ${({ theme, disabled }) => (disabled ? theme.neutral2 : theme.neutral1)} !important; + display: flex; +` + +interface StepCounterProps { + value: string + onUserInput: (value: string) => void + decrement: () => string + increment: () => string + decrementDisabled?: boolean + incrementDisabled?: boolean + feeAmount?: FeeAmount + label?: string + width?: string + locked?: boolean // disable input + title: ReactNode + tokenA?: string + tokenB?: string +} + +const StepCounter = ({ + value, + decrement, + increment, + decrementDisabled = false, + incrementDisabled = false, + width, + locked, + onUserInput, + title, + tokenA, + tokenB, +}: StepCounterProps) => { + // for focus state, styled components doesnt let you select input parent container + const [active, setActive] = useState(false) + + // let user type value and only update parent value on blur + const [localValue, setLocalValue] = useState('') + const [useLocalValue, setUseLocalValue] = useState(false) + + // animation if parent value updates local value + const [pulsing, setPulsing] = useState(false) + + const handleOnFocus = () => { + setUseLocalValue(true) + setActive(true) + } + + const handleOnBlur = useCallback(() => { + setUseLocalValue(false) + setActive(false) + onUserInput(localValue) // trigger update on parent value + }, [localValue, onUserInput]) + + // for button clicks + const handleDecrement = useCallback(() => { + setUseLocalValue(false) + onUserInput(decrement()) + }, [decrement, onUserInput]) + + const handleIncrement = useCallback(() => { + setUseLocalValue(false) + onUserInput(increment()) + }, [increment, onUserInput]) + + useEffect(() => { + if (localValue !== value && !useLocalValue) { + setTimeout(() => { + setLocalValue(value) // reset local value to match parent + setPulsing(true) // trigger animation + setTimeout(function () { + setPulsing(false) + }, 1800) + }, 0) + } + }, [localValue, useLocalValue, value]) + + return ( + + + + + {title} + + { + setLocalValue(val) + }} + /> + + + {tokenB} per {tokenA} + + + + + + {!locked && ( + + + + + + )} + {!locked && ( + + + + + + )} + + + + ) +} + +export default StepCounter diff --git a/apps/web/src/components/LiquidityChartRangeInput/Area.tsx b/apps/web/src/components/LiquidityChartRangeInput/Area.tsx new file mode 100644 index 0000000..7a0d36f --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/Area.tsx @@ -0,0 +1,47 @@ +import { area, curveStepAfter, ScaleLinear } from 'd3' +import { useMemo } from 'react' +import styled from 'styled-components' + +import { ChartEntry } from './types' + +const Path = styled.path<{ fill?: string }>` + opacity: 0.5; + stroke: ${({ fill, theme }) => fill ?? theme.accent1}; + fill: ${({ fill, theme }) => fill ?? theme.accent1}; +` + +export const Area = ({ + series, + xScale, + yScale, + xValue, + yValue, + fill, +}: { + series: ChartEntry[] + xScale: ScaleLinear + yScale: ScaleLinear + xValue: (d: ChartEntry) => number + yValue: (d: ChartEntry) => number + fill?: string +}) => + useMemo( + () => ( + xScale(xValue(d as ChartEntry))) + .y1((d: unknown) => yScale(yValue(d as ChartEntry))) + .y0(yScale(0))( + series.filter((d) => { + const value = xScale(xValue(d)) + return value > 0 && value <= window.innerWidth + }) as Iterable<[number, number]> + ) ?? undefined + } + /> + ), + [fill, series, xScale, xValue, yScale, yValue] + ) diff --git a/apps/web/src/components/LiquidityChartRangeInput/AxisBottom.tsx b/apps/web/src/components/LiquidityChartRangeInput/AxisBottom.tsx new file mode 100644 index 0000000..dd27a9a --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/AxisBottom.tsx @@ -0,0 +1,43 @@ +import { NumberValue, ScaleLinear, axisBottom, Axis as d3Axis, select } from 'd3' +import { useMemo } from 'react' +import styled from 'styled-components' + +const StyledGroup = styled.g` + line { + display: none; + } + + text { + color: ${({ theme }) => theme.neutral2}; + transform: translateY(5px); + } +` + +const Axis = ({ axisGenerator }: { axisGenerator: d3Axis }) => { + const axisRef = (axis: SVGGElement) => { + axis && + select(axis) + .call(axisGenerator) + .call((g) => g.select('.domain').remove()) + } + + return +} + +export const AxisBottom = ({ + xScale, + innerHeight, + offset = 0, +}: { + xScale: ScaleLinear + innerHeight: number + offset?: number +}) => + useMemo( + () => ( + + + + ), + [innerHeight, offset, xScale] + ) diff --git a/apps/web/src/components/LiquidityChartRangeInput/Brush.tsx b/apps/web/src/components/LiquidityChartRangeInput/Brush.tsx new file mode 100644 index 0000000..659f8bf --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/Brush.tsx @@ -0,0 +1,273 @@ +import { brushHandleAccentPath, brushHandlePath, OffScreenHandle } from 'components/LiquidityChartRangeInput/svg' +import { BrushBehavior, brushX, D3BrushEvent, ScaleLinear, select } from 'd3' +import usePrevious from 'hooks/usePrevious' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import styled from 'styled-components' + +const Handle = styled.path<{ color: string }>` + cursor: ew-resize; + pointer-events: none; + + stroke-width: 3; + stroke: ${({ color }) => color}; + fill: ${({ color }) => color}; +` + +const HandleAccent = styled.path` + cursor: ew-resize; + pointer-events: none; + + stroke-width: 1.5; + stroke: ${({ theme }) => theme.white}; + opacity: ${({ theme }) => theme.opacity.hover}; +` + +const LabelGroup = styled.g<{ visible: boolean }>` + opacity: ${({ visible }) => (visible ? '1' : '0')}; + transition: opacity 300ms; +` + +const TooltipBackground = styled.rect` + fill: ${({ theme }) => theme.surface3}; +` + +const Tooltip = styled.text` + text-anchor: middle; + font-size: 13px; + fill: ${({ theme }) => theme.neutral1}; +` + +// flips the handles draggers when close to the container edges +const FLIP_HANDLE_THRESHOLD_PX = 20 + +// margin to prevent tick snapping from putting the brush off screen +const BRUSH_EXTENT_MARGIN_PX = 2 + +/** + * Returns true if every element in `a` maps to the + * same pixel coordinate as elements in `b` + */ +const compare = (a: [number, number], b: [number, number], xScale: ScaleLinear): boolean => { + // normalize pixels to 1 decimals + const aNorm = a.map((x) => xScale(x).toFixed(1)) + const bNorm = b.map((x) => xScale(x).toFixed(1)) + return aNorm.every((v, i) => v === bNorm[i]) +} + +export const Brush = ({ + id, + xScale, + interactive, + brushLabelValue, + brushExtent, + setBrushExtent, + innerWidth, + innerHeight, + westHandleColor, + eastHandleColor, +}: { + id: string + xScale: ScaleLinear + interactive: boolean + brushLabelValue: (d: 'w' | 'e', x: number) => string + brushExtent: [number, number] + setBrushExtent: (extent: [number, number], mode: string | undefined) => void + innerWidth: number + innerHeight: number + westHandleColor: string + eastHandleColor: string +}) => { + const brushRef = useRef(null) + const brushBehavior = useRef | null>(null) + + // only used to drag the handles on brush for performance + const [localBrushExtent, setLocalBrushExtent] = useState<[number, number] | null>(brushExtent) + const [showLabels, setShowLabels] = useState(false) + const [hovering, setHovering] = useState(false) + + const previousBrushExtent = usePrevious(brushExtent) + + const brushed = useCallback( + (event: D3BrushEvent) => { + const { type, selection, mode } = event + + if (!selection) { + setLocalBrushExtent(null) + return + } + + const scaled = (selection as [number, number]).map(xScale.invert) as [number, number] + + // avoid infinite render loop by checking for change + if (type === 'end' && !compare(brushExtent, scaled, xScale)) { + setBrushExtent(scaled, mode) + } + + setLocalBrushExtent(scaled) + }, + [xScale, brushExtent, setBrushExtent] + ) + + // keep local and external brush extent in sync + // i.e. snap to ticks on bruhs end + useEffect(() => { + setLocalBrushExtent(brushExtent) + }, [brushExtent]) + + // initialize the brush + useEffect(() => { + if (!brushRef.current) return + + brushBehavior.current = brushX() + .extent([ + [Math.max(0 + BRUSH_EXTENT_MARGIN_PX, xScale(0)), 0], + [innerWidth - BRUSH_EXTENT_MARGIN_PX, innerHeight], + ]) + .handleSize(30) + .filter(() => interactive) + .on('brush end', brushed) + + brushBehavior.current(select(brushRef.current)) + + if (previousBrushExtent && compare(brushExtent, previousBrushExtent, xScale)) { + select(brushRef.current) + .transition() + .call(brushBehavior.current.move as any, brushExtent.map(xScale)) + } + + // brush linear gradient + select(brushRef.current) + .selectAll('.selection') + .attr('stroke', 'none') + .attr('fill-opacity', '0.1') + .attr('fill', `url(#${id}-gradient-selection)`) + }, [brushExtent, brushed, id, innerHeight, innerWidth, interactive, previousBrushExtent, xScale]) + + // respond to xScale changes only + useEffect(() => { + if (!brushRef.current || !brushBehavior.current) return + + brushBehavior.current.move(select(brushRef.current) as any, brushExtent.map(xScale) as any) + }, [brushExtent, xScale]) + + // show labels when local brush changes + useEffect(() => { + setShowLabels(true) + const timeout = setTimeout(() => setShowLabels(false), 1500) + return () => clearTimeout(timeout) + }, [localBrushExtent]) + + // variables to help render the SVGs + const flipWestHandle = localBrushExtent && xScale(localBrushExtent[0]) > FLIP_HANDLE_THRESHOLD_PX + const flipEastHandle = localBrushExtent && xScale(localBrushExtent[1]) > innerWidth - FLIP_HANDLE_THRESHOLD_PX + + const showWestArrow = localBrushExtent && (xScale(localBrushExtent[0]) < 0 || xScale(localBrushExtent[1]) < 0) + const showEastArrow = + localBrushExtent && (xScale(localBrushExtent[0]) > innerWidth || xScale(localBrushExtent[1]) > innerWidth) + + const westHandleInView = + localBrushExtent && xScale(localBrushExtent[0]) >= 0 && xScale(localBrushExtent[0]) <= innerWidth + const eastHandleInView = + localBrushExtent && xScale(localBrushExtent[1]) >= 0 && xScale(localBrushExtent[1]) <= innerWidth + + return useMemo( + () => ( + <> + + + + + + + {/* clips at exactly the svg area */} + + + + + + {/* will host the d3 brush */} + setHovering(true)} + onMouseLeave={() => setHovering(false)} + /> + + {/* custom brush handles */} + {localBrushExtent && ( + <> + {/* west handle */} + {westHandleInView ? ( + + + + + + + + + + {brushLabelValue('w', localBrushExtent[0])} + + + + ) : null} + + {/* east handle */} + {eastHandleInView ? ( + + + + + + + + + + {brushLabelValue('e', localBrushExtent[1])} + + + + ) : null} + + {showWestArrow && } + + {showEastArrow && ( + + + + )} + + )} + + ), + [ + brushLabelValue, + eastHandleColor, + eastHandleInView, + flipEastHandle, + flipWestHandle, + hovering, + id, + innerHeight, + innerWidth, + localBrushExtent, + showEastArrow, + showLabels, + showWestArrow, + westHandleColor, + westHandleInView, + xScale, + ] + ) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx b/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx new file mode 100644 index 0000000..af1b0d4 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/Chart.tsx @@ -0,0 +1,147 @@ +import { max, scaleLinear, ZoomTransform } from 'd3' +import { useEffect, useMemo, useRef, useState } from 'react' +import { Bound } from 'state/mint/v3/actions' + +import { Area } from './Area' +import { AxisBottom } from './AxisBottom' +import { Brush } from './Brush' +import { Line } from './Line' +import { ChartEntry, LiquidityChartRangeInputProps } from './types' +import Zoom, { ZoomOverlay } from './Zoom' + +const xAccessor = (d: ChartEntry) => d.price0 +const yAccessor = (d: ChartEntry) => d.activeLiquidity + +export function Chart({ + id = 'liquidityChartRangeInput', + data: { series, current }, + ticksAtLimit, + styles, + dimensions: { width, height }, + margins, + interactive = true, + brushDomain, + brushLabels, + onBrushDomainChange, + zoomLevels, +}: LiquidityChartRangeInputProps) { + const zoomRef = useRef(null) + + const [zoom, setZoom] = useState(null) + + const [innerHeight, innerWidth] = useMemo( + () => [height - margins.top - margins.bottom, width - margins.left - margins.right], + [width, height, margins] + ) + + const { xScale, yScale } = useMemo(() => { + const scales = { + xScale: scaleLinear() + .domain([current * zoomLevels.initialMin, current * zoomLevels.initialMax] as number[]) + .range([0, innerWidth]), + yScale: scaleLinear() + .domain([0, max(series, yAccessor)] as number[]) + .range([innerHeight, 0]), + } + + if (zoom) { + const newXscale = zoom.rescaleX(scales.xScale) + scales.xScale.domain(newXscale.domain()) + } + + return scales + }, [current, zoomLevels.initialMin, zoomLevels.initialMax, innerWidth, series, innerHeight, zoom]) + + useEffect(() => { + // reset zoom as necessary + setZoom(null) + }, [zoomLevels]) + + useEffect(() => { + if (!brushDomain) { + onBrushDomainChange(xScale.domain() as [number, number], undefined) + } + }, [brushDomain, onBrushDomainChange, xScale]) + + return ( + <> + { + onBrushDomainChange( + [current * zoomLevels.initialMin, current * zoomLevels.initialMax] as [number, number], + 'reset' + ) + }} + showResetButton={Boolean(ticksAtLimit[Bound.LOWER] || ticksAtLimit[Bound.UPPER])} + zoomLevels={zoomLevels} + /> + + + + + + + {brushDomain && ( + // mask to highlight selected area + + + + )} + + + + + + + {brushDomain && ( + // duplicate area chart with mask for selected area + + + + )} + + + + + + + + + + + + + ) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/Line.tsx b/apps/web/src/components/LiquidityChartRangeInput/Line.tsx new file mode 100644 index 0000000..e7a9264 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/Line.tsx @@ -0,0 +1,24 @@ +import { ScaleLinear } from 'd3' +import { useMemo } from 'react' +import styled from 'styled-components' + +const StyledLine = styled.line` + opacity: 0.5; + stroke-width: 2; + stroke: ${({ theme }) => theme.neutral1}; + fill: none; +` + +export const Line = ({ + value, + xScale, + innerHeight, +}: { + value: number + xScale: ScaleLinear + innerHeight: number +}) => + useMemo( + () => , + [value, xScale, innerHeight] + ) diff --git a/apps/web/src/components/LiquidityChartRangeInput/Zoom.tsx b/apps/web/src/components/LiquidityChartRangeInput/Zoom.tsx new file mode 100644 index 0000000..f588373 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/Zoom.tsx @@ -0,0 +1,131 @@ +import { ButtonGray } from 'components/Button' +import { ScaleLinear, select, zoom, ZoomBehavior, zoomIdentity, ZoomTransform } from 'd3' +import { useEffect, useMemo, useRef } from 'react' +import { RefreshCcw, ZoomIn, ZoomOut } from 'react-feather' +import styled from 'styled-components' + +import { ZoomLevels } from './types' + +const Wrapper = styled.div<{ count: number }>` + display: grid; + grid-template-columns: repeat(${({ count }) => count.toString()}, 1fr); + grid-gap: 6px; + + position: absolute; + top: -32px; + right: 0; +` + +const Button = styled(ButtonGray)` + &:hover { + background-color: ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral1}; + } + + width: 32px; + height: 32px; + padding: 4px; +` + +export const ZoomOverlay = styled.rect` + fill: transparent; + cursor: grab; + + &:active { + cursor: grabbing; + } +` + +export default function Zoom({ + svg, + xScale, + setZoom, + width, + height, + resetBrush, + showResetButton, + zoomLevels, +}: { + svg: SVGElement | null + xScale: ScaleLinear + setZoom: (transform: ZoomTransform) => void + width: number + height: number + resetBrush: () => void + showResetButton: boolean + zoomLevels: ZoomLevels +}) { + const zoomBehavior = useRef>() + + const [zoomIn, zoomOut, zoomInitial, zoomReset] = useMemo( + () => [ + () => + svg && + zoomBehavior.current && + select(svg as Element) + .transition() + .call(zoomBehavior.current.scaleBy, 2), + () => + svg && + zoomBehavior.current && + select(svg as Element) + .transition() + .call(zoomBehavior.current.scaleBy, 0.5), + () => + svg && + zoomBehavior.current && + select(svg as Element) + .transition() + .call(zoomBehavior.current.scaleTo, 0.5), + () => + svg && + zoomBehavior.current && + select(svg as Element) + .call(zoomBehavior.current.transform, zoomIdentity.translate(0, 0).scale(1)) + .transition() + .call(zoomBehavior.current.scaleTo, 0.5), + ], + [svg] + ) + + useEffect(() => { + if (!svg) return + + zoomBehavior.current = zoom() + .scaleExtent([zoomLevels.min, zoomLevels.max]) + .extent([ + [0, 0], + [width, height], + ]) + .on('zoom', ({ transform }: { transform: ZoomTransform }) => setZoom(transform)) + + select(svg as Element).call(zoomBehavior.current) + }, [height, width, setZoom, svg, xScale, zoomBehavior, zoomLevels, zoomLevels.max, zoomLevels.min]) + + useEffect(() => { + // reset zoom to initial on zoomLevel change + zoomInitial() + }, [zoomInitial, zoomLevels]) + + return ( + + {showResetButton && ( + + )} + + + + ) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/hooks.ts b/apps/web/src/components/LiquidityChartRangeInput/hooks.ts new file mode 100644 index 0000000..b342553 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/hooks.ts @@ -0,0 +1,49 @@ +import { Currency } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import { TickProcessed, usePoolActiveLiquidity } from 'hooks/usePoolTickData' +import { useCallback, useMemo } from 'react' + +import { ChartEntry } from './types' + +export function useDensityChartData({ + currencyA, + currencyB, + feeAmount, +}: { + currencyA?: Currency + currencyB?: Currency + feeAmount?: FeeAmount +}) { + const { isLoading, error, data } = usePoolActiveLiquidity(currencyA, currencyB, feeAmount) + + const formatData = useCallback(() => { + if (!data?.length) { + return undefined + } + + const newData: ChartEntry[] = [] + + for (let i = 0; i < data.length; i++) { + const t: TickProcessed = data[i] + + const chartEntry = { + activeLiquidity: parseFloat(t.liquidityActive.toString()), + price0: parseFloat(t.price0), + } + + if (chartEntry.activeLiquidity > 0) { + newData.push(chartEntry) + } + } + + return newData + }, [data]) + + return useMemo(() => { + return { + isLoading, + error, + formattedData: !isLoading ? formatData() : undefined, + } + }, [isLoading, error, formatData]) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/index.tsx b/apps/web/src/components/LiquidityChartRangeInput/index.tsx new file mode 100644 index 0000000..c1b67de --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/index.tsx @@ -0,0 +1,209 @@ +import { Trans } from '@lingui/macro' +import { Currency, Price, Token } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import { AutoColumn, ColumnCenter } from 'components/Column' +import Loader from 'components/Icons/LoadingSpinner' +import { useColor } from 'hooks/useColor' +import { saturate } from 'polished' +import { ReactNode, useCallback, useMemo } from 'react' +import { BarChart2, CloudOff, Inbox } from 'react-feather' +import { batch } from 'react-redux' +import { Bound } from 'state/mint/v3/actions' +import styled, { useTheme } from 'styled-components' +import { ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +import { Chart } from './Chart' +import { useDensityChartData } from './hooks' +import { ZoomLevels } from './types' + +const ZOOM_LEVELS: Record = { + [FeeAmount.LOWEST]: { + initialMin: 0.999, + initialMax: 1.001, + min: 0.00001, + max: 1.5, + }, + [FeeAmount.LOW]: { + initialMin: 0.999, + initialMax: 1.001, + min: 0.00001, + max: 1.5, + }, + [FeeAmount.MEDIUM]: { + initialMin: 0.5, + initialMax: 2, + min: 0.00001, + max: 20, + }, + [FeeAmount.HIGH]: { + initialMin: 0.5, + initialMax: 2, + min: 0.00001, + max: 20, + }, +} + +const ChartWrapper = styled.div` + position: relative; + width: 100%; + max-height: 200px; + justify-content: center; + align-content: center; +` + +function InfoBox({ message, icon }: { message?: ReactNode; icon: ReactNode }) { + return ( + + {icon} + {message && ( + + {message} + + )} + + ) +} + +export default function LiquidityChartRangeInput({ + currencyA, + currencyB, + feeAmount, + ticksAtLimit, + price, + priceLower, + priceUpper, + onLeftRangeInput, + onRightRangeInput, + interactive, +}: { + currencyA?: Currency + currencyB?: Currency + feeAmount?: FeeAmount + ticksAtLimit: { [bound in Bound]?: boolean | undefined } + price?: number + priceLower?: Price + priceUpper?: Price + onLeftRangeInput: (typedValue: string) => void + onRightRangeInput: (typedValue: string) => void + interactive: boolean +}) { + const theme = useTheme() + + const tokenAColor = useColor(currencyA) + const tokenBColor = useColor(currencyB) + + const isSorted = currencyA && currencyB && currencyA?.wrapped.sortsBefore(currencyB?.wrapped) + + const { isLoading, error, formattedData } = useDensityChartData({ + currencyA, + currencyB, + feeAmount, + }) + + const onBrushDomainChangeEnded = useCallback( + (domain: [number, number], mode: string | undefined) => { + let leftRangeValue = Number(domain[0]) + const rightRangeValue = Number(domain[1]) + + if (leftRangeValue <= 0) { + leftRangeValue = 1 / 10 ** 6 + } + + batch(() => { + // simulate user input for auto-formatting and other validations + if ( + (!ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER] || mode === 'handle' || mode === 'reset') && + leftRangeValue > 0 + ) { + onLeftRangeInput(leftRangeValue.toFixed(6)) + } + + if ((!ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER] || mode === 'reset') && rightRangeValue > 0) { + // todo: remove this check. Upper bound for large numbers + // sometimes fails to parse to tick. + if (rightRangeValue < 1e35) { + onRightRangeInput(rightRangeValue.toFixed(6)) + } + } + }) + }, + [isSorted, onLeftRangeInput, onRightRangeInput, ticksAtLimit] + ) + + interactive = interactive && Boolean(formattedData?.length) + + const brushDomain: [number, number] | undefined = useMemo(() => { + const leftPrice = isSorted ? priceLower : priceUpper?.invert() + const rightPrice = isSorted ? priceUpper : priceLower?.invert() + + return leftPrice && rightPrice + ? [parseFloat(leftPrice?.toSignificant(6)), parseFloat(rightPrice?.toSignificant(6))] + : undefined + }, [isSorted, priceLower, priceUpper]) + + const { formatDelta } = useFormatter() + const brushLabelValue = useCallback( + (d: 'w' | 'e', x: number) => { + if (!price) return '' + + if (d === 'w' && ticksAtLimit[isSorted ? Bound.LOWER : Bound.UPPER]) return '0' + if (d === 'e' && ticksAtLimit[isSorted ? Bound.UPPER : Bound.LOWER]) return '∞' + + const percent = (x < price ? -1 : 1) * ((Math.max(x, price) - Math.min(x, price)) / price) * 100 + + return price ? `${(Math.sign(percent) < 0 ? '-' : '') + formatDelta(percent)}` : '' + }, + [formatDelta, isSorted, price, ticksAtLimit] + ) + + const isUninitialized = !currencyA || !currencyB || (formattedData === undefined && !isLoading) + + return ( + + {isUninitialized ? ( + Your position will appear here.} + icon={} + /> + ) : isLoading ? ( + } /> + ) : error ? ( + Liquidity data not available.} + icon={} + /> + ) : !formattedData || formattedData.length === 0 || !price ? ( + There is no liquidity data.} + icon={} + /> + ) : ( + + + + )} + + ) +} diff --git a/apps/web/src/components/LiquidityChartRangeInput/svg.tsx b/apps/web/src/components/LiquidityChartRangeInput/svg.tsx new file mode 100644 index 0000000..f8227b5 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/svg.tsx @@ -0,0 +1,61 @@ +/** + * Generates an SVG path for the east brush handle. + * Apply `scale(-1, 1)` to generate west brush handle. + * + * |```````\ + * | | | | + * |______/ + * | + * | + * | + * | + * | + * + * https://medium.com/@dennismphil/one-side-rounded-rectangle-using-svg-fb31cf318d90 + */ +export const brushHandlePath = (height: number) => + [ + // handle + `M 0 0`, // move to origin + `v ${height}`, // vertical line + 'm 1 0', // move 1px to the right + `V 0`, // second vertical line + `M 0 1`, // move to origin + + // head + 'h 12', // horizontal line + 'q 2 0, 2 2', // rounded corner + 'v 22', // vertical line + 'q 0 2 -2 2', // rounded corner + 'h -12', // horizontal line + `z`, // close path + ].join(' ') + +export const brushHandleAccentPath = () => + [ + 'm 5 7', // move to first accent + 'v 14', // vertical line + 'M 0 0', // move to origin + 'm 9 7', // move to second accent + 'v 14', // vertical line + 'z', + ].join(' ') + +export const OffScreenHandle = ({ + color, + size = 10, + margin = 10, +}: { + color: string + size?: number + margin?: number +}) => ( + +) diff --git a/apps/web/src/components/LiquidityChartRangeInput/types.ts b/apps/web/src/components/LiquidityChartRangeInput/types.ts new file mode 100644 index 0000000..1598459 --- /dev/null +++ b/apps/web/src/components/LiquidityChartRangeInput/types.ts @@ -0,0 +1,61 @@ +import { Bound } from 'state/mint/v3/actions' + +export interface ChartEntry { + activeLiquidity: number + price0: number +} + +interface Dimensions { + width: number + height: number +} + +interface Margins { + top: number + right: number + bottom: number + left: number +} + +export interface ZoomLevels { + initialMin: number + initialMax: number + min: number + max: number +} + +export interface LiquidityChartRangeInputProps { + // to distringuish between multiple charts in the DOM + id?: string + + data: { + series: ChartEntry[] + current: number + } + ticksAtLimit: { [bound in Bound]?: boolean | undefined } + + styles: { + area: { + // color of the ticks in range + selection: string + } + + brush: { + handle: { + west: string + east: string + } + } + } + + dimensions: Dimensions + margins: Margins + + interactive?: boolean + + brushLabels: (d: 'w' | 'e', x: number) => string + brushDomain?: [number, number] + onBrushDomainChange: (domain: [number, number], mode: string | undefined) => void + + zoomLevels: ZoomLevels +} diff --git a/apps/web/src/components/Loader/SpinningLoader.tsx b/apps/web/src/components/Loader/SpinningLoader.tsx new file mode 100644 index 0000000..0e87b2e --- /dev/null +++ b/apps/web/src/components/Loader/SpinningLoader.tsx @@ -0,0 +1,62 @@ +import styled, { keyframes } from 'styled-components' + +const StyledPollingDot = styled.div` + width: 8px; + height: 8px; + min-height: 8px; + min-width: 8px; + border-radius: 50%; + position: relative; + background-color: ${({ theme }) => theme.surface3}; + transition: 250ms ease background-color; +` + +const StyledPolling = styled.div` + display: flex; + height: 16px; + width: 16px; + margin-right: 2px; + margin-left: 2px; + align-items: center; + color: ${({ theme }) => theme.neutral1}; + transition: 250ms ease color; + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium` + display: none; + `} +` + +const rotate360 = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +` + +const Spinner = styled.div` + animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite; + transform: translateZ(0); + border-top: 1px solid transparent; + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; + border-left: 2px solid ${({ theme }) => theme.neutral1}; + background: transparent; + width: 14px; + height: 14px; + border-radius: 50%; + position: relative; + transition: 250ms ease border-color; + left: -3px; + top: -3px; +` + +export default function SpinningLoader() { + return ( + + + + + + ) +} diff --git a/apps/web/src/components/Loader/styled.tsx b/apps/web/src/components/Loader/styled.tsx new file mode 100644 index 0000000..fee899a --- /dev/null +++ b/apps/web/src/components/Loader/styled.tsx @@ -0,0 +1,57 @@ +import styled, { css, keyframes } from 'styled-components' + +export const loadingAnimation = keyframes` + 0% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } +` + +const shimmerMixin = css` + animation: ${loadingAnimation} 1.5s infinite; + animation-fill-mode: both; + background: linear-gradient( + to left, + ${({ theme }) => theme.surface1} 25%, + ${({ theme }) => theme.surface3} 50%, + ${({ theme }) => theme.surface1} 75% + ); + background-size: 400%; + will-change: background-position; +` + +export const LoadingRows = styled.div` + display: grid; + + & > div { + ${shimmerMixin} + border-radius: 12px; + height: 2.4em; + } +` + +export const LoadingRow = styled.div<{ height: number; width: number }>` + ${shimmerMixin} + border-radius: 12px; + height: ${({ height }) => height}px; + width: ${({ width }) => width}px; +` + +export const loadingOpacityMixin = css<{ $loading: boolean }>` + filter: ${({ $loading }) => ($loading ? 'grayscale(1)' : 'none')}; + opacity: ${({ $loading }) => ($loading ? '0.6' : '1')}; + transition: ${({ $loading, theme }) => + $loading ? 'none' : `opacity ${theme.transition.duration.medium} ${theme.transition.timing.inOut}`}; +` + +export const LoadingOpacityContainer = styled.div<{ $loading: boolean }>` + ${loadingOpacityMixin} +` + +export const LoadingFullscreen = styled.div` + ${shimmerMixin} + inset: 0; + position: absolute; +` diff --git a/apps/web/src/components/Logo/AssetLogo.tsx b/apps/web/src/components/Logo/AssetLogo.tsx new file mode 100644 index 0000000..35ba6eb --- /dev/null +++ b/apps/web/src/components/Logo/AssetLogo.tsx @@ -0,0 +1,51 @@ +import { ChainId, Currency } from '@uniswap/sdk-core' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import React from 'react' +import styled from 'styled-components' + +export const MissingImageLogo = styled.div<{ size?: string }>` + --size: ${({ size }) => size}; + border-radius: 100px; + color: ${({ theme }) => theme.neutral1}; + background-color: ${({ theme }) => theme.surface3}; + font-size: calc(var(--size) / 3); + font-weight: 535; + height: ${({ size }) => size ?? '24px'}; + line-height: ${({ size }) => size ?? '24px'}; + text-align: center; + width: ${({ size }) => size ?? '24px'}; + display: flex; + align-items: center; + justify-content: center; +` + +export type AssetLogoBaseProps = { + symbol?: string | null + primaryImg?: string | null + size?: string + style?: React.CSSProperties + currency?: Currency | null +} +type AssetLogoProps = AssetLogoBaseProps & { isNative?: boolean; address?: string | null; chainId?: number } + +const LogoContainer = styled.div` + position: relative; + display: flex; +` + +/** + * Renders an image by prioritizing a list of sources, and then eventually a fallback triangle alert + */ +export default function AssetLogo({ + currency, + + chainId = ChainId.MAINNET, + size = '24px', + style, +}: AssetLogoProps) { + return ( + + + + ) +} diff --git a/apps/web/src/components/Logo/ChainLogo.tsx b/apps/web/src/components/Logo/ChainLogo.tsx new file mode 100644 index 0000000..0735050 --- /dev/null +++ b/apps/web/src/components/Logo/ChainLogo.tsx @@ -0,0 +1,133 @@ +import { ChainId } from '@uniswap/sdk-core' +import { getChainInfo } from 'constants/chainInfo' +import { isSupportedChain, SupportedInterfaceChain } from 'constants/chains' +import { CSSProperties, FunctionComponent } from 'react' +import { useTheme } from 'styled-components' +import { useIsDarkMode } from 'theme/components/ThemeToggle' + +import { ReactComponent as arbitrum } from './ChainSymbols/arbitrum.svg' +import { ReactComponent as avax } from './ChainSymbols/avax.svg' +import { ReactComponent as base } from './ChainSymbols/base.svg' +import { ReactComponent as bnb } from './ChainSymbols/bnb.svg' +import { ReactComponent as celo } from './ChainSymbols/celo.svg' +import { ReactComponent as celoLight } from './ChainSymbols/celo_light.svg' +import { ReactComponent as ethereum } from './ChainSymbols/ethereum.svg' +import { ReactComponent as optimism } from './ChainSymbols/optimism.svg' +import { ReactComponent as polygon } from './ChainSymbols/polygon.svg' + +type SVG = FunctionComponent> +type ChainUI = { Symbol: SVG; bgColor: string; textColor: string } + +export function getChainUI(chainId: SupportedInterfaceChain, darkMode: boolean): ChainUI +export function getChainUI(chainId: ChainId, darkMode: boolean): ChainUI | undefined { + switch (chainId) { + case ChainId.MAINNET: + case ChainId.GOERLI: + case ChainId.SEPOLIA: + return { + Symbol: ethereum, + bgColor: '#6B8AFF33', + textColor: '#6B8AFF', + } + case ChainId.POLYGON: + case ChainId.POLYGON_MUMBAI: + return { + Symbol: polygon, + bgColor: '#9558FF33', + textColor: '#9558FF', + } + case ChainId.ARBITRUM_ONE: + case ChainId.ARBITRUM_GOERLI: + return { + Symbol: arbitrum, + bgColor: '#00A3FF33', + textColor: '#00A3FF', + } + case ChainId.OPTIMISM: + case ChainId.OPTIMISM_GOERLI: + return { + Symbol: optimism, + bgColor: '#FF042033', + textColor: '#FF0420', + } + case ChainId.CELO: + case ChainId.CELO_ALFAJORES: + return darkMode + ? { + Symbol: celo, + bgColor: '#FCFF5233', + textColor: '#FCFF52', + } + : { + Symbol: celoLight, + bgColor: '#FCFF5299', + textColor: '#655947', + } + case ChainId.AVALANCHE: + return { + Symbol: avax, + bgColor: '#E8414233', + textColor: '#E84142', + } + case ChainId.BNB: + return { + Symbol: bnb, + bgColor: '#EAB20033', + textColor: '#EAB200', + } + case ChainId.BASE: + return { + Symbol: base, + bgColor: '#0052FF33', + textColor: '#0052FF', + } + default: + return undefined + } +} + +export const getDefaultBorderRadius = (size: number) => size / 2 - 4 + +type ChainLogoProps = { + chainId: ChainId + className?: string + size?: number + borderRadius?: number + style?: CSSProperties + testId?: string + fillContainer?: boolean +} +export function ChainLogo({ + chainId, + className, + style, + size = 12, + borderRadius = getDefaultBorderRadius(size), + testId, + fillContainer = false, +}: ChainLogoProps) { + const darkMode = useIsDarkMode() + const { surface2 } = useTheme() + + if (!isSupportedChain(chainId)) return null + const { label } = getChainInfo(chainId) + + const { Symbol, bgColor } = getChainUI(chainId, darkMode) + const iconSize = fillContainer ? '100%' : size + + return ( + + {`${label} logo`} + + + + + ) +} diff --git a/apps/web/src/components/Logo/ChainSymbols/arbitrum.svg b/apps/web/src/components/Logo/ChainSymbols/arbitrum.svg new file mode 100644 index 0000000..ebcbdd5 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/arbitrum.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/avax.svg b/apps/web/src/components/Logo/ChainSymbols/avax.svg new file mode 100644 index 0000000..ee1f505 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/avax.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/base.svg b/apps/web/src/components/Logo/ChainSymbols/base.svg new file mode 100644 index 0000000..39a6229 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/base.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/bnb.svg b/apps/web/src/components/Logo/ChainSymbols/bnb.svg new file mode 100644 index 0000000..9c04309 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/bnb.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/celo.svg b/apps/web/src/components/Logo/ChainSymbols/celo.svg new file mode 100644 index 0000000..48bffb3 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/celo.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/celo_light.svg b/apps/web/src/components/Logo/ChainSymbols/celo_light.svg new file mode 100644 index 0000000..3a99877 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/celo_light.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/ethereum.svg b/apps/web/src/components/Logo/ChainSymbols/ethereum.svg new file mode 100644 index 0000000..15053e9 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/ethereum.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/optimism.svg b/apps/web/src/components/Logo/ChainSymbols/optimism.svg new file mode 100644 index 0000000..83b6d2a --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/optimism.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Logo/ChainSymbols/polygon.svg b/apps/web/src/components/Logo/ChainSymbols/polygon.svg new file mode 100644 index 0000000..074d2d6 --- /dev/null +++ b/apps/web/src/components/Logo/ChainSymbols/polygon.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Logo/CurrencyLogo.tsx b/apps/web/src/components/Logo/CurrencyLogo.tsx new file mode 100644 index 0000000..3aaca07 --- /dev/null +++ b/apps/web/src/components/Logo/CurrencyLogo.tsx @@ -0,0 +1,22 @@ +import { Currency } from '@uniswap/sdk-core' +import { TokenInfo } from '@uniswap/token-lists' + +import AssetLogo, { AssetLogoBaseProps } from './AssetLogo' + +export default function CurrencyLogo( + props: AssetLogoBaseProps & { + currency?: Currency | null + } +) { + return ( + + ) +} diff --git a/apps/web/src/components/Logo/HolidayUniIcon.tsx b/apps/web/src/components/Logo/HolidayUniIcon.tsx new file mode 100644 index 0000000..5437642 --- /dev/null +++ b/apps/web/src/components/Logo/HolidayUniIcon.tsx @@ -0,0 +1,17 @@ +import { t } from '@lingui/macro' +import { ReactElement } from 'react' + +import { ReactComponent as WinterUni } from '../../assets/svg/winter-uni.svg' +import { SVGProps } from './UniIcon' + +const MONTH_TO_HOLIDAY_UNI: { [date: string]: (props: SVGProps) => ReactElement } = { + '12': (props) => , + '1': (props) => , +} + +export default function HolidayUniIcon(props: SVGProps): ReactElement | null { + // months in javascript are 0 indexed... + const currentMonth = `${new Date().getMonth() + 1}` + const HolidayUni = MONTH_TO_HOLIDAY_UNI[currentMonth] + return HolidayUni ? : null +} diff --git a/apps/web/src/components/Logo/QueryTokenLogo.tsx b/apps/web/src/components/Logo/QueryTokenLogo.tsx new file mode 100644 index 0000000..2040db8 --- /dev/null +++ b/apps/web/src/components/Logo/QueryTokenLogo.tsx @@ -0,0 +1,24 @@ +import { ChainId } from '@uniswap/sdk-core' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import { SearchToken } from 'graphql/data/SearchTokens' +import { TokenQueryData } from 'graphql/data/Token' +import { TopToken } from 'graphql/data/TopTokens' +import { gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { useMemo } from 'react' + +import { AssetLogoBaseProps } from './AssetLogo' + +export default function QueryTokenLogo( + props: AssetLogoBaseProps & { + token?: TopToken | TokenQueryData | SearchToken + } +) { + const chainId = + (props.token?.chain ? supportedChainIdFromGQLChain(props.token?.chain) : ChainId.MAINNET) ?? ChainId.MAINNET + const currency = props.token ? gqlToCurrency(props.token) : undefined + const logoUrl = props.token?.project?.logoUrl + + return ( + [currency], [currency])} chainId={chainId} images={[logoUrl]} {...props} /> + ) +} diff --git a/apps/web/src/components/Logo/UniIcon.tsx b/apps/web/src/components/Logo/UniIcon.tsx new file mode 100644 index 0000000..10d80f1 --- /dev/null +++ b/apps/web/src/components/Logo/UniIcon.tsx @@ -0,0 +1,34 @@ +import styled from 'styled-components' + +import HolidayUniIcon from './HolidayUniIcon' + +// ESLint reports `fill` is missing, whereas it exists on an SVGProps type +export type SVGProps = React.SVGProps & { + fill?: string + height?: string | number + width?: string | number + gradientId?: string + clickable?: boolean +} + +export const UniIcon = ({ clickable, ...props }: SVGProps) => ( + + {HolidayUniIcon(props) !== null ? ( + + ) : ( + + + + )} + +) + +const Container = styled.div<{ clickable?: boolean }>` + position: relative; + cursor: ${({ clickable }) => (clickable ? 'pointer' : 'auto')}; +` diff --git a/apps/web/src/components/Logo/UniswapXBrandMark.tsx b/apps/web/src/components/Logo/UniswapXBrandMark.tsx new file mode 100644 index 0000000..501b615 --- /dev/null +++ b/apps/web/src/components/Logo/UniswapXBrandMark.tsx @@ -0,0 +1,23 @@ +import { Trans } from '@lingui/macro' +import { ThemedText } from 'theme/components' + +import UniswapXRouterLabel, { UnswapXRouterLabelProps } from '../RouterLabel/UniswapXRouterLabel' + +type UniswapXBrandMarkProps = Omit & { + fontWeight?: 'bold' +} + +export default function UniswapXBrandMark({ fontWeight, ...props }: UniswapXBrandMarkProps): JSX.Element { + return ( + + + UniswapX + + + ) +} diff --git a/apps/web/src/components/Menu/index.tsx b/apps/web/src/components/Menu/index.tsx new file mode 100644 index 0000000..ab0e131 --- /dev/null +++ b/apps/web/src/components/Menu/index.tsx @@ -0,0 +1,132 @@ +import { FunctionComponent, PropsWithChildren, useRef } from 'react' +import { Link } from 'react-router-dom' +import styled, { css } from 'styled-components' +import { ExternalLink } from 'theme/components' + +import { ReactComponent as MenuIcon } from '../../assets/images/menu.svg' +import { useOnClickOutside } from '../../hooks/useOnClickOutside' +import { useModalIsOpen, useToggleModal } from '../../state/application/hooks' +import { ApplicationModal } from '../../state/application/reducer' + +export enum FlyoutAlignment { + LEFT = 'LEFT', + RIGHT = 'RIGHT', +} + +const StyledMenuIcon = styled(MenuIcon)` + path { + stroke: ${({ theme }) => theme.neutral1}; + } +` + +const StyledMenu = styled.div` + display: flex; + justify-content: center; + align-items: center; + position: relative; + border: none; + text-align: left; +` + +const MenuFlyout = styled.span<{ flyoutAlignment?: FlyoutAlignment }>` + min-width: 196px; + max-height: 350px; + overflow: auto; + background-color: ${({ theme }) => theme.surface1}; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), + 0px 24px 32px rgba(0, 0, 0, 0.01); + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 12px; + padding: 0.5rem; + display: flex; + flex-direction: column; + font-size: 16px; + position: absolute; + top: 3rem; + z-index: 100; + + ${({ flyoutAlignment = FlyoutAlignment.RIGHT }) => + flyoutAlignment === FlyoutAlignment.RIGHT + ? css` + right: 0rem; + ` + : css` + left: 0rem; + `}; +` + +const MenuItem = styled(ExternalLink)` + display: flex; + flex: 1; + flex-direction: row; + align-items: center; + padding: 0.5rem 0.5rem; + justify-content: space-between; + color: ${({ theme }) => theme.neutral2}; + :hover { + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + text-decoration: none; + } +` + +const InternalMenuItem = styled(Link)` + flex: 1; + padding: 0.5rem 0.5rem; + color: ${({ theme }) => theme.neutral2}; + width: max-content; + text-decoration: none; + :hover { + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + text-decoration: none; + } + > svg { + margin-right: 8px; + } +` + +interface MenuProps { + modal: ApplicationModal + flyoutAlignment?: FlyoutAlignment + ToggleUI?: FunctionComponent> + menuItems: { + content: any + link: string + external: boolean + }[] +} + +const ExternalMenuItem = styled(MenuItem)` + width: max-content; + text-decoration: none; +` + +export const Menu = ({ modal, flyoutAlignment = FlyoutAlignment.RIGHT, ToggleUI, menuItems, ...rest }: MenuProps) => { + const node = useRef() + const open = useModalIsOpen(modal) + const toggle = useToggleModal(modal) + useOnClickOutside(node, open ? toggle : undefined) + const ToggleElement = ToggleUI || StyledMenuIcon + + return ( + + + {open && ( + + {menuItems.map(({ content, link, external }, i) => + external ? ( + + {content} + + ) : ( + + {content} + + ) + )} + + )} + + ) +} diff --git a/apps/web/src/components/Modal/index.tsx b/apps/web/src/components/Modal/index.tsx new file mode 100644 index 0000000..001ee52 --- /dev/null +++ b/apps/web/src/components/Modal/index.tsx @@ -0,0 +1,177 @@ +import { DialogContent, DialogOverlay } from '@reach/dialog' +import React from 'react' +import { animated, easings, useSpring, useTransition } from 'react-spring' +import { useGesture } from 'react-use-gesture' +import styled, { css } from 'styled-components' +import { Z_INDEX } from 'theme/zIndex' +import { isMobile } from 'uniswap/src/utils/platform' + +export const MODAL_TRANSITION_DURATION = 200 + +const AnimatedDialogOverlay = animated(DialogOverlay) + +const StyledDialogOverlay = styled(AnimatedDialogOverlay)<{ $scrollOverlay?: boolean }>` + will-change: transform, opacity; + &[data-reach-dialog-overlay] { + z-index: ${Z_INDEX.modalBackdrop}; + background-color: transparent; + overflow: hidden; + + display: flex; + align-items: center; + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + align-items: flex-end; + } + overflow-y: ${({ $scrollOverlay }) => $scrollOverlay && 'scroll'}; + justify-content: center; + + background-color: ${({ theme }) => theme.scrim}; + } +` + +type StyledDialogProps = { + $minHeight?: number | false + $maxHeight?: number + $scrollOverlay?: boolean + $hideBorder?: boolean + $maxWidth: number +} + +const AnimatedDialogContent = animated(DialogContent) +const StyledDialogContent = styled(AnimatedDialogContent)` + overflow-y: auto; + + &[data-reach-dialog-content] { + margin: auto; + background-color: ${({ theme }) => theme.surface2}; + border: ${({ theme, $hideBorder }) => !$hideBorder && `1px solid ${theme.surface3}`}; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + padding: 0px; + width: 50vw; + overflow-y: auto; + overflow-x: hidden; + max-width: ${({ $maxWidth }) => $maxWidth}px; + ${({ $maxHeight }) => + $maxHeight && + css` + max-height: ${$maxHeight}vh; + `} + ${({ $minHeight }) => + $minHeight && + css` + min-height: ${$minHeight}vh; + `} + display: ${({ $scrollOverlay }) => ($scrollOverlay ? 'inline-table' : 'flex')}; + border-radius: 20px; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.md}px) { + width: 65vw; + } + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + margin: 0; + width: 100vw; + border-radius: 20px; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + } + } +` + +interface ModalProps { + isOpen: boolean + onDismiss?: () => void + onSwipe?: () => void + height?: number // takes precedence over minHeight and maxHeight + minHeight?: number | false + maxHeight?: number + maxWidth?: number + initialFocusRef?: React.RefObject + children?: React.ReactNode + $scrollOverlay?: boolean + hideBorder?: boolean + slideIn?: boolean +} + +export default function Modal({ + isOpen, + onDismiss, + minHeight = false, + maxHeight = 90, + maxWidth = 420, + height, + initialFocusRef, + children, + onSwipe = onDismiss, + $scrollOverlay, + hideBorder = false, + slideIn, +}: ModalProps) { + const fadeTransition = useTransition(isOpen, { + config: { duration: MODAL_TRANSITION_DURATION }, + from: { opacity: 0 }, + enter: { opacity: 1 }, + leave: { opacity: 0 }, + }) + + const slideTransition = useTransition(isOpen, { + config: { duration: MODAL_TRANSITION_DURATION, easing: easings.easeInOutCubic }, + from: { transform: 'translateY(40px)' }, + enter: { transform: 'translateY(0px)' }, + leave: { transform: 'translateY(40px)' }, + }) + + const [{ y }, set] = useSpring(() => ({ y: 0, config: { mass: 1, tension: 210, friction: 20 } })) + const bind = useGesture({ + onDrag: (state) => { + set({ + y: state.down ? state.movement[1] : 0, + }) + if (state.movement[1] > 300 || (state.velocity > 3 && state.direction[1] > 0)) { + onSwipe?.() + } + }, + }) + + return ( + <> + {fadeTransition( + ({ opacity }, item) => + item && ( + + {slideTransition( + (styles, item) => + item && ( + `translateY(${(y as number) > 0 ? y : 0}px)`) }, + } + : slideIn + ? { style: styles } + : {})} + aria-label="dialog" + $minHeight={height ?? minHeight} + $maxHeight={height ?? maxHeight} + $scrollOverlay={$scrollOverlay} + $hideBorder={hideBorder} + $maxWidth={maxWidth} + > + {/* prevents the automatic focusing of inputs on mobile by the reach dialog */} + {!initialFocusRef && isMobile ?
: null} + {children} + + ) + )} + + ) + )} + + ) +} diff --git a/apps/web/src/components/ModalViews/index.tsx b/apps/web/src/components/ModalViews/index.tsx new file mode 100644 index 0000000..5f4d8b8 --- /dev/null +++ b/apps/web/src/components/ModalViews/index.tsx @@ -0,0 +1,70 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { ArrowUpCircle } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { CloseIcon, CustomLightSpinner, ThemedText } from 'theme/components' + +import Circle from '../../assets/images/blue-loader.svg' +import { ExternalLink } from '../../theme/components' +import { ExplorerDataType, getExplorerLink } from '../../utils/getExplorerLink' +import { AutoColumn, ColumnCenter } from '../Column' +import { RowBetween } from '../Row' + +const ConfirmOrLoadingWrapper = styled.div` + width: 100%; + padding: 24px; +` + +const ConfirmedIcon = styled(ColumnCenter)` + padding: 60px 0; +` + +export function LoadingView({ children, onDismiss }: { children: any; onDismiss: () => void }) { + return ( + + +
+ + + + + + + {children} + + Confirm this transaction in your wallet + + + + ) +} + +export function SubmittedView({ children, onDismiss, hash }: { children: any; onDismiss: () => void; hash?: string }) { + const theme = useTheme() + const { chainId } = useWeb3React() + + return ( + + +
+ + + + + + + {children} + {chainId && hash && ( + + + View transaction on Explorer + + + )} + + + ) +} diff --git a/apps/web/src/components/NavBar/Bag.tsx b/apps/web/src/components/NavBar/Bag.tsx new file mode 100644 index 0000000..6e02209 --- /dev/null +++ b/apps/web/src/components/NavBar/Bag.tsx @@ -0,0 +1,47 @@ +import { NavIcon } from 'components/NavBar/NavIcon' +import { useIsNftProfilePage } from 'hooks/useIsNftPage' +import { BagIcon, HundredsOverflowIcon, TagIcon } from 'nft/components/icons' +import { useBag, useSellAsset } from 'nft/hooks' +import { useCallback } from 'react' +import styled from 'styled-components' + +const CounterDot = styled.div` + background-color: ${({ theme }) => theme.accent1}; + border-radius: 100px; + color: ${({ theme }) => theme.deprecated_accentTextLightPrimary}; + font-size: 10px; + line-height: 12px; + min-height: 16px; + min-width: 16px; + padding: 2px 4px; + position: absolute; + right: 0px; + text-align: center; + top: 4px; +` + +export const Bag = () => { + const itemsInBag = useBag((state) => state.itemsInBag) + const sellAssets = useSellAsset((state) => state.sellAssets) + const isProfilePage = useIsNftProfilePage() + + const { bagExpanded, setBagExpanded } = useBag(({ bagExpanded, setBagExpanded }) => ({ bagExpanded, setBagExpanded })) + + const handleIconClick = useCallback(() => { + setBagExpanded({ bagExpanded: !bagExpanded }) + }, [bagExpanded, setBagExpanded]) + + const bagQuantity = isProfilePage ? sellAssets.length : itemsInBag.length + const bagHasItems = bagQuantity > 0 + + return ( + + {isProfilePage ? ( + + ) : ( + + )} + {bagHasItems && {bagQuantity > 99 ? : bagQuantity}} + + ) +} diff --git a/apps/web/src/components/NavBar/Blur.tsx b/apps/web/src/components/NavBar/Blur.tsx new file mode 100644 index 0000000..dce0452 --- /dev/null +++ b/apps/web/src/components/NavBar/Blur.tsx @@ -0,0 +1,35 @@ +import styled from 'styled-components' + +const MAX_STRENGTH = 5 +const BLUR_STEPS = 20 +const BLUR_FADE = '#fff' + +const NAV_HEIGHT = 72 + +const BlurGroup = styled.div` + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + background-image: linear-gradient(${BLUR_FADE}, rgba(${BLUR_FADE}, 0)); +` + +const BlurLayer = styled.div<{ index: number }>` + position: absolute; + top: 0; + left: 0; + right: 0; + height: ${({ index }) => (NAV_HEIGHT / BLUR_STEPS) * index}px; + backdrop-filter: blur(${({ index }) => (MAX_STRENGTH / BLUR_STEPS) * (BLUR_STEPS - index)}px); +` + +export default function Blur() { + return ( + + {Array.from(Array(BLUR_STEPS), (_, index) => ( + + ))} + + ) +} diff --git a/apps/web/src/components/NavBar/ChainSelector.tsx b/apps/web/src/components/NavBar/ChainSelector.tsx new file mode 100644 index 0000000..0a88739 --- /dev/null +++ b/apps/web/src/components/NavBar/ChainSelector.tsx @@ -0,0 +1,165 @@ +import { t } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { showTestnetsAtom } from 'components/AccountDrawer/TestnetsToggle' +import { BaseButton } from 'components/Button' +import { ChainLogo } from 'components/Logo/ChainLogo' +import { MouseoverTooltip } from 'components/Tooltip' +import { getConnection } from 'connection' +import { ConnectionType } from 'connection/types' +import { WalletConnectV2 } from 'connection/WalletConnectV2' +import { getChainInfo } from 'constants/chainInfo' +import { getChainPriority, L1_CHAIN_IDS, L2_CHAIN_IDS, TESTNET_CHAIN_IDS } from 'constants/chains' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import useSelectChain from 'hooks/useSelectChain' +import useSyncChainQuery from 'hooks/useSyncChainQuery' +import { useAtomValue } from 'jotai/utils' +import { Portal } from 'nft/components/common/Portal' +import { Column } from 'nft/components/Flex' +import { useIsMobile } from 'nft/hooks' +import { useCallback, useMemo, useRef, useState } from 'react' +import { AlertTriangle, ChevronDown, ChevronUp } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { getSupportedChainIdsFromWalletConnectSession } from 'utils/getSupportedChainIdsFromWalletConnectSession' + +import ChainSelectorRow from './ChainSelectorRow' +import { NavDropdown } from './NavDropdown' + +const NETWORK_SELECTOR_CHAINS = [...L1_CHAIN_IDS, ...L2_CHAIN_IDS] + +const ChainSelectorWrapper = styled.div` + position: relative; +` + +const ChainSelectorButton = styled(BaseButton)<{ isOpen: boolean }>` + display: flex; + background: ${({ theme, isOpen }) => (isOpen ? theme.accent2 : 'none')}; + padding: 10px 8px; + gap: 4px; + border-radius: 12px; + height: 40px; + color: ${({ theme }) => theme.neutral1}; + transition: ${({ theme }) => + `${theme.transition.duration.medium} ${theme.transition.timing.ease} background-color, ${theme.transition.duration.medium} ${theme.transition.timing.ease} margin`}; + + &:hover { + background-color: ${({ theme }) => theme.deprecated_stateOverlayHover}; + } +` + +function useWalletSupportedChains(): ChainId[] { + const { connector } = useWeb3React() + const connectionType = getConnection(connector).type + + switch (connectionType) { + case ConnectionType.WALLET_CONNECT_V2: + case ConnectionType.UNISWAP_WALLET_V2: + return getSupportedChainIdsFromWalletConnectSession((connector as WalletConnectV2).provider?.session) + default: + return NETWORK_SELECTOR_CHAINS + } +} + +export const ChainSelector = ({ leftAlign }: { leftAlign?: boolean }) => { + const { chainId } = useWeb3React() + const [isOpen, setIsOpen] = useState(false) + const isMobile = useIsMobile() + + const theme = useTheme() + + const showTestnets = useAtomValue(showTestnetsAtom) + const walletSupportsChain = useWalletSupportedChains() + + const [supportedChains, unsupportedChains] = useMemo(() => { + const { supported, unsupported } = NETWORK_SELECTOR_CHAINS.filter((chain: number) => { + return showTestnets || !TESTNET_CHAIN_IDS.includes(chain) + }) + .sort((a, b) => getChainPriority(a) - getChainPriority(b)) + .reduce( + (acc, chain) => { + if (walletSupportsChain.includes(chain)) { + acc.supported.push(chain) + } else { + acc.unsupported.push(chain) + } + return acc + }, + { supported: [], unsupported: [] } as Record + ) + return [supported, unsupported] + }, [showTestnets, walletSupportsChain]) + + const ref = useRef(null) + const modalRef = useRef(null) + useOnClickOutside(ref, () => setIsOpen(false), [modalRef]) + + const info = getChainInfo(chainId) + + const selectChain = useSelectChain() + useSyncChainQuery() + + const [pendingChainId, setPendingChainId] = useState(undefined) + + const onSelectChain = useCallback( + async (targetChainId: ChainId) => { + setPendingChainId(targetChainId) + await selectChain(targetChainId) + setPendingChainId(undefined) + setIsOpen(false) + }, + [selectChain, setIsOpen] + ) + + if (!chainId) { + return null + } + + const isSupported = !!info + + const dropdown = ( + + + {supportedChains.map((selectorChain) => ( + + ))} + {unsupportedChains.map((selectorChain) => ( + undefined} + targetChain={selectorChain} + key={selectorChain} + isPending={false} + /> + ))} + + + ) + + const chevronProps = { + height: 20, + width: 20, + color: theme.neutral2, + } + + return ( + + + setIsOpen(!isOpen)} isOpen={isOpen}> + {!isSupported ? ( + + ) : ( + + )} + {isOpen ? : } + + + {isOpen && (isMobile ? {dropdown} : <>{dropdown})} + + ) +} diff --git a/apps/web/src/components/NavBar/ChainSelectorRow.test.tsx b/apps/web/src/components/NavBar/ChainSelectorRow.test.tsx new file mode 100644 index 0000000..46c82d6 --- /dev/null +++ b/apps/web/src/components/NavBar/ChainSelectorRow.test.tsx @@ -0,0 +1,45 @@ +import { ChainId, SUPPORTED_CHAINS } from '@uniswap/sdk-core' +import { render } from 'test-utils/render' + +import ChainSelectorRow from './ChainSelectorRow' + +describe('ChainSelectorRow', () => { + SUPPORTED_CHAINS.forEach((chainId) => { + it(`should match snapshot for chainId ${chainId}`, () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + }) + }) + + it('should be clickable when enabled', () => { + const onSelectChain = jest.fn() + const { getByTestId } = render( + + ) + const button = getByTestId('Optimism-selector') + button.click() + expect(onSelectChain).toHaveBeenCalled() + }) + + it('should not be clickable when disabled', () => { + const onSelectChain = jest.fn() + const { getByTestId } = render( + + ) + const button = getByTestId('Optimism-selector') + button.click() + expect(onSelectChain).not.toHaveBeenCalled() + }) +}) diff --git a/apps/web/src/components/NavBar/ChainSelectorRow.tsx b/apps/web/src/components/NavBar/ChainSelectorRow.tsx new file mode 100644 index 0000000..4adf3b5 --- /dev/null +++ b/apps/web/src/components/NavBar/ChainSelectorRow.tsx @@ -0,0 +1,102 @@ +import { Trans } from '@lingui/macro' +import { BrowserEvent, SharedEventName } from '@uniswap/analytics-events' +import { ChainId } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { TraceEvent } from 'analytics' +import Loader from 'components/Icons/LoadingSpinner' +import { ChainLogo } from 'components/Logo/ChainLogo' +import { getChainInfo } from 'constants/chainInfo' +import { CheckMarkIcon } from 'nft/components/icons' +import styled, { useTheme } from 'styled-components' + +const LOGO_SIZE = 20 + +const Container = styled.button<{ disabled: boolean }>` + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: ${({ theme }) => theme.neutral1}; + cursor: ${({ disabled }) => (disabled ? 'auto' : 'pointer')}; + display: grid; + grid-template-columns: min-content 1fr min-content; + justify-content: space-between; + line-height: 20px; + opacity: ${({ disabled }) => (disabled ? 0.6 : 1)}; + padding: 10px 8px; + text-align: left; + transition: ${({ theme }) => theme.transition.duration.medium} ${({ theme }) => theme.transition.timing.ease} + background-color; + width: 240px; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + width: 100%; + } + + &:hover { + background-color: ${({ disabled, theme }) => (disabled ? 'none' : theme.surface3)}; + } +` +const Label = styled.div` + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +` +const Status = styled.div` + grid-column: 3; + grid-row: 1; + display: flex; + align-items: center; + width: ${LOGO_SIZE}px; +` +const CaptionText = styled.div` + color: ${({ theme }) => theme.neutral2}; + font-size: 12px; + grid-column: 2; + grid-row: 2; +` + +interface ChainSelectorRowProps { + disabled?: boolean + targetChain: ChainId + onSelectChain: (targetChain: number) => void + isPending: boolean +} +export default function ChainSelectorRow({ disabled, targetChain, onSelectChain, isPending }: ChainSelectorRowProps) { + const { chainId } = useWeb3React() + const active = chainId === targetChain + const chainInfo = getChainInfo(targetChain) + const label = chainInfo?.label + + const theme = useTheme() + + return ( + + { + if (!disabled) onSelectChain(targetChain) + }} + > + + {label && } + {disabled && ( + + Unsupported by your wallet + + )} + {isPending && ( + + Approve in wallet + + )} + + {active && } + {!active && isPending && } + + + + ) +} diff --git a/apps/web/src/components/NavBar/More/Menu.tsx b/apps/web/src/components/NavBar/More/Menu.tsx new file mode 100644 index 0000000..5e73bd9 --- /dev/null +++ b/apps/web/src/components/NavBar/More/Menu.tsx @@ -0,0 +1,137 @@ +import { Trans } from '@lingui/macro' +import Column from 'components/Column' +import { ScrollBarStyles } from 'components/Common' +import Row from 'components/Row' +import { Socials } from 'pages/Landing/sections/Footer' +import { Link } from 'react-router-dom' +import { Text } from 'rebass' +import { useOpenModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled, { css } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ExternalLink, ThemedText } from 'theme/components' + +import { menuContent, MenuItem, MenuSection } from './menuContent' +import { MobileAppLogo } from './MobileAppLogo' + +const Container = styled.div` + width: 295px; + max-height: 85vh; + padding: 24px; + margin-top: 12px; + margin-bottom: 8px; + background: ${({ theme }) => theme.surface1}; + user-select: none; + overflow: auto; + ${ScrollBarStyles} + height: unset; + + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + box-shadow: 0px 0px 10px 0px rgba(34, 34, 34, 0.04); + + position: absolute; + right: 0px; + top: 30px; + bottom: unset; + @media screen and (max-width: ${BREAKPOINTS.md}px) { + top: unset; + bottom: 50px; + } +` +const LinkStyles = css` + font-size: 16px; + text-decoration: none; + color: ${({ theme }) => theme.neutral2}; + &:hover { + color: ${({ theme }) => theme.accent1}; + opacity: 1; + } +` +const StyledInternalLink = styled(Link)<{ canHide?: boolean }>` + ${LinkStyles} + @media screen and (max-width: ${BREAKPOINTS.md}px), (min-width: ${BREAKPOINTS.xl}px) { + display: ${({ canHide }) => (canHide ? 'none' : 'block')}; + } +` +const StyledExternalLink = styled(ExternalLink)` + ${LinkStyles} +` +const Separator = styled.div` + width: 100%; + height: 1px; + background: ${({ theme }) => theme.surface3}; +` +const StyledRow = styled(Row)` + cursor: pointer; + :hover { + color: ${({ theme }) => theme.accent1}; + } +` +const StyledSocials = styled(Socials)` + height: 20px; +` +function Item({ label, href, internal, overflow, closeMenu }: MenuItem) { + return internal ? ( + + {label} + + ) : ( + {label} + ) +} +function Section({ title, items, closeMenu }: MenuSection) { + return ( + + {title} + {items.map((item, index) => ( + + ))} + + ) +} +export function Menu({ close }: { close: () => void }) { + const openGetTheAppModal = useOpenModal(ApplicationModal.GET_THE_APP) + + return ( + + + {menuContent.map((sectionContent, index) => ( +
+ ))} + + { + close() + openGetTheAppModal() + }} + > + + + + Download Uniswap + + + Available on iOS and Android + + + + + + + ) +} diff --git a/apps/web/src/components/NavBar/More/MobileAppLogo.tsx b/apps/web/src/components/NavBar/More/MobileAppLogo.tsx new file mode 100644 index 0000000..4354eb5 --- /dev/null +++ b/apps/web/src/components/NavBar/More/MobileAppLogo.tsx @@ -0,0 +1,161 @@ +import styled from 'styled-components' + +const AppIcon = styled.div` + display: flex; + justify-content: center; + align-items: center; + width: 41px; + height: 41px; + background: white; + border: 0.5px solid ${(props) => props.theme.surface3}; + border-radius: 8px; +` +export function MobileAppLogo() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/web/src/components/NavBar/More/index.tsx b/apps/web/src/components/NavBar/More/index.tsx new file mode 100644 index 0000000..424d070 --- /dev/null +++ b/apps/web/src/components/NavBar/More/index.tsx @@ -0,0 +1,51 @@ +import { ColumnCenter } from 'components/Column' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { vars } from 'nft/css/sprinkles.css' +import { useRef, useState } from 'react' +import { ChevronDown } from 'react-feather' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' + +import { Menu } from './Menu' + +const Wrapper = styled.div` + width: 100%; + display: flex; + flex: grow; + justify-content: center; + align-items: center; + position: relative; + margin: 4px 0px; +` +const IconContainer = styled(ColumnCenter)<{ isActive: boolean }>` + min-height: 100%; + justify-content: center; + border-radius: 14px; + padding: 9px 14px; + cursor: pointer; + color: ${({ isActive, theme }) => (isActive ? theme.neutral1 : theme.neutral2)}; + :hover { + background: ${vars.color.lightGrayOverlay}; + } +` +const ChevronIcon = styled(ChevronDown)<{ $rotated: boolean }>` + @media screen and (max-width: ${BREAKPOINTS.md}px) { + rotate: 180deg; + } + transition: transform 0.3s ease; + transform: ${({ $rotated }) => ($rotated ? 'rotate(180deg)' : 'none')}; +` +export function More() { + const [isOpen, setIsOpen] = useState(false) + const ref = useRef(null) + useOnClickOutside(ref, () => setIsOpen(false)) + + return ( + + setIsOpen(!isOpen)} data-testid="nav-more-button"> + + + {isOpen && setIsOpen(false)} />} + + ) +} diff --git a/apps/web/src/components/NavBar/More/menuContent.ts b/apps/web/src/components/NavBar/More/menuContent.ts new file mode 100644 index 0000000..4ba11c0 --- /dev/null +++ b/apps/web/src/components/NavBar/More/menuContent.ts @@ -0,0 +1,47 @@ +import { t } from '@lingui/macro' + +export interface MenuSection { + title: string + items: MenuItem[] + closeMenu?: () => void +} + +export interface MenuItem { + label: string + href: string + internal?: boolean + overflow?: boolean + closeMenu?: () => void +} + +export const menuContent: MenuSection[] = [ + { + title: t`App`, + items: [ + { label: t`Pool`, href: '/pool', internal: true, overflow: true }, + { label: t`Vote`, href: '/vote', internal: true }, + { label: t`Analytics`, href: 'https://info.uniswap.org/' }, + ], + }, + { + title: t`Company`, + items: [ + { label: t`Careers`, href: 'https://boards.greenhouse.io/uniswaplabs' }, + { label: t`Blog`, href: 'https://blog.uniswap.org/' }, + ], + }, + { + title: t`Protocol`, + items: [ + { label: t`Governance`, href: 'https://uniswap.org/governance' }, + { label: t`Developers`, href: 'https://uniswap.org/developers' }, + ], + }, + { + title: t`Need help?`, + items: [ + { label: t`Contact us`, href: 'https://support.uniswap.org/hc/en-us/requests/new' }, + { label: t`Help Center`, href: 'https://support.uniswap.org/hc/en-us' }, + ], + }, +] diff --git a/apps/web/src/components/NavBar/NavDropdown.css.ts b/apps/web/src/components/NavBar/NavDropdown.css.ts new file mode 100644 index 0000000..75b889b --- /dev/null +++ b/apps/web/src/components/NavBar/NavDropdown.css.ts @@ -0,0 +1,41 @@ +import { style } from '@vanilla-extract/css' + +import { sprinkles } from '../../nft/css/sprinkles.css' + +const baseNavDropdown = style([ + sprinkles({ + background: 'surface2', + borderStyle: 'solid', + borderColor: 'surface3', + borderWidth: '1px', + paddingBottom: '8', + paddingTop: '8', + }), +]) + +export const NavDropdown = style([ + baseNavDropdown, + sprinkles({ + position: 'absolute', + borderRadius: '12', + }), + { boxShadow: '0px 4px 12px 0px #00000026' }, +]) + +export const mobileNavDropdown = style([ + baseNavDropdown, + sprinkles({ + position: 'fixed', + borderTopRightRadius: '12', + borderTopLeftRadius: '12', + top: 'unset', + bottom: '50', + left: '0', + right: '0', + width: 'full', + }), + { + borderRightWidth: '0px', + borderLeftWidth: '0px', + }, +]) diff --git a/apps/web/src/components/NavBar/NavDropdown.tsx b/apps/web/src/components/NavBar/NavDropdown.tsx new file mode 100644 index 0000000..1697730 --- /dev/null +++ b/apps/web/src/components/NavBar/NavDropdown.tsx @@ -0,0 +1,20 @@ +import { Box, BoxProps } from 'nft/components/Box' +import { useIsMobile } from 'nft/hooks' +import { ForwardedRef, forwardRef } from 'react' +import { Z_INDEX } from 'theme/zIndex' + +import * as styles from './NavDropdown.css' + +export const NavDropdown = forwardRef((props: BoxProps, ref: ForwardedRef) => { + const isMobile = useIsMobile() + return ( + + ) +}) + +NavDropdown.displayName = 'NavDropdown' diff --git a/apps/web/src/components/NavBar/NavIcon.css.ts b/apps/web/src/components/NavBar/NavIcon.css.ts new file mode 100644 index 0000000..9f2683e --- /dev/null +++ b/apps/web/src/components/NavBar/NavIcon.css.ts @@ -0,0 +1,26 @@ +import { style } from '@vanilla-extract/css' + +import { sprinkles, vars } from '../../nft/css/sprinkles.css' + +export const navIcon = style([ + sprinkles({ + alignItems: 'center', + background: 'transparent', + position: 'relative', + display: 'flex', + flexDirection: 'column', + border: 'none', + justifyContent: 'center', + textAlign: 'center', + cursor: 'pointer', + borderRadius: '16', + transition: '250', + }), + { + ':hover': { + background: vars.color.lightGrayOverlay, + }, + zIndex: 1, + transform: `translateY(-1px) translateX(4px)`, + }, +]) diff --git a/apps/web/src/components/NavBar/NavIcon.tsx b/apps/web/src/components/NavBar/NavIcon.tsx new file mode 100644 index 0000000..6f00f78 --- /dev/null +++ b/apps/web/src/components/NavBar/NavIcon.tsx @@ -0,0 +1,36 @@ +import { t } from '@lingui/macro' +import { Box } from 'nft/components/Box' +import { ReactNode } from 'react' + +import * as styles from './NavIcon.css' + +interface NavIconProps { + children: ReactNode + isActive?: boolean + label?: string + onClick: () => void + activeBackground?: boolean +} + +export const NavIcon = ({ + children, + isActive, + label = t`Navigation button`, + onClick, + activeBackground, +}: NavIconProps) => { + return ( + + {children} + + ) +} diff --git a/apps/web/src/components/NavBar/RecentlySearchedAssets.ts b/apps/web/src/components/NavBar/RecentlySearchedAssets.ts new file mode 100644 index 0000000..e9ea4ac --- /dev/null +++ b/apps/web/src/components/NavBar/RecentlySearchedAssets.ts @@ -0,0 +1,109 @@ +import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' +import { SearchToken } from 'graphql/data/SearchTokens' +import { Chain, NftCollection, useRecentlySearchedAssetsQuery } from 'graphql/data/__generated__/types-and-hooks' +import { logSentryErrorForUnsupportedChain, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { useAtom } from 'jotai' +import { atomWithStorage, useAtomValue } from 'jotai/utils' +import { GenieCollection } from 'nft/types' +import { useCallback, useMemo } from 'react' +import { getNativeTokenDBAddress } from 'utils/nativeTokens' + +type RecentlySearchedAsset = { + isNft?: boolean + address: string + chain: Chain +} + +// Temporary measure used until backend supports addressing by "NATIVE" +const NATIVE_QUERY_ADDRESS_INPUT = null as unknown as string +function getQueryAddress(chain: Chain) { + return getNativeTokenDBAddress(chain) ?? NATIVE_QUERY_ADDRESS_INPUT +} + +const recentlySearchedAssetsAtom = atomWithStorage('recentlySearchedAssets', []) + +export function useAddRecentlySearchedAsset() { + const [searchHistory, updateSearchHistory] = useAtom(recentlySearchedAssetsAtom) + + return useCallback( + (asset: RecentlySearchedAsset) => { + // Removes the new asset if it was already in the array + const newHistory = searchHistory.filter( + (oldAsset) => !(oldAsset.address === asset.address && oldAsset.chain === asset.chain) + ) + newHistory.unshift(asset) + updateSearchHistory(newHistory) + }, + [searchHistory, updateSearchHistory] + ) +} + +export function useRecentlySearchedAssets() { + const history = useAtomValue(recentlySearchedAssetsAtom) + const shortenedHistory = useMemo(() => history.slice(0, 4), [history]) + + const { data: queryData, loading } = useRecentlySearchedAssetsQuery({ + variables: { + collectionAddresses: shortenedHistory.filter((asset) => asset.isNft).map((asset) => asset.address), + contracts: shortenedHistory + .filter((asset) => !asset.isNft) + .map((token) => ({ + address: token.address === NATIVE_CHAIN_ID ? getQueryAddress(token.chain) : token.address, + chain: token.chain, + })), + }, + }) + + const data = useMemo(() => { + if (shortenedHistory.length === 0) return [] + else if (!queryData) return undefined + // Collects both tokens and collections in a map, so they can later be returned in original order + const resultsMap: { [key: string]: GenieCollection | SearchToken } = {} + + const queryCollections = queryData?.nftCollections?.edges.map((edge) => edge.node as NonNullable) + const collections = queryCollections?.map( + (queryCollection): GenieCollection => { + return { + address: queryCollection.nftContracts?.[0]?.address ?? '', + isVerified: queryCollection?.isVerified, + name: queryCollection?.name, + stats: { + floor_price: queryCollection?.markets?.[0]?.floorPrice?.value, + total_supply: queryCollection?.numAssets, + }, + imageUrl: queryCollection?.image?.url ?? '', + } + }, + [queryCollections] + ) + collections?.forEach((collection) => (resultsMap[collection.address] = collection)) + queryData.tokens?.filter(Boolean).forEach((token) => { + resultsMap[token.address ?? `NATIVE-${token.chain}`] = token + }) + + const data: (SearchToken | GenieCollection)[] = [] + shortenedHistory.forEach((asset) => { + if (asset.address === NATIVE_CHAIN_ID) { + // Handles special case where wMATIC data needs to be used for MATIC + const chain = supportedChainIdFromGQLChain(asset.chain) + if (!chain) { + logSentryErrorForUnsupportedChain({ + extras: { asset }, + errorMessage: 'Invalid chain retrieved from Seach Token/Collection Query', + }) + return + } + const native = nativeOnChain(chain) + const queryAddress = getQueryAddress(asset.chain)?.toLowerCase() ?? `NATIVE-${asset.chain}` + const result = resultsMap[queryAddress] + if (result) data.push({ ...result, address: NATIVE_CHAIN_ID, ...native }) + } else { + const result = resultsMap[asset.address] + if (result) data.push(result) + } + }) + return data + }, [queryData, shortenedHistory]) + + return { data, loading } +} diff --git a/apps/web/src/components/NavBar/SearchBar.css.ts b/apps/web/src/components/NavBar/SearchBar.css.ts new file mode 100644 index 0000000..46820fa --- /dev/null +++ b/apps/web/src/components/NavBar/SearchBar.css.ts @@ -0,0 +1,239 @@ +import { style } from '@vanilla-extract/css' +import { subhead, subheadSmall } from 'nft/css/common.css' + +import { breakpoints, sprinkles, vars } from '../../nft/css/sprinkles.css' + +const DESKTOP_NAVBAR_WIDTH = 330 +const DESKTOP_NAVBAR_WIDTH_MD = 360 +const DESKTOP_NAVBAR_WIDTH_L = 480 +const DESKTOP_NAVBAR_WIDTH_XL = 520 +const DESKTOP_NAVBAR_WIDTH_XXL = 640 + +const baseSearchStyle = style([ + sprinkles({ + paddingY: '8', + width: { sm: 'viewWidth' }, + borderStyle: 'solid', + borderWidth: '1px', + borderColor: 'surface3', + }), + { + backdropFilter: 'blur(60px)', + '@media': { + [`screen and (min-width: ${breakpoints.sm}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH_MD}px`, + }, + }, + }, +]) + +const baseSearchNftStyle = style([ + baseSearchStyle, + { + '@media': { + [`screen and (min-width: ${breakpoints.md}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH}px`, + }, + [`screen and (min-width: ${breakpoints.lg}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH_MD}px`, + }, + [`screen and (min-width: ${breakpoints.xl}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH_L}px`, + }, + [`screen and (min-width: ${breakpoints.xxl}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH_XL}px`, + }, + [`screen and (min-width: ${breakpoints.xxxl}px)`]: { + width: `${DESKTOP_NAVBAR_WIDTH_XXL}px`, + }, + }, + }, +]) + +export const searchBarContainerNft = style([ + sprinkles({ + right: '0', + top: '0', + zIndex: '3', + display: 'flex', + maxHeight: 'searchResultsMaxHeight', + overflow: 'hidden', + }), + { + backdropFilter: 'blur(60px)', + borderRadius: '16px', + }, +]) + +export const searchBarContainerDisableBlur = style({ + backdropFilter: 'none', +}) + +export const searchBar = style([ + baseSearchStyle, + sprinkles({ + color: 'neutral2', + paddingX: '12', + }), +]) + +export const nftSearchBar = style([ + baseSearchNftStyle, + sprinkles({ + color: 'neutral2', + paddingX: '12', + }), + { + backdropFilter: 'blur(60px)', + }, +]) + +export const searchBarInput = style([ + sprinkles({ + padding: '0', + fontSize: '16', + fontWeight: 'book', + color: { default: 'neutral1', placeholder: 'neutral2' }, + border: 'none', + background: 'none', + lineHeight: '24', + height: 'full', + }), +]) + +export const searchBarDropdownNft = style([ + baseSearchNftStyle, + sprinkles({ + borderBottomLeftRadius: '16', + borderBottomRightRadius: '16', + height: { sm: 'viewHeight', md: 'auto' }, + backgroundColor: 'surface1', + }), + { + backdropFilter: 'blur(60px)', + borderTop: 'none', + }, +]) + +export const searchBarScrollable = sprinkles({ + overflowY: 'auto', +}) + +export const suggestionRow = style([ + sprinkles({ + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingY: '8', + paddingX: '16', + cursor: 'pointer', + }), + { + ':hover': { + background: vars.color.lightGrayOverlay, + }, + textDecoration: 'none', + }, +]) + +export const suggestionImage = sprinkles({ + width: '36', + height: '36', + borderRadius: 'round', + marginRight: '8', +}) + +export const suggestionPrimaryContainer = style([ + sprinkles({ + alignItems: 'flex-start', + }), + { + width: '90%', + }, +]) + +export const suggestionSecondaryContainer = sprinkles({ + textAlign: 'right', + alignItems: 'flex-end', +}) + +export const primaryText = style([ + subhead, + sprinkles({ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + color: 'neutral1', + }), + { + lineHeight: '24px', + }, +]) + +export const secondaryText = style([ + subheadSmall, + sprinkles({ + color: 'neutral2', + }), + { + lineHeight: '20px', + }, +]) + +export const imageHolder = style([ + suggestionImage, + sprinkles({ + background: 'surface2', + flexShrink: '0', + }), +]) + +export const suggestionIcon = sprinkles({ + display: 'flex', + flexShrink: '0', +}) + +export const sectionHeader = style([ + subheadSmall, + sprinkles({ + color: 'neutral2', + }), + { + lineHeight: '20px', + }, +]) + +export const notFoundContainer = style([ + sectionHeader, + sprinkles({ + paddingY: '4', + paddingLeft: '16', + }), +]) + +export const hidden = style([ + sprinkles({ + visibility: 'hidden', + opacity: '0', + padding: '0', + height: '0', + }), +]) +export const visible = style([ + sprinkles({ + visibility: 'visible', + opacity: '1', + height: 'full', + }), +]) + +export const searchContentLeftAlign = style({ + '@media': { + [`screen and (min-width: ${breakpoints.lg}px)`]: { + transform: 'translateX(0)', + transition: `transform ${vars.time[125]}`, + transitionTimingFunction: 'ease-in', + }, + }, +}) diff --git a/apps/web/src/components/NavBar/SearchBar.test.tsx b/apps/web/src/components/NavBar/SearchBar.test.tsx new file mode 100644 index 0000000..796beee --- /dev/null +++ b/apps/web/src/components/NavBar/SearchBar.test.tsx @@ -0,0 +1,32 @@ +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { useIsMobile, useIsTablet } from 'nft/hooks' +import { useIsNavSearchInputVisible } from 'nft/hooks/useIsNavSearchInputVisible' +import { mocked } from 'test-utils/mocked' +import { render, screen } from 'test-utils/render' + +import { SearchBar } from './SearchBar' + +jest.mock('hooks/useDisableNFTRoutes') +jest.mock('nft/hooks') +jest.mock('nft/hooks/useIsNavSearchInputVisible') + +describe('disable nft on searchbar', () => { + beforeEach(() => { + mocked(useIsMobile).mockReturnValue(false) + mocked(useIsTablet).mockReturnValue(false) + mocked(useIsNavSearchInputVisible).mockReturnValue(true) + }) + + it('should render text with nfts', () => { + mocked(useDisableNFTRoutes).mockReturnValue(false) + const { container } = render() + expect(container).toMatchSnapshot() + expect(screen.queryByPlaceholderText('Search tokens and NFT collections')).toBeVisible() + }) + it('should render text without nfts', () => { + mocked(useDisableNFTRoutes).mockReturnValue(true) + const { container } = render() + expect(container).toMatchSnapshot() + expect(screen.queryByPlaceholderText('Search tokens')).toBeVisible() + }) +}) diff --git a/apps/web/src/components/NavBar/SearchBar.tsx b/apps/web/src/components/NavBar/SearchBar.tsx new file mode 100644 index 0000000..0194e75 --- /dev/null +++ b/apps/web/src/components/NavBar/SearchBar.tsx @@ -0,0 +1,225 @@ +// eslint-disable-next-line no-restricted-imports +import { t } from '@lingui/macro' +import { useLingui } from '@lingui/react' +import { BrowserEvent, InterfaceElementName, InterfaceEventName, InterfaceSectionName } from '@uniswap/analytics-events' +import { useWeb3React } from '@web3-react/core' +import { sendAnalyticsEvent, Trace, TraceEvent, useTrace } from 'analytics' +import clsx from 'clsx' +import { Search } from 'components/Icons/Search' +import { useCollectionSearch } from 'graphql/data/nft/CollectionSearch' +import { useSearchTokens } from 'graphql/data/SearchTokens' +import useDebounce from 'hooks/useDebounce' +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { useIsNftPage } from 'hooks/useIsNftPage' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { organizeSearchResults } from 'lib/utils/searchBar' +import { Box } from 'nft/components/Box' +import { Column, Row } from 'nft/components/Flex' +import { magicalGradientOnHover } from 'nft/css/common.css' +import { useIsMobile, useIsTablet } from 'nft/hooks' +import { useIsNavSearchInputVisible } from 'nft/hooks/useIsNavSearchInputVisible' +import { ChangeEvent, useCallback, useEffect, useReducer, useRef, useState } from 'react' +import { useLocation } from 'react-router-dom' +import styled from 'styled-components' + +import { ChevronLeftIcon, NavMagnifyingGlassIcon } from '../../nft/components/icons' +import { NavIcon } from './NavIcon' +import * as styles from './SearchBar.css' +import { SearchBarDropdown } from './SearchBarDropdown' + +const KeyShortCut = styled.div` + background-color: ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral2}; + padding: 0px 8px; + width: 20px; + height: 20px; + border-radius: 4px; + font-size: 12px; + font-weight: 535; + line-height: 16px; + display: flex; + align-items: center; + opacity: 0.6; + backdrop-filter: blur(60px); +` + +export const SearchBar = () => { + const [isOpen, toggleOpen] = useReducer((state: boolean) => !state, false) + const [searchValue, setSearchValue] = useState('') + const debouncedSearchValue = useDebounce(searchValue, 300) + const searchRef = useRef(null) + const inputRef = useRef(null) + const { pathname } = useLocation() + const isMobile = useIsMobile() + const isTablet = useIsTablet() + const isNavSearchInputVisible = useIsNavSearchInputVisible() + const shouldDisableNFTRoutes = useDisableNFTRoutes() + + useOnClickOutside(searchRef, () => { + isOpen && toggleOpen() + }) + + const { data: collections, loading: collectionsAreLoading } = useCollectionSearch(debouncedSearchValue) + + const { chainId } = useWeb3React() + const { data: tokens, loading: tokensAreLoading } = useSearchTokens(debouncedSearchValue, chainId ?? 1) + + const isNFTPage = useIsNftPage() + + const [reducedTokens, reducedCollections] = organizeSearchResults(isNFTPage, tokens ?? [], collections ?? []) + + // close dropdown on escape + useEffect(() => { + const escapeKeyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'Escape' && isOpen) { + event.preventDefault() + toggleOpen() + } + } + + document.addEventListener('keydown', escapeKeyDownHandler) + + return () => { + document.removeEventListener('keydown', escapeKeyDownHandler) + } + }, [isOpen, toggleOpen, collections]) + + // clear searchbar when changing pages + useEffect(() => { + setSearchValue('') + }, [pathname]) + + // auto set cursor when searchbar is opened + useEffect(() => { + if (isOpen) { + inputRef.current?.focus() + } + }, [isOpen]) + + const isMobileOrTablet = isMobile || isTablet || !isNavSearchInputVisible + + const trace = useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH }) + + const navbarSearchEventProperties = { + navbar_search_input_text: debouncedSearchValue, + hasInput: debouncedSearchValue && debouncedSearchValue.length > 0, + ...trace, + } + + const { i18n } = useLingui() // subscribe to locale changes + const placeholderText = isMobileOrTablet + ? t(i18n)`Search` + : shouldDisableNFTRoutes + ? t(i18n)`Search tokens` + : t(i18n)`Search tokens and NFT collections` + + const handleKeyPress = useCallback( + (event: any) => { + if (event.key === '/' && event.target.tagName !== 'INPUT') { + event.preventDefault() + !isOpen && toggleOpen() + } + }, + [isOpen] + ) + + useEffect(() => { + const innerRef = inputRef.current + + if (innerRef !== null) { + //only mount the listener when input available as ref + document.addEventListener('keydown', handleKeyPress) + } + + return () => { + if (innerRef !== null) { + document.removeEventListener('keydown', handleKeyPress) + } + } + }, [handleKeyPress, inputRef]) + + return ( + + + !isOpen && toggleOpen()} + gap="12" + > + + + + + + + + + + ) => { + !isOpen && toggleOpen() + setSearchValue(event.target.value) + }} + onBlur={() => sendAnalyticsEvent(InterfaceEventName.NAVBAR_SEARCH_EXITED, navbarSearchEventProperties)} + className={`${styles.searchBarInput} ${styles.searchContentLeftAlign}`} + value={searchValue} + ref={inputRef} + width="full" + /> + + {!isOpen && /} + + + {isOpen && ( + 0} + isLoading={tokensAreLoading || collectionsAreLoading} + /> + )} + + + {isMobileOrTablet && ( + + + + )} + + ) +} diff --git a/apps/web/src/components/NavBar/SearchBarDropdown.test.tsx b/apps/web/src/components/NavBar/SearchBarDropdown.test.tsx new file mode 100644 index 0000000..9dcf66d --- /dev/null +++ b/apps/web/src/components/NavBar/SearchBarDropdown.test.tsx @@ -0,0 +1,32 @@ +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { mocked } from 'test-utils/mocked' +import { render } from 'test-utils/render' + +import { SearchBarDropdown } from './SearchBarDropdown' + +jest.mock('hooks/useDisableNFTRoutes') + +const SearchBarDropdownProps = { + toggleOpen: () => void 0, + tokens: [], + collections: [], + queryText: '', + hasInput: false, + isLoading: false, +} + +describe('disable nft on searchbar dropdown', () => { + it('should render popular nft collections', () => { + mocked(useDisableNFTRoutes).mockReturnValue(false) + const { container } = render() + expect(container).toMatchSnapshot() + expect(container).toHaveTextContent('Popular NFT collections') + }) + it('should not render popular nft collections', () => { + mocked(useDisableNFTRoutes).mockReturnValue(true) + const { container } = render() + expect(container).toMatchSnapshot() + expect(container).not.toHaveTextContent('Popular NFT collections') + expect(container).not.toHaveTextContent('NFT') + }) +}) diff --git a/apps/web/src/components/NavBar/SearchBarDropdown.tsx b/apps/web/src/components/NavBar/SearchBarDropdown.tsx new file mode 100644 index 0000000..e10371a --- /dev/null +++ b/apps/web/src/components/NavBar/SearchBarDropdown.tsx @@ -0,0 +1,365 @@ +import { Trans } from '@lingui/macro' +import { InterfaceSectionName, NavBarSearchTypes } from '@uniswap/analytics-events' +import { ChainId } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { useTrace } from 'analytics' +import clsx from 'clsx' +import Badge from 'components/Badge' +import { ChainLogo } from 'components/Logo/ChainLogo' +import { SearchToken } from 'graphql/data/SearchTokens' +import useTrendingTokens from 'graphql/data/TrendingTokens' +import { HistoryDuration, SafetyLevel } from 'graphql/data/__generated__/types-and-hooks' +import { useTrendingCollections } from 'graphql/data/nft/TrendingCollections' +import { BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS } from 'graphql/data/util' +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { useIsNftPage } from 'hooks/useIsNftPage' +import { Box } from 'nft/components/Box' +import { Column, Row } from 'nft/components/Flex' +import { subheadSmall } from 'nft/css/common.css' +import { GenieCollection, TrendingCollection } from 'nft/types' +import { useEffect, useMemo, useState } from 'react' +import { useLocation } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +import { ClockIcon, TrendingArrow } from '../../nft/components/icons' +import { SuspendConditionally } from '../Suspense/SuspendConditionally' +import { SuspenseWithPreviousRenderAsFallback } from '../Suspense/SuspenseWithPreviousRenderAsFallback' +import { useRecentlySearchedAssets } from './RecentlySearchedAssets' +import * as styles from './SearchBar.css' +import { CollectionRow, SkeletonRow, TokenRow } from './SuggestionRow' + +function isCollection(suggestion: GenieCollection | SearchToken | TrendingCollection) { + return (suggestion as SearchToken).decimals === undefined +} + +interface SearchBarDropdownSectionProps { + toggleOpen: () => void + suggestions: (GenieCollection | SearchToken)[] + header: JSX.Element + headerIcon?: JSX.Element + hoveredIndex?: number + startingIndex: number + setHoveredIndex: (index: number | undefined) => void + isLoading?: boolean + eventProperties: Record +} + +const SearchBarDropdownSection = ({ + toggleOpen, + suggestions, + header, + headerIcon = undefined, + hoveredIndex, + startingIndex, + setHoveredIndex, + isLoading, + eventProperties, +}: SearchBarDropdownSectionProps) => { + return ( + + + {headerIcon ? headerIcon : null} + {header} + + + {suggestions.map((suggestion, index) => + isLoading || !suggestion ? ( + + ) : isCollection(suggestion) ? ( + + ) : ( + + ) + )} + + + ) +} + +function isKnownToken(token: SearchToken) { + return token.project?.safetyLevel == SafetyLevel.Verified || token.project?.safetyLevel == SafetyLevel.MediumWarning +} + +const ChainComingSoonBadge = styled(Badge)` + align-items: center; + background-color: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral2}; + display: flex; + flex-direction: row; + justify-content: flex-start; + opacity: 1; + padding: 8px; + margin: 16px 16px 4px; + width: calc(100% - 32px); + gap: 8px; +` + +interface SearchBarDropdownProps { + toggleOpen: () => void + tokens: SearchToken[] + collections: GenieCollection[] + queryText: string + hasInput: boolean + isLoading: boolean +} + +export const SearchBarDropdown = (props: SearchBarDropdownProps) => { + const { isLoading } = props + const { chainId } = useWeb3React() + const showChainComingSoonBadge = chainId && BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.includes(chainId) && !isLoading + + return ( + + + + + + + + {showChainComingSoonBadge && ( + + + + + + + )} + + + ) +} + +function SearchBarDropdownContents({ + toggleOpen, + tokens, + collections, + queryText, + hasInput, +}: SearchBarDropdownProps): JSX.Element { + const [hoveredIndex, setHoveredIndex] = useState(0) + const { data: searchHistory } = useRecentlySearchedAssets() + const shortenedHistory = useMemo(() => searchHistory?.slice(0, 2) ?? [...Array(2)], [searchHistory]) + const { pathname } = useLocation() + const isNFTPage = useIsNftPage() + const isTokenPage = pathname.includes('/tokens') + const shouldDisableNFTRoutes = useDisableNFTRoutes() + + const { data: trendingCollections, loading: trendingCollectionsAreLoading } = useTrendingCollections( + 3, + HistoryDuration.Day + ) + + const formattedTrendingCollections = useMemo(() => { + return !trendingCollectionsAreLoading + ? trendingCollections + ?.map((collection) => ({ + ...collection, + collectionAddress: collection.address, + floorPrice: collection.floor, + stats: { + total_supply: collection.totalSupply, + one_day_change: collection.floorChange, + floor_price: collection.floor, + }, + })) + .slice(0, isNFTPage ? 3 : 2) ?? [] + : [...Array(isNFTPage ? 3 : 2)] + }, [trendingCollections, isNFTPage, trendingCollectionsAreLoading]) + + const { data: trendingTokenData } = useTrendingTokens(useWeb3React().chainId) + + const trendingTokensLength = isTokenPage ? 3 : 2 + const trendingTokens = useMemo( + () => trendingTokenData?.slice(0, trendingTokensLength) ?? [...Array(trendingTokensLength)], + [trendingTokenData, trendingTokensLength] + ) + + const totalSuggestions = hasInput + ? tokens.length + collections.length + : Math.min(shortenedHistory.length, 2) + + (isNFTPage || !isTokenPage ? formattedTrendingCollections?.length ?? 0 : 0) + + (isTokenPage || !isNFTPage ? trendingTokens?.length ?? 0 : 0) + + // Navigate search results via arrow keys + useEffect(() => { + const keyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'ArrowUp') { + event.preventDefault() + if (!hoveredIndex) { + setHoveredIndex(totalSuggestions - 1) + } else { + setHoveredIndex(hoveredIndex - 1) + } + } else if (event.key === 'ArrowDown') { + event.preventDefault() + if (hoveredIndex && hoveredIndex === totalSuggestions - 1) { + setHoveredIndex(0) + } else { + setHoveredIndex((hoveredIndex ?? -1) + 1) + } + } + } + + document.addEventListener('keydown', keyDownHandler) + + return () => { + document.removeEventListener('keydown', keyDownHandler) + } + }, [toggleOpen, hoveredIndex, totalSuggestions]) + + const hasVerifiedCollection = collections.some((collection) => collection.isVerified) + const hasKnownToken = tokens.some(isKnownToken) + const showCollectionsFirst = + (isNFTPage && (hasVerifiedCollection || !hasKnownToken)) || (!isNFTPage && !hasKnownToken && hasVerifiedCollection) + + const trace = JSON.stringify(useTrace({ section: InterfaceSectionName.NAVBAR_SEARCH })) + + const eventProperties = { + total_suggestions: totalSuggestions, + query_text: queryText, + ...JSON.parse(trace), + } + + const tokenSearchResults = + tokens.length > 0 ? ( + Tokens} + /> + ) : ( + + No tokens found. + + ) + + const collectionSearchResults = + collections.length > 0 ? ( + NFT collections} + /> + ) : ( + No NFT collections found. + ) + + return hasInput ? ( + // Empty or Up to 8 combined tokens and nfts + + {showCollectionsFirst ? ( + <> + {collectionSearchResults} + {tokenSearchResults} + + ) : ( + <> + {tokenSearchResults} + {collectionSearchResults} + + )} + + ) : ( + // Recent Searches, Trending Tokens, Trending Collections + + {shortenedHistory.length > 0 && ( + Recent searches} + headerIcon={} + isLoading={!searchHistory} + /> + )} + {!isNFTPage && ( + Popular tokens} + headerIcon={} + isLoading={!trendingTokenData} + /> + )} + {Boolean(!isTokenPage && !shouldDisableNFTRoutes) && ( + Popular NFT collections} + headerIcon={} + isLoading={trendingCollectionsAreLoading} + /> + )} + + ) +} + +function ComingSoonText({ chainId }: { chainId: ChainId }) { + switch (chainId) { + case ChainId.AVALANCHE: + return Coming soon: search and explore tokens on Avalanche Chain + default: + return null + } +} diff --git a/apps/web/src/components/NavBar/SuggestionRow.tsx b/apps/web/src/components/NavBar/SuggestionRow.tsx new file mode 100644 index 0000000..99b83f4 --- /dev/null +++ b/apps/web/src/components/NavBar/SuggestionRow.tsx @@ -0,0 +1,229 @@ +import { InterfaceEventName } from '@uniswap/analytics-events' +import { sendAnalyticsEvent } from 'analytics' +import clsx from 'clsx' +import QueryTokenLogo from 'components/Logo/QueryTokenLogo' +import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon' +import { checkSearchTokenWarning } from 'constants/tokenSafety' +import { SearchToken } from 'graphql/data/SearchTokens' +import { Chain, TokenStandard } from 'graphql/data/__generated__/types-and-hooks' +import { getTokenDetailsURL } from 'graphql/data/util' +import { Box } from 'nft/components/Box' +import { Column, Row } from 'nft/components/Flex' +import { VerifiedIcon } from 'nft/components/icons' +import { vars } from 'nft/css/sprinkles.css' +import { GenieCollection } from 'nft/types' +import { useCallback, useEffect, useState } from 'react' +import { Link, useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { DeltaArrow, DeltaText } from '../Tokens/TokenDetails/Delta' +import { useAddRecentlySearchedAsset } from './RecentlySearchedAssets' +import * as styles from './SearchBar.css' + +const PriceChangeContainer = styled.div` + display: flex; + align-items: center; + padding-top: 4px; + gap: 2px; +` + +interface CollectionRowProps { + collection: GenieCollection + isHovered: boolean + setHoveredIndex: (index: number | undefined) => void + toggleOpen: () => void + index: number + eventProperties: Record +} + +export const CollectionRow = ({ + collection, + isHovered, + setHoveredIndex, + toggleOpen, + index, + eventProperties, +}: CollectionRowProps) => { + const { formatNumberOrString } = useFormatter() + + const [brokenImage, setBrokenImage] = useState(false) + const [loaded, setLoaded] = useState(false) + + const addRecentlySearchedAsset = useAddRecentlySearchedAsset() + const navigate = useNavigate() + + const handleClick = useCallback(() => { + addRecentlySearchedAsset({ ...collection, isNft: true, chain: Chain.Ethereum }) + toggleOpen() + sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties }) + }, [addRecentlySearchedAsset, collection, toggleOpen, eventProperties]) + + useEffect(() => { + const keyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'Enter' && isHovered) { + event.preventDefault() + navigate(`/nfts/collection/${collection.address}`) + handleClick() + } + } + document.addEventListener('keydown', keyDownHandler) + return () => { + document.removeEventListener('keydown', keyDownHandler) + } + }, [toggleOpen, isHovered, collection, navigate, handleClick]) + + return ( + !isHovered && setHoveredIndex(index)} + onMouseLeave={() => isHovered && setHoveredIndex(undefined)} + className={styles.suggestionRow} + style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }} + > + + {!brokenImage && collection.imageUrl ? ( + setBrokenImage(true)} + onLoad={() => setLoaded(true)} + /> + ) : ( + + )} + + + {collection.name} + {collection.isVerified && } + + + {formatNumberOrString({ input: collection?.stats?.total_supply, type: NumberType.WholeNumber })} items + + + + {collection.stats?.floor_price ? ( + + + + {formatNumberOrString({ input: collection.stats?.floor_price, type: NumberType.NFTToken })} ETH + + + Floor + + ) : null} + + ) +} + +interface TokenRowProps { + token: SearchToken + isHovered: boolean + setHoveredIndex: (index: number | undefined) => void + toggleOpen: () => void + index: number + eventProperties: Record +} + +export const TokenRow = ({ token, isHovered, setHoveredIndex, toggleOpen, index, eventProperties }: TokenRowProps) => { + const addRecentlySearchedAsset = useAddRecentlySearchedAsset() + const navigate = useNavigate() + const { formatFiatPrice, formatDelta } = useFormatter() + + const handleClick = useCallback(() => { + const address = !token.address && token.standard === TokenStandard.Native ? NATIVE_CHAIN_ID : token.address + address && addRecentlySearchedAsset({ address, chain: token.chain }) + + toggleOpen() + sendAnalyticsEvent(InterfaceEventName.NAVBAR_RESULT_SELECTED, { ...eventProperties }) + }, [addRecentlySearchedAsset, token, toggleOpen, eventProperties]) + + const tokenDetailsPath = getTokenDetailsURL({ ...token }) + // Close the modal on escape + useEffect(() => { + const keyDownHandler = (event: KeyboardEvent) => { + if (event.key === 'Enter' && isHovered) { + event.preventDefault() + navigate(tokenDetailsPath) + handleClick() + } + } + document.addEventListener('keydown', keyDownHandler) + return () => { + document.removeEventListener('keydown', keyDownHandler) + } + }, [toggleOpen, isHovered, token, navigate, handleClick, tokenDetailsPath]) + + return ( + !isHovered && setHoveredIndex(index)} + onMouseLeave={() => isHovered && setHoveredIndex(undefined)} + className={styles.suggestionRow} + style={{ background: isHovered ? vars.color.lightGrayOverlay : 'none' }} + > + + + + + {token.name} + + + {token.symbol} + + + + + {!!token.market?.price?.value && ( + <> + + {formatFiatPrice({ price: token.market.price.value })} + + + + + + {formatDelta(Math.abs(token.market?.pricePercentChange?.value ?? 0))} + + + + + )} + + + ) +} + +export const SkeletonRow = () => { + return ( + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/web/src/components/NavBar/UkBanner.tsx b/apps/web/src/components/NavBar/UkBanner.tsx new file mode 100644 index 0000000..76bcbce --- /dev/null +++ b/apps/web/src/components/NavBar/UkBanner.tsx @@ -0,0 +1,82 @@ +import { t, Trans } from '@lingui/macro' +import { useOpenModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled from 'styled-components' +import { ButtonText, ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' + +export const UK_BANNER_HEIGHT = 65 +export const UK_BANNER_HEIGHT_MD = 113 +export const UK_BANNER_HEIGHT_SM = 137 + +const BannerWrapper = styled.div` + position: relative; + display: flex; + background-color: ${({ theme }) => theme.surface1}; + padding: 20px; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + z-index: ${Z_INDEX.fixed}; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { + flex-direction: column; + } +` + +const BannerTextWrapper = styled(ThemedText.BodySecondary)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.md}px`}) { + @supports (-webkit-line-clamp: 2) { + white-space: initial; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + } + } + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + @supports (-webkit-line-clamp: 3) { + white-space: initial; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + } + } +` + +const ReadMoreWrapper = styled(ButtonText)` + flex-shrink: 0; + width: max-content; + + :focus { + text-decoration: none; + } +` + +export const bannerText = t` + This web application is provided as a tool for users to interact with the Uniswap Protocol on + their own initiative, with no endorsement or recommendation of cryptocurrency trading activities. In doing so, + Uniswap is not recommending that users or potential users engage in cryptoasset trading activity, and users or + potential users of the web application should not regard this webpage or its contents as involving any form of + recommendation, invitation or inducement to deal in cryptoassets. +` + +export function UkBanner() { + const openDisclaimer = useOpenModal(ApplicationModal.UK_DISCLAIMER) + + return ( + + {t`UK disclaimer:` + ' ' + bannerText} + + + Read more + + + + ) +} diff --git a/apps/web/src/components/NavBar/UkDisclaimerModal.tsx b/apps/web/src/components/NavBar/UkDisclaimerModal.tsx new file mode 100644 index 0000000..f45c2ca --- /dev/null +++ b/apps/web/src/components/NavBar/UkDisclaimerModal.tsx @@ -0,0 +1,62 @@ +import { Trans } from '@lingui/macro' +import { ButtonEmphasis, ButtonSize, ThemeButton } from 'components/Button' +import Column from 'components/Column' +import Modal from 'components/Modal' +import { X } from 'react-feather' +import { useCloseModal, useModalIsOpen } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled from 'styled-components' +import { ButtonText, ThemedText } from 'theme/components' + +import { bannerText } from './UkBanner' + +const Wrapper = styled(Column)` + padding: 8px; +` + +const ButtonContainer = styled(Column)` + padding: 8px 12px 4px; +` + +const CloseIconWrapper = styled(ButtonText)` + display: flex; + color: ${({ theme }) => theme.neutral1}; + justify-content: flex-end; + padding: 8px 0px 4px; + + :focus { + text-decoration: none; + } +` + +const StyledThemeButton = styled(ThemeButton)` + width: 100%; +` + +export function UkDisclaimerModal() { + const isOpen = useModalIsOpen(ApplicationModal.UK_DISCLAIMER) + const closeModal = useCloseModal() + + return ( + + + closeModal()}> + + + + + Disclaimer for UK residents + + + {bannerText} + + + + closeModal()}> + Dismiss + + + + + ) +} diff --git a/apps/web/src/components/NavBar/__snapshots__/ChainSelectorRow.test.tsx.snap b/apps/web/src/components/NavBar/__snapshots__/ChainSelectorRow.test.tsx.snap new file mode 100644 index 0000000..ac257b4 --- /dev/null +++ b/apps/web/src/components/NavBar/__snapshots__/ChainSelectorRow.test.tsx.snap @@ -0,0 +1,1795 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ChainSelectorRow should match snapshot for chainId 1 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 5 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 10 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 56 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 137 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 420 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 8453 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 42161 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 42220 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 43114 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 44787 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 80001 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 84531 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 421613 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 421614 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 11155111 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 2; + grid-row: 1; + font-size: 16px; + font-weight: 485; +} + +.c2 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; + +exports[`ChainSelectorRow should match snapshot for chainId 11155420 1`] = ` +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: none; + border: none; + border-radius: 12px; + color: #222222; + cursor: pointer; + display: grid; + grid-template-columns: min-content 1fr min-content; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + line-height: 20px; + opacity: 1; + padding: 10px 8px; + text-align: left; + -webkit-transition: 250ms ease background-color; + transition: 250ms ease background-color; + width: 240px; +} + +.c0:hover { + background-color: #22222212; +} + +.c1 { + grid-column: 3; + grid-row: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 20px; +} + +@media only screen and (max-width:640px) { + .c0 { + width: 100%; + } +} + +
+ + + + + +
+`; diff --git a/apps/web/src/components/NavBar/__snapshots__/SearchBar.test.tsx.snap b/apps/web/src/components/NavBar/__snapshots__/SearchBar.test.tsx.snap new file mode 100644 index 0000000..96bffcb --- /dev/null +++ b/apps/web/src/components/NavBar/__snapshots__/SearchBar.test.tsx.snap @@ -0,0 +1,197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disable nft on searchbar should render text with nfts 1`] = ` +.c0 { + background-color: #22222212; + color: #7D7D7D; + padding: 0px 8px; + width: 20px; + height: 20px; + border-radius: 4px; + font-size: 12px; + font-weight: 535; + line-height: 16px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + opacity: 0.6; + -webkit-backdrop-filter: blur(60px); + backdrop-filter: blur(60px); +} + +
+ + +
+
+
+
+ + + +
+
+ + + +
+
+ +
+ / +
+
+
+
+ + +
+`; + +exports[`disable nft on searchbar should render text without nfts 1`] = ` +.c0 { + background-color: #22222212; + color: #7D7D7D; + padding: 0px 8px; + width: 20px; + height: 20px; + border-radius: 4px; + font-size: 12px; + font-weight: 535; + line-height: 16px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + opacity: 0.6; + -webkit-backdrop-filter: blur(60px); + backdrop-filter: blur(60px); +} + +
+ + +
+
+
+
+ + + +
+
+ + + +
+
+ +
+ / +
+
+
+
+ + +
+`; diff --git a/apps/web/src/components/NavBar/__snapshots__/SearchBarDropdown.test.tsx.snap b/apps/web/src/components/NavBar/__snapshots__/SearchBarDropdown.test.tsx.snap new file mode 100644 index 0000000..b741d79 --- /dev/null +++ b/apps/web/src/components/NavBar/__snapshots__/SearchBarDropdown.test.tsx.snap @@ -0,0 +1,360 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`disable nft on searchbar dropdown should not render popular nft collections 1`] = ` +
+ + +
+
+
+
+
+ + + +
+ Popular tokens +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+`; + +exports[`disable nft on searchbar dropdown should render popular nft collections 1`] = ` +
+ + +
+
+
+
+
+ + + +
+ Popular tokens +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +
+ Popular NFT collections +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + +
+`; diff --git a/apps/web/src/components/NavBar/index.tsx b/apps/web/src/components/NavBar/index.tsx new file mode 100644 index 0000000..82bd3a2 --- /dev/null +++ b/apps/web/src/components/NavBar/index.tsx @@ -0,0 +1,171 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { UniIcon } from 'components/Logo/UniIcon' +import Web3Status from 'components/Web3Status' +import { chainIdToBackendName } from 'graphql/data/util' +import { useDisableNFTRoutes } from 'hooks/useDisableNFTRoutes' +import { useIsLandingPage } from 'hooks/useIsLandingPage' +import { useIsNftPage } from 'hooks/useIsNftPage' +import { useIsPoolsPage } from 'hooks/useIsPoolsPage' +import { Box } from 'nft/components/Box' +import { Row } from 'nft/components/Flex' +import { useProfilePageState } from 'nft/hooks' +import { ProfilePageStateType } from 'nft/types' +import { GetTheAppButton } from 'pages/Landing/components/DownloadApp/GetTheAppButton' +import { ReactNode, useCallback } from 'react' +import { NavLink, NavLinkProps, useLocation, useNavigate } from 'react-router-dom' +import styled from 'styled-components' + +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { Chain } from 'graphql/data/__generated__/types-and-hooks' +import { Z_INDEX } from 'theme/zIndex' +import { useIsNavSearchInputVisible } from '../../nft/hooks/useIsNavSearchInputVisible' +import { Bag } from './Bag' +import Blur from './Blur' +import { ChainSelector } from './ChainSelector' +import { More } from './More' +import { SearchBar } from './SearchBar' +import * as styles from './style.css' + +const Nav = styled.nav` + padding: ${({ theme }) => `${theme.navVerticalPad}px 12px`}; + width: 100%; + height: ${({ theme }) => theme.navHeight}px; + z-index: ${Z_INDEX.sticky}; +` + +interface MenuItemProps { + href: string + id?: NavLinkProps['id'] + isActive?: boolean + children: ReactNode + dataTestId?: string +} + +const MenuItem = ({ href, dataTestId, id, isActive, children }: MenuItemProps) => { + return ( + + {children} + + ) +} + +export const PageTabs = () => { + const { pathname } = useLocation() + const { chainId: connectedChainId } = useWeb3React() + const chainName = chainIdToBackendName(connectedChainId) + + const isPoolActive = useIsPoolsPage() + const isNftPage = useIsNftPage() + + const shouldDisableNFTRoutes = useDisableNFTRoutes() + + return ( + <> + + Swap + + + Explore + + {!shouldDisableNFTRoutes && ( + + NFTs + + )} + + + Pool + + + + + ) +} + +const Navbar = ({ blur }: { blur: boolean }) => { + const isNftPage = useIsNftPage() + const isLandingPage = useIsLandingPage() + const sellPageState = useProfilePageState((state) => state.state) + const navigate = useNavigate() + const isNavSearchInputVisible = useIsNavSearchInputVisible() + + const { account } = useWeb3React() + const [accountDrawerOpen, toggleAccountDrawer] = useAccountDrawer() + const handleUniIconClick = useCallback(() => { + if (account) { + return + } + if (accountDrawerOpen) { + toggleAccountDrawer() + } + navigate({ + pathname: '/', + search: '?intro=true', + }) + }, [account, accountDrawerOpen, navigate, toggleAccountDrawer]) + + return ( + <> + {blur && } + + + ) +} + +export default Navbar diff --git a/apps/web/src/components/NavBar/style.css.ts b/apps/web/src/components/NavBar/style.css.ts new file mode 100644 index 0000000..ababbc2 --- /dev/null +++ b/apps/web/src/components/NavBar/style.css.ts @@ -0,0 +1,99 @@ +import { style } from '@vanilla-extract/css' + +import { subhead } from '../../nft/css/common.css' +import { sprinkles, vars } from '../../nft/css/sprinkles.css' + +export const logoContainer = style([ + sprinkles({ + display: 'flex', + marginRight: '12', + alignItems: 'center', + cursor: 'pointer', + }), +]) + +export const logo = style([ + sprinkles({ + display: 'block', + color: 'accent1', + }), +]) + +export const baseSideContainer = style([ + sprinkles({ + display: 'flex', + width: 'full', + flex: '1', + flexShrink: '2', + }), +]) + +export const leftSideContainer = style([ + baseSideContainer, + sprinkles({ + alignItems: 'center', + justifyContent: 'flex-start', + }), +]) + +export const searchContainer = style([ + sprinkles({ + flex: '1', + flexShrink: '1', + justifyContent: { lg: 'flex-end', xl: 'center' }, + display: { sm: 'none' }, + alignSelf: 'center', + + alignItems: 'flex-start', + }), + { height: '42px' }, +]) + +export const rightSideContainer = style([ + baseSideContainer, + sprinkles({ + alignItems: 'center', + justifyContent: 'flex-end', + }), +]) + +const baseMenuItem = style([ + subhead, + sprinkles({ + paddingY: '8', + paddingX: { sm: '6', md: '14' }, + marginY: '4', + borderRadius: '14', + marginX: { sm: '4', md: '0' }, + transition: '250', + height: 'min', + width: 'full', + textAlign: 'center', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + gap: '4', + }), + { + lineHeight: '22px', + textDecoration: 'none', + ':hover': { + background: vars.color.lightGrayOverlay, + }, + }, +]) + +export const menuItem = style([ + baseMenuItem, + sprinkles({ + color: 'neutral2', + }), +]) + +export const activeMenuItem = style([ + baseMenuItem, + sprinkles({ + color: 'neutral1', + background: 'none', + }), +]) diff --git a/apps/web/src/components/NavigationTabs/index.tsx b/apps/web/src/components/NavigationTabs/index.tsx new file mode 100644 index 0000000..eed5e7e --- /dev/null +++ b/apps/web/src/components/NavigationTabs/index.tsx @@ -0,0 +1,118 @@ +import { Trans } from '@lingui/macro' +import { Percent } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import SettingsTab from 'components/Settings' +import { ReactNode } from 'react' +import { ArrowLeft } from 'react-feather' +import { Link, useNavigate } from 'react-router-dom' +import { Box } from 'rebass' +import { useAppDispatch } from 'state/hooks' +import { resetMintState } from 'state/mint/actions' +import { resetMintState as resetMintV3State } from 'state/mint/v3/actions' +import styled, { useTheme } from 'styled-components' +import { ThemedText } from 'theme/components' +import { flexRowNoWrap } from 'theme/styles' + +import { RowBetween } from '../Row' + +const Tabs = styled.div` + ${flexRowNoWrap}; + align-items: center; + border-radius: 3rem; + justify-content: space-evenly; +` + +const StyledLink = styled(Link)<{ flex?: string }>` + flex: ${({ flex }) => flex ?? 'none'}; + display: flex; + align-items: center; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium` + flex: none; + margin-right: 10px; + `}; +` + +const FindPoolTabsText = styled(ThemedText.H1Small)` + position: absolute; + left: 50%; + transform: translateX(-50%); +` + +const StyledArrowLeft = styled(ArrowLeft)` + color: ${({ theme }) => theme.neutral1}; +` + +export function FindPoolTabs({ origin }: { origin: string }) { + return ( + + + + + + + Import V2 pool + + + + ) +} + +const AddRemoveTitleText = styled(ThemedText.H1Small)<{ $center: boolean }>` + flex: 1; + margin: auto; + text-align: ${({ $center }) => ($center ? 'center' : 'start')}; +` + +export function AddRemoveTabs({ + adding, + creating, + autoSlippage, + children, +}: { + adding: boolean + creating: boolean + autoSlippage: Percent + showBackLink?: boolean + children?: ReactNode +}) { + const { chainId } = useWeb3React() + const theme = useTheme() + // reset states on back + const dispatch = useAppDispatch() + const navigate = useNavigate() + + return ( + + + { + e.preventDefault() + navigate(-1) + + if (adding) { + // not 100% sure both of these are needed + dispatch(resetMintState()) + dispatch(resetMintV3State()) + } + }} + flex={children ? '1' : undefined} + > + + + + {creating ? ( + Create a pair + ) : adding ? ( + Add liquidity + ) : ( + Remove liquidity + )} + + {children && {children}} + + + + ) +} diff --git a/apps/web/src/components/NetworkAlert/NetworkAlert.tsx b/apps/web/src/components/NetworkAlert/NetworkAlert.tsx new file mode 100644 index 0000000..dca6101 --- /dev/null +++ b/apps/web/src/components/NetworkAlert/NetworkAlert.tsx @@ -0,0 +1,75 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { getChainUI } from 'components/Logo/ChainLogo' +import { RowBetween } from 'components/Row' +import { getChainInfo } from 'constants/chainInfo' +import { isSupportedChain } from 'constants/chains' +import { ArrowUpRight } from 'react-feather' +import styled from 'styled-components' +import { ExternalLink, HideSmall, ThemedText } from 'theme/components' +import { useIsDarkMode } from 'theme/components/ThemeToggle' + +import Column from '../Column' + +const BridgeLink = styled(ExternalLink)<{ bgColor: string }>` + color: ${({ color }) => color}; + background: ${({ bgColor }) => bgColor}; + align-items: center; + border-radius: 8px; + color: white; + display: flex; + font-size: 16px; + justify-content: space-between; + padding: 12px 18px 12px 12px; + text-decoration: none !important; + width: 100%; + + border-radius: 20px; + display: flex; + flex-direction: row; + gap: 12px; + overflow: hidden; + position: relative; + width: 100%; + + margin-top: 16px; +` + +const TitleText = styled(ThemedText.BodyPrimary)<{ $color: string }>` + font-weight: 535; + color: ${({ $color }) => $color}; +` + +const SubtitleText = styled(ThemedText.BodySmall)<{ $color: string }>` + line-height: 20px; + color: ${({ $color }) => $color}; +` + +export function NetworkAlert() { + const { chainId } = useWeb3React() + const darkMode = useIsDarkMode() + + if (!chainId || !isSupportedChain(chainId)) return null + + const { Symbol: ChainSymbol, bgColor, textColor } = getChainUI(chainId, darkMode) + const { label, bridge } = getChainInfo(chainId) + + return bridge ? ( + + + + + + {label} token bridge + + + + Deposit tokens to the {label} network. + + + + + + + ) : null +} diff --git a/apps/web/src/components/NumericalInput/index.tsx b/apps/web/src/components/NumericalInput/index.tsx new file mode 100644 index 0000000..7e128d2 --- /dev/null +++ b/apps/web/src/components/NumericalInput/index.tsx @@ -0,0 +1,133 @@ +import { loadingOpacityMixin } from 'components/Loader/styled' +import { SupportedLocale } from 'constants/locales' +import React, { forwardRef } from 'react' +import styled from 'styled-components' +import { escapeRegExp } from 'utils' +import { useFormatterLocales } from 'utils/formatNumbers' + +const StyledInput = styled.input<{ error?: boolean; fontSize?: string; align?: string; disabled?: boolean }>` + color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; + width: 0; + position: relative; + font-weight: 485; + outline: none; + border: none; + flex: 1 1 auto; + background-color: transparent; + font-size: ${({ fontSize }) => fontSize ?? '28px'}; + text-align: ${({ align }) => align && align}; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0px; + -webkit-appearance: textfield; + text-align: right; + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + [type='number'] { + -moz-appearance: textfield; + } + + ::-webkit-outer-spin-button, + ::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } +` + +function localeUsesComma(locale: SupportedLocale): boolean { + const decimalSeparator = new Intl.NumberFormat(locale).format(1.1)[1] + + return decimalSeparator === ',' +} + +const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group + +interface InputProps extends Omit, 'ref' | 'onChange' | 'as'> { + value: string | number + onUserInput: (input: string) => void + error?: boolean + fontSize?: string + align?: 'right' | 'left' + prependSymbol?: string + maxDecimals?: number +} + +const Input = forwardRef( + ({ value, onUserInput, placeholder, prependSymbol, maxDecimals, ...rest }: InputProps, ref) => { + const { formatterLocale } = useFormatterLocales() + + const enforcer = (nextUserInput: string) => { + if (nextUserInput === '' || inputRegex.test(escapeRegExp(nextUserInput))) { + const decimalGroups = nextUserInput.split('.') + if (maxDecimals && decimalGroups.length > 1 && decimalGroups[1].length > maxDecimals) { + return + } + + onUserInput(nextUserInput) + } + } + + const formatValueWithLocale = (value: string | number) => { + const [searchValue, replaceValue] = localeUsesComma(formatterLocale) ? [/\./g, ','] : [/,/g, '.'] + return value.toString().replace(searchValue, replaceValue) + } + + const valueFormattedWithLocale = formatValueWithLocale(value) + + return ( + { + if (prependSymbol) { + const value = event.target.value + + // cut off prepended symbol + const formattedValue = value.toString().includes(prependSymbol) + ? value.toString().slice(prependSymbol.length, value.toString().length + 1) + : value + + // replace commas with periods, because uniswap exclusively uses period as the decimal separator + enforcer(formattedValue.replace(/,/g, '.')) + } else { + enforcer(event.target.value.replace(/,/g, '.')) + } + }} + // universal input options + inputMode="decimal" + autoComplete="off" + autoCorrect="off" + // text-specific options + type="text" + pattern="^[0-9]*[.,]?[0-9]*$" + placeholder={placeholder || '0'} + minLength={1} + maxLength={79} + spellCheck="false" + /> + ) + } +) + +Input.displayName = 'Input' + +const MemoizedInput = React.memo(Input) +export { MemoizedInput as Input } +// const inputRegex = RegExp(`^\\d*(?:\\\\[.])?\\d*$`) // match escaped "." characters via in a non-capturing group + +export const StyledNumericalInput = styled(MemoizedInput)<{ $loading: boolean }>` + ${loadingOpacityMixin}; + text-align: left; + font-size: 36px; + font-weight: 485; + max-height: 44px; +` diff --git a/apps/web/src/components/Polling/ChainConnectivityWarning.tsx b/apps/web/src/components/Polling/ChainConnectivityWarning.tsx new file mode 100644 index 0000000..e8aa1f0 --- /dev/null +++ b/apps/web/src/components/Polling/ChainConnectivityWarning.tsx @@ -0,0 +1,83 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { getChainInfoOrDefault, L2ChainInfo } from 'constants/chainInfo' +import { AlertTriangle } from 'react-feather' +import styled from 'styled-components' +import { MEDIA_WIDTHS } from 'theme' +import { ExternalLink } from 'theme/components' + +const BodyRow = styled.div` + color: ${({ theme }) => theme.neutral1}; + font-size: 12px; + font-weight: 485; + font-size: 14px; + line-height: 20px; +` +const CautionTriangle = styled(AlertTriangle)` + color: ${({ theme }) => theme.deprecated_accentWarning}; +` +const Link = styled(ExternalLink)` + color: ${({ theme }) => theme.black}; + text-decoration: underline; +` +const TitleRow = styled.div` + align-items: center; + display: flex; + justify-content: flex-start; + margin-bottom: 8px; +` +const TitleText = styled.div` + color: ${({ theme }) => theme.neutral1}; + font-weight: 535; + font-size: 16px; + line-height: 24px; + margin: 0px 12px; +` +const Wrapper = styled.div` + background-color: ${({ theme }) => theme.surface1}; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + bottom: 60px; + z-index: 2; + display: none; + max-width: 348px; + padding: 16px 20px; + position: fixed; + right: 16px; + @media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToMedium}px) { + display: block; + } +` + +export function ChainConnectivityWarning() { + const { chainId } = useWeb3React() + const info = getChainInfoOrDefault(chainId) + const label = info?.label + + return ( + + + + + Network warning + + + + {chainId === ChainId.MAINNET ? ( + You may have lost your network connection. + ) : ( + {label} might be down right now, or you may have lost your network connection. + )}{' '} + {(info as L2ChainInfo).statusPage !== undefined && ( + + Check network status{' '} + + here. + + + )} + + + ) +} diff --git a/apps/web/src/components/Polling/index.tsx b/apps/web/src/components/Polling/index.tsx new file mode 100644 index 0000000..c18889e --- /dev/null +++ b/apps/web/src/components/Polling/index.tsx @@ -0,0 +1,168 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { RowFixed } from 'components/Row' +import { + AVERAGE_L1_BLOCK_TIME, + DEFAULT_MS_BEFORE_WARNING, + getBlocksPerMainnetEpochForChainId, + getChainInfo, +} from 'constants/chainInfo' +import useCurrentBlockTimestamp from 'hooks/useCurrentBlockTimestamp' +import { useIsLandingPage } from 'hooks/useIsLandingPage' +import { useIsNftPage } from 'hooks/useIsNftPage' +import useMachineTimeMs from 'hooks/useMachineTime' +import useBlockNumber from 'lib/hooks/useBlockNumber' +import { useEffect, useMemo, useState } from 'react' +import styled, { keyframes } from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { MouseoverTooltip } from '../Tooltip' +import { ChainConnectivityWarning } from './ChainConnectivityWarning' + +const StyledPolling = styled.div` + align-items: center; + bottom: 0; + color: ${({ theme }) => theme.neutral3}; + display: none; + padding: 1rem; + position: fixed; + right: 0; + transition: 250ms ease color; + + a { + color: unset; + } + a:hover { + color: unset; + text-decoration: none; + } + + @media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) { + display: flex; + } +` +const StyledPollingBlockNumber = styled(ThemedText.DeprecatedSmall)<{ + breathe: boolean + hovering: boolean + warning: boolean +}>` + color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.success)}; + transition: opacity 0.25s ease; + opacity: ${({ breathe, hovering }) => (hovering ? 0.7 : breathe ? 1 : 0.5)}; + :hover { + opacity: 1; + } + + a { + color: unset; + } + a:hover { + text-decoration: none; + color: unset; + } +` +const StyledPollingDot = styled.div<{ warning: boolean }>` + width: 8px; + height: 8px; + min-height: 8px; + min-width: 8px; + border-radius: 50%; + position: relative; + background-color: ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.success)}; + transition: 250ms ease background-color; +` + +const rotate360 = keyframes` + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +` + +const Spinner = styled.div<{ warning: boolean }>` + animation: ${rotate360} 1s cubic-bezier(0.83, 0, 0.17, 1) infinite; + transform: translateZ(0); + + border-top: 1px solid transparent; + border-right: 1px solid transparent; + border-bottom: 1px solid transparent; + border-left: 2px solid ${({ theme, warning }) => (warning ? theme.deprecated_yellow3 : theme.success)}; + background: transparent; + width: 14px; + height: 14px; + border-radius: 50%; + position: relative; + transition: 250ms ease border-color; + + left: -3px; + top: -3px; +` + +export default function Polling() { + const { chainId } = useWeb3React() + const blockNumber = useBlockNumber() + const [isMounting, setIsMounting] = useState(false) + const [isHover, setIsHover] = useState(false) + const isNftPage = useIsNftPage() + const isLandingPage = useIsLandingPage() + + const waitMsBeforeWarning = useMemo( + () => (chainId ? getChainInfo(chainId)?.blockWaitMsBeforeWarning : undefined) ?? DEFAULT_MS_BEFORE_WARNING, + [chainId] + ) + const machineTime = useMachineTimeMs(AVERAGE_L1_BLOCK_TIME) + const blockTime = useCurrentBlockTimestamp( + useMemo(() => ({ blocksPerFetch: /* 5m / 12s = */ 25 * getBlocksPerMainnetEpochForChainId(chainId) }), [chainId]) + ) + const warning = Boolean(!!blockTime && machineTime - blockTime.mul(1000).toNumber() > waitMsBeforeWarning) + + useEffect( + () => { + if (!blockNumber) { + return + } + + setIsMounting(true) + const mountingTimer = setTimeout(() => setIsMounting(false), 1000) + + // this will clear Timeout when component unmount like in willComponentUnmount + return () => { + clearTimeout(mountingTimer) + } + }, + [blockNumber] //useEffect will run only one time + //if you pass a value to array, like this [data] than clearTimeout will run every time this value changes (useEffect re-run) + ) + + //TODO - chainlink gas oracle is really slow. Can we get a better data source? + + const blockExternalLinkHref = useMemo(() => { + if (!chainId || !blockNumber) return '' + return getExplorerLink(chainId, blockNumber.toString(), ExplorerDataType.BLOCK) + }, [blockNumber, chainId]) + + if (isNftPage || isLandingPage) { + return null + } + + return ( + + setIsHover(true)} onMouseLeave={() => setIsHover(false)}> + + + The most recent block number on this network. Prices update on every block.} + > + {blockNumber}  + + + + {isMounting && }{' '} + + {warning && } + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/ChartData.ts b/apps/web/src/components/Pools/PoolDetails/ChartData.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts b/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts new file mode 100644 index 0000000..4d4c12b --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/ChartSection/hooks.ts @@ -0,0 +1,72 @@ +import { Token } from '@uniswap/sdk-core' +import { PriceChartData } from 'components/Charts/PriceChart' +import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer' +import { ChartType } from 'components/Charts/utils' +import { + ChartQueryResult, + DataQuality, + checkDataQuality, + withUTCTimestamp, +} from 'components/Tokens/TokenDetails/ChartSection/util' + +import { + Chain, + HistoryDuration, + usePoolPriceHistoryQuery, + usePoolVolumeHistoryQuery, +} from 'graphql/data/__generated__/types-and-hooks' +import { PoolData } from 'graphql/data/pools/usePoolData' +import { UTCTimestamp } from 'lightweight-charts' +import { useMemo } from 'react' + +type PDPChartQueryVars = { address: string; chain: Chain; duration: HistoryDuration; isV3: boolean } +export function usePDPPriceChartData( + variables: PDPChartQueryVars, + poolData: PoolData | undefined, + tokenA: Token | undefined, + tokenB: Token | undefined, + isReversed: boolean +): ChartQueryResult { + const { data, loading } = usePoolPriceHistoryQuery({ variables }) + + return useMemo(() => { + const { priceHistory } = data?.v2Pair ?? data?.v3Pool ?? {} + const referenceToken = isReversed ? tokenA : tokenB + + const entries = + priceHistory?.map((price) => { + const value = + poolData?.token0.address === referenceToken?.address.toLowerCase() ? price.token0Price : price.token1Price + + return { + time: price.timestamp as UTCTimestamp, + value, + open: value, + high: value, + low: value, + close: value, + } + }) ?? [] + + // TODO(WEB-3769): Append current price based on active tick to entries + /* const dataQuality = checkDataQuality(entries, ChartType.PRICE, variables.duration) */ + const dataQuality = loading || !priceHistory ? DataQuality.INVALID : DataQuality.VALID + + return { chartType: ChartType.PRICE, entries, loading, dataQuality } + }, [data?.v2Pair, data?.v3Pool, isReversed, loading, poolData?.token0.address, tokenA, tokenB]) +} + +export function usePDPVolumeChartData( + variables: PDPChartQueryVars +): ChartQueryResult { + const { data, loading } = usePoolVolumeHistoryQuery({ variables }) + + return useMemo(() => { + const { historicalVolume } = data?.v2Pair ?? data?.v3Pool ?? {} + const entries = historicalVolume?.map(withUTCTimestamp) ?? [] + + const dataQuality = checkDataQuality(entries, ChartType.VOLUME, variables.duration) + + return { chartType: ChartType.VOLUME, entries, loading, dataQuality } + }, [data?.v2Pair, data?.v3Pool, loading, variables.duration]) +} diff --git a/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx b/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx new file mode 100644 index 0000000..310a8b5 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/ChartSection/index.tsx @@ -0,0 +1,408 @@ +import { Trans, t } from '@lingui/macro' +import { ChainId, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import { ChartHeader } from 'components/Charts/ChartHeader' +import { Chart } from 'components/Charts/ChartModel' +import { LiquidityBarChartModel, useLiquidityBarData } from 'components/Charts/LiquidityChart' +import { LiquidityBarData } from 'components/Charts/LiquidityChart/renderer' +import { ChartSkeleton } from 'components/Charts/LoadingState' +import { PriceChartData, PriceChartDelta, PriceChartModel } from 'components/Charts/PriceChart' +import { refitChartContentAtom } from 'components/Charts/TimeSelector' +import { VolumeChart } from 'components/Charts/VolumeChart' +import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer' +import { ChartType, PriceChartType } from 'components/Charts/utils' +import PillMultiToggle from 'components/Toggle/PillMultiToggle' +import { ChartActionsContainer, DEFAULT_PILL_TIME_SELECTOR_OPTIONS } from 'components/Tokens/TokenDetails/ChartSection' +import { ChartTypeDropdown } from 'components/Tokens/TokenDetails/ChartSection/ChartTypeSelector' +import { ChartQueryResult, DataQuality } from 'components/Tokens/TokenDetails/ChartSection/util' +import { LoadingChart } from 'components/Tokens/TokenDetails/Skeleton' +import { DISPLAYS, TimePeriodDisplay, getTimePeriodFromDisplay } from 'components/Tokens/TokenTable/TimeSelector' +import { Chain, ProtocolVersion } from 'graphql/data/__generated__/types-and-hooks' +import { PoolData } from 'graphql/data/pools/usePoolData' +import { TimePeriod, gqlToCurrency, supportedChainIdFromGQLChain, toHistoryDuration } from 'graphql/data/util' +import useStablecoinPrice from 'hooks/useStablecoinPrice' +import { useAtomValue } from 'jotai/utils' +import { useMemo, useState } from 'react' +import styled, { useTheme } from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { usePDPPriceChartData, usePDPVolumeChartData } from './hooks' + +const PDP_CHART_HEIGHT_PX = 356 +const PDP_CHART_SELECTOR_OPTIONS = [ChartType.VOLUME, ChartType.PRICE, ChartType.LIQUIDITY] as const +export type PoolsDetailsChartType = (typeof PDP_CHART_SELECTOR_OPTIONS)[number] + +const TimePeriodSelectorContainer = styled.div` + @media only screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + width: 100%; + } +` +const ChartTypeSelectorContainer = styled.div` + display: flex; + gap: 8px; + + @media only screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + width: 100%; + } +` + +const StyledChart: typeof Chart = styled(Chart)` + height: ${PDP_CHART_HEIGHT_PX}px; +` + +const PDPChartTypeSelector = ({ + chartType, + onChartTypeChange, +}: { + chartType: PoolsDetailsChartType + onChartTypeChange: (c: PoolsDetailsChartType) => void +}) => ( + + + +) + +interface ChartSectionProps { + poolData?: PoolData + loading: boolean + isReversed: boolean + chain?: Chain +} + +/** Represents a variety of query result shapes, discriminated via additional `chartType` field. */ +type ActiveQuery = + | ChartQueryResult + | ChartQueryResult + | ChartQueryResult + +type TDPChartState = { + timePeriod: TimePeriod + setTimePeriod: (timePeriod: TimePeriod) => void + setChartType: (chartType: PoolsDetailsChartType) => void + activeQuery: ActiveQuery + dataQuality?: DataQuality +} + +function usePDPChartState( + poolData: PoolData | undefined, + tokenA: Token | undefined, + tokenB: Token | undefined, + isReversed: boolean, + chain: Chain, + protocolVersion: ProtocolVersion +): TDPChartState { + const [timePeriod, setTimePeriod] = useState(TimePeriod.DAY) + const [chartType, setChartType] = useState(ChartType.VOLUME) + + const isV3 = protocolVersion === ProtocolVersion.V3 + const variables = { address: poolData?.address ?? '', chain, duration: toHistoryDuration(timePeriod), isV3 } + + const priceQuery = usePDPPriceChartData(variables, poolData, tokenA, tokenB, isReversed) + const volumeQuery = usePDPVolumeChartData(variables) + + return useMemo(() => { + const activeQuery = (() => { + switch (chartType) { + case ChartType.PRICE: + return priceQuery + case ChartType.VOLUME: + return volumeQuery + case ChartType.LIQUIDITY: + return { + chartType: ChartType.LIQUIDITY as const, + entries: [], + loading: false, + dataQuality: DataQuality.VALID, + } + } + })() + + return { + timePeriod, + setTimePeriod, + setChartType, + activeQuery, + } + }, [chartType, volumeQuery, priceQuery, timePeriod]) +} + +export default function ChartSection(props: ChartSectionProps) { + const [currencyA, currencyB] = [ + props.poolData?.token0 && gqlToCurrency(props.poolData.token0), + props.poolData?.token1 && gqlToCurrency(props.poolData.token1), + ] + + const { setChartType, timePeriod, setTimePeriod, activeQuery } = usePDPChartState( + props.poolData, + currencyA?.wrapped, + currencyB?.wrapped, + props.isReversed, + props.chain ?? Chain.Ethereum, + props.poolData?.protocolVersion ?? ProtocolVersion.V3 + ) + + const refitChartContent = useAtomValue(refitChartContentAtom) + + // TODO(WEB-3740): Integrate BE tick query, remove special casing for liquidity chart + const loading = props.loading || (activeQuery.chartType !== ChartType.LIQUIDITY ? activeQuery?.loading : false) + + const ChartBody = (() => { + if (!currencyA || !currencyB || !props.poolData || !props.chain) { + return + } + + const selectedChartProps = { + ...props, + feeTier: Number(props.poolData.feeTier), + height: PDP_CHART_HEIGHT_PX, + timePeriod, + tokenA: currencyA.wrapped, + tokenB: currencyB.wrapped, + chainId: supportedChainIdFromGQLChain(props.chain) ?? ChainId.MAINNET, + } + + // TODO(WEB-3740): Integrate BE tick query, remove special casing for liquidity chart + if (activeQuery.chartType === ChartType.LIQUIDITY) { + return + } + if (activeQuery.dataQuality === DataQuality.INVALID || !currencyA || !currencyB) { + const errorText = loading ? undefined : Unable to display historical data for the current pool. + return + } + + const stale = activeQuery.dataQuality === DataQuality.STALE + + switch (activeQuery.chartType) { + case ChartType.PRICE: + return + case ChartType.VOLUME: + return + } + })() + + // BE does not support hourly price data for pools + const filteredTimeOptions = useMemo(() => { + if (activeQuery.chartType === ChartType.PRICE) { + return DEFAULT_PILL_TIME_SELECTOR_OPTIONS.filter((option) => option.value !== TimePeriodDisplay.HOUR) + } + return DEFAULT_PILL_TIME_SELECTOR_OPTIONS + }, [activeQuery.chartType]) + + return ( +
+ {ChartBody} + + + {activeQuery.chartType !== ChartType.LIQUIDITY && ( + + { + const time = getTimePeriodFromDisplay(option as TimePeriodDisplay) + if (time === timePeriod) { + refitChartContent?.() + } else { + setTimePeriod(time) + } + }} + /> + + )} + +
+ ) +} + +const PriceDisplayContainer = styled.div` + display: flex; + flex-wrap: wrap; + column-gap: 4px; +` + +const ChartPriceText = styled(ThemedText.HeadlineMedium)` + ${EllipsisStyle} + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + font-size: 24px !important; + line-height: 32px !important; + } +` + +function PriceChart({ + tokenA, + tokenB, + isReversed, + data, + stale, +}: { + tokenA: Token + tokenB: Token + isReversed: boolean + data: PriceChartData[] + stale: boolean +}) { + const { formatCurrencyAmount, formatPrice } = useFormatter() + const [primaryToken, referenceToken] = isReversed ? [tokenB, tokenA] : [tokenA, tokenB] + + const params = useMemo(() => ({ data, stale, type: PriceChartType.LINE }), [data, stale]) + + const stablecoinPrice = useStablecoinPrice(primaryToken) + + const lastPrice = data[data.length - 1] + return ( + + {(crosshairData) => { + const displayValue = crosshairData ?? lastPrice + const currencyBAmountRaw = Math.floor( + (displayValue.value ?? displayValue.close) * 10 ** referenceToken.decimals + ) + const priceDisplay = ( + + + {`1 ${primaryToken.symbol} = ${formatCurrencyAmount({ + amount: CurrencyAmount.fromRawAmount(referenceToken, currencyBAmountRaw), + })} + ${referenceToken.symbol}`} + + + {stablecoinPrice ? '(' + formatPrice({ price: stablecoinPrice }) + ')' : ''} + + + ) + return ( + } + valueFormatterType={NumberType.FiatTokenPrice} + time={crosshairData?.time} + /> + ) + }} + + ) +} + +const FadeInHeading = styled(ThemedText.H1Medium)` + ${textFadeIn}; + line-height: 32px; +` +const FadeInSubheader = styled(ThemedText.SubHeader)` + ${textFadeIn} +` + +function LiquidityTooltipDisplay({ + data, + tokenADescriptor, + tokenBDescriptor, + currentTick, +}: { + data: LiquidityBarData + tokenADescriptor: string + tokenBDescriptor: string + currentTick?: number +}) { + const { formatNumber } = useFormatter() + if (!currentTick) return null + + const displayValue0 = + data.tick >= currentTick + ? formatNumber({ + input: data.amount0Locked, + type: NumberType.TokenQuantityStats, + }) + : 0 + const displayValue1 = + data.tick <= currentTick + ? formatNumber({ + input: data.amount1Locked, + type: NumberType.TokenQuantityStats, + }) + : 0 + + return ( + <> + {t`${tokenADescriptor} liquidity: ${displayValue0}`} + {t`${tokenBDescriptor} liquidity: ${displayValue1}`} + + ) +} + +function LiquidityChart({ + tokenA, + tokenB, + feeTier, + isReversed, + chainId, +}: { + tokenA: Token + tokenB: Token + feeTier: FeeAmount + isReversed: boolean + chainId: ChainId +}) { + const tokenADescriptor = tokenA.symbol ?? tokenA.name ?? t`Token A` + const tokenBDescriptor = tokenB.symbol ?? tokenB.name ?? t`Token B` + + const { tickData, activeTick, loading } = useLiquidityBarData({ + tokenA, + tokenB, + feeTier, + isReversed, + chainId, + }) + + const theme = useTheme() + const params = useMemo(() => { + return { + data: tickData?.barData ?? [], + tokenAColor: isReversed ? theme.token1 : theme.token0, + tokenBColor: isReversed ? theme.token0 : theme.token1, + highlightColor: theme.surface3, + activeTick, + activeTickProgress: tickData?.activeRangePercentage, + } + }, [activeTick, isReversed, theme, tickData]) + + if (loading) return + + return ( + ( + + ) + : undefined + } + > + {(crosshair) => { + const displayPoint = crosshair ?? tickData?.activeRangeData + const display = ( +
+ {`1 ${tokenADescriptor} = ${displayPoint?.price0} ${tokenBDescriptor}`} + {`1 ${tokenBDescriptor} = ${displayPoint?.price1} ${tokenADescriptor}`} + {displayPoint && displayPoint.tick === activeTick && ( + + Active tick range + + )} +
+ ) + return + }} +
+ ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.test.tsx new file mode 100644 index 0000000..99ebec2 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.test.tsx @@ -0,0 +1,96 @@ +import userEvent from '@testing-library/user-event' +import { ChartType, PriceChartType } from 'components/Charts/utils' +import store from 'state' +import { addSerializedToken } from 'state/user/reducer' +import { act, render, screen } from 'test-utils/render' + +import { usdcWethPoolAddress, validBEPoolToken0, validBEPoolToken1 } from 'test-utils/pools/fixtures' +import { PoolsDetailsChartType } from './ChartSection' +import { PoolDetailsBreadcrumb, PoolDetailsHeader } from './PoolDetailsHeader' + +describe('PoolDetailsHeader', () => { + beforeEach(() => { + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }) + ) + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + }, + }) + ) + }) + + const mockBreadcrumbProps = { + chainId: 1, + token0: validBEPoolToken0, + token1: validBEPoolToken1, + poolAddress: usdcWethPoolAddress, + } + + const mockHeaderProps = { + chainId: 1, + poolAddress: usdcWethPoolAddress, + token0: validBEPoolToken0, + token1: validBEPoolToken1, + chartType: ChartType.PRICE as PoolsDetailsChartType, + onChartTypeChange: jest.fn(), + priceChartType: PriceChartType.LINE, + onPriceChartTypeChange: jest.fn(), + feeTier: 500, + toggleReversed: jest.fn(), + } + + it('loading skeleton is shown', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByTestId('pdp-header-loading-skeleton')).toBeInTheDocument() + }) + + it('renders breadcrumb text correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText(/Explore/i)).toBeInTheDocument() + expect(screen.getByText(/Pools/i)).toBeInTheDocument() + expect(screen.getAllByText(/USDC\s*\/\s*WETH/i).length).toBeGreaterThan(0) + expect(screen.getByText(/0x88e6...5640/i)).toBeInTheDocument() + }) + + it('renders header text correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + const usdcLink = document.querySelector( + 'a[href="/explore/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"]' + ) + const wethLink = document.querySelector( + 'a[href="/explore/tokens/ethereum/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"]' + ) + expect(usdcLink?.textContent).toBe('USDC') + expect(wethLink?.textContent).toBe('WETH') + expect(screen.getByText('0.05%')).toBeInTheDocument() + }) + + it('calls toggleReversed when arrows are clicked', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('toggle-tokens-reverse-arrows'))) + + expect(mockHeaderProps.toggleReversed).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx new file mode 100644 index 0000000..3ba43b1 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsHeader.tsx @@ -0,0 +1,488 @@ +import { t, Trans } from '@lingui/macro' +import { ChainId, Percent } from '@uniswap/sdk-core' +import blankTokenUrl from 'assets/svg/blank_token.svg' +import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav' +import Column from 'components/Column' +import { EtherscanLogo } from 'components/Icons/Etherscan' +import { ExplorerIcon } from 'components/Icons/ExplorerIcon' +import { ChainLogo } from 'components/Logo/ChainLogo' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import Row from 'components/Row' +import { LoadingBubble } from 'components/Tokens/loading' +import ShareButton from 'components/Tokens/TokenDetails/ShareButton' +import { StyledExternalLink } from 'components/Tokens/TokenDetails/TokenDetailsHeader' +import { BIPS_BASE } from 'constants/misc' +import { ProtocolVersion, Token } from 'graphql/data/__generated__/types-and-hooks' +import { chainIdToBackendName, getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util' +import useTokenLogoSource from 'hooks/useAssetLogoSource' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { useScreenSize } from 'hooks/useScreenSize' +import React, { useReducer, useRef } from 'react' +import { ChevronRight, ExternalLink as ExternalLinkIcon } from 'react-feather' +import { Link } from 'react-router-dom' +import styled, { css, useTheme } from 'styled-components' +import { ClickableStyle, EllipsisStyle, ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { shortenAddress } from 'utilities/src/addresses' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { DropdownSelector } from 'components/DropdownSelector' +import { ReverseArrow } from 'components/Icons/ReverseArrow' +import { ActionButtonStyle, ActionMenuFlyoutStyle } from 'components/Tokens/TokenDetails/shared' +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { useFormatter } from 'utils/formatNumbers' +import { DetailBubble } from './shared' + +const HeaderContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: 'flex-start'; + width: 100%; + ${textFadeIn}; + animation-duration: ${({ theme }) => theme.transition.duration.medium}; +` + +const Badge = styled(ThemedText.LabelMicro)` + background: ${({ theme }) => theme.surface2}; + padding: 2px 6px; + border-radius: 4px; +` + +const ToggleReverseArrows = styled(ReverseArrow)` + ${ClickableStyle} + fill: ${({ theme }) => theme.neutral2}; +` + +const IconBubble = styled(LoadingBubble)` + width: 32px; + height: 32px; + border-radius: 50%; +` + +interface PoolDetailsBreadcrumbProps { + chainId?: number + poolAddress?: string + token0?: Token + token1?: Token + loading?: boolean +} + +export function PoolDetailsBreadcrumb({ chainId, poolAddress, token0, token1, loading }: PoolDetailsBreadcrumbProps) { + const chainName = chainIdToBackendName(chainId) + const exploreOrigin = `/explore/${chainName.toLowerCase()}` + const poolsOrigin = `/explore/pools/${chainName.toLowerCase()}` + + return ( + + + Explore + + + Pools + + {loading || !poolAddress ? ( + + ) : ( + + )} + + ) +} + +const StyledPoolDetailsTitle = styled.div` + display: flex; + flex-direction: row; + gap: 12px; + width: max-content; + align-items: center; +` + +const PoolName = styled(ThemedText.HeadlineMedium)` + font-size: 24px !important; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + font-size: 18px !important; + line-height: 24px !important; + } +` + +const PoolDetailsTitle = ({ + token0, + token1, + chainId, + feeTier, + protocolVersion, + toggleReversed, +}: { + token0?: Token + token1?: Token + chainId?: number + feeTier?: number + protocolVersion?: ProtocolVersion + toggleReversed: React.DispatchWithoutAction +}) => { + const { formatPercent } = useFormatter() + const feePercent = feeTier && formatPercent(new Percent(feeTier, BIPS_BASE * 100)) + return ( + +
+ + + {token0?.symbol} + +  /  + + {token1?.symbol} + + +
+ {protocolVersion === ProtocolVersion.V2 && v2} + {!!feePercent && {feePercent}} + +
+ ) +} + +const ContractsDropdownRowContainer = styled(Row)` + align-items: center; + text-decoration: none; + cursor: pointer; + gap: 12px; + padding: 10px 8px; + border-radius: 8px; + &:hover { + background: ${({ theme }) => theme.surface3}; + } +` + +const ContractsDropdownRow = ({ + address, + chainId, + tokens, +}: { + address?: string + chainId?: number + tokens: (Token | undefined)[] +}) => { + const theme = useTheme() + const currency = tokens[0] && gqlToCurrency(tokens[0]) + const isPool = tokens.length === 2 + const isNative = address === NATIVE_CHAIN_ID + const explorerUrl = + chainId && + address && + getExplorerLink( + chainId, + address, + isNative ? ExplorerDataType.NATIVE : isPool ? ExplorerDataType.ADDRESS : ExplorerDataType.TOKEN + ) + + if (!chainId || !explorerUrl) { + return ( + + + + ) + } + + return ( + + + + {isPool ? ( + + ) : ( + + )} + {isPool ? Pool : tokens[0]?.symbol} + {shortenAddress(address)} + + + + + ) +} + +const ContractsModalContainer = css` + ${ActionMenuFlyoutStyle} + min-width: 235px; + border-radius: 16px; + ${EllipsisStyle} +` + +const PoolDetailsHeaderActions = ({ + chainId, + poolAddress, + poolName, + token0, + token1, +}: { + chainId?: number + poolAddress?: string + poolName: string + token0?: Token + token1?: Token +}) => { + const theme = useTheme() + + const [contractsModalIsOpen, toggleContractsModal] = useReducer((s) => !s, false) + const contractsRef = useRef(null) + useOnClickOutside(contractsRef, contractsModalIsOpen ? toggleContractsModal : undefined) + + return ( + +
+ + ) : ( + + ) + } + internalMenuItems={ + <> + + + + + } + tooltipText={t`Explorers`} + hideChevron + buttonCss={ActionButtonStyle} + menuFlyoutCss={ContractsModalContainer} + /> +
+ +
+ ) +} + +const StyledLink = styled(Link)` + color: ${({ theme }) => theme.neutral1}; + text-decoration: none; + ${ClickableStyle} +` + +interface PoolDetailsHeaderProps { + chainId?: number + poolAddress?: string + token0?: Token + token1?: Token + feeTier?: number + protocolVersion?: ProtocolVersion + toggleReversed: React.DispatchWithoutAction + loading?: boolean +} + +export function PoolDetailsHeader({ + chainId, + poolAddress, + token0, + token1, + feeTier, + protocolVersion, + toggleReversed, + loading, +}: PoolDetailsHeaderProps) { + const screenSize = useScreenSize() + const shouldColumnBreak = !screenSize['sm'] + const poolName = `${token0?.symbol} / ${token1?.symbol}` + const tokens = [token0, token1] + + if (loading) { + return ( + + {shouldColumnBreak ? ( + + + + + ) : ( + + + + + )} + + ) + } + return ( + + {shouldColumnBreak ? ( + + + {chainId && } + + + + + ) : ( + <> + + {chainId && } + + + + + )} + + ) +} + +const StyledLogoParentContainer = styled.div` + position: relative; + top: 0; + left: 0; +` +export function DoubleTokenAndChainLogo({ + chainId, + tokens, + size = 32, +}: { + chainId: number + tokens: Array + size?: number +}) { + return ( + + + + + ) +} + +const L2_LOGO_SIZE_FACTOR = 3 / 8 + +const L2LogoContainer = styled.div<{ size: number }>` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 2px; + width: ${({ size }) => size * L2_LOGO_SIZE_FACTOR}px; + height: ${({ size }) => size * L2_LOGO_SIZE_FACTOR}px; + left: 60%; + position: absolute; + top: 60%; + outline: 2px solid ${({ theme }) => theme.surface1}; + display: flex; + align-items: center; + justify-content: center; +` + +function SquareL2Logo({ chainId, size }: { chainId: ChainId; size: number }) { + if (chainId === ChainId.MAINNET) return null + + return ( + + + + ) +} + +export function DoubleTokenLogo({ + chainId, + tokens, + size = 32, +}: { + chainId: number + tokens: Array + size?: number +}) { + const token0IsNative = tokens?.[0]?.address === NATIVE_CHAIN_ID + const token1IsNative = tokens?.[1]?.address === NATIVE_CHAIN_ID + const [src, nextSrc] = useTokenLogoSource({ + address: tokens?.[0]?.address, + chainId, + primaryImg: token0IsNative ? undefined : tokens?.[0]?.project?.logo?.url, + isNative: token0IsNative, + }) + const [src2, nextSrc2] = useTokenLogoSource({ + address: tokens?.[1]?.address, + chainId, + primaryImg: token1IsNative ? undefined : tokens?.[1]?.project?.logo?.url, + isNative: token1IsNative, + }) + + return +} + +const DoubleLogoContainer = styled.div<{ size: number }>` + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; + img { + width: ${({ size }) => size / 2}px; + height: ${({ size }) => size}px; + object-fit: cover; + } + img:first-child { + border-radius: ${({ size }) => `${size / 2}px 0 0 ${size / 2}px`}; + object-position: 0 0; + } + img:last-child { + border-radius: ${({ size }) => `0 ${size / 2}px ${size / 2}px 0`}; + object-position: 100% 0; + } +` + +const CircleLogoImage = styled.img<{ size: number }>` + width: ${({ size }) => size / 2}px; + height: ${({ size }) => size}px; + border-radius: 50%; +` + +interface DoubleLogoProps { + logo1?: string + logo2?: string + onError1?: () => void + onError2?: () => void + size: number +} + +function DoubleLogo({ logo1, onError1, logo2, onError2, size }: DoubleLogoProps) { + return ( + + + + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.test.tsx new file mode 100644 index 0000000..17b116d --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.test.tsx @@ -0,0 +1,63 @@ +import { ChainId } from '@uniswap/sdk-core' +import { USDC_MAINNET } from 'constants/tokens' +import store from 'state' +import { addSerializedToken } from 'state/user/reducer' +import { usdcWethPoolAddress, validBEPoolToken0, validBEPoolToken1 } from 'test-utils/pools/fixtures' +import { render, screen } from 'test-utils/render' + +import { PoolDetailsLink } from './PoolDetailsLink' + +describe('PoolDetailsHeader', () => { + beforeEach(() => { + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }) + ) + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + }, + }) + ) + }) + + it('renders link for pool address', async () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText('USDC / WETH')).toBeInTheDocument() + expect(screen.getByTestId('pdp-pool-logo-USDC-WETH')).toBeInTheDocument() + expect(screen.getByTestId(`copy-address-${usdcWethPoolAddress}`)).toBeInTheDocument() + expect(screen.getByTestId(`explorer-url-https://etherscan.io/address/${usdcWethPoolAddress}`)).toBeInTheDocument() + }) + + it('renders link for token address', async () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText('USDC')).toBeInTheDocument() + expect(screen.getByTestId('pdp-token-logo-USDC')).toBeInTheDocument() + expect(screen.getByTestId(`copy-address-${USDC_MAINNET.address}`)).toBeInTheDocument() + expect(screen.getByTestId(`explorer-url-https://etherscan.io/token/${USDC_MAINNET.address}`)).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx new file mode 100644 index 0000000..b72881e --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsLink.tsx @@ -0,0 +1,190 @@ +import { t, Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { EtherscanLogo } from 'components/Icons/Etherscan' +import { ExplorerIcon } from 'components/Icons/ExplorerIcon' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import Row from 'components/Row' +import Tooltip, { TooltipSize } from 'components/Tooltip' +import { Token } from 'graphql/data/__generated__/types-and-hooks' +import { chainIdToBackendName, getTokenDetailsURL, gqlToCurrency } from 'graphql/data/util' +import useCopyClipboard from 'hooks/useCopyClipboard' +import { useCallback, useState } from 'react' +import { ChevronRight, Copy } from 'react-feather' +import { useNavigate } from 'react-router-dom' +import styled, { useTheme } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' +import { isAddress, shortenAddress } from 'utilities/src/addresses' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { DoubleTokenAndChainLogo } from './PoolDetailsHeader' +import { DetailBubble, SmallDetailBubble } from './shared' + +const TokenName = styled(ThemedText.BodyPrimary)` + display: none; + + @media (max-width: ${BREAKPOINTS.lg}px) and (min-width: ${BREAKPOINTS.xs}px) { + display: block; + } + ${EllipsisStyle} +` + +const TokenTextWrapper = styled(Row)<{ isClickable?: boolean }>` + gap: 8px; + margin-right: 12px; + ${EllipsisStyle} + ${({ isClickable }) => isClickable && ClickableStyle} +` + +const SymbolText = styled(ThemedText.BodyPrimary)` + flex-shrink: 0; + + @media (max-width: ${BREAKPOINTS.lg}px) and (min-width: ${BREAKPOINTS.xs}px) { + color: ${({ theme }) => theme.neutral2}; + } +` + +const CopyAddress = styled(Row)` + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + background-color: ${({ theme }) => theme.surface3}; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: max-content; + flex-shrink: 0; + ${ClickableStyle} +` +const StyledCopyIcon = styled(Copy)` + width: 16px; + height: 16px; + color: ${({ theme }) => theme.neutral2}; + flex-shrink: 0; +` + +const ExplorerWrapper = styled.div` + padding: 8px; + border-radius: 20px; + background-color: ${({ theme }) => theme.surface3}; + display: flex; + ${ClickableStyle} +` + +const ButtonsRow = styled(Row)` + gap: 8px; + flex-shrink: 0; + width: max-content; +` + +interface PoolDetailsLinkProps { + address?: string + chainId?: number + tokens: (Token | undefined)[] + loading?: boolean +} + +export function PoolDetailsLink({ address, chainId, tokens, loading }: PoolDetailsLinkProps) { + const theme = useTheme() + const isNative = address === NATIVE_CHAIN_ID + const currency = tokens[0] && gqlToCurrency(tokens[0]) + const [isCopied, setCopied] = useCopyClipboard() + const copy = useCallback(() => { + const checksummedAddress = isAddress(address) + checksummedAddress && setCopied(checksummedAddress) + }, [address, setCopied]) + + const isPool = tokens.length === 2 + const explorerUrl = + address && + chainId && + getExplorerLink( + chainId, + address, + isNative ? ExplorerDataType.NATIVE : isPool ? ExplorerDataType.ADDRESS : ExplorerDataType.TOKEN + ) + + const navigate = useNavigate() + const chainName = chainIdToBackendName(chainId) + const handleTokenTextClick = useCallback(() => { + if (!isPool) { + navigate(getTokenDetailsURL({ address: tokens[0]?.address, chain: chainName })) + } + }, [navigate, tokens, isPool, chainName]) + + const [truncateAddress, setTruncateAddress] = useState(false) + const onTextRender = useCallback( + (textRef: HTMLElement) => { + if (textRef) { + const hasOverflow = textRef.clientWidth < textRef.scrollWidth + if (hasOverflow) { + setTruncateAddress((prev) => (prev ? 'both' : 'start')) + } + } + }, + // This callback must run after it sets truncateAddress to 'start' to see if it needs to 'both'. + // It checks if the textRef has overflow, and sets truncateAddress accordingly to avoid it. + // eslint-disable-next-line react-hooks/exhaustive-deps + [truncateAddress] + ) + + if (loading || !address || !chainId) { + return ( + + + + + ) + } + + return ( + + + {isPool ? ( + + ) : ( + + )} + {isPool ? Pool : tokens[0]?.name} + + {isPool ? ( + `${tokens[0]?.symbol} / ${tokens[1]?.symbol}` + ) : ( + + {tokens[0]?.symbol} + + )} + + + + {!isNative && ( + + + {shortenAddress(address, truncateAddress ? 2 : undefined, truncateAddress === 'both' ? 2 : undefined)} + + + + )} + {explorerUrl && ( + + + {chainId === ChainId.MAINNET ? ( + + ) : ( + + )} + + + )} + + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionTable.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionTable.test.tsx new file mode 100644 index 0000000..2ceb537 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionTable.test.tsx @@ -0,0 +1,51 @@ +import { Price, WETH9 } from '@uniswap/sdk-core' +import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { PositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools/cache' +import { USDC_MAINNET } from 'constants/tokens' +import { render, screen } from 'test-utils/render' + +import { PoolDetailsPositionsTable } from './PoolDetailsPositionsTable' + +const mockPositionInfo: PositionInfo = { + owner: '0x1234', + chainId: 1, + pool: { + token0: WETH9[1], + token1: USDC_MAINNET, + fee: 5000 as FeeAmount, + } as Pool, + details: { + token0: WETH9[1].address, + token1: USDC_MAINNET.address, + }, + position: { + token0PriceLower: new Price(WETH9[1], USDC_MAINNET, 1000000000000, 1), + token0PriceUpper: new Price(WETH9[1], USDC_MAINNET, 1, 1000000000000), + }, + inRange: true, + closed: false, +} as PositionInfo + +describe('PoolDetailsPositionsTable', () => { + it('renders with PositionStatus In Range', () => { + const { asFragment } = render() + expect(screen.getByText('In range')).not.toBeNull() + expect(screen.getByTestId('position-min-0')).not.toBeNull() + expect(screen.getByTestId('position-max-1')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders with PositionStatus Closed', () => { + const closedMockPositionInfo = { ...mockPositionInfo, closed: true, inRange: false } + const { asFragment } = render() + expect(screen.getByText('Closed')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders with PositionStatus Out Of Range', () => { + const outOfRangeMockPositionInfo = { ...mockPositionInfo, inRange: false } + const { asFragment } = render() + expect(screen.getByText('Out of range')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionsTable.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionsTable.tsx new file mode 100644 index 0000000..93d628b --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsPositionsTable.tsx @@ -0,0 +1,179 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { PositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools/cache' +import Column from 'components/Column' +import { ClosedCircle, DoubleArrow } from 'components/Pools/PoolDetails/icons' +import { DoubleTokenLogo } from 'components/Pools/PoolDetails/PoolDetailsHeader' +import Row from 'components/Row' +import { BIPS_BASE } from 'constants/misc' +import { chainIdToBackendName } from 'graphql/data/util' +import { useSwitchChain } from 'hooks/useSwitchChain' +import { useCallback } from 'react' +import { AlertTriangle } from 'react-feather' +import { useNavigate } from 'react-router-dom' +import { Bound } from 'state/mint/v3/actions' +import styled, { useTheme } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle, ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +const PositionTableWrapper = styled(Column)` + gap: 24px; + margin-top: 24px; + width: 100%; +` + +const PositionWrapper = styled(Column)` + gap: 4px; + background: ${({ theme }) => theme.surface2}; + border-radius: 12px; + width: 100%; + ${ClickableStyle} + padding: 16px; +` + +const FeeTier = styled(ThemedText.LabelMicro)` + padding: 4px 6px; + background: ${({ theme }) => theme.surface2}; +` + +const StatusWrapper = styled(Row)<{ status: PositionStatus }>` + gap: 8px; + width: max-content; + margin-right: 0; + margin-left: auto; + color: ${({ theme, status }) => + status === PositionStatus.IN_RANGE + ? theme.success + : status === PositionStatus.OUT_OF_RANGE + ? theme.deprecated_accentWarning + : theme.neutral2}; +` + +const RangeWrapper = styled(Row)` + gap: 10px; + + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + flex-direction: column; + gap: 4px; + align-items: flex-start; + } +` + +const StyledDoubleArrow = styled(DoubleArrow)` + @media screen and (max-width: ${BREAKPOINTS.sm}px) { + display: none; + } +` + +const RangeText = styled(ThemedText.Caption)` + width: max-content; + white-space: nowrap; +` + +const StyledDot = styled.div` + width: 8px; + height: 8px; + border-radius: 50%; + background-color: ${({ theme }) => theme.success}; +` + +enum PositionStatus { + IN_RANGE = 'In range', + OUT_OF_RANGE = 'Out of range', + CLOSED = 'Closed', +} + +function PositionRow({ positionInfo }: { positionInfo: PositionInfo }) { + const tokens = [ + { + id: positionInfo.details.token0, + address: positionInfo.details.token0, + chain: chainIdToBackendName(positionInfo.chainId), + }, + { + id: positionInfo.details.token0, + address: positionInfo.details.token1, + chain: chainIdToBackendName(positionInfo.chainId), + }, + ] + const { chainId: walletChainId, connector } = useWeb3React() + const navigate = useNavigate() + const switchChain = useSwitchChain() + const theme = useTheme() + const { formatTickPrice } = useFormatter() + + const onClick = useCallback(async () => { + if (walletChainId !== positionInfo.chainId) await switchChain(connector, positionInfo.chainId) + navigate('/pool/' + positionInfo.details.tokenId) + }, [connector, navigate, positionInfo.chainId, positionInfo.details.tokenId, switchChain, walletChainId]) + + const status = positionInfo.inRange + ? PositionStatus.IN_RANGE + : positionInfo.closed + ? PositionStatus.CLOSED + : PositionStatus.OUT_OF_RANGE + + const priceUpper = positionInfo.position.token0PriceLower.invert() + const priceLower = positionInfo.position.token0PriceUpper.invert() + + const ticksAtLimit = { + LOWER: parseFloat(priceLower.toFixed(0)) === 0, + UPPER: parseFloat(priceUpper.toFixed(0)) > Number.MAX_SAFE_INTEGER, + } + + return ( + + + + + {positionInfo.pool.token0.symbol}/{positionInfo.pool.token1.symbol} + + {positionInfo.pool.fee / BIPS_BASE}% + + {status} + {status === PositionStatus.IN_RANGE && } + {status === PositionStatus.OUT_OF_RANGE && } + {status === PositionStatus.CLOSED && } + + + + + Min:  + {formatTickPrice({ + price: priceLower, + atLimit: ticksAtLimit, + direction: Bound.LOWER, + })} +   + {positionInfo.pool.token0.symbol}  + per  + {positionInfo.pool.token1.symbol} + + + + Max:  + {formatTickPrice({ + price: priceUpper, + atLimit: ticksAtLimit, + direction: Bound.UPPER, + })} +   + {positionInfo.pool.token0.symbol}  + per  + {positionInfo.pool.token1.symbol} + + + + ) +} + +export function PoolDetailsPositionsTable({ positions }: { positions: PositionInfo[] }) { + return ( + + {positions.map((position, index) => ( + + ))} + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.test.tsx new file mode 100644 index 0000000..e7ad269 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.test.tsx @@ -0,0 +1,83 @@ +import { enableNetConnect } from 'nock' +import store from 'state' +import { addSerializedToken } from 'state/user/reducer' +import { validPoolDataResponse } from 'test-utils/pools/fixtures' +import { act, render, screen } from 'test-utils/render' +import { BREAKPOINTS } from 'theme' + +import { PoolDetailsStats } from './PoolDetailsStats' + +describe('PoolDetailsStats', () => { + const mockProps = { + poolData: validPoolDataResponse.data, + isReversed: false, + chainId: 1, + } + + beforeEach(() => { + // Enable network connections for retrieving token logos + enableNetConnect() + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }) + ) + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + }, + }) + ) + }) + + it('renders stats text correctly', async () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: BREAKPOINTS.xl, + }) + + const { asFragment } = render() + // After the first render, the extracted color is updated to an a11y compliant color + // This is why we need to wrap the fragment in act(...) + await act(async () => { + await asFragment + }) + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText(/Stats/i)).toBeInTheDocument() + expect(screen.getByText('90.9M')).toBeInTheDocument() + expect(screen.getByText('USDC')).toBeInTheDocument() + expect(screen.getByText('82.5K')).toBeInTheDocument() + expect(screen.getByText('ETH')).toBeInTheDocument() + expect(screen.getByText(/TVL/i)).toBeInTheDocument() + expect(screen.getByText('$223.2M')).toBeInTheDocument() + expect(screen.getByTestId('pool-balance-chart')).toBeInTheDocument() + }) + + it('pool balance chart not visible on mobile', async () => { + Object.defineProperty(window, 'innerWidth', { + writable: true, + configurable: true, + value: BREAKPOINTS.lg, + }) + const { asFragment } = render() + await act(async () => { + await asFragment + }) + expect(asFragment()).toMatchSnapshot() + + expect(screen.queryByTestId('pool-balance-chart')).toBeNull() + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx new file mode 100644 index 0000000..1a7bc92 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStats.tsx @@ -0,0 +1,290 @@ +import { Trans } from '@lingui/macro' +import { Currency } from '@uniswap/sdk-core' +import Column from 'components/Column' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import Row from 'components/Row' +import { DeltaArrow } from 'components/Tokens/TokenDetails/Delta' +import { LoadingBubble } from 'components/Tokens/loading' +import { Token } from 'graphql/data/__generated__/types-and-hooks' +import { chainIdToBackendName, getTokenDetailsURL, unwrapToken } from 'graphql/data/util' +import { useCurrency } from 'hooks/Tokens' +import { useScreenSize } from 'hooks/useScreenSize' +import { ReactNode, useMemo } from 'react' +import { Link } from 'react-router-dom' +import { Text } from 'rebass' +import styled, { css, useTheme } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle, ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' +import { PoolData } from 'graphql/data/pools/usePoolData' +import { DetailBubble } from './shared' + +const HeaderText = styled(Text)` + font-weight: 485; + font-size: 24px; + line-height: 36px; + @media (max-width: ${BREAKPOINTS.lg}px) { + width: 100%; + } +` + +const StatsWrapper = styled(Column)<{ loaded?: boolean }>` + gap: 24px; + padding: 20px; + border-radius: 20px; + background: ${({ theme }) => theme.surface2}; + width: 100%; + z-index: 1; + margin-top: ${({ loaded }) => loaded && '-24px'}; + + @media (max-width: ${BREAKPOINTS.lg}px) { + flex-direction: row; + background: transparent; + flex-wrap: wrap; + padding: 20px 0px; + justify-content: space-between; + margin-top: 0px; + } +` + +const StatItemColumn = styled(Column)` + gap: 8px; + flex: 1; + min-width: 180px; + + @media (max-width: ${BREAKPOINTS.sm}px) { + min-width: 150px; + } +` + +const PoolBalanceSymbols = styled(Row)` + justify-content: space-between; + + @media (max-width: ${BREAKPOINTS.lg}px) { + flex-direction: column; + } +` + +const PoolBalanceTokenNamesContainer = styled(Row)` + font-weight: 485; + font-size: 16px; + line-height: 24px; + width: max-content; + + @media (max-width: ${BREAKPOINTS.lg}px) { + font-size: 20px; + line-height: 28px; + width: 100%; + } +` + +const StyledLink = styled(Link)` + display: flex; + align-items: center; + color: ${({ theme }) => theme.neutral1}; + ${ClickableStyle} +` + +const leftBarChartStyles = css` + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + border-right: 1px solid ${({ theme }) => theme.surface2}; +` + +const rightBarChartStyles = css` + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + border-left: 1px solid ${({ theme }) => theme.surface2}; +` + +const BalanceChartSide = styled.div<{ percent: number; $color: string; isLeft: boolean }>` + height: 8px; + width: ${({ percent }) => percent * 100}%; + background: ${({ $color }) => $color}; + ${({ isLeft }) => (isLeft ? leftBarChartStyles : rightBarChartStyles)} +` + +const StatSectionBubble = styled(LoadingBubble)` + width: 180px; + height: 40px; +` + +const StatHeaderBubble = styled(LoadingBubble)` + width: 116px; + height: 24px; + border-radius: 8px; +` + +type TokenFullData = Token & { + price: number + tvl: number + percent: number + currency?: Currency +} + +const PoolBalanceTokenNames = ({ token, chainId }: { token: TokenFullData; chainId?: number }) => { + const isScreenSize = useScreenSize() + const screenIsNotLarge = isScreenSize['lg'] + const { formatNumber } = useFormatter() + const unwrappedToken = chainId ? unwrapToken(chainId, token) : token + const isNative = unwrappedToken?.address === NATIVE_CHAIN_ID + const currency = isNative && chainId ? nativeOnChain(chainId) : token.currency + return ( + + {!screenIsNotLarge && } + {formatNumber({ + input: token.tvl, + type: NumberType.TokenQuantityStats, + })} +   + + {screenIsNotLarge && ( + + )} + {unwrappedToken.symbol} + + + ) +} + +interface PoolDetailsStatsProps { + poolData?: PoolData + isReversed?: boolean + chainId?: number + loading?: boolean +} + +export function PoolDetailsStats({ poolData, isReversed, chainId, loading }: PoolDetailsStatsProps) { + const isScreenSize = useScreenSize() + const screenIsNotLarge = isScreenSize['lg'] + const theme = useTheme() + + const currency0 = useCurrency(poolData?.token0?.address, chainId) + const currency1 = useCurrency(poolData?.token1?.address, chainId) + + const [token0, token1] = useMemo(() => { + if (poolData && poolData.tvlToken0 && poolData.token0Price && poolData.tvlToken1 && poolData.token1Price) { + const fullWidth = poolData?.tvlToken0 * poolData?.token0Price + poolData?.tvlToken1 * poolData?.token1Price + const token0FullData: TokenFullData = { + ...poolData?.token0, + price: poolData?.token0Price, + tvl: poolData?.tvlToken0, + percent: (poolData?.tvlToken0 * poolData?.token0Price) / fullWidth, + currency: currency0, + } + const token1FullData: TokenFullData = { + ...poolData?.token1, + price: poolData?.token1Price, + tvl: poolData?.tvlToken1, + percent: (poolData?.tvlToken1 * poolData?.token1Price) / fullWidth, + currency: currency1, + } + return isReversed ? [token1FullData, token0FullData] : [token0FullData, token1FullData] + } else { + return [undefined, undefined] + } + }, [currency0, currency1, isReversed, poolData]) + + if (loading || !token0 || !token1 || !poolData) { + return ( + + + + + {Array.from({ length: 4 }).map((_, i) => ( + + + + + ))} + + ) + } + + return ( + + + Stats + + + + Pool balances + + + + + + {screenIsNotLarge && ( + + + + + )} + + {poolData?.tvlUSD && ( + TVL} value={poolData.tvlUSD} delta={poolData.tvlUSDChange} /> + )} + {poolData?.volumeUSD24H !== undefined && ( + 24H volume} value={poolData.volumeUSD24H} delta={poolData.volumeUSD24HChange} /> + )} + {poolData?.volumeUSD24H !== undefined && poolData?.feeTier !== undefined && ( + 24H fees} value={poolData.volumeUSD24H * (poolData.feeTier / 1000000)} /> + )} + + ) +} + +const StatsTextContainer = styled(Row)` + gap: 4px; + width: 100%; + align-items: flex-end; + + @media (max-width: ${BREAKPOINTS.lg}px) { + flex-direction: column; + gap: 0px; + align-items: flex-start; + } +` + +const StatItemText = styled(Text)` + color: ${({ theme }) => theme.neutral1}; + font-size: 36px; + font-weight: 485; + line-height: 44px; + + @media (max-width: ${BREAKPOINTS.lg}px) { + font-size: 20px; + line-height: 28px; + } +` + +function StatItem({ title, value, delta }: { title: ReactNode; value: number; delta?: number }) { + const { formatNumber, formatDelta } = useFormatter() + + return ( + + {title} + + + {formatNumber({ + input: value, + type: NumberType.FiatTokenStats, + })} + + {!!delta && ( + + + {formatDelta(delta)} + + )} + + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx new file mode 100644 index 0000000..cd7bdf9 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.test.tsx @@ -0,0 +1,84 @@ +import userEvent from '@testing-library/user-event' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import store from 'state' +import { addSerializedToken } from 'state/user/reducer' +import { mocked } from 'test-utils/mocked' +import { useMultiChainPositionsReturnValue, validBEPoolToken0, validBEPoolToken1 } from 'test-utils/pools/fixtures' +import { act, render, screen } from 'test-utils/render' + +import { PoolDetailsStatsButtons } from './PoolDetailsStatsButtons' + +jest.mock('components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions') + +describe('PoolDetailsStatsButton', () => { + const mockProps = { + chainId: 1, + token0: validBEPoolToken0, + token1: validBEPoolToken1, + feeTier: 500, + } + + const mockPropsTokensReversed = { + ...mockProps, + token0: validBEPoolToken1, + token1: validBEPoolToken0, + } + + beforeEach(() => { + mocked(useMultiChainPositions).mockReturnValue(useMultiChainPositionsReturnValue) + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + symbol: 'USDC', + name: 'USD Coin', + decimals: 6, + }, + }) + ) + store.dispatch( + addSerializedToken({ + serializedToken: { + chainId: 1, + address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + symbol: 'WETH', + name: 'Wrapped Ether', + decimals: 18, + }, + }) + ) + }) + + it('loading skeleton shown correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByTestId('pdp-buttons-loading-skeleton')).toBeVisible() + }) + + it('renders both buttons correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByTestId('pool-details-add-liquidity-button')).toBeVisible() + expect(screen.getByTestId('pool-details-swap-button')).toBeVisible() + }) + + it('clicking swap reveals swap modal', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('pool-details-swap-button'))) + expect(screen.getByTestId('pool-details-swap-modal')).toBeVisible() + expect(screen.getByTestId('pool-details-close-button')).toBeVisible() + }) + + it('clicking add liquidity goes to correct url', async () => { + render() + + await act(() => userEvent.click(screen.getByTestId('pool-details-add-liquidity-button'))) + expect(globalThis.window.location.href).toContain( + '/add/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/500' + ) + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx new file mode 100644 index 0000000..056739c --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsStatsButtons.tsx @@ -0,0 +1,298 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { Scrim } from 'components/AccountDrawer' +import { PositionInfo } from 'components/AccountDrawer/MiniPortfolio/Pools/cache' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import Column from 'components/Column' +import { CurrencySelect } from 'components/CurrencyInputPanel/SwapCurrencyInputPanel' +import { ReverseArrow } from 'components/Icons/ReverseArrow' +import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' +import Row from 'components/Row' +import { SwapWrapperOuter } from 'components/swap/styled' +import { LoadingBubble } from 'components/Tokens/loading' +import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage' +import { checkWarning, getPriorityWarning, NotFoundWarning } from 'constants/tokenSafety' +import { Token } from 'graphql/data/__generated__/types-and-hooks' +import { chainIdToBackendName, gqlToCurrency } from 'graphql/data/util' +import { useScreenSize } from 'hooks/useScreenSize' +import { useSwitchChain } from 'hooks/useSwitchChain' +import { Swap } from 'pages/Swap' +import { useMemo, useReducer } from 'react' +import { Plus, X } from 'react-feather' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle, ThemedText } from 'theme/components' +import { opacify } from 'theme/utils' +import { Z_INDEX } from 'theme/zIndex' +import { currencyId } from 'utils/currencyId' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +const PoolDetailsStatsButtonsRow = styled(Row)` + gap: 12px; + z-index: 1; + + @media (max-width: ${BREAKPOINTS.lg}px) { + gap: 8px; + position: fixed; + bottom: 0px; + left: 0; + margin: 8px; + width: calc(100% - 16px); + background: ${({ theme }) => theme.surface1}; + padding: 12px 32px; + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 20px; + backdrop-filter: blur(10px); + & > :first-child { + margin-right: auto; + } + z-index: ${Z_INDEX.sticky}; + } + @media (max-width: ${BREAKPOINTS.md}px) { + bottom: 56px; + } +` + +const PoolButton = styled.button<{ $open?: boolean; $hideOnMobile?: boolean; $fixedWidth?: boolean }>` + display: flex; + flex-direction: row; + padding: 12px 16px 12px 12px; + border: unset; + border-radius: 900px; + width: ${({ $open }) => ($open ? '100px' : '50%')}; + gap: 8px; + color: ${({ theme, $open }) => ($open ? theme.neutral1 : theme.accent1)}; + background-color: ${({ theme, $open }) => ($open ? theme.surface1 : opacify(12, theme.accent1))}; + justify-content: center; + transition: ${({ theme }) => `width ${theme.transition.duration.medium} ${theme.transition.timing.inOut}`}; + border: ${({ theme, $open }) => $open && `1px solid ${theme.surface3}`}; + ${ClickableStyle} + @media (max-width: ${BREAKPOINTS.lg}px) { + width: ${({ $fixedWidth }) => $fixedWidth && '120px'}; + @media (max-width: ${BREAKPOINTS.sm}px) { + display: ${({ $hideOnMobile }) => $hideOnMobile && 'none'}; + width: ${({ $fixedWidth }) => !$fixedWidth && '100%'}; + } +` + +const SwapIcon = styled(ReverseArrow)` + fill: ${({ theme }) => theme.accent1}; + rotate: 90deg; +` + +const ButtonBubble = styled(LoadingBubble)` + height: 44px; + width: 175px; + border-radius: 900px; +` + +const SwapModalWrapper = styled(Column)<{ open?: boolean }>` + z-index: 0; + gap: 24px; + visibility: ${({ open }) => (open ? 'visible' : 'hidden')}; + opacity: ${({ open }) => (open ? '1' : '0')}; + max-height: ${({ open }) => (open ? '100vh' : '0')}; + transition: ${({ theme }) => `max-height ${theme.transition.duration.medium} ${theme.transition.timing.ease}`}; + padding-bottom: ${({ open }) => (open ? '24px' : '0')}; + + ${SwapWrapperOuter} { + &:before { + background-color: unset; + } + } + + // Need to override the default visibility to properly hide + ${CurrencySelect} { + visibility: ${({ open }) => (open ? 'visible' : 'hidden')}; + } + + @media (max-width: ${BREAKPOINTS.lg}px) { + position: fixed; + width: calc(100% - 16px); + padding: 0px 12px 12px; + border-radius: 24px; + max-width: 480px; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + z-index: ${Z_INDEX.fixed}; + background: ${({ theme }) => theme.surface1}; + transition: ${({ theme }) => `opacity ${theme.transition.duration.medium} ${theme.transition.timing.ease}`}; + } +` + +const MobileBalance = styled(Column)` + gap: 2px; + display: none; + @media (max-width: ${BREAKPOINTS.lg}px) { + display: flex; + } +` + +interface PoolDetailsStatsButtonsProps { + chainId?: number + token0?: Token + token1?: Token + feeTier?: number + loading?: boolean +} + +function findMatchingPosition(positions: PositionInfo[], token0?: Token, token1?: Token, feeTier?: number) { + return positions?.find( + (position) => + (position?.details.token0.toLowerCase() === token0?.address || + position?.details.token0.toLowerCase() === token1?.address) && + (position?.details.token1.toLowerCase() === token0?.address || + position?.details.token1.toLowerCase() === token1?.address) && + position?.details.fee == feeTier && + !position.closed + ) +} + +export function PoolDetailsStatsButtons({ chainId, token0, token1, feeTier, loading }: PoolDetailsStatsButtonsProps) { + const { chainId: walletChainId, connector, account } = useWeb3React() + const { positions: userOwnedPositions } = useMultiChainPositions(account ?? '', chainId ? [chainId] : undefined) + const position = userOwnedPositions && findMatchingPosition(userOwnedPositions, token0, token1, feeTier) + const tokenId = position?.details.tokenId + const switchChain = useSwitchChain() + const navigate = useNavigate() + const currency0 = token0 && gqlToCurrency(token0) + const currency1 = token1 && gqlToCurrency(token1) + + // Mobile Balance Data + const { data: balanceQuery } = useCachedPortfolioBalancesQuery({ account }) + const { balance0, balance1, balance0Fiat, balance1Fiat } = useMemo(() => { + const filteredBalances = balanceQuery?.portfolios?.[0].tokenBalances?.filter( + (tokenBalance) => tokenBalance.token?.chain === chainIdToBackendName(chainId) + ) + const tokenBalance0 = filteredBalances?.find((tokenBalance) => tokenBalance.token?.address === token0?.address) + const tokenBalance1 = filteredBalances?.find((tokenBalance) => tokenBalance.token?.address === token1?.address) + return { + balance0: tokenBalance0?.quantity ?? 0, + balance1: tokenBalance1?.quantity ?? 0, + balance0Fiat: tokenBalance0?.denominatedValue?.value ?? 0, + balance1Fiat: tokenBalance1?.denominatedValue?.value ?? 0, + } + }, [balanceQuery?.portfolios, chainId, token0?.address, token1?.address]) + const { formatNumber } = useFormatter() + const formattedBalance0 = formatNumber({ + input: balance0, + type: NumberType.TokenNonTx, + }) + const formattedBalance1 = formatNumber({ + input: balance1, + type: NumberType.TokenNonTx, + }) + const totalFiatValue = balance0Fiat + balance1Fiat + const formattedFiatValue = formatNumber({ + input: totalFiatValue, + type: NumberType.PortfolioBalance, + }) + + const handleAddLiquidity = async () => { + if (currency0 && currency1) { + if (walletChainId !== chainId && chainId) await switchChain(connector, chainId) + navigate(`/add/${currencyId(currency0)}/${currencyId(currency1)}/${feeTier}${tokenId ? `/${tokenId}` : ''}`) + } + } + const [swapModalOpen, toggleSwapModalOpen] = useReducer((state) => !state, false) + const isScreenSize = useScreenSize() + const screenSizeLargerThanTablet = isScreenSize['lg'] + const isMobile = !isScreenSize['sm'] + const token0Warning = token0?.address ? checkWarning(token0?.address) : null + const token1Warning = token1?.address ? checkWarning(token1?.address) : null + const priorityWarning = getPriorityWarning(token0Warning, token1Warning) + + if (loading || !currency0 || !currency1) + return ( + + + + + + + + ) + + return ( + + + {account && ( + + + Your balances + + + + {formattedBalance0} {currency0.symbol} + + | + + {formattedBalance1} {currency1.symbol} + + {Boolean(totalFiatValue) && !isMobile && ({formattedFiatValue})} + + + )} + + {swapModalOpen ? ( + <> + {screenSizeLargerThanTablet && } + + Close + + + ) : ( + <> + {screenSizeLargerThanTablet && } + + Swap + + + )} + + + {screenSizeLargerThanTablet && } + + Add liquidity + + + + + + {Boolean(priorityWarning) && ( + + )} + + + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsTable.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTable.tsx new file mode 100644 index 0000000..1ab8201 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTable.tsx @@ -0,0 +1,85 @@ +import { Trans } from '@lingui/macro' +import { Pool } from '@uniswap/v3-sdk' +import { useWeb3React } from '@web3-react/core' +import useMultiChainPositions from 'components/AccountDrawer/MiniPortfolio/Pools/useMultiChainPositions' +import Column from 'components/Column' +import { PoolDetailsPositionsTable } from 'components/Pools/PoolDetails/PoolDetailsPositionsTable' +import Row from 'components/Row' +import { ProtocolVersion, Token } from 'graphql/data/__generated__/types-and-hooks' +import { supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util' +import { useMemo, useState } from 'react' +import { useParams } from 'react-router-dom' +import styled from 'styled-components' +import { ClickableStyle, ThemedText } from 'theme/components' + +import { PoolDetailsTransactionsTable } from './PoolDetailsTransactionsTable' + +enum PoolDetailsTableTabs { + TRANSACTIONS = 'transactions', + POSITIONS = 'positions', +} + +const TableHeader = styled(ThemedText.HeadlineMedium)<{ active: boolean }>` + color: ${({ theme, active }) => !active && theme.neutral2}; + ${({ disabled }) => !disabled && ClickableStyle} + user-select: none; +` + +export function PoolDetailsTableTab({ + poolAddress, + token0, + token1, + protocolVersion, +}: { + poolAddress: string + token0?: Token + token1?: Token + protocolVersion?: ProtocolVersion +}) { + const [activeTable, setActiveTable] = useState(PoolDetailsTableTabs.TRANSACTIONS) + const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) + const chainId = supportedChainIdFromGQLChain(chainName) + const { account } = useWeb3React() + const { positions } = useMultiChainPositions(account ?? '', [chainId]) + const positionsInThisPool = useMemo( + () => + positions?.filter( + (position) => + Pool.getAddress(position.pool.token0, position.pool.token1, position.pool.fee).toLowerCase() === + poolAddress.toLowerCase() + ) ?? [], + [poolAddress, positions] + ) + return ( + + + setActiveTable(PoolDetailsTableTabs.TRANSACTIONS)} + disabled={!positionsInThisPool.length} + > + Transactions + + {Boolean(positionsInThisPool.length) && ( + setActiveTable(PoolDetailsTableTabs.POSITIONS)} + > + Positions + {` (${positionsInThisPool?.length})`} + + )} + + {activeTable === PoolDetailsTableTabs.TRANSACTIONS ? ( + + ) : ( + + )} + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionTable.test.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionTable.test.tsx new file mode 100644 index 0000000..a439312 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionTable.test.tsx @@ -0,0 +1,83 @@ +import { ApolloError } from '@apollo/client' +import { getAbbreviatedTimeString } from 'components/Table/utils' +import Router from 'react-router-dom' +import { mocked } from 'test-utils/mocked' +import { usdcWethPoolAddress, validParams } from 'test-utils/pools/fixtures' +import { render, screen } from 'test-utils/render' + +import { PoolTableTransactionType, usePoolTransactions } from 'graphql/data/pools/usePoolTransactions' +import { PoolDetailsTransactionsTable } from './PoolDetailsTransactionsTable' + +jest.mock('graphql/data/pools/usePoolTransactions') +jest.mock('components/Table/utils') +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})) + +describe('PoolDetailsTransactionsTable', () => { + beforeEach(() => { + jest.spyOn(Router, 'useParams').mockReturnValue(validParams) + }) + + it('renders loading state', () => { + mocked(usePoolTransactions).mockReturnValue({ + loading: true, + error: undefined, + transactions: [], + loadMore: jest.fn(), + }) + + const { asFragment } = render() + expect(screen.getAllByTestId('cell-loading-bubble')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders error state', () => { + mocked(usePoolTransactions).mockReturnValue({ + loading: false, + error: new ApolloError({ errorMessage: 'error fetching data' }), + transactions: [], + loadMore: jest.fn(), + }) + + const { asFragment } = render() + expect(screen.getByTestId('table-error-modal')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders data filled state', () => { + const mockData = [ + { + timestamp: 1000000, + transaction: '0x123', + pool: { + token0: { + id: 'Token0', + symbol: 'Token0', + }, + token1: { + id: 'Token1', + symbol: 'Token1', + }, + }, + maker: '0xabc', + amount0: 200, + amount1: 300, + amountUSD: 400, + type: PoolTableTransactionType.BUY, + }, + ] + mocked(usePoolTransactions).mockReturnValue({ + transactions: mockData, + loading: false, + error: undefined, + loadMore: jest.fn(), + }) + mocked(getAbbreviatedTimeString).mockReturnValue('1mo ago') + + const { asFragment } = render() + expect(screen.getByTestId('pool-details-transactions-table')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionsTable.tsx b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionsTable.tsx new file mode 100644 index 0000000..3eb2ff3 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/PoolDetailsTransactionsTable.tsx @@ -0,0 +1,302 @@ +import { Trans } from '@lingui/macro' +import { createColumnHelper } from '@tanstack/react-table' +import Row from 'components/Row' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { Filter } from 'components/Table/Filter' +import { FilterHeaderRow, HeaderArrow, HeaderSortText, TimestampCell } from 'components/Table/styled' +import { ProtocolVersion, Token } from 'graphql/data/__generated__/types-and-hooks' +import { + PoolTableTransaction, + PoolTableTransactionType, + usePoolTransactions, +} from 'graphql/data/pools/usePoolTransactions' +import { supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util' +import { OrderDirection, Transaction_OrderBy } from 'graphql/thegraph/__generated__/types-and-hooks' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { useMemo, useReducer, useState } from 'react' +import { useParams } from 'react-router-dom' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { shortenAddress } from 'utilities/src/addresses' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const StyledExternalLink = styled(ExternalLink)` + color: ${({ theme }) => theme.neutral2}; + stroke: ${({ theme }) => theme.neutral2}; +` + +const TableWrapper = styled.div` + min-height: 256px; +` + +type PoolTxTableSortState = { + sortBy: Transaction_OrderBy + sortDirection: OrderDirection +} + +enum PoolTransactionColumn { + Timestamp, + Type, + MakerAddress, + FiatValue, + InputAmount, + OutputAmount, +} + +const PoolTransactionColumnWidth: { [key in PoolTransactionColumn]: number } = { + [PoolTransactionColumn.Timestamp]: 120, + [PoolTransactionColumn.Type]: 144, + [PoolTransactionColumn.MakerAddress]: 100, + [PoolTransactionColumn.FiatValue]: 125, + [PoolTransactionColumn.InputAmount]: 125, + [PoolTransactionColumn.OutputAmount]: 125, +} + +export function PoolDetailsTransactionsTable({ + poolAddress, + token0, + token1, + protocolVersion, +}: { + poolAddress: string + token0?: Token + token1?: Token + protocolVersion?: ProtocolVersion +}) { + const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) + const chainId = supportedChainIdFromGQLChain(chainName) + const activeLocalCurrency = useActiveLocalCurrency() + const { formatNumber, formatFiatPrice } = useFormatter() + const [filterModalIsOpen, toggleFilterModal] = useReducer((s) => !s, false) + const [filter, setFilters] = useState([ + PoolTableTransactionType.BUY, + PoolTableTransactionType.SELL, + PoolTableTransactionType.BURN, + PoolTableTransactionType.MINT, + ]) + + const [sortState] = useState({ + sortBy: Transaction_OrderBy.Timestamp, + sortDirection: OrderDirection.Desc, + }) + const { transactions, loading, loadMore, error } = usePoolTransactions( + poolAddress, + chainId, + filter, + token0, + protocolVersion + ) + + const showLoadingSkeleton = loading || !!error + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return [ + columnHelper.accessor((row) => row, { + id: 'timestamp', + header: () => ( + + + {sortState.sortBy === Transaction_OrderBy.Timestamp && } + + Time + + + + ), + cell: (row) => ( + + + + ), + }), + columnHelper.accessor( + (row) => { + let color, text + if (row.type === PoolTableTransactionType.BUY) { + color = 'success' + text = ( + + Buy {token0?.symbol} + + ) + } else if (row.type === PoolTableTransactionType.SELL) { + color = 'critical' + text = ( + + Sell {token0?.symbol} + + ) + } else { + color = row.type === PoolTableTransactionType.MINT ? 'success' : 'critical' + text = row.type === PoolTableTransactionType.MINT ? Add : Remove + } + return {text} + }, + { + id: 'swap-type', + header: () => ( + + toggleFilterModal()}> + + + Type + + + + ), + cell: (PoolTransactionTableType) => ( + + {PoolTransactionTableType.getValue?.()} + + ), + } + ), + columnHelper.accessor((row) => row.amountUSD, { + id: 'fiat-value', + header: () => ( + + + {activeLocalCurrency} + + + ), + cell: (fiat) => ( + + {formatFiatPrice({ price: fiat.getValue?.() })} + + ), + }), + columnHelper.accessor( + (row) => (row.pool.token0.id.toLowerCase() === token0?.address?.toLowerCase() ? row.amount0 : row.amount1), + { + id: 'input-amount', + header: () => ( + + {token0?.symbol} + + ), + cell: (inputTokenAmount) => ( + + + {formatNumber({ input: Math.abs(inputTokenAmount.getValue?.() ?? 0), type: NumberType.TokenTx })} + + + ), + } + ), + columnHelper.accessor( + (row) => (row.pool.token0.id.toLowerCase() === token0?.address?.toLowerCase() ? row.amount1 : row.amount0), + { + id: 'output-amount', + header: () => ( + + {token1?.symbol} + + ), + cell: (outputTokenAmount) => ( + + + {formatNumber({ input: Math.abs(outputTokenAmount.getValue?.() ?? 0), type: NumberType.TokenTx })} + + + ), + } + ), + columnHelper.accessor((row) => row.maker, { + id: 'maker-address', + header: () => ( + + + Wallet + + + ), + cell: (makerAddress) => ( + + + {shortenAddress(makerAddress.getValue?.(), 0)} + + + ), + }), + ] + }, [ + activeLocalCurrency, + chainId, + filter, + filterModalIsOpen, + formatFiatPrice, + formatNumber, + showLoadingSkeleton, + sortState.sortBy, + token0?.address, + token0?.symbol, + token1?.symbol, + ]) + + return ( + + + + ) +} diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap new file mode 100644 index 0000000..b5a25f9 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsHeader.test.tsx.snap @@ -0,0 +1,739 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsHeader loading skeleton is shown 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c3 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c5 { + height: 40px; + width: 137px; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: 'flex-start'; + -webkit-box-align: 'flex-start'; + -ms-flex-align: 'flex-start'; + align-items: 'flex-start'; + width: 100%; + -webkit-animation: iAjNNh 125ms ease-in; + animation: iAjNNh 125ms ease-in; + -webkit-animation-duration: 250ms; + animation-duration: 250ms; +} + +.c4 { + width: 32px; + height: 32px; + border-radius: 50%; +} + + + +
+
+
+
+
+
+ + + +`; + +exports[`PoolDetailsHeader renders breadcrumb text correctly 1`] = ` + + .c2 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c3 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + display: inline-block; + height: inherit; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + color: #7D7D7D; + font-size: 16px; + line-height: 24px; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 4px; + margin-bottom: 20px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #7D7D7D; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c1:hover { + color: #CECECE; +} + +.c4 { + gap: 6px; +} + +.c5 { + font-weight: inherit; + font-size: inherit; + line-height: inherit; + color: #222222; + white-space: nowrap; + margin: 0; +} + +.c6 { + cursor: pointer; + gap: 10px; + white-space: nowrap; +} + + + + + + + +`; + +exports[`PoolDetailsHeader renders header text correctly 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c13 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 12px; +} + +.c14 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + gap: 8px; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c10 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c16 { + display: inline-block; + height: inherit; +} + +.c17 { + height: 100%; + color: #222222; + background-color: #FFFFFF; + margin: 0; + padding: 2px 6px 2px 14px; + border-radius: 12px; + font-size: 16px; + line-height: 24px; + font-weight: 535; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + border: 1px solid #22222212; + outline: none; +} + +.c17:hover { + cursor: pointer; + background-color: #F9F9F9; + opacity: 1; +} + +.c17:focus { + background-color: #22222212; +} + +.c15 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + border: none; + text-align: left; + width: 100%; +} + +.c19 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + gap: 8px; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + border: none; + font-weight: 535; + width: 100%; + vertical-align: middle; + white-space: nowrap; +} + +.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 8px 12px; + border-radius: 20px; + border: none; + background-color: #F9F9F9; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c18:hover { + opacity: 0.6; +} + +.c18:active { + opacity: 0.4; +} + +.c18:hover { + background-color: #2222221f; +} + +.c18:focus { + background-color: #2222221f; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: 'flex-start'; + -webkit-box-align: 'flex-start'; + -ms-flex-align: 'flex-start'; + align-items: 'flex-start'; + width: 100%; + -webkit-animation: iAjNNh 125ms ease-in; + animation: iAjNNh 125ms ease-in; + -webkit-animation-duration: 250ms; + animation-duration: 250ms; +} + +.c11 { + background: #F9F9F9; + padding: 2px 6px; + border-radius: 4px; +} + +.c12 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + fill: #7D7D7D; +} + +.c12:hover { + opacity: 0.6; +} + +.c12:active { + opacity: 0.4; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + gap: 12px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c8 { + font-size: 24px !important; +} + +.c9 { + color: #222222; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c9:hover { + opacity: 0.6; +} + +.c9:active { + opacity: 0.4; +} + +.c3 { + position: relative; + top: 0; + left: 0; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c4 img { + width: 16px; + height: 32px; + object-fit: cover; +} + +.c4 img:first-child { + border-radius: 16px 0 0 16px; + object-position: 0 0; +} + +.c4 img:last-child { + border-radius: 0 16px 16px 0; + object-position: 100% 0; +} + +.c5 { + width: 16px; + height: 32px; + border-radius: 50%; +} + +@media screen and (max-width:640px) { + .c8 { + font-size: 18px !important; + line-height: 24px !important; + } +} + + + +
+
+
+
+ + +
+
+
+
+ +
+
+ 0.05% +
+ + + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+`; diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsLink.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsLink.test.tsx.snap new file mode 100644 index 0000000..8fd8b74 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsLink.test.tsx.snap @@ -0,0 +1,700 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsHeader renders link for pool address 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: space-between; + -webkit-box-align: space-between; + -ms-flex-align: space-between; + align-items: space-between; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c14 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c14:hover { + opacity: 0.6; +} + +.c14:active { + opacity: 0.4; +} + +.c11 { + display: inline-block; + height: inherit; +} + +.c4 { + position: relative; + top: 0; + left: 0; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c5 img { + width: 10px; + height: 20px; + object-fit: cover; +} + +.c5 img:first-child { + border-radius: 10px 0 0 10px; + object-position: 0 0; +} + +.c5 img:last-child { + border-radius: 0 10px 10px 0; + object-position: 100% 0; +} + +.c6 { + width: 10px; + height: 20px; + border-radius: 50%; +} + +.c8 { + display: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c3 { + gap: 8px; + margin-right: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c9 { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c12 { + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + background-color: #22222212; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c12:hover { + opacity: 0.6; +} + +.c12:active { + opacity: 0.4; +} + +.c13 { + width: 16px; + height: 16px; + color: #7D7D7D; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c15 { + padding: 8px; + border-radius: 20px; + background-color: #22222212; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c15:hover { + opacity: 0.6; +} + +.c15:active { + opacity: 0.4; +} + +.c10 { + gap: 8px; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +@media (max-width:1024px) and (min-width:396px) { + .c8 { + display: block; + } +} + +@media (max-width:1024px) and (min-width:396px) { + .c9 { + color: #7D7D7D; + } +} + + + +
+
+
+
+ + +
+
+
+ Pool +
+
+ USDC / WETH +
+
+
+
+
+ 0x88e6...5640 + + + + +
+
+ +
+ + + + +
+
+
+
+
+
+
+`; + +exports[`PoolDetailsHeader renders link for token address 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: space-between; + -webkit-box-align: space-between; + -ms-flex-align: space-between; + align-items: space-between; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c15 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c15:hover { + opacity: 0.6; +} + +.c15:active { + opacity: 0.4; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c6 { + width: 20px; + height: 20px; + border-radius: 50%; +} + +.c4 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c12 { + display: inline-block; + height: inherit; +} + +.c8 { + display: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c3 { + gap: 8px; + margin-right: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c3:hover { + opacity: 0.6; +} + +.c3:active { + opacity: 0.4; +} + +.c9 { + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c13 { + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + background-color: #22222212; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c13:hover { + opacity: 0.6; +} + +.c13:active { + opacity: 0.4; +} + +.c14 { + width: 16px; + height: 16px; + color: #7D7D7D; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; +} + +.c16 { + padding: 8px; + border-radius: 20px; + background-color: #22222212; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c16:hover { + opacity: 0.6; +} + +.c16:active { + opacity: 0.4; +} + +.c11 { + gap: 8px; + -webkit-flex-shrink: 0; + -ms-flex-negative: 0; + flex-shrink: 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +@media (max-width:1024px) and (min-width:396px) { + .c8 { + display: block; + } +} + +@media (max-width:1024px) and (min-width:396px) { + .c9 { + color: #7D7D7D; + } +} + + + +
+
+
+
+ +
+
+
+ USD Coin +
+
+
+ USDC + + + +
+
+
+
+
+
+ 0xA0b8...eB48 + + + + +
+
+ +
+ + + + +
+
+
+
+
+
+
+`; diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsPositionTable.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsPositionTable.test.tsx.snap new file mode 100644 index 0000000..5aa31c5 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsPositionTable.test.tsx.snap @@ -0,0 +1,886 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsPositionsTable renders with PositionStatus Closed 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c8 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c12 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c5 img { + width: 8px; + height: 16px; + object-fit: cover; +} + +.c5 img:first-child { + border-radius: 8px 0 0 8px; + object-position: 0 0; +} + +.c5 img:last-child { + border-radius: 0 8px 8px 0; + object-position: 100% 0; +} + +.c6 { + width: 8px; + height: 16px; + border-radius: 50%; +} + +.c1 { + gap: 24px; + margin-top: 24px; + width: 100%; +} + +.c2 { + gap: 4px; + background: #F9F9F9; + border-radius: 12px; + width: 100%; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + padding: 16px; +} + +.c2:hover { + opacity: 0.6; +} + +.c2:active { + opacity: 0.4; +} + +.c9 { + padding: 4px 6px; + background: #F9F9F9; +} + +.c11 { + gap: 8px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + margin-right: 0; + margin-left: auto; + color: #7D7D7D; +} + +.c13 { + gap: 10px; +} + +.c14 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + white-space: nowrap; +} + +@media screen and (max-width:640px) { + .c13 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 4px; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + +@media screen and (max-width:640px) { + .c15 { + display: none; + } +} + + + +
+
+
+
+ + +
+
+ WETH/USDC +
+
+ 0.5% +
+
+
+ Closed +
+ + + + + + + + + + + +
+
+
+
+ Min: 0 WETH per USDC +
+ + + +
+ Max: 1.00 WETH per USDC +
+
+
+
+
+
+
+`; + +exports[`PoolDetailsPositionsTable renders with PositionStatus In Range 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c8 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c12 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c5 img { + width: 8px; + height: 16px; + object-fit: cover; +} + +.c5 img:first-child { + border-radius: 8px 0 0 8px; + object-position: 0 0; +} + +.c5 img:last-child { + border-radius: 0 8px 8px 0; + object-position: 100% 0; +} + +.c6 { + width: 8px; + height: 16px; + border-radius: 50%; +} + +.c1 { + gap: 24px; + margin-top: 24px; + width: 100%; +} + +.c2 { + gap: 4px; + background: #F9F9F9; + border-radius: 12px; + width: 100%; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + padding: 16px; +} + +.c2:hover { + opacity: 0.6; +} + +.c2:active { + opacity: 0.4; +} + +.c9 { + padding: 4px 6px; + background: #F9F9F9; +} + +.c11 { + gap: 8px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + margin-right: 0; + margin-left: auto; + color: #40B66B; +} + +.c14 { + gap: 10px; +} + +.c15 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + white-space: nowrap; +} + +.c13 { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: #40B66B; +} + +@media screen and (max-width:640px) { + .c14 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 4px; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + +@media screen and (max-width:640px) { + .c16 { + display: none; + } +} + + + +
+
+
+
+ + +
+
+ WETH/USDC +
+
+ 0.5% +
+
+
+ In range +
+
+
+
+
+
+ Min: 0 WETH per USDC +
+ + + +
+ Max: 1.00 WETH per USDC +
+
+
+
+ + + +`; + +exports[`PoolDetailsPositionsTable renders with PositionStatus Out Of Range 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c7 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c8 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c12 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c5 img { + width: 8px; + height: 16px; + object-fit: cover; +} + +.c5 img:first-child { + border-radius: 8px 0 0 8px; + object-position: 0 0; +} + +.c5 img:last-child { + border-radius: 0 8px 8px 0; + object-position: 100% 0; +} + +.c6 { + width: 8px; + height: 16px; + border-radius: 50%; +} + +.c1 { + gap: 24px; + margin-top: 24px; + width: 100%; +} + +.c2 { + gap: 4px; + background: #F9F9F9; + border-radius: 12px; + width: 100%; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + padding: 16px; +} + +.c2:hover { + opacity: 0.6; +} + +.c2:active { + opacity: 0.4; +} + +.c9 { + padding: 4px 6px; + background: #F9F9F9; +} + +.c11 { + gap: 8px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + margin-right: 0; + margin-left: auto; + color: #EEB317; +} + +.c13 { + gap: 10px; +} + +.c14 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + white-space: nowrap; +} + +@media screen and (max-width:640px) { + .c13 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 4px; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + +@media screen and (max-width:640px) { + .c15 { + display: none; + } +} + + + +
+
+
+
+ + +
+
+ WETH/USDC +
+
+ 0.5% +
+
+
+ Out of range +
+ + + + + +
+
+
+
+ Min: 0 WETH per USDC +
+ + + +
+ Max: 1.00 WETH per USDC +
+
+
+
+
+
+
+`; diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStats.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStats.test.tsx.snap new file mode 100644 index 0000000..c98852c --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStats.test.tsx.snap @@ -0,0 +1,908 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsStats pool balance chart not visible on mobile 1`] = ` + + .c5 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c15 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + padding: 4px 0px; +} + +.c6 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c16 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 4px 0px; +} + +.c4 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c11 { + width: 20px; + height: 20px; + border-radius: 50%; +} + +.c9 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c17 { + color: #FF5F52; +} + +.c2 { + font-weight: 485; + font-size: 24px; + line-height: 36px; +} + +.c1 { + gap: 24px; + padding: 20px; + border-radius: 20px; + background: #F9F9F9; + width: 100%; + z-index: 1; + margin-top: -24px; +} + +.c3 { + gap: 8px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + min-width: 180px; +} + +.c7 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c8 { + font-weight: 485; + font-size: 16px; + line-height: 24px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.c12 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #222222; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c12:hover { + opacity: 0.6; +} + +.c12:active { + opacity: 0.4; +} + +.c13 { + gap: 4px; + width: 100%; + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; +} + +.c14 { + color: #222222; + font-size: 36px; + font-weight: 485; + line-height: 44px; +} + +@media (max-width:1024px) { + .c2 { + width: 100%; + } +} + +@media (max-width:1024px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + background: transparent; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 20px 0px; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-top: 0px; + } +} + +@media (max-width:640px) { + .c3 { + min-width: 150px; + } +} + +@media (max-width:1024px) { + .c7 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } +} + +@media (max-width:1024px) { + .c8 { + font-size: 20px; + line-height: 28px; + width: 100%; + } +} + +@media (max-width:1024px) { + .c13 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 0px; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + +@media (max-width:1024px) { + .c14 { + font-size: 20px; + line-height: 28px; + } +} + + + +
+
+ Stats +
+
+
+ Pool balances +
+
+
+
+
+ +
+
+ 90.9M  + + USDC + +
+
+
+
+ +
+
+ 82.5K  + + ETH + +
+
+
+
+
+ TVL +
+
+
+ $223.2M +
+
+ + + +
+ 0.37% +
+
+
+
+
+
+ 24H volume +
+
+
+ $194.3M +
+
+ + + +
+ 17.75% +
+
+
+
+
+
+ 24H fees +
+
+
+ $97.1K +
+
+
+
+
+
+
+`; + +exports[`PoolDetailsStats renders stats text correctly 1`] = ` + + .c5 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c17 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + padding: 4px 0px; +} + +.c6 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c18 { + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + padding: 4px 0px; +} + +.c4 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c11 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c12 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c10 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c19 { + color: #FF5F52; +} + +.c2 { + font-weight: 485; + font-size: 24px; + line-height: 36px; +} + +.c1 { + gap: 24px; + padding: 20px; + border-radius: 20px; + background: #F9F9F9; + width: 100%; + z-index: 1; + margin-top: -24px; +} + +.c3 { + gap: 8px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + min-width: 180px; +} + +.c7 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c8 { + font-weight: 485; + font-size: 16px; + line-height: 24px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; +} + +.c9 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #222222; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c9:hover { + opacity: 0.6; +} + +.c9:active { + opacity: 0.4; +} + +.c13 { + height: 8px; + width: 99.99999996483845%; + background: #FC72FF; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + border-right: 1px solid #F9F9F9; +} + +.c14 { + height: 8px; + width: 3.5161546763980574e-8%; + background: #4C82FB; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + border-left: 1px solid #F9F9F9; +} + +.c15 { + gap: 4px; + width: 100%; + -webkit-align-items: flex-end; + -webkit-box-align: flex-end; + -ms-flex-align: flex-end; + align-items: flex-end; +} + +.c16 { + color: #222222; + font-size: 36px; + font-weight: 485; + line-height: 44px; +} + +@media (max-width:1024px) { + .c2 { + width: 100%; + } +} + +@media (max-width:1024px) { + .c1 { + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + background: transparent; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + padding: 20px 0px; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-top: 0px; + } +} + +@media (max-width:640px) { + .c3 { + min-width: 150px; + } +} + +@media (max-width:1024px) { + .c7 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } +} + +@media (max-width:1024px) { + .c8 { + font-size: 20px; + line-height: 28px; + width: 100%; + } +} + +@media (max-width:1024px) { + .c15 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 0px; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + +@media (max-width:1024px) { + .c16 { + font-size: 20px; + line-height: 28px; + } +} + + + +
+
+ Stats +
+
+
+ Pool balances +
+
+
+ 90.9M  + +
+
+ +
+
+ USDC +
+
+
+ 82.5K  + +
+
+ +
+
+ ETH +
+
+
+
+
+
+
+
+
+
+ TVL +
+
+
+ $223.2M +
+
+ + + +
+ 0.37% +
+
+
+
+
+
+ 24H volume +
+
+
+ $194.3M +
+
+ + + +
+ 17.75% +
+
+
+
+
+
+ 24H fees +
+
+
+ $97.1K +
+
+
+
+ + + +`; diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap new file mode 100644 index 0000000..0954a47 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsStatsButtons.test.tsx.snap @@ -0,0 +1,1503 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsStatsButton loading skeleton shown correctly 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c5 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c2 { + gap: 12px; + z-index: 1; +} + +.c6 { + height: 44px; + width: 175px; + border-radius: 900px; +} + +.c4 { + gap: 2px; + display: none; +} + +@media (max-width:1024px) { + .c2 { + gap: 8px; + position: fixed; + bottom: 0px; + left: 0; + margin: 8px; + width: calc(100% - 16px); + background: #FFFFFF; + padding: 12px 32px; + border: 1px solid #22222212; + border-radius: 20px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + z-index: 1020; + } + + .c2 > :first-child { + margin-right: auto; + } +} + +@media (max-width:768px) { + .c2 { + bottom: 56px; + } +} + +@media (max-width:1024px) { + .c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + } +} + + + +
+
+
+
+
+
+
+ + + +`; + +exports[`PoolDetailsStatsButton renders both buttons correctly 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c32 { + box-sizing: border-box; + margin: 0; + min-width: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + display: inline-block; + text-align: center; + line-height: inherit; + -webkit-text-decoration: none; + text-decoration: none; + font-size: inherit; + padding-left: 16px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + color: white; + background-color: primary; + border: 0; + border-radius: 4px; +} + +.c50 { + box-sizing: border-box; + margin: 0; + min-width: 0; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + display: inline-block; + text-align: center; + line-height: inherit; + -webkit-text-decoration: none; + text-decoration: none; + font-size: inherit; + padding-left: 16px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + color: white; + background-color: primary; + border: 0; + border-radius: 4px; + font-weight: 535; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c12 { + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c14 { + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c5 { + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c28 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c17 { + outline: none; + border: none; + font-size: inherit; + padding: 0; + margin: 0; + background: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + -webkit-transition-timing-function: ease-in-out; + transition-timing-function: ease-in-out; + -webkit-transition-property: opacity,color,background-color; + transition-property: opacity,color,background-color; +} + +.c17:hover { + opacity: 0.6; +} + +.c17:focus { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.c58 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c58:hover { + opacity: 0.6; +} + +.c58:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 24px; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c48 { + display: grid; + grid-auto-rows: auto; + grid-row-gap: 4px; +} + +.c53 { + background-color: transparent; + bottom: 0; + border-radius: inherit; + height: 100%; + left: 0; + position: absolute; + right: 0; + top: 0; + -webkit-transition: 150ms ease background-color; + transition: 150ms ease background-color; + width: 100%; +} + +.c34 { + padding: 16px; + width: 100%; + line-height: 24px; + font-weight: 535; + text-align: center; + border-radius: 16px; + outline: none; + border: 1px solid transparent; + color: #222222; + -webkit-text-decoration: none; + text-decoration: none; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-flex-wrap: nowrap; + -ms-flex-wrap: nowrap; + flex-wrap: nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + cursor: pointer; + position: relative; + z-index: 1; + will-change: transform; + -webkit-transition: -webkit-transform 450ms ease; + -webkit-transition: transform 450ms ease; + transition: transform 450ms ease; + -webkit-transform: perspective(1px) translateZ(0); + -ms-transform: perspective(1px) translateZ(0); + transform: perspective(1px) translateZ(0); +} + +.c34:disabled { + opacity: 50%; + cursor: auto; + pointer-events: none; +} + +.c34 > * { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.c34 > a { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c51 { + background-color: #FFEFFF; + color: #FC72FF; + font-size: 20px; + font-weight: 535; +} + +.c51:focus { + box-shadow: 0 0 0 1pt #FFEFFF; + background-color: #FFEFFF; +} + +.c51:hover { + background-color: #FFEFFF; +} + +.c51:active { + box-shadow: 0 0 0 1pt #FFEFFF; + background-color: #FFEFFF; +} + +.c51:hover .c52 { + background-color: #98A1C014; +} + +.c51:active .c52 { + background-color: #B8C0DC3d; +} + +.c51:disabled { + opacity: 0.4; +} + +.c51:disabled:hover { + cursor: auto; + background-color: transparent; + box-shadow: none; + border: 1px solid transparent; + outline: none; +} + +.c35 { + background-color: #FFFFFF; + color: #7D7D7D; + border: 1px solid #22222212; + font-size: 16px; + font-weight: 535; +} + +.c35:hover { + background-color: #ececec; +} + +.c35:active { + background-color: #e0e0e0; +} + +.c45 { + -webkit-filter: none; + filter: none; + opacity: 1; + -webkit-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; +} + +.c19 { + display: inline-block; + height: inherit; +} + +.c39 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c40 { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.c38 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c60 { + z-index: 1020; + overflow: hidden; + top: 0; + left: 0; + position: fixed; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.60); + opacity: 0; + pointer-events: none; +} + +.c30 { + color: #222222; + pointer-events: auto; + width: 0; + position: relative; + font-weight: 485; + outline: none; + border: none; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + background-color: transparent; + font-size: 28px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding: 0px; + -webkit-appearance: textfield; + text-align: right; +} + +.c30::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c30 [type='number'] { + -moz-appearance: textfield; +} + +.c30::-webkit-outer-spin-button, +.c30::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.c30::-webkit-input-placeholder { + color: #CECECE; +} + +.c30::-moz-placeholder { + color: #CECECE; +} + +.c30:-ms-input-placeholder { + color: #CECECE; +} + +.c30::placeholder { + color: #CECECE; +} + +.c31 { + -webkit-filter: none; + filter: none; + opacity: 1; + -webkit-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; + text-align: left; + font-size: 36px; + font-weight: 485; + max-height: 44px; +} + +.c26 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column nowrap; + -ms-flex-flow: column nowrap; + flex-flow: column nowrap; + position: relative; + border-radius: 20px; + z-index: 1; + width: initial; + -webkit-transition: height 1s ease; + transition: height 1s ease; + will-change: height; +} + +.c27 { + min-height: 44px; + border-radius: 20px; + width: initial; +} + +.c36 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background-color: #FFFFFF; + opacity: 1; + color: #222222; + cursor: pointer; + height: 36px; + border-radius: 18px; + outline: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + border: 1px solid #22222212; + font-size: 24px; + font-weight: 485; + width: initial; + padding: 4px 8px 4px 4px; + gap: 8px; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + margin-left: 12px; + box-shadow: 0px 0px 10px 0px rgba(34,34,34,0.04); + visibility: visible; + -webkit-animation: none; + animation: none; +} + +.c36:hover, +.c36:active { + background-color: #F9F9F9; +} + +.c36:before { + background-size: 100%; + border-radius: inherit; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + content: ''; +} + +.c36:hover:before { + background-color: #98A1C014; +} + +.c36:active:before { + background-color: #B8C0DC3d; +} + +.c29 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row nowrap; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c43 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: row nowrap; + -ms-flex-flow: row nowrap; + flex-flow: row nowrap; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #7D7D7D; + font-size: 0.75rem; + line-height: 1rem; +} + +.c43 span:hover { + cursor: pointer; + color: #4a4a4a; +} + +.c44 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + min-height: 24px; + padding: 8px 0px 0px 0px; +} + +.c37 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + width: 100%; +} + +.c42 { + margin: 0 0.25rem 0 0.35rem; + height: 35%; + margin-left: 8px; +} + +.c42 path { + stroke: #222222; + stroke-width: 2px; +} + +.c41 { + margin: 0 0.25rem 0 0.25rem; + font-size: 20px; + font-weight: 535; +} + +.c10 { + position: relative; + z-index: 1; + -webkit-transition: -webkit-transform 250ms ease; + -webkit-transition: transform 250ms ease; + transition: transform 250ms ease; + border-radius: 24px; +} + +.c11 { + border-radius: 24px; + z-index: -1; +} + +.c46 { + border-radius: 12px; + height: 40px; + width: 40px; + position: relative; + margin-top: -18px; + margin-bottom: -18px; + margin-left: auto; + margin-right: auto; + background-color: #F9F9F9; + border: 4px solid; + border-color: #FFFFFF; + z-index: 2; +} + +.c46:hover { + cursor: pointer; + opacity: 0.8; +} + +.c25 { + background-color: #F9F9F9; + border-radius: 16px; + color: #7D7D7D; + font-size: 14px; + font-weight: 500; + height: 120px; + line-height: 20px; + padding: 16px; + position: relative; +} + +.c25:before { + box-sizing: border-box; + background-size: 100%; + border-radius: inherit; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; + content: ''; + border: 1px solid #F9F9F9; +} + +.c25:hover:before { + border-color: #98A1C014; +} + +.c25:focus-within:before { + border-color: #B8C0DC3d; +} + +.c49 { + border-bottom: 1px solid #FFFFFF; +} + +.c47 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.c18 { + color: #222222; + background-color: #22222212; + padding: 8px 16px; + border-radius: 20px; + gap: 4px; + font-weight: 485; +} + +.c18:focus { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c18:active { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c20 { + color: #7D7D7D; + padding: 8px 16px; + border-radius: 20px; + gap: 4px; + font-weight: 485; +} + +.c20:focus { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c20:active { + -webkit-text-decoration: none; + text-decoration: none; +} + +.c54 { + width: 100%; + padding: 12px 20px 16px; + background-color: #FF5F521f; + border-radius: 16px; + border: 1px solid #22222212; + color: #FF5F52; +} + +.c55 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-weight: 535; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; +} + +.c56 { + font-weight: 535; + font-size: 16px; + line-height: 24px; + margin-left: 7px; +} + +.c57 { + margin-top: 8px; + font-size: 12px; + line-height: 16px; + color: #7D7D7D; +} + +.c59 { + color: #FC72FF; + font-weight: 535; +} + +.c24 { + height: 24px; + width: 24px; +} + +.c24 > * { + fill: #7D7D7D; +} + +.c22 { + border: none; + background-color: transparent; + margin: 0; + padding: 0; + cursor: pointer; + outline: none; +} + +.c22:not([disabled]):hover { + opacity: 0.7; +} + +.c23 { + padding: 6px 12px; + border-radius: 16px; +} + +.c21 { + position: relative; +} + +.c13 { + margin-bottom: 12px; + padding-right: 4px; + color: #7D7D7D; +} + +.c15 { + gap: 0px; +} + +.c15 .c16 { + padding: 8px 12px; +} + +.c3 { + gap: 12px; + z-index: 1; +} + +.c4 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + padding: 12px 16px 12px 12px; + border: unset; + border-radius: 900px; + width: 50%; + gap: 8px; + color: #FC72FF; + background-color: #FC72FF1f; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-transition: width 250ms ease-in-out; + transition: width 250ms ease-in-out; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c4:hover { + opacity: 0.6; +} + +.c4:active { + opacity: 0.4; +} + +.c6 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + padding: 12px 16px 12px 12px; + border: unset; + border-radius: 900px; + width: 50%; + gap: 8px; + color: #FC72FF; + background-color: #FC72FF1f; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-transition: width 250ms ease-in-out; + transition: width 250ms ease-in-out; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c6:hover { + opacity: 0.6; +} + +.c6:active { + opacity: 0.4; +} + +.c8 { + z-index: 0; + gap: 24px; + visibility: hidden; + opacity: 0; + max-height: 0; + -webkit-transition: max-height 250ms ease; + transition: max-height 250ms ease; + padding-bottom: 0; +} + +.c8 .c9:before { + background-color: unset; +} + +.c8 .c33 { + visibility: hidden; +} + +@media only screen and (max-width:1024px) { + .c60 { + opacity: 0; + pointer-events: none; + -webkit-transition: opacity 250ms ease-in-out; + transition: opacity 250ms ease-in-out; + } +} + +@media (max-width:1024px) { + .c3 { + gap: 8px; + position: fixed; + bottom: 0px; + left: 0; + margin: 8px; + width: calc(100% - 16px); + background: #FFFFFF; + padding: 12px 32px; + border: 1px solid #22222212; + border-radius: 20px; + -webkit-backdrop-filter: blur(10px); + backdrop-filter: blur(10px); + z-index: 1020; + } + + .c3 > :first-child { + margin-right: auto; + } +} + +@media (max-width:768px) { + .c3 { + bottom: 56px; + } +} + +@media (max-width:1024px) { + +} + +@media (max-width:1024px) { + +} + +@media (max-width:1024px) { + .c8 { + position: fixed; + width: calc(100% - 16px); + padding: 0px 12px 12px; + border-radius: 24px; + max-width: 480px; + left: 50%; + top: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + z-index: 1030; + background: #FFFFFF; + -webkit-transition: opacity 250ms ease; + transition: opacity 250ms ease; + } +} + + + +
+
+ + +
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+
+ You pay +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+
+ You receive +
+
+
+ +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+
+
+ + + + + +
+ Warning +
+
+
+ These tokens aren't traded on leading U.S. centralized exchanges or frequently swapped on Uniswap. Always conduct your own research before trading. + + Learn more + +
+
+
+
+
+ + + +`; diff --git a/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsTransactionTable.test.tsx.snap b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsTransactionTable.test.tsx.snap new file mode 100644 index 0000000..c33bcb1 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/__snapshots__/PoolDetailsTransactionTable.test.tsx.snap @@ -0,0 +1,3013 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolDetailsTransactionsTable renders data filled state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c12 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c28 { + color: #40B66B; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c29 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c21 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c21:hover { + opacity: 0.6; +} + +.c21:active { + opacity: 0.4; +} + +.c33 { + height: 16px; + width: 16px; +} + +.c33 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c34 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c25 { + display: inline-block; + height: inherit; +} + +.c2 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c19 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c19::-webkit-scrollbar { + display: none; +} + +.c31 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c32 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c23 { + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #222222; +} + +.c23:hover { + opacity: 0.6; +} + +.c23:active { + opacity: 0.4; +} + +.c11 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c13 { + color: #222222; +} + +.c15 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + gap: 4px; +} + +.c15:hover { + opacity: 0.6; +} + +.c15:active { + opacity: 0.4; +} + +.c24 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 8px; + width: 100%; +} + +.c27 { + display: none; + height: 16px; + width: 16px; + color: #7D7D7D; +} + +.c22:hover .c26 { + display: block; +} + +.c9 { + min-width: 120px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c14 { + min-width: 144px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 125px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c18 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c16 { + position: relative; +} + +.c30 { + color: #7D7D7D; + stroke: #7D7D7D; +} + +.c0 { + min-height: 256px; +} + +@media not all and (hover:none) { + .c20:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ + + + +
+ Time +
+
+
+
+
+
+
+ + + + +
+ Type +
+
+
+
+
+
+
+ USD +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet +
+
+
+
+
+
+
+ +
+
+
+ + Buy  + +
+
+
+
+
+
+ $400.00 +
+
+
+
+
+
+ 300 +
+
+
+
+
+
+ 200 +
+
+
+
+
+ +
+ +
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; + +exports[`PoolDetailsTransactionsTable renders error state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c12 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c24 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c27 { + height: 16px; + width: 16px; +} + +.c27 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c28 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c23 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + width: 320px; + padding: 12px; + gap: 12px; + background-color: #00000004; + -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(24px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + border: 1px #22222212 solid; + border-radius: 20px; +} + +.c2 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c21 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c21::-webkit-scrollbar { + display: none; +} + +.c25 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c26 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; + opacity: 0.4; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c11 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c13 { + color: #222222; +} + +.c15 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + gap: 4px; +} + +.c15:hover { + opacity: 0.6; +} + +.c15:active { + opacity: 0.4; +} + +.c18 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c9 { + min-width: 120px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c14 { + min-width: 144px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 125px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c20 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c19 { + width: 75%; + height: 16px; +} + +.c16 { + position: relative; +} + +.c0 { + min-height: 256px; +} + +@media not all and (hover:none) { + .c22:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ + + + +
+ Time +
+
+
+
+
+
+
+ + + + +
+ Type +
+
+
+
+
+
+
+ USD +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+
+
+ Error loading data +
+
+ Data is unavailable at the moment; we're working on a fix +
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; + +exports[`PoolDetailsTransactionsTable renders loading state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c12 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c25 { + height: 16px; + width: 16px; +} + +.c25 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c26 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c21 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c21::-webkit-scrollbar { + display: none; +} + +.c23 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c24 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c11 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c13 { + color: #222222; +} + +.c15 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + gap: 4px; +} + +.c15:hover { + opacity: 0.6; +} + +.c15:active { + opacity: 0.4; +} + +.c18 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c9 { + min-width: 120px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c14 { + min-width: 144px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 125px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c20 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c19 { + width: 75%; + height: 16px; +} + +.c16 { + position: relative; +} + +.c0 { + min-height: 256px; +} + +@media not all and (hover:none) { + .c22:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ + + + +
+ Time +
+
+
+
+
+
+
+ + + + +
+ Type +
+
+
+
+
+
+
+ USD +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Wallet +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; diff --git a/apps/web/src/components/Pools/PoolDetails/icons.tsx b/apps/web/src/components/Pools/PoolDetails/icons.tsx new file mode 100644 index 0000000..75dabd0 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/icons.tsx @@ -0,0 +1,34 @@ +type SVGProps = React.SVGProps & { + fill?: string + height?: string | number + width?: string | number + gradientId?: string +} + +export const ClosedCircle = (props: SVGProps) => ( + + + + + + + + + + + +) + +export const DoubleArrow = (props: SVGProps) => ( + + + +) diff --git a/apps/web/src/components/Pools/PoolDetails/shared.ts b/apps/web/src/components/Pools/PoolDetails/shared.ts new file mode 100644 index 0000000..a787f33 --- /dev/null +++ b/apps/web/src/components/Pools/PoolDetails/shared.ts @@ -0,0 +1,13 @@ +import { LoadingBubble } from 'components/Tokens/loading' +import styled from 'styled-components' + +export const DetailBubble = styled(LoadingBubble)<{ $height?: number; $width?: number }>` + height: ${({ $height }) => ($height ? `${$height}px` : '16px')}; + width: ${({ $width }) => ($width ? `${$width}px` : '80px')}; +` + +export const SmallDetailBubble = styled(LoadingBubble)` + height: 20px; + width: 20px; + border-radius: 100px; +` diff --git a/apps/web/src/components/Pools/PoolTable/PoolTable.test.tsx b/apps/web/src/components/Pools/PoolTable/PoolTable.test.tsx new file mode 100644 index 0000000..16ed0ec --- /dev/null +++ b/apps/web/src/components/Pools/PoolTable/PoolTable.test.tsx @@ -0,0 +1,72 @@ +import { ApolloError } from '@apollo/client' +import { Percent } from '@uniswap/sdk-core' +import { ProtocolVersion } from 'graphql/data/__generated__/types-and-hooks' +import { useTopPools } from 'graphql/data/pools/useTopPools' +import Router from 'react-router-dom' +import { mocked } from 'test-utils/mocked' +import { validBEPoolToken0, validBEPoolToken1, validParams } from 'test-utils/pools/fixtures' +import { render, screen } from 'test-utils/render' + +import { TopPoolTable } from './PoolTable' + +jest.mock('graphql/data/pools/useTopPools') +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})) + +describe('PoolTable', () => { + beforeEach(() => { + jest.spyOn(Router, 'useParams').mockReturnValue(validParams) + }) + + it('renders loading state', () => { + mocked(useTopPools).mockReturnValue({ + loading: true, + error: undefined, + topPools: [], + }) + + const { asFragment } = render() + expect(screen.getAllByTestId('cell-loading-bubble')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders error state', () => { + mocked(useTopPools).mockReturnValue({ + loading: false, + error: new ApolloError({ errorMessage: 'error fetching data' }), + topPools: [], + }) + + const { asFragment } = render() + expect(screen.getByTestId('table-error-modal')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders data filled state', () => { + const mockData = [ + { + token0: validBEPoolToken0, + token1: validBEPoolToken1, + feeTier: 10000, + hash: '0x123', + txCount: 200, + tvl: 300, + volume24h: 400, + volumeWeek: 500, + turnover: new Percent(6, 100), + protocolVersion: ProtocolVersion.V3, + }, + ] + mocked(useTopPools).mockReturnValue({ + topPools: mockData, + loading: false, + error: undefined, + }) + + const { asFragment } = render() + expect(screen.getByTestId('top-pools-explore-table')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/Pools/PoolTable/PoolTable.tsx b/apps/web/src/components/Pools/PoolTable/PoolTable.tsx new file mode 100644 index 0000000..45f5604 --- /dev/null +++ b/apps/web/src/components/Pools/PoolTable/PoolTable.tsx @@ -0,0 +1,378 @@ +import { ApolloError } from '@apollo/client' +import { Trans } from '@lingui/macro' +import { ColumnDef, createColumnHelper } from '@tanstack/react-table' +import { ChainId, Percent } from '@uniswap/sdk-core' +import { DoubleTokenAndChainLogo } from 'components/Pools/PoolDetails/PoolDetailsHeader' +import Row from 'components/Row' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { ClickableHeaderRow, HeaderArrow, HeaderSortText } from 'components/Table/styled' +import { NameText } from 'components/Tokens/TokenTable' +import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' +import { MouseoverTooltip } from 'components/Tooltip' +import { BIPS_BASE } from 'constants/misc' +import { ProtocolVersion, Token } from 'graphql/data/__generated__/types-and-hooks' +import { PoolSortFields, TablePool, useTopPools } from 'graphql/data/pools/useTopPools' +import { + OrderDirection, + chainIdToBackendName, + supportedChainIdFromGQLChain, + unwrapToken, + validateUrlChainParam, +} from 'graphql/data/util' +import { useAtom } from 'jotai' +import { atomWithReset, useAtomValue, useResetAtom, useUpdateAtom } from 'jotai/utils' +import { ReactElement, ReactNode, useCallback, useEffect, useMemo } from 'react' +import { useParams } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +const HEADER_DESCRIPTIONS: Record = { + [PoolSortFields.TVL]: undefined, + [PoolSortFields.Volume24h]: undefined, + [PoolSortFields.VolumeWeek]: undefined, + [PoolSortFields.TxCount]: undefined, + [PoolSortFields.Turnover]: ( + + Turnover refers to the amount of trading volume relative to total value locked (TVL) within a pool. Turnover = 24H + Fees / TVL + + ), +} + +const TableWrapper = styled.div` + margin: 0 auto; + max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}; +` + +const Badge = styled(ThemedText.LabelMicro)` + padding: 2px 6px; + background: ${({ theme }) => theme.surface2}; + border-radius: 5px; +` + +interface PoolTableValues { + index: number + poolDescription: ReactElement + txCount: number + tvl: number + volume24h: number + volumeWeek: number + turnover: Percent + link: string +} + +export enum PoolTableColumns { + Index, + PoolDescription, + Transactions, + TVL, + Volume24h, + VolumeWeek, + Turnover, +} + +function PoolDescription({ + token0, + token1, + feeTier, + chainId, + protocolVersion = ProtocolVersion.V3, +}: { + token0: Token + token1: Token + feeTier: number + chainId: ChainId + protocolVersion: ProtocolVersion +}) { + const tokens = [token0, token1] + return ( + + + + {token0.symbol}/{token1.symbol} + + {protocolVersion === ProtocolVersion.V2 && {protocolVersion.toLowerCase()}} + {feeTier / BIPS_BASE}% + + ) +} + +// Used to keep track of sorting state for Pool Tables +// declared as atomWithReset because sortMethodAtom and sortAscendingAtom are shared across multiple Pool Table instances - want to be able to reset sorting state between instances +export const sortMethodAtom = atomWithReset(PoolSortFields.TVL) +export const sortAscendingAtom = atomWithReset(false) + +function useSetSortMethod(newSortMethod: PoolSortFields) { + const [sortMethod, setSortMethod] = useAtom(sortMethodAtom) + const setSortAscending = useUpdateAtom(sortAscendingAtom) + + return useCallback(() => { + if (sortMethod === newSortMethod) { + setSortAscending((sortAscending) => !sortAscending) + } else { + setSortMethod(newSortMethod) + setSortAscending(false) + } + }, [sortMethod, setSortMethod, setSortAscending, newSortMethod]) +} + +const HEADER_TEXT: Record = { + [PoolSortFields.TVL]: TVL, + [PoolSortFields.Volume24h]: 1 day volume, + [PoolSortFields.VolumeWeek]: 7 day volume, + [PoolSortFields.Turnover]: Turnover, + [PoolSortFields.TxCount]: Transactions, +} + +function PoolTableHeader({ + category, + isCurrentSortMethod, + direction, +}: { + category: PoolSortFields + isCurrentSortMethod: boolean + direction: OrderDirection +}) { + const handleSortCategory = useSetSortMethod(category) + return ( + + + {isCurrentSortMethod && } + {HEADER_TEXT[category]} + + + ) +} + +export function TopPoolTable() { + const chainName = validateUrlChainParam(useParams<{ chainName?: string }>().chainName) + const chainId = supportedChainIdFromGQLChain(chainName) + const sortMethod = useAtomValue(sortMethodAtom) + const sortAscending = useAtomValue(sortAscendingAtom) + + const resetSortMethod = useResetAtom(sortMethodAtom) + const resetSortAscending = useResetAtom(sortAscendingAtom) + useEffect(() => { + resetSortMethod() + resetSortAscending() + }, [resetSortAscending, resetSortMethod]) + + const { topPools, loading, error } = useTopPools( + { sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc }, + chainId + ) + + return ( + + + + ) +} + +export function PoolsTable({ + pools, + loading, + error, + loadMore, + chainId, + maxWidth, + maxHeight, + hiddenColumns, +}: { + pools?: TablePool[] + loading: boolean + error?: ApolloError + loadMore?: ({ onComplete }: { onComplete?: () => void }) => void + chainId: ChainId + maxWidth?: number + maxHeight?: number + hiddenColumns?: PoolTableColumns[] +}) { + const { formatNumber, formatPercent } = useFormatter() + const sortAscending = useAtomValue(sortAscendingAtom) + const orderDirection = sortAscending ? OrderDirection.Asc : OrderDirection.Desc + const sortMethod = useAtomValue(sortMethodAtom) + + const poolTableValues: PoolTableValues[] | undefined = useMemo( + () => + pools?.map((pool, index) => { + return { + index: index + 1, + poolDescription: ( + + ), + txCount: pool.txCount, + tvl: pool.tvl, + volume24h: pool.volume24h, + volumeWeek: pool.volumeWeek, + turnover: pool.turnover, + link: `/explore/pools/${chainIdToBackendName(chainId).toLowerCase()}/${pool.hash}`, + } + }) ?? [], + [chainId, pools] + ) + + const showLoadingSkeleton = loading || !!error + // TODO(WEB-3236): once GQL BE Pool query add 1 day, 7 day, turnover sort support + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return [ + !hiddenColumns?.includes(PoolTableColumns.Index) + ? columnHelper.accessor((row) => row.index, { + id: 'index', + header: () => ( + + # + + ), + cell: (index) => ( + + {index.getValue?.()} + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.PoolDescription) + ? columnHelper.accessor((row) => row.poolDescription, { + id: 'poolDescription', + header: () => ( + + + Pool + + + ), + cell: (poolDescription) => ( + + {poolDescription.getValue?.()} + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.Transactions) + ? columnHelper.accessor((row) => row.txCount, { + id: 'transactions', + header: () => ( + + + + ), + cell: (txCount) => ( + + + {formatNumber({ input: txCount.getValue?.(), type: NumberType.NFTCollectionStats })} + + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.TVL) + ? columnHelper.accessor((row) => row.tvl, { + id: 'tvl', + header: () => ( + + + + ), + cell: (tvl) => ( + + + {formatNumber({ input: tvl.getValue?.(), type: NumberType.FiatTokenStats })} + + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.Volume24h) + ? columnHelper.accessor((row) => row.volume24h, { + id: 'volume24h', + header: () => ( + + + + ), + cell: (volume24h) => ( + + + {formatNumber({ input: volume24h.getValue?.(), type: NumberType.FiatTokenStats })} + + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.VolumeWeek) + ? columnHelper.accessor((row) => row.volumeWeek, { + id: 'volumeWeek', + header: () => ( + + + + ), + cell: (volumeWeek) => ( + + + {formatNumber({ input: volumeWeek.getValue?.(), type: NumberType.FiatTokenStats })} + + + ), + }) + : null, + !hiddenColumns?.includes(PoolTableColumns.Turnover) + ? columnHelper.accessor((row) => row.turnover, { + id: 'turnover', + header: () => ( + + + + ), + cell: (turnover) => ( + + {formatPercent(turnover.getValue?.())} + + ), + }) + : null, + // Filter out null values + ].filter(Boolean) as ColumnDef[] + }, [formatNumber, formatPercent, hiddenColumns, orderDirection, showLoadingSkeleton, sortMethod]) + + return ( +
+ ) +} diff --git a/apps/web/src/components/Pools/PoolTable/__snapshots__/PoolTable.test.tsx.snap b/apps/web/src/components/Pools/PoolTable/__snapshots__/PoolTable.test.tsx.snap new file mode 100644 index 0000000..308377c --- /dev/null +++ b/apps/web/src/components/Pools/PoolTable/__snapshots__/PoolTable.test.tsx.snap @@ -0,0 +1,3267 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PoolTable renders data filled state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c21 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c10 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c25 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + display: inline-block; + height: inherit; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c30 { + height: 16px; + width: 16px; +} + +.c30 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c31 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c2 { + max-width: 1200px; + max-height: px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + top: 73px; + position: -webkit-sticky; + position: sticky; + position: -webkit-sticky; + z-index: 990; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c18 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c18::-webkit-scrollbar { + display: none; +} + +.c28 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c29 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c19 { + color: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; +} + +.c14 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c14:hover { + opacity: 0.6; +} + +.c14:active { + opacity: 0.4; +} + +.c15 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c16 { + color: #222222; +} + +.c9 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c12 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c26 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c22 { + position: relative; + top: 0; + left: 0; +} + +.c23 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c23 img { + width: 14px; + height: 28px; + object-fit: cover; +} + +.c23 img:first-child { + border-radius: 14px 0 0 14px; + object-position: 0 0; +} + +.c23 img:last-child { + border-radius: 0 14px 14px 0; + object-position: 100% 0; +} + +.c24 { + width: 14px; + height: 28px; + border-radius: 50%; +} + +.c0 { + margin: 0 auto; + max-width: 1200px; +} + +.c27 { + padding: 2px 6px; + background: #F9F9F9; + border-radius: 5px; +} + +@media not all and (hover:none) { + .c20:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+
+ Transactions +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+ +
+
+ + + + Loading +
+
+
+
+
+
+
+
+`; + +exports[`PoolTable renders error state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c23 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + display: inline-block; + height: inherit; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c20 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c26 { + height: 16px; + width: 16px; +} + +.c26 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c27 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + width: 320px; + padding: 12px; + gap: 12px; + background-color: #00000004; + -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(24px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + border: 1px #22222212 solid; + border-radius: 20px; +} + +.c2 { + max-width: 1200px; + max-height: px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + top: 73px; + position: -webkit-sticky; + position: sticky; + position: -webkit-sticky; + z-index: 990; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c18 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c18::-webkit-scrollbar { + display: none; +} + +.c24 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c25 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; + opacity: 0.4; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c14 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c14:hover { + opacity: 0.6; +} + +.c14:active { + opacity: 0.4; +} + +.c15 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c16 { + color: #222222; +} + +.c9 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c12 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c21 { + width: 75%; + height: 16px; +} + +.c0 { + margin: 0 auto; + max-width: 1200px; +} + +@media not all and (hover:none) { + .c19:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+
+ Transactions +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+
+
+ Error loading data +
+
+ Data is unavailable at the moment; we're working on a fix +
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; + +exports[`PoolTable renders loading state 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c10 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c13 { + display: inline-block; + height: inherit; +} + +.c1 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c20 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c24 { + height: 16px; + width: 16px; +} + +.c24 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c25 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c2 { + max-width: 1200px; + max-height: px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c3 { + width: 100%; + position: relative; + top: 73px; + position: -webkit-sticky; + position: sticky; + position: -webkit-sticky; + z-index: 990; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c18 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c18::-webkit-scrollbar { + display: none; +} + +.c22 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c23 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c6 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c7 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c7::-webkit-scrollbar { + display: none; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c8:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c8:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c14 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c14:hover { + opacity: 0.6; +} + +.c14:active { + opacity: 0.4; +} + +.c15 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c16 { + color: #222222; +} + +.c9 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c12 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c17 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c21 { + width: 75%; + height: 16px; +} + +.c0 { + margin: 0 auto; + max-width: 1200px; +} + +@media not all and (hover:none) { + .c19:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+
+ Transactions +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; diff --git a/apps/web/src/components/Popover/index.tsx b/apps/web/src/components/Popover/index.tsx new file mode 100644 index 0000000..90dedab --- /dev/null +++ b/apps/web/src/components/Popover/index.tsx @@ -0,0 +1,144 @@ +import { Options, Placement } from '@popperjs/core' +import Portal from '@reach/portal' +import useInterval from 'lib/hooks/useInterval' +import React, { CSSProperties, useCallback, useMemo, useState } from 'react' +import { usePopper } from 'react-popper' +import styled from 'styled-components' +import { Z_INDEX } from 'theme/zIndex' + +const PopoverContainer = styled.div<{ show: boolean }>` + z-index: ${Z_INDEX.popover}; + pointer-events: none; + visibility: ${(props) => (props.show ? 'visible' : 'hidden')}; + opacity: ${(props) => (props.show ? 1 : 0)}; + transition: visibility 150ms linear, opacity 150ms linear; + color: ${({ theme }) => theme.neutral2}; +` + +const ReferenceElement = styled.div` + display: inline-block; + height: inherit; +` + +const Arrow = styled.div` + width: 8px; + height: 8px; + z-index: 9998; + + ::before { + position: absolute; + width: 8px; + height: 8px; + box-sizing: border-box; + z-index: 9998; + + content: ''; + border: 1px solid ${({ theme }) => theme.surface3}; + transform: rotate(45deg); + background: ${({ theme }) => theme.surface1}; + } + + &.arrow-top { + bottom: -4px; + ::before { + border-top: none; + border-left: none; + } + } + + &.arrow-bottom { + top: -4px; + ::before { + border-bottom: none; + border-right: none; + } + } + + &.arrow-left { + right: -4px; + + ::before { + border-bottom: none; + border-left: none; + } + } + + &.arrow-right { + left: -4px; + ::before { + border-right: none; + border-top: none; + } + } +` + +export interface PopoverProps { + content: React.ReactNode + show: boolean + children?: React.ReactNode + placement?: Placement + offsetX?: number + offsetY?: number + hideArrow?: boolean + showInline?: boolean + style?: CSSProperties +} + +export default function Popover({ + content, + show, + children, + placement = 'auto', + offsetX = 8, + offsetY = 8, + hideArrow = false, + showInline = false, + style, +}: PopoverProps) { + const [referenceElement, setReferenceElement] = useState(null) + const [popperElement, setPopperElement] = useState(null) + const [arrowElement, setArrowElement] = useState(null) + + const options: Options = useMemo( + () => ({ + placement, + strategy: 'fixed', + modifiers: [ + { name: 'offset', options: { offset: [offsetX, offsetY] } }, + { name: 'arrow', options: { element: arrowElement } }, + { name: 'preventOverflow', options: { padding: 8 } }, + ], + }), + [placement, offsetX, offsetY, arrowElement] + ) + + const { styles, update, attributes } = usePopper(referenceElement, show ? popperElement : null, options) + + const updateCallback = useCallback(() => { + update && update() + }, [update]) + useInterval(updateCallback, show ? 100 : null) + + return showInline ? ( + {content} + ) : ( + <> + + {children} + + + + {content} + {!hideArrow && ( + + )} + + + + ) +} diff --git a/apps/web/src/components/Popups/ClaimPopup.tsx b/apps/web/src/components/Popups/ClaimPopup.tsx new file mode 100644 index 0000000..bbc1f3d --- /dev/null +++ b/apps/web/src/components/Popups/ClaimPopup.tsx @@ -0,0 +1,123 @@ +import { Trans } from '@lingui/macro' +import { CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { useCallback, useEffect } from 'react' +import { Heart, X } from 'react-feather' +import styled, { keyframes } from 'styled-components' +import { ThemedText } from 'theme/components' + +import tokenLogo from '../../assets/images/token-logo.png' +import { + useModalIsOpen, + useShowClaimPopup, + useToggleSelfClaimModal, + useToggleShowClaimPopup, +} from '../../state/application/hooks' +import { ApplicationModal } from '../../state/application/reducer' +import { useUserHasAvailableClaim, useUserUnclaimedAmount } from '../../state/claim/hooks' +import { ButtonPrimary } from '../Button' +import { AutoColumn } from '../Column' +import { CardBGImage, CardNoise } from '../earn/styled' + +const StyledClaimPopup = styled(AutoColumn)` + background: radial-gradient(76.02% 75.41% at 1.84% 0%, #ff007a 0%, #021d43 100%); + border-radius: 20px; + padding: 1.5rem; + overflow: hidden; + position: relative; + max-width: 360px; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1); +` + +const StyledClose = styled(X)` + position: absolute; + right: 10px; + top: 10px; + + :hover { + cursor: pointer; + } +` + +const rotate = keyframes` + 0% { + transform: perspective(1000px) rotateY(0deg); + } + + 100% { + transform: perspective(1000px) rotateY(360deg); + } +` + +const UniToken = styled.img` + animation: ${rotate} 5s cubic-bezier(0.83, 0, 0.17, 1) infinite; +` + +export default function ClaimPopup() { + const { account } = useWeb3React() + + // dont store these in persisted state yet + const showClaimPopup: boolean = useShowClaimPopup() + const toggleShowClaimPopup = useToggleShowClaimPopup() + + // toggle for showing this modal + const showClaimModal = useModalIsOpen(ApplicationModal.SELF_CLAIM) + const toggleSelfClaimModal = useToggleSelfClaimModal() + const handleToggleSelfClaimModal = useCallback(() => { + toggleSelfClaimModal() + }, [toggleSelfClaimModal]) + + // const userHasAvailableclaim = useUserHasAvailableClaim() + const userHasAvailableclaim: boolean = useUserHasAvailableClaim(account) + const unclaimedAmount: CurrencyAmount | undefined = useUserUnclaimedAmount(account) + + // listen for available claim and show popup if needed + useEffect(() => { + if (userHasAvailableclaim) { + toggleShowClaimPopup() + } + // the toggleShowClaimPopup function changes every time the popup changes, so this will cause an infinite loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userHasAvailableclaim]) + + return ( + <> + {showClaimPopup && !showClaimModal && ( + + + + + + {' '} + + {unclaimedAmount?.toFixed(0, { groupSeparator: ',' } ?? '-')} UNI + + + + 🎉 + {' '} + UNI has arrived{' '} + + 🎉 + + + + + Thanks for being part of the Uniswap community + + + + + + Claim your UNI tokens + + + + )} + + ) +} diff --git a/apps/web/src/components/Popups/PopupContent.tsx b/apps/web/src/components/Popups/PopupContent.tsx new file mode 100644 index 0000000..2ce6f42 --- /dev/null +++ b/apps/web/src/components/Popups/PopupContent.tsx @@ -0,0 +1,169 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { useOpenOffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal' +import { signatureToActivity, transactionToActivity } from 'components/AccountDrawer/MiniPortfolio/Activity/parseLocal' +import { Activity } from 'components/AccountDrawer/MiniPortfolio/Activity/types' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import PortfolioRow from 'components/AccountDrawer/MiniPortfolio/PortfolioRow' +import Column, { AutoColumn } from 'components/Column' +import AlertTriangleFilled from 'components/Icons/AlertTriangleFilled' +import { AutoRow } from 'components/Row' +import { getChainInfo } from 'constants/chainInfo' +import { TransactionStatus } from 'graphql/data/__generated__/types-and-hooks' +import { useAllTokensMultichain } from 'hooks/Tokens' +import useENSName from 'hooks/useENSName' +import { X } from 'react-feather' +import { useOrder } from 'state/signatures/hooks' +import { useTransaction } from 'state/transactions/hooks' +import styled from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const StyledClose = styled(X)<{ $padding: number }>` + position: absolute; + right: ${({ $padding }) => `${$padding}px`}; + top: ${({ $padding }) => `${$padding}px`}; + color: ${({ theme }) => theme.neutral2}; + + :hover { + cursor: pointer; + } +` +const PopupContainer = styled.div<{ padded?: boolean }>` + display: inline-block; + width: 100%; + background-color: ${({ theme }) => theme.surface1}; + position: relative; + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 16px; + overflow: hidden; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + transition: ${({ theme }) => `visibility ${theme.transition.duration.fast} ease-in-out`}; + + padding: ${({ padded }) => (padded ? '20px 35px 20px 20px' : '2px 0px')}; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + min-width: 290px; + &:not(:last-of-type) { + margin-right: 20px; + } +`} +` + +const RowNoFlex = styled(AutoRow)` + flex-wrap: nowrap; +` + +const ColumnContainer = styled(AutoColumn)` + margin: 0 12px; +` + +const PopupAlertTriangle = styled(AlertTriangleFilled)` + flex-shrink: 0; + width: 32px; + height: 32px; +` + +export function FailedNetworkSwitchPopup({ chainId, onClose }: { chainId: ChainId; onClose: () => void }) { + const chainInfo = getChainInfo(chainId) + + return ( + + + + + + + Failed to switch networks + + + + To use Uniswap on {chainInfo.label}, switch the network in your wallet’s settings. + + + + + ) +} + +const Descriptor = styled(ThemedText.BodySmall)` + ${EllipsisStyle} +` + +type ActivityPopupContentProps = { activity: Activity; onClick: () => void; onClose: () => void } +function ActivityPopupContent({ activity, onClick, onClose }: ActivityPopupContentProps) { + const success = activity.status === TransactionStatus.Confirmed && !activity.cancelled + const { ENSName } = useENSName(activity?.otherAccount) + + return ( + + + + + + ) : ( + + ) + } + title={{activity.title}} + descriptor={ + + {activity.descriptor} + {ENSName ?? activity.otherAccount} + + } + onClick={onClick} + /> + + ) +} + +export function TransactionPopupContent({ + chainId, + hash, + onClose, +}: { + chainId: ChainId + hash: string + onClose: () => void +}) { + const transaction = useTransaction(hash) + const tokens = useAllTokensMultichain() + const { formatNumber } = useFormatter() + if (!transaction) return null + + const activity = transactionToActivity(transaction, chainId, tokens, formatNumber) + + if (!activity) return null + + const onClick = () => + window.open(getExplorerLink(activity.chainId, activity.hash, ExplorerDataType.TRANSACTION), '_blank') + + return +} + +export function UniswapXOrderPopupContent({ orderHash, onClose }: { orderHash: string; onClose: () => void }) { + const order = useOrder(orderHash) + const tokens = useAllTokensMultichain() + const openOffchainActivityModal = useOpenOffchainActivityModal() + const { formatNumber } = useFormatter() + if (!order) return null + + const activity = signatureToActivity(order, tokens, formatNumber) + + if (!activity) return null + + const onClick = () => + openOffchainActivityModal(order, { inputLogo: activity?.logos?.[0], outputLogo: activity?.logos?.[1] }) + + return +} diff --git a/apps/web/src/components/Popups/PopupItem.tsx b/apps/web/src/components/Popups/PopupItem.tsx new file mode 100644 index 0000000..65c1bca --- /dev/null +++ b/apps/web/src/components/Popups/PopupItem.tsx @@ -0,0 +1,45 @@ +import { useWeb3React } from '@web3-react/core' +import { useEffect } from 'react' + +import { useRemovePopup } from '../../state/application/hooks' +import { PopupContent, PopupType } from '../../state/application/reducer' +import { FailedNetworkSwitchPopup, TransactionPopupContent, UniswapXOrderPopupContent } from './PopupContent' + +export default function PopupItem({ + removeAfterMs, + content, + popKey, +}: { + removeAfterMs: number | null + content: PopupContent + popKey: string +}) { + const removePopup = useRemovePopup() + const onClose = () => removePopup(popKey) + + useEffect(() => { + if (removeAfterMs === null) return undefined + + const timeout = setTimeout(() => { + removePopup(popKey) + }, removeAfterMs) + + return () => { + clearTimeout(timeout) + } + }, [popKey, removeAfterMs, removePopup]) + + const { chainId } = useWeb3React() + + switch (content.type) { + case PopupType.Transaction: { + return chainId ? : null + } + case PopupType.Order: { + return + } + case PopupType.FailedSwitchNetwork: { + return + } + } +} diff --git a/apps/web/src/components/Popups/index.tsx b/apps/web/src/components/Popups/index.tsx new file mode 100644 index 0000000..a521b22 --- /dev/null +++ b/apps/web/src/components/Popups/index.tsx @@ -0,0 +1,80 @@ +import { useAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { useActivePopups } from 'state/application/hooks' +import styled from 'styled-components' +import { Z_INDEX } from 'theme/zIndex' + +import { AutoColumn } from '../Column' +import ClaimPopup from './ClaimPopup' +import PopupItem from './PopupItem' + +const MobilePopupWrapper = styled.div` + position: relative; + max-width: 100%; + margin: 0 auto; + display: none; + padding-left: 20px; + padding-right: 20px; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + display: block; + padding-top: 20px; + `}; +` + +const MobilePopupInner = styled.div` + height: 99%; + overflow-x: auto; + overflow-y: hidden; + display: flex; + flex-direction: row; + -webkit-overflow-scrolling: touch; + ::-webkit-scrollbar { + display: none; + } +` + +const FixedPopupColumn = styled(AutoColumn)<{ + drawerOpen: boolean +}>` + position: fixed; + top: ${({ drawerOpen }) => `${64 + (drawerOpen ? -50 : 0)}px`}; + right: 1rem; + max-width: 348px !important; + width: 100%; + z-index: ${Z_INDEX.modal}; + transition: ${({ theme }) => `top ${theme.transition.timing.inOut} ${theme.transition.duration.slow}`}; + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + display: none; + `}; +` + +export default function Popups() { + const [isAccountDrawerOpen] = useAccountDrawer() + + // get all popups + const activePopups = useActivePopups() + + return ( + <> + + + {activePopups.map((item) => ( + + ))} + + {activePopups?.length > 0 && ( + + + {activePopups // reverse so new items up front + .slice(0) + .reverse() + .map((item) => ( + + ))} + + + )} + + ) +} diff --git a/apps/web/src/components/PositionCard/Sushi.tsx b/apps/web/src/components/PositionCard/Sushi.tsx new file mode 100644 index 0000000..22f47f7 --- /dev/null +++ b/apps/web/src/components/PositionCard/Sushi.tsx @@ -0,0 +1,75 @@ +import { Trans } from '@lingui/macro' +import { Token } from '@uniswap/sdk-core' +import Badge, { BadgeVariant } from 'components/Badge' +import { transparentize } from 'polished' +import { Link } from 'react-router-dom' +import { Text } from 'rebass' +import styled from 'styled-components' + +import { FixedHeightRow } from '.' +import { useColor } from '../../hooks/useColor' +import { unwrappedToken } from '../../utils/unwrappedToken' +import { ButtonEmpty } from '../Button' +import { LightCard } from '../Card' +import { AutoColumn } from '../Column' +import DoubleCurrencyLogo from '../DoubleLogo' +import { CardNoise } from '../earn/styled' +import { AutoRow, RowFixed } from '../Row' +import { Dots } from '../swap/styled' + +const StyledPositionCard = styled(LightCard)<{ bgColor: any }>` + border: none; + background: ${({ theme, bgColor }) => + `radial-gradient(91.85% 100% at 1.84% 0%, ${transparentize(0.8, bgColor)} 0%, ${theme.surface2} 100%) `}; + position: relative; + overflow: hidden; +` + +interface PositionCardProps { + tokenA: Token + tokenB: Token + liquidityToken: Token + border?: string +} + +export default function SushiPositionCard({ tokenA, tokenB, liquidityToken, border }: PositionCardProps) { + const currency0 = unwrappedToken(tokenA) + const currency1 = unwrappedToken(tokenB) + + const backgroundColor = useColor(tokenA) + + return ( + + + + + + + + {!currency0 || !currency1 ? ( + + Loading + + ) : ( + `${currency0.symbol}/${currency1.symbol}` + )} + + + Sushi + + + + Migrate + + + + + + ) +} diff --git a/apps/web/src/components/PositionCard/V2.tsx b/apps/web/src/components/PositionCard/V2.tsx new file mode 100644 index 0000000..3ae8caf --- /dev/null +++ b/apps/web/src/components/PositionCard/V2.tsx @@ -0,0 +1,212 @@ +import { Trans } from '@lingui/macro' +import { CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { useWeb3React } from '@web3-react/core' +import JSBI from 'jsbi' +import { transparentize } from 'polished' +import { useState } from 'react' +import { ChevronDown, ChevronUp } from 'react-feather' +import { Link } from 'react-router-dom' +import { Text } from 'rebass' +import styled from 'styled-components' + +import { FixedHeightRow } from '.' +import { BIG_INT_ZERO } from '../../constants/misc' +import { useColor } from '../../hooks/useColor' +import { useTotalSupply } from '../../hooks/useTotalSupply' +import { useTokenBalance } from '../../state/connection/hooks' +import { currencyId } from '../../utils/currencyId' +import { unwrappedToken } from '../../utils/unwrappedToken' +import { ButtonEmpty, ButtonPrimary, ButtonSecondary } from '../Button' +import { LightCard } from '../Card' +import { AutoColumn } from '../Column' +import DoubleCurrencyLogo from '../DoubleLogo' +import { CardNoise } from '../earn/styled' +import CurrencyLogo from '../Logo/CurrencyLogo' +import { AutoRow, RowBetween, RowFixed } from '../Row' +import { Dots } from '../swap/styled' + +const StyledPositionCard = styled(LightCard)<{ bgColor: any }>` + border: none; + background: ${({ theme, bgColor }) => + `radial-gradient(91.85% 100% at 1.84% 0%, ${transparentize(0.8, bgColor)} 0%, ${theme.surface2} 100%) `}; + position: relative; + overflow: hidden; +` + +interface PositionCardProps { + pair: Pair + showUnwrapped?: boolean + border?: string + stakedBalance?: CurrencyAmount // optional balance to indicate that liquidity is deposited in mining pool +} + +export default function V2PositionCard({ pair, border, stakedBalance }: PositionCardProps) { + const { account } = useWeb3React() + + const currency0 = unwrappedToken(pair.token0) + const currency1 = unwrappedToken(pair.token1) + + const [showMore, setShowMore] = useState(false) + + const userDefaultPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken) + const totalPoolTokens = useTotalSupply(pair.liquidityToken) + + // if staked balance balance provided, add to standard liquidity amount + const userPoolBalance = stakedBalance ? userDefaultPoolBalance?.add(stakedBalance) : userDefaultPoolBalance + + const poolTokenPercentage = + !!userPoolBalance && + !!totalPoolTokens && + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? new Percent(userPoolBalance.quotient, totalPoolTokens.quotient) + : undefined + + const [token0Deposited, token1Deposited] = + !!pair && + !!totalPoolTokens && + !!userPoolBalance && + // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? [ + pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false), + pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false), + ] + : [undefined, undefined] + + const backgroundColor = useColor(pair?.token0) + + return ( + + + + + + + + {!currency0 || !currency1 ? ( + + Loading + + ) : ( + `${currency0.symbol}/${currency1.symbol}` + )} + + + + setShowMore(!showMore)} + > + {showMore ? ( + <> + Manage + + + ) : ( + <> + Manage + + + )} + + + + + {showMore && ( + + + + Your total pool tokens: + + + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} + + + {stakedBalance && ( + + + Pool tokens in rewards pool: + + + {stakedBalance.toSignificant(4)} + + + )} + + + + Pooled {currency0.symbol}: + + + {token0Deposited ? ( + + + {token0Deposited?.toSignificant(6)} + + + + ) : ( + '-' + )} + + + + + + Pooled {currency1.symbol}: + + + {token1Deposited ? ( + + + {token1Deposited?.toSignificant(6)} + + + + ) : ( + '-' + )} + + + + + Your pool share: + + + {poolTokenPercentage + ? (poolTokenPercentage.toFixed(2) === '0.00' ? '<0.01' : poolTokenPercentage.toFixed(2)) + '%' + : '-'} + + + + {userDefaultPoolBalance && JSBI.greaterThan(userDefaultPoolBalance.quotient, BIG_INT_ZERO) && ( + + + Migrate + + + Remove + + + )} + + )} + + + ) +} diff --git a/apps/web/src/components/PositionCard/index.tsx b/apps/web/src/components/PositionCard/index.tsx new file mode 100644 index 0000000..7662fd3 --- /dev/null +++ b/apps/web/src/components/PositionCard/index.tsx @@ -0,0 +1,357 @@ +import { Trans } from '@lingui/macro' +import { CurrencyAmount, Percent, Token } from '@uniswap/sdk-core' +import { Pair } from '@uniswap/v2-sdk' +import { useWeb3React } from '@web3-react/core' +import JSBI from 'jsbi' +import { transparentize } from 'polished' +import { useState } from 'react' +import { ChevronDown, ChevronUp } from 'react-feather' +import { Link } from 'react-router-dom' +import { Text } from 'rebass' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' + +import { BIG_INT_ZERO } from '../../constants/misc' +import { useColor } from '../../hooks/useColor' +import { useTotalSupply } from '../../hooks/useTotalSupply' +import { useTokenBalance } from '../../state/connection/hooks' +import { currencyId } from '../../utils/currencyId' +import { unwrappedToken } from '../../utils/unwrappedToken' +import { ButtonEmpty, ButtonPrimary, ButtonSecondary } from '../Button' +import { GrayCard, LightCard } from '../Card' +import { AutoColumn } from '../Column' +import DoubleCurrencyLogo from '../DoubleLogo' +import { CardNoise } from '../earn/styled' +import CurrencyLogo from '../Logo/CurrencyLogo' +import { AutoRow, RowBetween, RowFixed } from '../Row' +import { Dots } from '../swap/styled' + +export const FixedHeightRow = styled(RowBetween)` + height: 24px; +` + +const StyledPositionCard = styled(LightCard)<{ bgColor: any }>` + border: none; + background: ${({ theme, bgColor }) => + `radial-gradient(91.85% 100% at 1.84% 0%, ${transparentize(0.8, bgColor)} 0%, ${theme.surface2} 100%) `}; + position: relative; + overflow: hidden; +` + +interface PositionCardProps { + pair: Pair + showUnwrapped?: boolean + border?: string + stakedBalance?: CurrencyAmount // optional balance to indicate that liquidity is deposited in mining pool +} + +export function MinimalPositionCard({ pair, showUnwrapped = false, border }: PositionCardProps) { + const { account } = useWeb3React() + + const currency0 = showUnwrapped ? pair.token0 : unwrappedToken(pair.token0) + const currency1 = showUnwrapped ? pair.token1 : unwrappedToken(pair.token1) + + const [showMore, setShowMore] = useState(false) + + const userPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken) + const totalPoolTokens = useTotalSupply(pair.liquidityToken) + + const poolTokenPercentage = + !!userPoolBalance && + !!totalPoolTokens && + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? new Percent(userPoolBalance.quotient, totalPoolTokens.quotient) + : undefined + + const [token0Deposited, token1Deposited] = + !!pair && + !!totalPoolTokens && + !!userPoolBalance && + // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? [ + pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false), + pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false), + ] + : [undefined, undefined] + + return ( + <> + {userPoolBalance && JSBI.greaterThan(userPoolBalance.quotient, JSBI.BigInt(0)) ? ( + + + + + + Your position + + + + setShowMore(!showMore)}> + + + + {currency0.symbol}/{currency1.symbol} + + + + + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} + + + + + + + Your pool share: + + + {poolTokenPercentage ? poolTokenPercentage.toFixed(6) + '%' : '-'} + + + + + {currency0.symbol}: + + {token0Deposited ? ( + + + {token0Deposited?.toSignificant(6)} + + + ) : ( + '-' + )} + + + + {currency1.symbol}: + + {token1Deposited ? ( + + + {token1Deposited?.toSignificant(6)} + + + ) : ( + '-' + )} + + + + + ) : ( + + + + ⭐️ + {' '} + + By adding liquidity you'll earn 0.3% of all trades on this pair proportional to your share of the + pool. Fees are added to the pool, accrue in real time and can be claimed by withdrawing your liquidity. + {' '} + + + )} + + ) +} + +export default function FullPositionCard({ pair, border, stakedBalance }: PositionCardProps) { + const { account } = useWeb3React() + + const currency0 = unwrappedToken(pair.token0) + const currency1 = unwrappedToken(pair.token1) + + const [showMore, setShowMore] = useState(false) + + const userDefaultPoolBalance = useTokenBalance(account ?? undefined, pair.liquidityToken) + const totalPoolTokens = useTotalSupply(pair.liquidityToken) + + // if staked balance balance provided, add to standard liquidity amount + const userPoolBalance = stakedBalance ? userDefaultPoolBalance?.add(stakedBalance) : userDefaultPoolBalance + + const poolTokenPercentage = + !!userPoolBalance && + !!totalPoolTokens && + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? new Percent(userPoolBalance.quotient, totalPoolTokens.quotient) + : undefined + + const [token0Deposited, token1Deposited] = + !!pair && + !!totalPoolTokens && + !!userPoolBalance && + // this condition is a short-circuit in the case where useTokenBalance updates sooner than useTotalSupply + JSBI.greaterThanOrEqual(totalPoolTokens.quotient, userPoolBalance.quotient) + ? [ + pair.getLiquidityValue(pair.token0, totalPoolTokens, userPoolBalance, false), + pair.getLiquidityValue(pair.token1, totalPoolTokens, userPoolBalance, false), + ] + : [undefined, undefined] + + const backgroundColor = useColor(pair?.token0) + + return ( + + + + + + + + {!currency0 || !currency1 ? ( + + Loading + + ) : ( + `${currency0.symbol}/${currency1.symbol}` + )} + + + + setShowMore(!showMore)}> + {showMore ? ( + <> + Manage + + + ) : ( + <> + Manage + + + )} + + + + + {showMore && ( + + + + Your total pool tokens: + + + {userPoolBalance ? userPoolBalance.toSignificant(4) : '-'} + + + {stakedBalance && ( + + + Pool tokens in rewards pool: + + + {stakedBalance.toSignificant(4)} + + + )} + + + + Pooled {currency0.symbol}: + + + {token0Deposited ? ( + + + {token0Deposited?.toSignificant(6)} + + + + ) : ( + '-' + )} + + + + + + Pooled {currency1.symbol}: + + + {token1Deposited ? ( + + + {token1Deposited?.toSignificant(6)} + + + + ) : ( + '-' + )} + + + + + Your pool share: + + + {poolTokenPercentage ? ( + + {poolTokenPercentage.toFixed(2) === '0.00' ? '<0.01' : poolTokenPercentage.toFixed(2)} % + + ) : ( + '-' + )} + + + + + + + View accrued fees and analytics + + + + {userDefaultPoolBalance && JSBI.greaterThan(userDefaultPoolBalance.quotient, BIG_INT_ZERO) && ( + + + Migrate + + + Add + + + Remove + + + )} + {stakedBalance && JSBI.greaterThan(stakedBalance.quotient, BIG_INT_ZERO) && ( + + Manage liquidity in rewards pool + + )} + + )} + + + ) +} diff --git a/apps/web/src/components/PositionList/index.tsx b/apps/web/src/components/PositionList/index.tsx new file mode 100644 index 0000000..d066737 --- /dev/null +++ b/apps/web/src/components/PositionList/index.tsx @@ -0,0 +1,107 @@ +import { Trans } from '@lingui/macro' +import PositionListItem from 'components/PositionListItem' +import React from 'react' +import styled from 'styled-components' +import { MEDIA_WIDTHS } from 'theme' +import { PositionDetails } from 'types/position' + +const DesktopHeader = styled.div` + display: none; + font-size: 14px; + padding: 16px; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + + @media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) { + align-items: center; + display: flex; + justify-content: space-between; + & > div:last-child { + text-align: right; + margin-right: 12px; + } + } +` + +const MobileHeader = styled.div` + font-weight: medium; + padding: 8px; + font-weight: 535; + padding: 16px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + + @media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) { + display: none; + } + + @media screen and (max-width: ${MEDIA_WIDTHS.deprecated_upToExtraSmall}px) { + display: flex; + flex-direction: row; + justify-content: space-between; + } +` + +const ToggleWrap = styled.div` + display: flex; + flex-direction: row; + align-items: center; +` + +const ToggleLabel = styled.button` + cursor: pointer; + background-color: transparent; + border: none; + color: ${({ theme }) => theme.accent1}; + font-size: 14px; + font-weight: 485; +` + +type PositionListProps = React.PropsWithChildren<{ + positions: PositionDetails[] + setUserHideClosedPositions: any + userHideClosedPositions: boolean +}> + +export default function PositionList({ + positions, + setUserHideClosedPositions, + userHideClosedPositions, +}: PositionListProps) { + return ( + <> + +
+ Your positions + {positions && ' (' + positions.length + ')'} +
+ + { + setUserHideClosedPositions(!userHideClosedPositions) + }} + > + {userHideClosedPositions ? Show closed positions : Hide closed positions} + +
+ + Your positions + + { + setUserHideClosedPositions(!userHideClosedPositions) + }} + > + {userHideClosedPositions ? Show closed positions : Hide closed positions} + + + + {positions.map((p) => ( + + ))} + + ) +} diff --git a/apps/web/src/components/PositionListItem/PositionListItem.test.tsx b/apps/web/src/components/PositionListItem/PositionListItem.test.tsx new file mode 100644 index 0000000..6f2b0b8 --- /dev/null +++ b/apps/web/src/components/PositionListItem/PositionListItem.test.tsx @@ -0,0 +1,48 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { ChainId, Token, WETH9 } from '@uniswap/sdk-core' +import { FeeAmount, Pool } from '@uniswap/v3-sdk' +import { USDC_MAINNET } from 'constants/tokens' +import { useToken } from 'hooks/Tokens' +import { PoolState, usePool } from 'hooks/usePools' +import { mocked } from 'test-utils/mocked' +import { render } from 'test-utils/render' + +import PositionListItem from '.' + +jest.mock('components/DoubleLogo') +jest.mock('hooks/Tokens') +jest.mock('hooks/usePools') +jest.mock('utils/unwrappedToken') + +beforeEach(() => { + mocked(useToken).mockImplementation((tokenAddress?: string | null | undefined) => { + if (!tokenAddress) return null + return new Token(1, tokenAddress, 6, 'symbol', 'name') + }) + mocked(usePool).mockReturnValue([ + PoolState.EXISTS, + // tokenA: Token, tokenB: Token, fee: FeeAmount, sqrtRatioX96: BigintIsh, liquidity: BigintIsh, tickCurrent: number + new Pool( + USDC_MAINNET, + WETH9[ChainId.MAINNET], + FeeAmount.MEDIUM, + '1745948049099224684665158875285708', + '4203610460178577802', + 200019 + ), + ]) +}) + +test('PositionListItem should render a position', () => { + const positionDetails = { + token0: USDC_MAINNET.address, + token1: WETH9[ChainId.MAINNET].address, + tokenId: BigNumber.from(479689), + fee: FeeAmount.MEDIUM, + liquidity: BigNumber.from('1341008833950736'), + tickLower: 200040, + tickUpper: 202560, + } + const { container } = render() + expect(container).not.toBeEmptyDOMElement() +}) diff --git a/apps/web/src/components/PositionListItem/index.tsx b/apps/web/src/components/PositionListItem/index.tsx new file mode 100644 index 0000000..721d54d --- /dev/null +++ b/apps/web/src/components/PositionListItem/index.tsx @@ -0,0 +1,270 @@ +import { BigNumber } from '@ethersproject/bignumber' +import { Trans } from '@lingui/macro' +import { Percent, Price, Token } from '@uniswap/sdk-core' +import { Position } from '@uniswap/v3-sdk' +import RangeBadge from 'components/Badge/RangeBadge' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import HoverInlineText from 'components/HoverInlineText' +import Loader from 'components/Icons/LoadingSpinner' +import { RowBetween } from 'components/Row' +import { useToken } from 'hooks/Tokens' +import useIsTickAtLimit from 'hooks/useIsTickAtLimit' +import { usePool } from 'hooks/usePools' +import { useMemo } from 'react' +import { Link } from 'react-router-dom' +import { Bound } from 'state/mint/v3/actions' +import styled from 'styled-components' +import { MEDIA_WIDTHS } from 'theme' +import { HideSmall, SmallOnly, ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' +import { unwrappedToken } from 'utils/unwrappedToken' + +import { DAI, USDC_MAINNET, USDT, WBTC, WRAPPED_NATIVE_CURRENCY } from '../../constants/tokens' + +const LinkRow = styled(Link)` + align-items: center; + display: flex; + cursor: pointer; + user-select: none; + display: flex; + flex-direction: column; + justify-content: space-between; + color: ${({ theme }) => theme.neutral1}; + padding: 16px; + text-decoration: none; + font-weight: 535; + + & > div:not(:first-child) { + text-align: center; + } + :hover { + background-color: ${({ theme }) => theme.deprecated_hoverDefault}; + } + + @media screen and (min-width: ${MEDIA_WIDTHS.deprecated_upToSmall}px) { + /* flex-direction: row; */ + } + + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + flex-direction: column; + row-gap: 8px; + `}; +` + +const DataLineItem = styled.div` + font-size: 14px; +` + +const RangeLineItem = styled(DataLineItem)` + display: flex; + flex-direction: row; + align-items: center; + margin-top: 4px; + width: 100%; +` + +const DoubleArrow = styled.span` + font-size: 12px; + margin: 0 2px; + color: ${({ theme }) => theme.neutral1}; +` + +const RangeText = styled(ThemedText.BodySmall)` + font-size: 14px !important; + word-break: break-word; + padding: 0.25rem 0.25rem; + border-radius: 8px; +` + +const FeeTierText = styled(ThemedText.UtilityBadge)` + font-size: 16px !important; + margin-left: 8px !important; + color: ${({ theme }) => theme.neutral3}; +` +const ExtentsText = styled(ThemedText.BodySmall)` + color: ${({ theme }) => theme.neutral2}; + display: inline-block; + line-height: 16px; + margin-right: 4px !important; + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToSmall` + display: none; + `}; +` + +const PrimaryPositionIdData = styled.div` + display: flex; + flex-direction: row; + align-items: center; + > * { + margin-right: 8px; + } +` + +interface PositionListItemProps { + token0: string + token1: string + tokenId: BigNumber + fee: number + liquidity: BigNumber + tickLower: number + tickUpper: number +} + +export function getPriceOrderingFromPositionForUI(position?: Position): { + priceLower?: Price + priceUpper?: Price + quote?: Token + base?: Token +} { + if (!position) { + return {} + } + + const token0 = position.amount0.currency + const token1 = position.amount1.currency + + // if token0 is a dollar-stable asset, set it as the quote token + const stables = [DAI, USDC_MAINNET, USDT] + if (stables.some((stable) => stable.equals(token0))) { + return { + priceLower: position.token0PriceUpper.invert(), + priceUpper: position.token0PriceLower.invert(), + quote: token0, + base: token1, + } + } + + // if token1 is an ETH-/BTC-stable asset, set it as the base token + const bases = [...Object.values(WRAPPED_NATIVE_CURRENCY), WBTC] + if (bases.some((base) => base && base.equals(token1))) { + return { + priceLower: position.token0PriceUpper.invert(), + priceUpper: position.token0PriceLower.invert(), + quote: token0, + base: token1, + } + } + + // if both prices are below 1, invert + if (position.token0PriceUpper.lessThan(1)) { + return { + priceLower: position.token0PriceUpper.invert(), + priceUpper: position.token0PriceLower.invert(), + quote: token0, + base: token1, + } + } + + // otherwise, just return the default + return { + priceLower: position.token0PriceLower, + priceUpper: position.token0PriceUpper, + quote: token1, + base: token0, + } +} + +export default function PositionListItem({ + token0: token0Address, + token1: token1Address, + tokenId, + fee: feeAmount, + liquidity, + tickLower, + tickUpper, +}: PositionListItemProps) { + const { formatDelta, formatTickPrice } = useFormatter() + + const token0 = useToken(token0Address) + const token1 = useToken(token1Address) + + const currency0 = token0 ? unwrappedToken(token0) : undefined + const currency1 = token1 ? unwrappedToken(token1) : undefined + + // construct Position from details returned + const [, pool] = usePool(currency0 ?? undefined, currency1 ?? undefined, feeAmount) + + const position = useMemo(() => { + if (pool) { + return new Position({ pool, liquidity: liquidity.toString(), tickLower, tickUpper }) + } + return undefined + }, [liquidity, pool, tickLower, tickUpper]) + + const tickAtLimit = useIsTickAtLimit(feeAmount, tickLower, tickUpper) + + // prices + const { priceLower, priceUpper, quote, base } = getPriceOrderingFromPositionForUI(position) + + const currencyQuote = quote && unwrappedToken(quote) + const currencyBase = base && unwrappedToken(base) + + // check if price is within range + const outOfRange: boolean = pool ? pool.tickCurrent < tickLower || pool.tickCurrent >= tickUpper : false + + const positionSummaryLink = '/pools/' + tokenId + + const removed = liquidity?.eq(0) + + return ( + + + + + +  {currencyQuote?.symbol} / {currencyBase?.symbol} + + + + {formatDelta(parseFloat(new Percent(feeAmount, 1_000_000).toSignificant()))} + + + + + + {priceLower && priceUpper ? ( + + + + Min: + + + + {formatTickPrice({ + price: priceLower, + atLimit: tickAtLimit, + direction: Bound.LOWER, + })}{' '} + + per + + {' '} + + {' '} + + + {' '} + + + + Max: + + + + {formatTickPrice({ + price: priceUpper, + atLimit: tickAtLimit, + direction: Bound.UPPER, + })}{' '} + + per{' '} + + + + + ) : ( + + )} + + ) +} diff --git a/apps/web/src/components/PositionPreview/index.tsx b/apps/web/src/components/PositionPreview/index.tsx new file mode 100644 index 0000000..3379298 --- /dev/null +++ b/apps/web/src/components/PositionPreview/index.tsx @@ -0,0 +1,194 @@ +import { Trans } from '@lingui/macro' +import { Currency } from '@uniswap/sdk-core' +import { Position } from '@uniswap/v3-sdk' +import RangeBadge from 'components/Badge/RangeBadge' +import { LightCard } from 'components/Card' +import { AutoColumn } from 'components/Column' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import { Break } from 'components/earn/styled' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import RateToggle from 'components/RateToggle' +import { RowBetween, RowFixed } from 'components/Row' +import { BIPS_BASE } from 'constants/misc' +import JSBI from 'jsbi' +import { ReactNode, useCallback, useState } from 'react' +import { Bound } from 'state/mint/v3/actions' +import { useTheme } from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' +import { unwrappedToken } from 'utils/unwrappedToken' + +export const PositionPreview = ({ + position, + title, + inRange, + baseCurrencyDefault, + ticksAtLimit, +}: { + position: Position + title?: ReactNode + inRange: boolean + baseCurrencyDefault?: Currency + ticksAtLimit: { [bound: string]: boolean | undefined } +}) => { + const theme = useTheme() + const { formatCurrencyAmount, formatDelta, formatPrice, formatTickPrice } = useFormatter() + + const currency0 = unwrappedToken(position.pool.token0) + const currency1 = unwrappedToken(position.pool.token1) + + // track which currency should be base + const [baseCurrency, setBaseCurrency] = useState( + baseCurrencyDefault + ? baseCurrencyDefault === currency0 + ? currency0 + : baseCurrencyDefault === currency1 + ? currency1 + : currency0 + : currency0 + ) + + const sorted = baseCurrency === currency0 + const quoteCurrency = sorted ? currency1 : currency0 + + const price = sorted ? position.pool.priceOf(position.pool.token0) : position.pool.priceOf(position.pool.token1) + + const priceLower = sorted ? position.token0PriceLower : position.token0PriceUpper.invert() + const priceUpper = sorted ? position.token0PriceUpper : position.token0PriceLower.invert() + + const handleRateChange = useCallback(() => { + setBaseCurrency(quoteCurrency) + }, [quoteCurrency]) + + const removed = position?.liquidity && JSBI.equal(position?.liquidity, JSBI.BigInt(0)) + + return ( + + + + + + {currency0?.symbol} / {currency1?.symbol} + + + + + + + + + + + {currency0?.symbol} + + + + {formatCurrencyAmount({ amount: position.amount0 })} + + + + + + + {currency1?.symbol} + + + + {formatCurrencyAmount({ amount: position.amount1 })} + + + + + + + Fee tier + + + {formatDelta(position?.pool?.fee / BIPS_BASE)} + + + + + + + + {title ? {title} :
} + + + + + + + + Min price + + + {formatTickPrice({ + price: priceLower, + atLimit: ticksAtLimit, + direction: Bound.LOWER, + })} + + + + {quoteCurrency.symbol} per {baseCurrency.symbol} + + + + Your position will be 100% composed of {baseCurrency?.symbol} at this price + + + + + + + + Max price + + + {formatTickPrice({ + price: priceUpper, + atLimit: ticksAtLimit, + direction: Bound.UPPER, + })} + + + + {quoteCurrency.symbol} per {baseCurrency.symbol} + + + + Your position will be 100% composed of {quoteCurrency?.symbol} at this price + + + + + + + + Current price + + {`${formatPrice({ + price, + type: NumberType.TokenTx, + })} `} + + + {quoteCurrency.symbol} per {baseCurrency.symbol} + + + + + + + ) +} diff --git a/apps/web/src/components/PrefetchBalancesWrapper/PrefetchBalancesWrapper.tsx b/apps/web/src/components/PrefetchBalancesWrapper/PrefetchBalancesWrapper.tsx new file mode 100644 index 0000000..536a0aa --- /dev/null +++ b/apps/web/src/components/PrefetchBalancesWrapper/PrefetchBalancesWrapper.tsx @@ -0,0 +1,93 @@ +import { useWeb3React } from '@web3-react/core' +import { usePortfolioBalancesLazyQuery, usePortfolioBalancesQuery } from 'graphql/data/__generated__/types-and-hooks' +import { GQL_MAINNET_CHAINS } from 'graphql/data/util' +import usePrevious from 'hooks/usePrevious' +import { atom, useAtom } from 'jotai' +import ms from 'ms' +import { PropsWithChildren, useCallback, useEffect } from 'react' + +import { usePendingActivity } from '../AccountDrawer/MiniPortfolio/Activity/hooks' + +/** Returns true if the number of pending activities has decreased */ +function useHasUpdatedTx() { + const { pendingActivityCount } = usePendingActivity() + const prevPendingActivityCount = usePrevious(pendingActivityCount) + + return !!prevPendingActivityCount && pendingActivityCount < prevPendingActivityCount +} + +// TODO(WEB-3004) - Add useCachedPortfolioBalanceUsd to simplify usage of useCachedPortfolioBalancesQuery +export function useCachedPortfolioBalancesQuery({ account }: { account?: string }) { + return usePortfolioBalancesQuery({ + skip: !account, + variables: { ownerAddress: account ?? '', chains: GQL_MAINNET_CHAINS }, + fetchPolicy: 'cache-only', // PrefetchBalancesWrapper handles balance fetching/staleness; this component only reads from cache + errorPolicy: 'all', + }) +} + +const hasUnfetchedBalancesAtom = atom(true) + +/* Prefetches & caches portfolio balances when the wrapped component is hovered or the user completes a transaction */ +export default function PrefetchBalancesWrapper({ + children, + shouldFetchOnAccountUpdate, + shouldFetchOnHover = true, + className, +}: PropsWithChildren<{ shouldFetchOnAccountUpdate: boolean; shouldFetchOnHover?: boolean; className?: string }>) { + const { account } = useWeb3React() + const [prefetchPortfolioBalances] = usePortfolioBalancesLazyQuery() + + // Use an atom to track unfetched state to avoid duplicating fetches if this component appears multiple times on the page. + const [hasUnfetchedBalances, setHasUnfetchedBalances] = useAtom(hasUnfetchedBalancesAtom) + const fetchBalances = useCallback( + (withDelay: boolean) => { + if (account) { + // Backend takes <2sec to get the updated portfolio value after a transaction + // This timeout is an interim solution while we're working on a websocket that'll ping the client when connected account gets changes + // TODO(WEB-3131): remove this timeout after websocket is implemented + setTimeout( + () => { + prefetchPortfolioBalances({ variables: { ownerAddress: account, chains: GQL_MAINNET_CHAINS } }) + setHasUnfetchedBalances(false) + }, + withDelay ? ms('3.5s') : 0 + ) + } + }, + [account, prefetchPortfolioBalances, setHasUnfetchedBalances] + ) + + const prevAccount = usePrevious(account) + + const hasUpdatedTx = useHasUpdatedTx() + // Listens for account changes & recently updated transactions to keep portfolio balances fresh in apollo cache + useEffect(() => { + const accountChanged = prevAccount !== undefined && prevAccount !== account + if (hasUpdatedTx || accountChanged) { + // The parent configures whether these conditions should trigger an immediate fetch, + // if not, we set a flag to fetch on next hover. + if (shouldFetchOnAccountUpdate) { + fetchBalances(true) + } else { + setHasUnfetchedBalances(true) + } + } + }, [account, prevAccount, shouldFetchOnAccountUpdate, fetchBalances, hasUpdatedTx, setHasUnfetchedBalances]) + + // Temporary workaround to fix balances on TDP - this fetches balances if shouldFetchOnAccountUpdate becomes true while hasUnfetchedBalances is true + // TODO(WEB-3071) remove this logic once balance provider refactor is done + useEffect(() => { + if (hasUnfetchedBalances && shouldFetchOnAccountUpdate) fetchBalances(true) + }, [fetchBalances, hasUnfetchedBalances, shouldFetchOnAccountUpdate]) + + const onHover = useCallback(() => { + if (hasUnfetchedBalances) fetchBalances(false) + }, [fetchBalances, hasUnfetchedBalances]) + + return ( +
+ {children} +
+ ) +} diff --git a/apps/web/src/components/PrivacyPolicy/index.tsx b/apps/web/src/components/PrivacyPolicy/index.tsx new file mode 100644 index 0000000..5312c66 --- /dev/null +++ b/apps/web/src/components/PrivacyPolicy/index.tsx @@ -0,0 +1,182 @@ +import { Trans } from '@lingui/macro' +import { SharedEventName } from '@uniswap/analytics-events' +import { sendAnalyticsEvent } from 'analytics' +import Card, { DarkGrayCard } from 'components/Card' +import Row, { AutoRow, RowBetween } from 'components/Row' +import { useEffect, useRef } from 'react' +import { ArrowDown, Info, X } from 'react-feather' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { isMobile } from 'uniswap/src/utils/platform' + +import { useModalIsOpen, useTogglePrivacyPolicy } from '../../state/application/hooks' +import { ApplicationModal } from '../../state/application/reducer' +import { AutoColumn } from '../Column' +import Modal from '../Modal' + +const Wrapper = styled.div` + max-height: 70vh; + overflow: auto; + padding: 0 1rem; +` + +const StyledExternalCard = styled(Card)` + background-color: ${({ theme }) => theme.accent2}; + padding: 0.5rem; + width: 100%; + + :hover, + :focus, + :active { + background-color: ${({ theme }) => theme.neutral3}; + } +` + +const HoverText = styled.div` + text-decoration: none; + color: ${({ theme }) => theme.neutral1}; + display: flex; + align-items: center; + + :hover { + cursor: pointer; + } +` + +const StyledLinkOut = styled(ArrowDown)` + transform: rotate(230deg); +` + +const EXTERNAL_APIS = [ + { + name: 'Auto Router', + description: The app fetches the optimal trade route from a Uniswap Labs server., + }, + { + name: 'Infura', + description: The app fetches on-chain data and constructs contract calls with an Infura API., + }, + { + name: 'TRM Labs', + description: ( + <> + + The app securely collects your wallet address and shares it with TRM Labs Inc. for risk and compliance + reasons. + {' '} + + Learn more + + + ), + }, + { + name: 'Google Analytics & Amplitude', + description: The app logs anonymized usage statistics in order to improve over time., + }, + { + name: 'The Graph', + description: The app fetches blockchain data from The Graph’s hosted service., + }, +] + +export function PrivacyPolicyModal() { + const node = useRef() + const open = useModalIsOpen(ApplicationModal.PRIVACY_POLICY) + const toggle = useTogglePrivacyPolicy() + + useEffect(() => { + if (!open) return + + sendAnalyticsEvent(SharedEventName.PAGE_VIEWED, { + category: 'Modal', + action: 'Show Legal', + }) + }, [open]) + + return ( + toggle()}> + + + + Legal & Privacy + + toggle()}> + + + + + + + ) +} + +function PrivacyPolicy() { + return ( + { + // prevent modal gesture handler from dismissing modal when content is scrolling + if (isMobile) { + e.stopPropagation() + } + }} + > + + + + + + + + + Uniswap Labs' Terms of Service + + + + + + + + + + + + + Privacy Policy + + + + + + + + + This app uses the following third-party APIs: + + + {EXTERNAL_APIS.map(({ name, description }, i) => ( + + + + + + {name} + + + {description} + + + ))} + + + + Learn more + + + + + + + ) +} diff --git a/apps/web/src/components/QuestionHelper/index.tsx b/apps/web/src/components/QuestionHelper/index.tsx new file mode 100644 index 0000000..adb1e14 --- /dev/null +++ b/apps/web/src/components/QuestionHelper/index.tsx @@ -0,0 +1,52 @@ +import { ReactNode, useCallback, useState } from 'react' +import { HelpCircle } from 'react-feather' +import styled from 'styled-components' + +import Tooltip from '../Tooltip' + +const QuestionWrapper = styled.div` + display: flex; + align-items: center; + justify-content: center; + padding: 0px; + width: 18px; + height: 18px; + border: none; + background: none; + outline: none; + cursor: default; + border-radius: 36px; + font-size: 12px; + border-radius: 12px; + + :hover, + :focus { + opacity: 0.7; + } +` + +const QuestionMark = styled.span` + font-size: 14px; + margin-left: 8px; + align-items: center; + color: ${({ theme }) => theme.neutral2}; + margin-top: 2.5px; +` + +export default function QuestionHelper({ text }: { text: ReactNode; size?: number }) { + const [show, setShow] = useState(false) + + const open = useCallback(() => setShow(true), [setShow]) + const close = useCallback(() => setShow(false), [setShow]) + return ( + + + + + + + + + + ) +} diff --git a/apps/web/src/components/RangeSelector/PresetsButtons.tsx b/apps/web/src/components/RangeSelector/PresetsButtons.tsx new file mode 100644 index 0000000..82fb265 --- /dev/null +++ b/apps/web/src/components/RangeSelector/PresetsButtons.tsx @@ -0,0 +1,29 @@ +import { Trans } from '@lingui/macro' +import { ButtonOutlined } from 'components/Button' +import { AutoRow } from 'components/Row' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const Button = styled(ButtonOutlined).attrs(() => ({ + padding: '6px', + $borderRadius: '8px', +}))` + color: ${({ theme }) => theme.neutral1}; + flex: 1; +` + +interface PresetsButtonsProps { + onSetFullRange: () => void +} + +export default function PresetsButtons({ onSetFullRange }: PresetsButtonsProps) { + return ( + + + + ) +} diff --git a/apps/web/src/components/RangeSelector/index.tsx b/apps/web/src/components/RangeSelector/index.tsx new file mode 100644 index 0000000..17a11e4 --- /dev/null +++ b/apps/web/src/components/RangeSelector/index.tsx @@ -0,0 +1,72 @@ +import { Trans } from '@lingui/macro' +import { Currency, Price, Token } from '@uniswap/sdk-core' +import StepCounter from 'components/InputStepCounter/InputStepCounter' +import { AutoRow } from 'components/Row' +import { Bound } from 'state/mint/v3/actions' + +// currencyA is the base token +export default function RangeSelector({ + priceLower, + priceUpper, + onLeftRangeInput, + onRightRangeInput, + getDecrementLower, + getIncrementLower, + getDecrementUpper, + getIncrementUpper, + currencyA, + currencyB, + feeAmount, + ticksAtLimit, +}: { + priceLower?: Price + priceUpper?: Price + getDecrementLower: () => string + getIncrementLower: () => string + getDecrementUpper: () => string + getIncrementUpper: () => string + onLeftRangeInput: (typedValue: string) => void + onRightRangeInput: (typedValue: string) => void + currencyA?: Currency | null + currencyB?: Currency | null + feeAmount?: number + ticksAtLimit: { [bound in Bound]?: boolean | undefined } +}) { + const tokenA = (currencyA ?? undefined)?.wrapped + const tokenB = (currencyB ?? undefined)?.wrapped + const isSorted = tokenA && tokenB && tokenA.sortsBefore(tokenB) + + const leftPrice = isSorted ? priceLower : priceUpper?.invert() + const rightPrice = isSorted ? priceUpper : priceLower?.invert() + + return ( + + Low price} + tokenA={currencyA?.symbol} + tokenB={currencyB?.symbol} + /> + High price} + /> + + ) +} diff --git a/apps/web/src/components/RateToggle/index.tsx b/apps/web/src/components/RateToggle/index.tsx new file mode 100644 index 0000000..13c7b93 --- /dev/null +++ b/apps/web/src/components/RateToggle/index.tsx @@ -0,0 +1,33 @@ +import { Trans } from '@lingui/macro' +import { Currency } from '@uniswap/sdk-core' +import { ToggleElement, ToggleWrapper } from 'components/Toggle/MultiToggle' + +// the order of displayed base currencies from left to right is always in sort order +// currencyA is treated as the preferred base currency +export default function RateToggle({ + currencyA, + currencyB, + handleRateToggle, +}: { + currencyA: Currency + currencyB: Currency + handleRateToggle: () => void +}) { + const tokenA = currencyA?.wrapped + const tokenB = currencyB?.wrapped + + const isSorted = tokenA && tokenB && tokenA.sortsBefore(tokenB) + + return tokenA && tokenB ? ( +
+ + + {isSorted ? currencyA.symbol : currencyB.symbol} + + + {isSorted ? currencyB.symbol : currencyA.symbol} + + +
+ ) : null +} diff --git a/apps/web/src/components/RouterLabel/RouterLabel.test.tsx b/apps/web/src/components/RouterLabel/RouterLabel.test.tsx new file mode 100644 index 0000000..5f961ad --- /dev/null +++ b/apps/web/src/components/RouterLabel/RouterLabel.test.tsx @@ -0,0 +1,24 @@ +import RouterLabel from '.' +import { + TEST_DUTCH_TRADE_ETH_INPUT, + TEST_TRADE_EXACT_INPUT, + TEST_TRADE_EXACT_INPUT_API, +} from '../../test-utils/constants' +import { render, screen } from '../../test-utils/render' + +describe('RouterLabel', () => { + it('renders correct label for UniswapX trade', () => { + render() + expect(screen.getByText('Uniswap X')).toBeInTheDocument() + }) + + it('renders correct label for classic trade with client routing', () => { + render() + expect(screen.getByText('Uniswap Client')).toBeInTheDocument() + }) + + it('renders correct label for classic trade with API routing', () => { + render() + expect(screen.getByText('Uniswap API')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/RouterLabel/UniswapXRouterLabel.test.tsx b/apps/web/src/components/RouterLabel/UniswapXRouterLabel.test.tsx new file mode 100644 index 0000000..ef91dd2 --- /dev/null +++ b/apps/web/src/components/RouterLabel/UniswapXRouterLabel.test.tsx @@ -0,0 +1,16 @@ +import { mocked } from 'test-utils/mocked' +import { render, screen } from 'test-utils/render' +import { v4 as uuid } from 'uuid' + +import UniswapXRouterLabel from './UniswapXRouterLabel' + +jest.mock('uuid') + +describe('UniswapXRouterLabel', () => { + it('matches snapshot', () => { + mocked(uuid).mockReturnValue('test-id') + const { asFragment } = render(test router label) + expect(screen.getByText('test router label')).toBeInTheDocument() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/RouterLabel/UniswapXRouterLabel.tsx b/apps/web/src/components/RouterLabel/UniswapXRouterLabel.tsx new file mode 100644 index 0000000..3c938ab --- /dev/null +++ b/apps/web/src/components/RouterLabel/UniswapXRouterLabel.tsx @@ -0,0 +1,68 @@ +import Row from 'components/Row' +import { useRef } from 'react' +import styled from 'styled-components' +import { v4 as uuid } from 'uuid' + +import { BoxProps } from '../../nft/components/Box' + +// Gradient with a fallback to solid color. +const Gradient = styled.div` + color: #4673fa; + + @supports (-webkit-background-clip: text) and (-webkit-text-fill-color: transparent) { + background-image: linear-gradient(91.39deg, #4673fa -101.76%, #9646fa 101.76%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } +` + +export { Gradient as UniswapXGradient } + +// Uniswap X SVG icon with gradient, copied from Figma. +// In order for gradient to work, we must give its definition a unique ID that does not collide +// with other occurences of this component on the page. +export const UniswapXRouterIcon = ({ testId }: { testId?: string }) => { + const componentIdRef = useRef(uuid()) + const componentId = `AutoRouterIconGradient${componentIdRef.current}` + + return ( + + + + + + + + + + ) +} + +export type UnswapXRouterLabelProps = BoxProps & { + disableTextGradient?: boolean + testId?: string +} + +export default function UniswapXRouterLabel({ + children, + disableTextGradient, + testId, + ...rest +}: UnswapXRouterLabelProps) { + return ( + + + {disableTextGradient ? children : {children}} + + ) +} diff --git a/apps/web/src/components/RouterLabel/__snapshots__/UniswapXRouterLabel.test.tsx.snap b/apps/web/src/components/RouterLabel/__snapshots__/UniswapXRouterLabel.test.tsx.snap new file mode 100644 index 0000000..d51fd77 --- /dev/null +++ b/apps/web/src/components/RouterLabel/__snapshots__/UniswapXRouterLabel.test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UniswapXRouterLabel matches snapshot 1`] = ` + + .c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: auto; +} + +.c1 { + width: auto; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c2 { + color: #4673fa; +} + +@supports (-webkit-background-clip:text) and (-webkit-text-fill-color:transparent) { + .c2 { + background-image: linear-gradient(91.39deg,#4673fa -101.76%,#9646fa 101.76%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } +} + + + +
+ + + + + + + + + +
+ test router label +
+
+
+
+
+`; diff --git a/apps/web/src/components/RouterLabel/index.tsx b/apps/web/src/components/RouterLabel/index.tsx new file mode 100644 index 0000000..6339f9e --- /dev/null +++ b/apps/web/src/components/RouterLabel/index.tsx @@ -0,0 +1,22 @@ +import { QuoteMethod, SubmittableTrade } from 'state/routing/types' +import { isUniswapXTrade } from 'state/routing/utils' +import { DefaultTheme } from 'styled-components' +import { ThemedText } from 'theme/components' + +import UniswapXRouterLabel from './UniswapXRouterLabel' + +export default function RouterLabel({ trade, color }: { trade: SubmittableTrade; color?: keyof DefaultTheme }) { + if (isUniswapXTrade(trade)) { + return ( + + Uniswap X + + ) + } + + if (trade.quoteMethod === QuoteMethod.CLIENT_SIDE_FALLBACK) { + return Uniswap Client + } + + return Uniswap API +} diff --git a/apps/web/src/components/RoutingDiagram/RoutingDiagram.test.tsx b/apps/web/src/components/RoutingDiagram/RoutingDiagram.test.tsx new file mode 100644 index 0000000..02f09cf --- /dev/null +++ b/apps/web/src/components/RoutingDiagram/RoutingDiagram.test.tsx @@ -0,0 +1,63 @@ +import { Protocol } from '@uniswap/router-sdk' +import { Currency, Percent } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens' +import { render } from 'test-utils/render' +import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries' + +import RoutingDiagram from './RoutingDiagram' + +const percent = (strings: TemplateStringsArray) => new Percent(parseInt(strings[0]), 100) + +const singleRoute: RoutingDiagramEntry = { + percent: percent`100`, + path: [[USDC_MAINNET, DAI, FeeAmount.LOW]], + protocol: Protocol.V3, +} + +const multiRoute: RoutingDiagramEntry[] = [ + { percent: percent`75`, path: [[USDC_MAINNET, DAI, FeeAmount.LOWEST]], protocol: Protocol.V2 }, + { + percent: percent`25`, + path: [ + [USDC_MAINNET, WBTC, FeeAmount.MEDIUM], + [WBTC, DAI, FeeAmount.HIGH], + ], + protocol: Protocol.V3, + }, +] + +jest.mock( + 'components/Logo/CurrencyLogo', + () => + ({ currency }: { currency: Currency }) => + `CurrencyLogo currency=${currency.symbol}` +) + +jest.mock( + 'components/DoubleLogo', + () => + ({ currency0, currency1 }: { currency0: Currency; currency1: Currency }) => + `DoubleCurrencyLogo currency0=${currency0.symbol} currency1=${currency1.symbol}` +) + +jest.mock('../Popover', () => () => 'Popover') + +jest.mock('hooks/useTokenInfoFromActiveList', () => ({ + useTokenInfoFromActiveList: (currency: Currency) => currency, +})) + +it('renders when no routes are provided', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) + +it('renders single route', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) + +it('renders multi route', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() +}) diff --git a/apps/web/src/components/RoutingDiagram/RoutingDiagram.tsx b/apps/web/src/components/RoutingDiagram/RoutingDiagram.tsx new file mode 100644 index 0000000..38ece6e --- /dev/null +++ b/apps/web/src/components/RoutingDiagram/RoutingDiagram.tsx @@ -0,0 +1,156 @@ +import { Trans } from '@lingui/macro' +import { Protocol } from '@uniswap/router-sdk' +import { Currency } from '@uniswap/sdk-core' +import { FeeAmount } from '@uniswap/v3-sdk' +import Badge from 'components/Badge' +import DoubleCurrencyLogo from 'components/DoubleLogo' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import Row, { AutoRow } from 'components/Row' +import { BIPS_BASE } from 'constants/misc' +import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList' +import { Box } from 'rebass' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' +import { RoutingDiagramEntry } from 'utils/getRoutingDiagramEntries' + +import { ReactComponent as DotLine } from '../../assets/svg/dot_line.svg' +import { MouseoverTooltip, TooltipSize } from '../Tooltip' + +const Wrapper = styled(Box)` + align-items: center; + width: 100%; +` + +const RouteContainerRow = styled(Row)` + display: grid; + grid-template-columns: 24px 1fr 24px; +` + +const RouteRow = styled(Row)` + align-items: center; + display: flex; + justify-content: center; + padding: 0.1rem 0.5rem; + position: relative; +` + +const PoolBadge = styled(Badge)` + display: flex; + padding: 4px 4px; +` + +const DottedLine = styled.div` + display: flex; + align-items: center; + position: absolute; + width: calc(100%); + z-index: 1; + opacity: 0.5; +` + +const DotColor = styled(DotLine)` + path { + stroke: ${({ theme }) => theme.surface3}; + } +` + +const OpaqueBadge = styled(Badge)` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 8px; + display: grid; + grid-gap: 4px; + grid-auto-flow: column; + justify-content: start; + padding: 4px 6px; + z-index: ${Z_INDEX.sticky}; +` + +const ProtocolBadge = styled(Badge)` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 4px; + color: ${({ theme }) => theme.neutral2}; + font-size: 10px; + padding: 2px 4px; + z-index: ${Z_INDEX.sticky + 1}; +` + +const MixedProtocolBadge = styled(ProtocolBadge)` + width: 60px; +` + +const BadgeText = styled(ThemedText.LabelMicro)` + word-break: normal; +` + +export default function RoutingDiagram({ + currencyIn, + currencyOut, + routes, +}: { + currencyIn: Currency + currencyOut: Currency + routes: RoutingDiagramEntry[] +}) { + const tokenIn = useTokenInfoFromActiveList(currencyIn) + const tokenOut = useTokenInfoFromActiveList(currencyOut) + + return ( + + {routes.map((entry, index) => ( + + + + + + ))} + + ) +} + +function Route({ entry: { percent, path, protocol } }: { entry: RoutingDiagramEntry }) { + return ( + + + + + + {protocol === Protocol.MIXED ? ( + + V3 + V2 + + ) : ( + + {protocol.toUpperCase()} + + )} + {percent.toSignificant(2)}% + + + {path.map(([currency0, currency1, feeAmount], index) => ( + + ))} + + + ) +} + +function Pool({ currency0, currency1, feeAmount }: { currency0: Currency; currency1: Currency; feeAmount: FeeAmount }) { + const tokenInfo0 = useTokenInfoFromActiveList(currency0) + const tokenInfo1 = useTokenInfoFromActiveList(currency1) + + // TODO - link pool icon to info.uniswap.org via query params + return ( + {tokenInfo0?.symbol + '/' + tokenInfo1?.symbol + ' ' + feeAmount / 10000}% pool} + size={TooltipSize.ExtraSmall} + > + + + + + {feeAmount / BIPS_BASE}% + + + ) +} diff --git a/apps/web/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap b/apps/web/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap new file mode 100644 index 0000000..a91bd8b --- /dev/null +++ b/apps/web/src/components/RoutingDiagram/__snapshots__/RoutingDiagram.test.tsx.snap @@ -0,0 +1,538 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders multi route 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c13 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: 100%; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c14 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 1px; +} + +.c15 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin: -1px; +} + +.c15 > * { + margin: 1px !important; +} + +.c10 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c12 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c7 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: #F9F9F9; + border: unset; + border-radius: 0.5rem; + color: #7D7D7D; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + padding: 4px 6px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + font-weight: 535; +} + +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; +} + +.c3 { + display: grid; + grid-template-columns: 24px 1fr 24px; +} + +.c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.1rem 0.5rem; + position: relative; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: absolute; + width: calc(100%); + z-index: 1; + opacity: 0.5; +} + +.c6 path { + stroke: #22222212; +} + +.c8 { + background-color: #F9F9F9; + border-radius: 8px; + display: grid; + grid-gap: 4px; + grid-auto-flow: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + padding: 4px 6px; + z-index: 1020; +} + +.c9 { + background-color: #F9F9F9; + border-radius: 4px; + color: #7D7D7D; + font-size: 10px; + padding: 2px 4px; + z-index: 1021; +} + +.c11 { + word-break: normal; +} + + + +
+
+ CurrencyLogo currency=USDC +
+
+ + dot_line.svg + +
+
+
+
+ V2 +
+
+
+ 75% +
+
+
+ Popover +
+
+ CurrencyLogo currency=DAI +
+
+ CurrencyLogo currency=USDC +
+
+ + dot_line.svg + +
+
+
+
+ V3 +
+
+
+ 25% +
+
+
+ PopoverPopover +
+
+ CurrencyLogo currency=DAI +
+
+
+
+
+`; + +exports[`renders single route 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c13 { + box-sizing: border-box; + margin: 0; + min-width: 0; + width: 100%; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c14 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 1px; +} + +.c15 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin: -1px; +} + +.c15 > * { + margin: 1px !important; +} + +.c10 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c12 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c7 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + background: #F9F9F9; + border: unset; + border-radius: 0.5rem; + color: #7D7D7D; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + padding: 4px 6px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + font-weight: 535; +} + +.c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; +} + +.c3 { + display: grid; + grid-template-columns: 24px 1fr 24px; +} + +.c4 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + padding: 0.1rem 0.5rem; + position: relative; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: absolute; + width: calc(100%); + z-index: 1; + opacity: 0.5; +} + +.c6 path { + stroke: #22222212; +} + +.c8 { + background-color: #F9F9F9; + border-radius: 8px; + display: grid; + grid-gap: 4px; + grid-auto-flow: column; + -webkit-box-pack: start; + -webkit-justify-content: start; + -ms-flex-pack: start; + justify-content: start; + padding: 4px 6px; + z-index: 1020; +} + +.c9 { + background-color: #F9F9F9; + border-radius: 4px; + color: #7D7D7D; + font-size: 10px; + padding: 2px 4px; + z-index: 1021; +} + +.c11 { + word-break: normal; +} + + + +
+
+ CurrencyLogo currency=USDC +
+
+ + dot_line.svg + +
+
+
+
+ V3 +
+
+
+ 100% +
+
+
+ Popover +
+
+ CurrencyLogo currency=DAI +
+
+
+
+
+`; + +exports[`renders when no routes are provided 1`] = ` + + .c0 { + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + width: 100%; +} + + + +
+ + + +`; diff --git a/apps/web/src/components/Row/index.tsx b/apps/web/src/components/Row/index.tsx new file mode 100644 index 0000000..abd83e7 --- /dev/null +++ b/apps/web/src/components/Row/index.tsx @@ -0,0 +1,53 @@ +import { Box } from 'rebass/styled-components' +import styled from 'styled-components' +import { Gap } from 'theme' + +// TODO(WEB-1983): +// Setting `width: 100%` by default prevents composability in complex flex layouts. +// Same applies to `RowFixed` and its negative margins. This component needs to be +// further investigated and improved to make UI work easier. +const Row = styled(Box)<{ + width?: string + align?: string + justify?: string + padding?: string + border?: string + borderRadius?: string + gap?: Gap | string +}>` + width: ${({ width }) => width ?? '100%'}; + display: flex; + padding: 0; + align-items: ${({ align }) => align ?? 'center'}; + justify-content: ${({ justify }) => justify ?? 'flex-start'}; + padding: ${({ padding }) => padding}; + border: ${({ border }) => border}; + border-radius: ${({ borderRadius }) => borderRadius}; + gap: ${({ gap, theme }) => gap && (theme.grids[gap as Gap] || gap)}; +` + +export const RowBetween = styled(Row)` + justify-content: space-between; +` + +export const RowFlat = styled.div` + display: flex; + align-items: flex-end; +` + +export const AutoRow = styled(Row)<{ gap?: string; justify?: string }>` + flex-wrap: wrap; + margin: ${({ gap }) => gap && `-${gap}`}; + justify-content: ${({ justify }) => justify && justify}; + + & > * { + margin: ${({ gap }) => gap} !important; + } +` + +export const RowFixed = styled(Row)<{ gap?: string; justify?: string }>` + width: fit-content; + margin: ${({ gap }) => gap && `-${gap}`}; +` + +export default Row diff --git a/apps/web/src/components/SearchModal/CommonBases.test.tsx b/apps/web/src/components/SearchModal/CommonBases.test.tsx new file mode 100644 index 0000000..76af44c --- /dev/null +++ b/apps/web/src/components/SearchModal/CommonBases.test.tsx @@ -0,0 +1,33 @@ +import { ChainId } from '@uniswap/sdk-core' +import { render } from 'test-utils/render' + +import CommonBases from './CommonBases' + +const mockOnSelect = jest.fn() + +describe('CommonBases', () => { + it('renders without crashing', () => { + const { container } = render( + + ) + expect(container).toMatchSnapshot() + }) + + it('renders correct number of common bases', () => { + const { getAllByTestId } = render( + + ) + const items = getAllByTestId(/common-base-/) + expect(items.length).toBe(6) + }) + + it('renders common bases on mobile', () => { + window.innerWidth = 400 + window.dispatchEvent(new Event('resize')) + const { getAllByTestId } = render( + + ) + const items = getAllByTestId(/common-base-/) + expect(items.length).toBe(6) + }) +}) diff --git a/apps/web/src/components/SearchModal/CommonBases.tsx b/apps/web/src/components/SearchModal/CommonBases.tsx new file mode 100644 index 0000000..871d4b7 --- /dev/null +++ b/apps/web/src/components/SearchModal/CommonBases.tsx @@ -0,0 +1,110 @@ +import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events' +import { Currency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { TraceEvent } from 'analytics' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' +import { AutoRow } from 'components/Row' +import { COMMON_BASES } from 'constants/routing' +import { useTokenInfoFromActiveList } from 'hooks/useTokenInfoFromActiveList' +import { getTokenAddress } from 'lib/utils/analytics' +import { Text } from 'rebass' +import styled from 'styled-components' +import { currencyId } from 'utils/currencyId' + +const BaseWrapper = styled.div<{ disable?: boolean }>` + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 18px; + display: flex; + padding: 6px; + padding-top: 5px; + padding-bottom: 5px; + padding-right: 12px; + line-height: 0px; + + align-items: center; + :hover { + cursor: ${({ disable }) => !disable && 'pointer'}; + background-color: ${({ theme }) => theme.deprecated_hoverDefault}; + } + + color: ${({ theme, disable }) => disable && theme.neutral1}; + background-color: ${({ theme, disable }) => disable && theme.surface3}; +` + +const formatAnalyticsEventProperties = ( + currency: Currency, + searchQuery: string, + isAddressSearch: string | false, + portfolioBalanceUsd: number | undefined +) => ({ + token_symbol: currency?.symbol, + token_chain_id: currency?.chainId, + token_address: getTokenAddress(currency), + is_suggested_token: true, + is_selected_from_list: false, + is_imported_by_user: false, + total_balances_usd: portfolioBalanceUsd, + ...(isAddressSearch === false + ? { search_token_symbol_input: searchQuery } + : { search_token_address_input: isAddressSearch }), +}) + +export default function CommonBases({ + chainId, + onSelect, + selectedCurrency, + searchQuery, + isAddressSearch, +}: { + chainId?: number + selectedCurrency?: Currency | null + onSelect: (currency: Currency) => void + searchQuery: string + isAddressSearch: string | false + portfolioBalanceUsd?: number +}) { + const bases = chainId !== undefined ? COMMON_BASES[chainId] ?? [] : [] + const { account } = useWeb3React() + const { data } = useCachedPortfolioBalancesQuery({ account }) + const portfolioBalanceUsd = data?.portfolios?.[0].tokensTotalDenominatedValue?.value + + return bases.length > 0 ? ( + + {bases.map((currency: Currency) => { + const isSelected = selectedCurrency?.equals(currency) + + return ( + + !isSelected && e.key === 'Enter' && onSelect(currency)} + onClick={() => !isSelected && onSelect(currency)} + disable={isSelected} + key={currencyId(currency)} + data-testid={`common-base-${currency.symbol}`} + > + + + {currency.symbol} + + + + ) + })} + + ) : null +} + +/** helper component to retrieve a base currency from the active token lists */ +function CurrencyLogoFromList({ currency }: { currency: Currency }) { + const token = useTokenInfoFromActiveList(currency) + + return +} diff --git a/apps/web/src/components/SearchModal/CurrencyList/index.css.ts b/apps/web/src/components/SearchModal/CurrencyList/index.css.ts new file mode 100644 index 0000000..3560902 --- /dev/null +++ b/apps/web/src/components/SearchModal/CurrencyList/index.css.ts @@ -0,0 +1,20 @@ +import { style } from '@vanilla-extract/css' +import { themeVars } from 'nft/css/sprinkles.css' + +export const scrollbarStyle = style([ + { + scrollbarWidth: 'thin', + scrollbarColor: `${themeVars.colors.surface3} transparent`, + height: '100%', + selectors: { + '&::-webkit-scrollbar': { + background: 'transparent', + width: '4px', + }, + '&::-webkit-scrollbar-thumb': { + background: `${themeVars.colors.surface3}`, + borderRadius: '8px', + }, + }, + }, +]) diff --git a/apps/web/src/components/SearchModal/CurrencyList/index.test.tsx b/apps/web/src/components/SearchModal/CurrencyList/index.test.tsx new file mode 100644 index 0000000..fb178bf --- /dev/null +++ b/apps/web/src/components/SearchModal/CurrencyList/index.test.tsx @@ -0,0 +1,94 @@ +import { screen } from '@testing-library/react' +import { Currency, CurrencyAmount as mockCurrencyAmount, Token as mockToken } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { DAI, USDC_MAINNET, WBTC } from 'constants/tokens' +import * as mockJSBI from 'jsbi' +import { mocked } from 'test-utils/mocked' +import { render } from 'test-utils/render' + +import CurrencyList, { CurrencyListRow } from '.' + +const noOp = function () { + // do nothing +} + +const mockCurrencyAmt = { + [DAI.address]: mockCurrencyAmount.fromRawAmount(DAI, mockJSBI.default.BigInt(100)), + [USDC_MAINNET.address]: mockCurrencyAmount.fromRawAmount(USDC_MAINNET, mockJSBI.default.BigInt(10)), + [WBTC.address]: mockCurrencyAmount.fromRawAmount(WBTC, mockJSBI.default.BigInt(1)), +} + +jest.mock( + 'components/Logo/CurrencyLogo', + () => + ({ currency }: { currency: Currency }) => + `CurrencyLogo currency=${currency.symbol}` +) + +jest.mock('../../../state/connection/hooks', () => { + return { + useCurrencyBalance: (currency: Currency) => { + return mockCurrencyAmt[(currency as mockToken)?.address] + }, + } +}) + +it('renders loading rows when isLoading is true', () => { + const component = render( + + ) + expect(component.findByTestId('loading-rows')).toBeTruthy() + expect(screen.queryByText('Wrapped BTC')).not.toBeInTheDocument() + expect(screen.queryByText('DAI')).not.toBeInTheDocument() + expect(screen.queryByText('USDC')).not.toBeInTheDocument() +}) + +it('renders currency rows correctly when currencies list is non-empty', () => { + render( + new CurrencyListRow(token, false))} + onCurrencySelect={noOp} + isLoading={false} + searchQuery="" + isAddressSearch="" + balances={{}} + /> + ) + expect(screen.getByText('Wrapped BTC')).toBeInTheDocument() + expect(screen.getByText('DAI')).toBeInTheDocument() + expect(screen.getByText('USDC')).toBeInTheDocument() +}) + +it('renders currency rows correctly with balances', () => { + mocked(useWeb3React).mockReturnValue({ + account: '0x52270d8234b864dcAC9947f510CE9275A8a116Db', + isActive: true, + } as ReturnType) + render( + new CurrencyListRow(token, false))} + onCurrencySelect={noOp} + isLoading={false} + searchQuery="" + isAddressSearch="" + showCurrencyAmount + balances={{ + [DAI.address.toLowerCase()]: { usdValue: 2, balance: 2 }, + }} + /> + ) + expect(screen.getByText('Wrapped BTC')).toBeInTheDocument() + expect(screen.getByText('DAI')).toBeInTheDocument() + expect(screen.getByText('USDC')).toBeInTheDocument() + expect(screen.getByText('2.00')).toBeInTheDocument() +}) diff --git a/apps/web/src/components/SearchModal/CurrencyList/index.tsx b/apps/web/src/components/SearchModal/CurrencyList/index.tsx new file mode 100644 index 0000000..6aff94b --- /dev/null +++ b/apps/web/src/components/SearchModal/CurrencyList/index.tsx @@ -0,0 +1,395 @@ +import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events' +import { Currency, CurrencyAmount, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { TraceEvent } from 'analytics' +import Loader from 'components/Icons/LoadingSpinner' +import { useCachedPortfolioBalancesQuery } from 'components/PrefetchBalancesWrapper/PrefetchBalancesWrapper' +import TokenSafetyIcon from 'components/TokenSafety/TokenSafetyIcon' +import { checkWarning } from 'constants/tokenSafety' +import { TokenBalances } from 'lib/hooks/useTokenList/sorting' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { CSSProperties, MutableRefObject, useCallback } from 'react' +import { FixedSizeList } from 'react-window' +import { Text } from 'rebass' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { useIsUserAddedToken } from '../../../hooks/Tokens' +import { TokenFromList } from '../../../state/lists/tokenFromList' +import Column, { AutoColumn } from '../../Column' +import CurrencyLogo from '../../Logo/CurrencyLogo' +import Row, { RowFixed } from '../../Row' +import { MouseoverTooltip, TooltipSize } from '../../Tooltip' +import { LoadingRows, MenuItem } from '../styled' +import { scrollbarStyle } from './index.css' + +function currencyKey(data: Currency | CurrencyListRow): string { + if (data instanceof CurrencyListSectionTitle) { + return data.label + } + + if (data instanceof CurrencyListRow) { + return data.currency?.isToken ? data.currency.address : 'ETHER' + } + + return data.isToken ? data.address : 'ETHER' +} + +const ROW_ITEM_SIZE = 56 + +const StyledBalanceText = styled(Text)` + white-space: nowrap; + overflow: hidden; + max-width: 5rem; + text-overflow: ellipsis; +` + +const CurrencyName = styled(Text)` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +` + +const Tag = styled.div` + background-color: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral2}; + font-size: 14px; + border-radius: 4px; + padding: 0.25rem 0.3rem 0.25rem 0.3rem; + max-width: 6rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + justify-self: flex-end; + margin-right: 4px; +` + +const WarningContainer = styled.div` + margin-left: 0.3em; +` + +const LabelContainer = styled.div` + display: flex; + align-items: center; + padding: 0 20px; +` + +function Balance({ balance }: { balance: CurrencyAmount }) { + const { formatNumberOrString } = useFormatter() + + return ( + + {formatNumberOrString({ + input: balance.toExact(), + type: NumberType.TokenNonTx, + })} + + ) +} + +const TagContainer = styled.div` + display: flex; + justify-content: flex-end; +` + +function TokenTags({ currency }: { currency: Currency }) { + if (!(currency instanceof TokenFromList)) { + return null + } + + const tags = currency.tags + if (!tags || tags.length === 0) return + + const tag = tags[0] + + return ( + + + {tag.name} + + {tags.length > 1 ? ( + `${name}: ${description}`) + .join('; \n')} + > + ... + + ) : null} + + ) +} + +const RowWrapper = styled(Row)` + height: 56px; +` + +export function CurrencyRow({ + currency, + onSelect, + isSelected, + otherSelected, + style, + showCurrencyAmount, + eventProperties, + balance, + disabled, + tooltip, +}: { + currency: Currency + onSelect: (hasWarning: boolean) => void + isSelected: boolean + otherSelected: boolean + style?: CSSProperties + showCurrencyAmount?: boolean + eventProperties: Record + balance?: CurrencyAmount + disabled?: boolean + tooltip?: string +}) { + const { account } = useWeb3React() + const key = currencyKey(currency) + const customAdded = useIsUserAddedToken(currency) + const warning = currency.isNative ? null : checkWarning(currency.address) + const isBlockedToken = !!warning && !warning.canProceed + const blockedTokenOpacity = '0.6' + const { data } = useCachedPortfolioBalancesQuery({ account }) + const portfolioBalanceUsd = data?.portfolios?.[0].tokensTotalDenominatedValue?.value + + const Wrapper = tooltip ? MouseoverTooltip : RowWrapper + + // only show add or remove buttons if not on selected list + return ( + + {tooltip}} + size={TooltipSize.ExtraSmall} + > + (e.key === 'Enter' ? onSelect(!!warning) : null)} + onClick={() => onSelect(!!warning)} + selected={otherSelected || isSelected} + dim={isBlockedToken} + disabled={disabled} + > + + + + + + {currency.name} + + + + + {currency.symbol} + + + + + + + {showCurrencyAmount && ( + + {account ? balance ? : : null} + + )} + + + + ) +} + +interface TokenRowProps { + data: Array + index: number + style: CSSProperties +} + +export const formatAnalyticsEventProperties = ( + token: Token, + index: number, + data: any[], + searchQuery: string, + isAddressSearch: string | false +) => ({ + token_symbol: token?.symbol, + token_address: token?.address, + is_suggested_token: false, + is_selected_from_list: true, + scroll_position: '', + token_list_index: index, + token_list_length: data.length, + ...(isAddressSearch === false + ? { search_token_symbol_input: searchQuery } + : { search_token_address_input: isAddressSearch }), +}) + +const LoadingRow = () => ( + +
+
+
+ +) + +/** + * This is used to display disabled currencies in the list. + */ +export class CurrencyListRow { + constructor( + public readonly currency: Currency | undefined, + public readonly disabled: boolean, + public readonly tooltip?: string + ) {} +} + +/** + * This is used to intersperse section titles into the list without needing to break up the data array + * and render multiple lists. + */ +export class CurrencyListSectionTitle extends CurrencyListRow { + constructor(public readonly label: string) { + super(undefined, false) + } +} + +export default function CurrencyList({ + height, + currencies, + selectedCurrency, + onCurrencySelect, + otherCurrency, + fixedListRef, + showCurrencyAmount, + isLoading, + searchQuery, + isAddressSearch, + balances, +}: { + height: number + currencies: Array + selectedCurrency?: Currency | null + onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void + otherCurrency?: Currency | null + fixedListRef?: MutableRefObject + showCurrencyAmount?: boolean + isLoading: boolean + searchQuery: string + isAddressSearch: string | false + balances: TokenBalances + disabled?: boolean +}) { + const Row = useCallback( + function TokenRow({ data, index, style }: TokenRowProps) { + const row: CurrencyListRow = data[index] + + if (row instanceof CurrencyListSectionTitle) { + return ( + + {row.label} + + ) + } + + if (!row.currency) { + return null + } + + const currency: Currency = row.currency + + const balance = + tryParseCurrencyAmount( + String(balances[currency.isNative ? 'ETH' : currency.address?.toLowerCase()]?.balance ?? 0), + currency + ) ?? CurrencyAmount.fromRawAmount(currency, 0) + + const isSelected = Boolean(currency && selectedCurrency && selectedCurrency.equals(currency)) + const otherSelected = Boolean(currency && otherCurrency && otherCurrency.equals(currency)) + const handleSelect = (hasWarning: boolean) => currency && onCurrencySelect(currency, hasWarning) + + const token = currency?.wrapped + + if (isLoading) { + return LoadingRow() + } else if (currency) { + return ( + + ) + } else { + return null + } + }, + [ + selectedCurrency, + otherCurrency, + isLoading, + onCurrencySelect, + showCurrencyAmount, + searchQuery, + isAddressSearch, + balances, + ] + ) + + const itemKey = useCallback((index: number, data: typeof currencies) => { + const currency = data[index] + return currencyKey(currency) + }, []) + + return ( +
+ {isLoading ? ( + + {LoadingRow} + + ) : ( + + {Row} + + )} +
+ ) +} diff --git a/apps/web/src/components/SearchModal/CurrencySearch.tsx b/apps/web/src/components/SearchModal/CurrencySearch.tsx new file mode 100644 index 0000000..1419b0b --- /dev/null +++ b/apps/web/src/components/SearchModal/CurrencySearch.tsx @@ -0,0 +1,257 @@ +import { t, Trans } from '@lingui/macro' +import { InterfaceEventName, InterfaceModalName } from '@uniswap/analytics-events' +import { Currency, CurrencyAmount } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { Trace } from 'analytics' +import { ChainSelector } from 'components/NavBar/ChainSelector' +import useDebounce from 'hooks/useDebounce' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import useToggle from 'hooks/useToggle' +import { useTokenBalances } from 'hooks/useTokenBalances' +import useNativeCurrency from 'lib/hooks/useNativeCurrency' +import tryParseCurrencyAmount from 'lib/utils/tryParseCurrencyAmount' +import { ChangeEvent, KeyboardEvent, RefObject, useCallback, useEffect, useRef, useState } from 'react' +import AutoSizer from 'react-virtualized-auto-sizer' +import { FixedSizeList } from 'react-window' +import { Text } from 'rebass' +import styled, { useTheme } from 'styled-components' +import { CloseIcon, ThemedText } from 'theme/components' + +import { useCurrencySearchResults } from 'components/SearchModal/useCurrencySearchResults' +import { isAddress } from 'utilities/src/addresses' +import Column from '../Column' +import Row, { RowBetween } from '../Row' +import CommonBases from './CommonBases' +import CurrencyList, { CurrencyRow, formatAnalyticsEventProperties } from './CurrencyList' +import { PaddedColumn, SearchInput, Separator } from './styled' + +const ContentWrapper = styled(Column)` + background-color: ${({ theme }) => theme.surface1}; + width: 100%; + overflow: hidden; + flex: 1 1; + position: relative; + border-radius: 20px; +` + +const ChainSelectorWrapper = styled.div` + background-color: ${({ theme }) => theme.surface2}; + border-radius: 12px; +` + +export interface CurrencySearchFilters { + showCommonBases?: boolean + disableNonToken?: boolean + onlyShowCurrenciesWithBalance?: boolean +} + +const DEFAULT_CURRENCY_SEARCH_FILTERS: CurrencySearchFilters = { + showCommonBases: true, + disableNonToken: false, + onlyShowCurrenciesWithBalance: false, +} + +interface CurrencySearchProps { + isOpen: boolean + onDismiss: () => void + selectedCurrency?: Currency | null + onCurrencySelect: (currency: Currency, hasWarning?: boolean) => void + otherSelectedCurrency?: Currency | null + showCurrencyAmount?: boolean + filters?: CurrencySearchFilters +} + +export function CurrencySearch({ + selectedCurrency, + onCurrencySelect, + otherSelectedCurrency, + showCurrencyAmount, + onDismiss, + isOpen, + filters, +}: CurrencySearchProps) { + const { showCommonBases } = { + ...DEFAULT_CURRENCY_SEARCH_FILTERS, + ...filters, + } + const { chainId } = useWeb3React() + const theme = useTheme() + + const [tokenLoaderTimerElapsed, setTokenLoaderTimerElapsed] = useState(false) + + // refs for fixed size lists + const fixedList = useRef() + + const [searchQuery, setSearchQuery] = useState('') + const debouncedQuery = useDebounce(searchQuery, 200) + const isAddressSearch = isAddress(debouncedQuery) + + const { searchCurrency, allCurrencyRows } = useCurrencySearchResults({ + searchQuery: debouncedQuery, + filters, + selectedCurrency, + otherSelectedCurrency, + }) + + const { balanceMap, loading: balancesAreLoading } = useTokenBalances() + + const isLoading = Boolean(balancesAreLoading && !tokenLoaderTimerElapsed) + + const native = useNativeCurrency(chainId) + + const handleCurrencySelect = useCallback( + (currency: Currency, hasWarning?: boolean) => { + onCurrencySelect(currency, hasWarning) + if (!hasWarning) onDismiss() + }, + [onDismiss, onCurrencySelect] + ) + + // clear the input on open + useEffect(() => { + if (isOpen) setSearchQuery('') + }, [isOpen]) + + // manage focus on modal show + const inputRef = useRef() + const handleInput = useCallback((event: ChangeEvent) => { + const input = event.target.value + const checksummedInput = isAddress(input) + setSearchQuery(checksummedInput || input) + fixedList.current?.scrollTo(0) + }, []) + + // Allows the user to select a currency by pressing Enter if it's the only currency in the list. + const handleEnter = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Enter') { + const currencyResults = allCurrencyRows.filter((currencyRow) => !!currencyRow.currency) + const s = debouncedQuery.toLowerCase().trim() + if (s === native?.symbol?.toLowerCase()) { + handleCurrencySelect(native) + } else if (currencyResults.length > 0) { + if ( + currencyResults[0]?.currency && + (currencyResults[0].currency.symbol?.toLowerCase() === debouncedQuery.trim().toLowerCase() || + currencyResults.length === 1) + ) { + handleCurrencySelect(currencyResults[0].currency) + } + } + } + }, + [allCurrencyRows, debouncedQuery, native, handleCurrencySelect] + ) + + // menu ui + const [open, toggle] = useToggle(false) + const node = useRef() + useOnClickOutside(node, open ? toggle : undefined) + + // Timeout token loader after 3 seconds to avoid hanging in a loading state. + useEffect(() => { + const tokenLoaderTimer = setTimeout(() => { + setTokenLoaderTimerElapsed(true) + }, 3000) + return () => clearTimeout(tokenLoaderTimer) + }, []) + + return ( + + + + + + Select a token + + + + + } + onChange={handleInput} + onKeyDown={handleEnter} + /> + + + + + {showCommonBases && ( + + )} + + + {searchCurrency ? ( + + searchCurrency && handleCurrencySelect(searchCurrency, hasWarning)} + otherSelected={Boolean( + searchCurrency && otherSelectedCurrency && otherSelectedCurrency.equals(searchCurrency) + )} + showCurrencyAmount={showCurrencyAmount} + eventProperties={formatAnalyticsEventProperties( + searchCurrency, + 0, + [searchCurrency], + searchQuery, + isAddressSearch + )} + balance={ + tryParseCurrencyAmount( + String( + balanceMap[searchCurrency.isNative ? 'ETH' : searchCurrency.address?.toLowerCase()]?.balance ?? 0 + ), + searchCurrency + ) ?? CurrencyAmount.fromRawAmount(searchCurrency, 0) + } + /> + + ) : allCurrencyRows.some((currencyRow) => !!currencyRow.currency) || isLoading ? ( +
+ + {({ height }: { height: number }) => ( + + )} + +
+ ) : ( + + + No results found. + + + )} +
+
+ ) +} diff --git a/apps/web/src/components/SearchModal/CurrencySearchModal.tsx b/apps/web/src/components/SearchModal/CurrencySearchModal.tsx new file mode 100644 index 0000000..88857c8 --- /dev/null +++ b/apps/web/src/components/SearchModal/CurrencySearchModal.tsx @@ -0,0 +1,106 @@ +import { Currency, Token } from '@uniswap/sdk-core' +import TokenSafety from 'components/TokenSafety' +import { memo, useCallback, useEffect, useState } from 'react' +import { useUserAddedTokens } from 'state/user/hooks' + +import useLast from '../../hooks/useLast' +import { useWindowSize } from '../../hooks/useWindowSize' +import Modal from '../Modal' +import { CurrencySearch, CurrencySearchFilters } from './CurrencySearch' + +interface CurrencySearchModalProps { + isOpen: boolean + onDismiss: () => void + selectedCurrency?: Currency | null + onCurrencySelect: (currency: Currency) => void + otherSelectedCurrency?: Currency | null + showCurrencyAmount?: boolean + currencySearchFilters?: CurrencySearchFilters +} + +enum CurrencyModalView { + search, + importToken, + tokenSafety, +} + +export default memo(function CurrencySearchModal({ + isOpen, + onDismiss, + onCurrencySelect, + selectedCurrency, + otherSelectedCurrency, + showCurrencyAmount = true, + currencySearchFilters, +}: CurrencySearchModalProps) { + const [modalView, setModalView] = useState(CurrencyModalView.search) + const lastOpen = useLast(isOpen) + const userAddedTokens = useUserAddedTokens() + + useEffect(() => { + if (isOpen && !lastOpen) { + setModalView(CurrencyModalView.search) + } + }, [isOpen, lastOpen]) + + const showTokenSafetySpeedbump = (token: Token) => { + setWarningToken(token) + setModalView(CurrencyModalView.tokenSafety) + } + + const handleCurrencySelect = useCallback( + (currency: Currency, hasWarning?: boolean) => { + if (hasWarning && currency.isToken && !userAddedTokens.find((token) => token.equals(currency))) { + showTokenSafetySpeedbump(currency) + } else { + onCurrencySelect(currency) + onDismiss() + } + }, + [onDismiss, onCurrencySelect, userAddedTokens] + ) + // used for token safety + const [warningToken, setWarningToken] = useState() + + const { height: windowHeight } = useWindowSize() + // change min height if not searching + let modalHeight: number | undefined = 80 + let content = null + switch (modalView) { + case CurrencyModalView.search: + if (windowHeight) { + // Converts pixel units to vh for Modal component + modalHeight = Math.min(Math.round((680 / windowHeight) * 100), 80) + } + content = ( + + ) + break + case CurrencyModalView.tokenSafety: + modalHeight = undefined + if (warningToken) { + content = ( + handleCurrencySelect(warningToken)} + onCancel={() => setModalView(CurrencyModalView.search)} + showCancel={true} + /> + ) + } + break + } + return ( + + {content} + + ) +}) diff --git a/apps/web/src/components/SearchModal/__snapshots__/CommonBases.test.tsx.snap b/apps/web/src/components/SearchModal/__snapshots__/CommonBases.test.tsx.snap new file mode 100644 index 0000000..0abaff3 --- /dev/null +++ b/apps/web/src/components/SearchModal/__snapshots__/CommonBases.test.tsx.snap @@ -0,0 +1,248 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`CommonBases renders without crashing 1`] = ` +.c0 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c1 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c2 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin: -4px; +} + +.c2 > * { + margin: 4px !important; +} + +.c5 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c6 { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.c4 { + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; +} + +.c3 { + border: 1px solid #22222212; + border-radius: 18px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 6px; + padding-top: 5px; + padding-bottom: 5px; + padding-right: 12px; + line-height: 0px; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; +} + +.c3:hover { + cursor: pointer; + background-color: #98A1C014; +} + +
+ + +
+
+
+
+ +
+
+
+ ETH +
+
+
+
+
+ +
+
+
+ DAI +
+
+
+
+
+ +
+
+
+ USDC +
+
+
+
+
+ +
+
+
+ USDT +
+
+
+
+
+ +
+
+
+ WBTC +
+
+
+
+
+ +
+
+
+ WETH +
+
+
+
+
+
+`; diff --git a/apps/web/src/components/SearchModal/styled.tsx b/apps/web/src/components/SearchModal/styled.tsx new file mode 100644 index 0000000..929d9a0 --- /dev/null +++ b/apps/web/src/components/SearchModal/styled.tsx @@ -0,0 +1,88 @@ +import searchIcon from 'assets/svg/search.svg' +import { LoadingRows as BaseLoadingRows } from 'components/Loader/styled' +import styled from 'styled-components' + +import { AutoColumn } from '../Column' +import { RowBetween } from '../Row' + +export const PaddedColumn = styled(AutoColumn)` + padding: 20px; +` + +export const MenuItem = styled(RowBetween)<{ dim?: boolean }>` + padding: 4px 20px; + height: 56px; + display: grid; + grid-template-columns: auto minmax(auto, 1fr) auto minmax(0, 72px); + grid-gap: 16px; + cursor: ${({ disabled }) => !disabled && 'pointer'}; + pointer-events: ${({ disabled }) => disabled && 'none'}; + :hover { + background-color: ${({ theme }) => theme.deprecated_hoverDefault}; + } + opacity: ${({ disabled, selected, dim }) => (dim || disabled || selected ? 0.4 : 1)}; +` + +export const SearchInput = styled.input` + background: no-repeat scroll 7px 7px; + background-image: url(${searchIcon}); + background-size: 20px 20px; + background-position: 12px center; + position: relative; + display: flex; + padding: 16px; + padding-left: 40px; + height: 40px; + align-items: center; + width: 100%; + white-space: nowrap; + background-color: ${({ theme }) => theme.surface2}; + border: none; + outline: none; + border-radius: 12px; + color: ${({ theme }) => theme.neutral1}; + border-style: solid; + border: 1px solid ${({ theme }) => theme.surface3}; + -webkit-appearance: none; + font-weight: 485; + + font-size: 16px; + + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + font-size: 16px; + } + transition: border 100ms; + :focus { + border: 1px solid ${({ theme }) => theme.surface3}; + background-color: ${({ theme }) => theme.surface2}; + outline: none; + } +` +export const Separator = styled.div` + width: 100%; + height: 1px; + background-color: ${({ theme }) => theme.surface3}; +` + +export const LoadingRows = styled(BaseLoadingRows)` + grid-column-gap: 0.5em; + grid-template-columns: repeat(12, 1fr); + max-width: 960px; + padding: 12px 20px; + + & > div:nth-child(4n + 1) { + grid-column: 1 / 8; + height: 1em; + margin-bottom: 0.25em; + } + & > div:nth-child(4n + 2) { + grid-column: 12; + height: 1em; + margin-top: 0.25em; + } + & > div:nth-child(4n + 3) { + grid-column: 1 / 4; + height: 0.75em; + } +` diff --git a/apps/web/src/components/SearchModal/useCurrencySearchResults.ts b/apps/web/src/components/SearchModal/useCurrencySearchResults.ts new file mode 100644 index 0000000..451e3d5 --- /dev/null +++ b/apps/web/src/components/SearchModal/useCurrencySearchResults.ts @@ -0,0 +1,135 @@ +import { t } from '@lingui/macro' +import { Currency, Token } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { CurrencyListRow, CurrencyListSectionTitle } from 'components/SearchModal/CurrencyList' +import { CurrencySearchFilters } from 'components/SearchModal/CurrencySearch' +import { useDefaultActiveTokens, useSearchInactiveTokenLists, useToken } from 'hooks/Tokens' +import { useTokenBalances } from 'hooks/useTokenBalances' +import { getTokenFilter } from 'lib/hooks/useTokenList/filtering' +import { getSortedPortfolioTokens, tokenQuerySortComparator } from 'lib/hooks/useTokenList/sorting' +import { useMemo } from 'react' +import { UserAddedToken } from 'types/tokens' + +interface CurrencySearchParams { + searchQuery?: string + filters?: CurrencySearchFilters + selectedCurrency?: Currency | null + otherSelectedCurrency?: Currency | null +} + +interface CurrencySearchResults { + searchCurrency?: Token | null + allCurrencyRows: CurrencyListRow[] +} + +export function useCurrencySearchResults({ + searchQuery, + filters, + selectedCurrency, + otherSelectedCurrency, +}: CurrencySearchParams): CurrencySearchResults { + const { chainId } = useWeb3React() + + // Queries for a single token directly by address, if the query is an address. + const searchToken = useToken(searchQuery) + + const defaultAndUserAddedTokens = useDefaultActiveTokens(chainId) + const { balanceMap, balanceList, loading: balancesAreLoading } = useTokenBalances() + + const { sortedCombinedTokens, portfolioTokens, sortedTokensWithoutPortfolio } = useMemo(() => { + const filteredListTokens = Object.values(defaultAndUserAddedTokens) + // Filter out tokens with balances so they aren't duplicated when we merge below. + .filter((token) => !(token.address?.toLowerCase() in balanceMap)) + + if (balancesAreLoading) { + const sortedCombinedTokens = searchQuery + ? filteredListTokens.filter(getTokenFilter(searchQuery)).sort(tokenQuerySortComparator(searchQuery)) + : filteredListTokens + return { + sortedCombinedTokens, + portfolioTokens: [], + sortedTokensWithoutPortfolio: sortedCombinedTokens, + } + } + + const portfolioTokens = getSortedPortfolioTokens(balanceList, balanceMap, chainId, { + hideSmallBalances: false, + hideSpam: true, + }) + const mergedTokens = [...(portfolioTokens ?? []), ...filteredListTokens] + + const tokenFilter = (token: Token) => { + if (filters?.onlyShowCurrenciesWithBalance) { + if (token.isNative && token.symbol) { + return balanceMap[token.symbol]?.usdValue > 0 + } + + return balanceMap[token.address?.toLowerCase()]?.usdValue > 0 + } + + if (token.isNative && filters?.disableNonToken) { + return false + } + + // If there is no query, filter out unselected user-added tokens with no balance. + if (!searchQuery && token instanceof UserAddedToken) { + if (selectedCurrency?.equals(token) || otherSelectedCurrency?.equals(token)) return true + return balanceMap[token.address.toLowerCase()]?.usdValue > 0 + } + + return true + } + + const sortedCombinedTokens = searchQuery + ? mergedTokens.filter(getTokenFilter(searchQuery)).sort(tokenQuerySortComparator(searchQuery)) + : mergedTokens + + return { + sortedCombinedTokens: sortedCombinedTokens.filter(tokenFilter), + sortedTokensWithoutPortfolio: filteredListTokens.filter(tokenFilter), + portfolioTokens: portfolioTokens.filter(tokenFilter), + } + }, [ + defaultAndUserAddedTokens, + balancesAreLoading, + balanceList, + balanceMap, + chainId, + searchQuery, + filters?.onlyShowCurrenciesWithBalance, + filters?.disableNonToken, + selectedCurrency, + otherSelectedCurrency, + ]) + + // if no results on main list, expand into inactive + const filteredInactiveTokens = useSearchInactiveTokenLists( + !filters?.onlyShowCurrenciesWithBalance && sortedCombinedTokens.length === 0 ? searchQuery : undefined + ) + + const finalCurrencyList: CurrencyListRow[] = useMemo(() => { + const currencyListRowMapper = (token: Token) => new CurrencyListRow(token, false) + if (searchQuery || portfolioTokens.length === 0) { + return [ + new CurrencyListSectionTitle(searchQuery ? t`Search results` : t`Popular tokens`), + ...sortedCombinedTokens.map(currencyListRowMapper), + ...filteredInactiveTokens.map(currencyListRowMapper), + ] + } else if (sortedTokensWithoutPortfolio.length === 0 && filteredInactiveTokens.length === 0) { + return [new CurrencyListSectionTitle(t`Your tokens`), ...portfolioTokens.map(currencyListRowMapper)] + } else { + return [ + new CurrencyListSectionTitle(t`Your tokens`), + ...portfolioTokens.map(currencyListRowMapper), + new CurrencyListSectionTitle(t`Popular tokens`), + ...sortedTokensWithoutPortfolio.map(currencyListRowMapper), + ...filteredInactiveTokens.map(currencyListRowMapper), + ] + } + }, [searchQuery, filteredInactiveTokens, portfolioTokens, sortedCombinedTokens, sortedTokensWithoutPortfolio]) + + return { + searchCurrency: searchToken, + allCurrencyRows: finalCurrencyList, + } +} diff --git a/apps/web/src/components/Settings/Input/index.tsx b/apps/web/src/components/Settings/Input/index.tsx new file mode 100644 index 0000000..eaaba7b --- /dev/null +++ b/apps/web/src/components/Settings/Input/index.tsx @@ -0,0 +1,47 @@ +import styled from 'styled-components' + +import Row from '../../Row' + +export const Input = styled.input` + width: 100%; + display: flex; + flex: 1; + font-size: 16px; + border: 0; + outline: none; + background: transparent; + text-align: right; + &::-webkit-outer-spin-button, + &::-webkit-inner-spin-button { + -webkit-appearance: none; + } + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } +` + +export const InputContainer = styled(Row)<{ error?: boolean }>` + padding: 8px 16px; + border-radius: 12px; + width: auto; + min-width: 100px; + flex: 1; + input { + color: ${({ theme, error }) => (error ? theme.critical : theme.neutral1)}; + } + border: 1px solid ${({ theme, error }) => (error ? theme.critical : theme.surface2)}; + ${({ theme, error }) => + error + ? ` + border: 1px solid ${theme.critical}; + :focus-within { + border-color: ${theme.deprecated_accentFailureSoft}; + } + ` + : ` + border: 1px solid ${theme.surface3}; + :focus-within { + border-color: ${theme.accent2}; + } + `} +` diff --git a/apps/web/src/components/Settings/MaxSlippageSettings/index.test.tsx b/apps/web/src/components/Settings/MaxSlippageSettings/index.test.tsx new file mode 100644 index 0000000..ef520fc --- /dev/null +++ b/apps/web/src/components/Settings/MaxSlippageSettings/index.test.tsx @@ -0,0 +1,104 @@ +import { Percent } from '@uniswap/sdk-core' +import store from 'state' +import { updateUserSlippageTolerance } from 'state/user/reducer' +import { SlippageTolerance } from 'state/user/types' +import { fireEvent, render, screen } from 'test-utils/render' + +import MaxSlippageSettings from '.' + +const AUTO_SLIPPAGE = new Percent(5, 10_000) + +const renderSlippageSettings = () => { + render() +} + +// Switch to custom mode by tapping on `Custom` label +const switchToCustomSlippage = () => { + fireEvent.click(screen.getByText('Custom')) +} + +const getSlippageInput = () => screen.queryByTestId('slippage-input') as HTMLInputElement + +describe('MaxSlippageSettings', () => { + describe('input', () => { + // Restore to default slippage before each unit test + beforeEach(() => { + store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: SlippageTolerance.Auto })) + }) + it('is not expanded by default', () => { + renderSlippageSettings() + expect(getSlippageInput()).not.toBeVisible() + }) + it('is expanded by default when custom slippage is set', () => { + store.dispatch(updateUserSlippageTolerance({ userSlippageTolerance: 10 })) + renderSlippageSettings() + expect(getSlippageInput()).toBeVisible() + }) + it('does not render auto slippage as a value, but a placeholder', () => { + renderSlippageSettings() + switchToCustomSlippage() + + expect(getSlippageInput().value).toBe('') + }) + it('renders custom slippage above the input', () => { + renderSlippageSettings() + switchToCustomSlippage() + + fireEvent.change(getSlippageInput(), { target: { value: '0.5' } }) + + expect(screen.queryAllByText('0.5%').length).toEqual(1) + }) + it('updates input value on blur with the slippage in store', () => { + renderSlippageSettings() + switchToCustomSlippage() + + const input = getSlippageInput() + fireEvent.change(input, { target: { value: '0.5' } }) + fireEvent.blur(input) + + expect(input.value).toBe('0.5') + }) + it('clears errors on blur and overwrites incorrect value with the latest correct value', () => { + renderSlippageSettings() + switchToCustomSlippage() + + const input = getSlippageInput() + fireEvent.change(input, { target: { value: '5' } }) + fireEvent.change(input, { target: { value: '50' } }) + fireEvent.change(input, { target: { value: '500' } }) + fireEvent.blur(input) + + expect(input.value).toBe('50') + }) + it('does not allow to enter more than 2 digits after the decimal point', () => { + renderSlippageSettings() + switchToCustomSlippage() + + const input = getSlippageInput() + fireEvent.change(input, { target: { value: '0.01' } }) + fireEvent.change(input, { target: { value: '0.011' } }) + + expect(input.value).toBe('0.01') + }) + it('does not accept non-numerical values', () => { + renderSlippageSettings() + switchToCustomSlippage() + + const input = getSlippageInput() + fireEvent.change(input, { target: { value: 'c' } }) + + expect(input.value).toBe('') + }) + it('does not set slippage when user enters `.` value', () => { + renderSlippageSettings() + switchToCustomSlippage() + + const input = getSlippageInput() + fireEvent.change(input, { target: { value: '.' } }) + expect(input.value).toBe('.') + + fireEvent.blur(input) + expect(input.value).toBe('') + }) + }) +}) diff --git a/apps/web/src/components/Settings/MaxSlippageSettings/index.tsx b/apps/web/src/components/Settings/MaxSlippageSettings/index.tsx new file mode 100644 index 0000000..127b88d --- /dev/null +++ b/apps/web/src/components/Settings/MaxSlippageSettings/index.tsx @@ -0,0 +1,189 @@ +import { Trans } from '@lingui/macro' +import { Percent } from '@uniswap/sdk-core' +import Expand from 'components/Expand' +import QuestionHelper from 'components/QuestionHelper' +import Row, { RowBetween } from 'components/Row' +import { useState } from 'react' +import { useUserSlippageTolerance } from 'state/user/hooks' +import { SlippageTolerance } from 'state/user/types' +import styled from 'styled-components' +import { CautionTriangle, ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' + +import { Input, InputContainer } from '../Input' + +enum SlippageError { + InvalidInput = 'InvalidInput', +} + +const Option = styled(Row)<{ isActive: boolean }>` + width: auto; + cursor: pointer; + padding: 6px 12px; + text-align: center; + gap: 4px; + border-radius: 12px; + background: ${({ isActive, theme }) => (isActive ? theme.surface3 : 'transparent')}; + pointer-events: ${({ isActive }) => isActive && 'none'}; +` + +const Switch = styled(Row)` + width: auto; + padding: 4px; + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 16px; +` + +const NUMBER_WITH_MAX_TWO_DECIMAL_PLACES = /^(?:\d*\.\d{0,2}|\d+)$/ +const MINIMUM_RECOMMENDED_SLIPPAGE = new Percent(5, 10_000) +const MAXIMUM_RECOMMENDED_SLIPPAGE = new Percent(1, 100) + +function useFormatPercentInput() { + const { formatPercent } = useFormatter() + + return (slippage: Percent) => formatPercent(slippage).slice(0, -1) // remove % sign +} + +export default function MaxSlippageSettings({ autoSlippage }: { autoSlippage: Percent }) { + const [userSlippageTolerance, setUserSlippageTolerance] = useUserSlippageTolerance() + const { formatPercent } = useFormatter() + const formatPercentInput = useFormatPercentInput() + + // In order to trigger `custom` mode, we need to set `userSlippageTolerance` to a value that is not `auto`. + // To do so, we use `autoSlippage` value. However, since users are likely to change that value, + // we render it as a placeholder instead of a value. + const defaultSlippageInputValue = + userSlippageTolerance !== SlippageTolerance.Auto && !userSlippageTolerance.equalTo(autoSlippage) + ? formatPercentInput(userSlippageTolerance) + : '' + + // If user has previously entered a custom slippage, we want to show that value in the input field + // instead of a placeholder. + const [slippageInput, setSlippageInput] = useState(defaultSlippageInputValue) + const [slippageError, setSlippageError] = useState(false) + + // If user has previously entered a custom slippage, we want to show the settings expanded by default. + const [isOpen, setIsOpen] = useState(defaultSlippageInputValue.length > 0) + + const parseSlippageInput = (value: string) => { + // Do not allow non-numerical characters in the input field or more than two decimals + if (value.length > 0 && !NUMBER_WITH_MAX_TWO_DECIMAL_PLACES.test(value)) { + return + } + + setSlippageInput(value) + setSlippageError(false) + + // If the input is empty, set the slippage to the default + if (value.length === 0) { + setUserSlippageTolerance(SlippageTolerance.Auto) + return + } + + if (value === '.') { + return + } + + // Parse user input and set the slippage if valid, error otherwise + try { + const parsed = Math.floor(Number.parseFloat(value) * 100) + if (parsed > 5000) { + setSlippageError(SlippageError.InvalidInput) + } else { + setUserSlippageTolerance(new Percent(parsed, 10_000)) + } + } catch (e) { + setSlippageError(SlippageError.InvalidInput) + } + } + + const tooLow = + userSlippageTolerance !== SlippageTolerance.Auto && userSlippageTolerance.lessThan(MINIMUM_RECOMMENDED_SLIPPAGE) + const tooHigh = + userSlippageTolerance !== SlippageTolerance.Auto && userSlippageTolerance.greaterThan(MAXIMUM_RECOMMENDED_SLIPPAGE) + + return ( + setIsOpen(!isOpen)} + header={ + + + Max. slippage + + Your transaction will revert if the price changes unfavorably by more than this percentage. + } + /> + + } + button={ + + {userSlippageTolerance === SlippageTolerance.Auto ? ( + Auto + ) : ( + formatPercent(userSlippageTolerance) + )} + + } + > + + + + + + + parseSlippageInput(e.target.value)} + onBlur={() => { + // When the input field is blurred, reset the input field to the default value + setSlippageInput(defaultSlippageInputValue) + setSlippageError(false) + }} + /> + % + + + {tooLow || tooHigh ? ( + + + + {tooLow ? ( + + Slippage below {formatPercent(MINIMUM_RECOMMENDED_SLIPPAGE)} may result in a failed transaction + + ) : ( + Your transaction may be frontrun and result in an unfavorable trade. + )} + + + ) : null} + + ) +} diff --git a/apps/web/src/components/Settings/MenuButton/index.test.tsx b/apps/web/src/components/Settings/MenuButton/index.test.tsx new file mode 100644 index 0000000..94027fa --- /dev/null +++ b/apps/web/src/components/Settings/MenuButton/index.test.tsx @@ -0,0 +1,42 @@ +import { Percent } from '@uniswap/sdk-core' +import { useUserSlippageTolerance } from 'state/user/hooks' +import { SlippageTolerance } from 'state/user/types' +import { mocked } from 'test-utils/mocked' +import { render, screen } from 'test-utils/render' +import { lightDeprecatedTheme } from 'theme/deprecatedColors' +import noop from 'utilities/src/react/noop' + +import MenuButton from '.' + +jest.mock('state/user/hooks') + +const renderButton = (compact = false) => { + render() +} + +describe('MenuButton', () => { + it('should render an icon when slippage is Auto', () => { + mocked(useUserSlippageTolerance).mockReturnValue([SlippageTolerance.Auto, noop]) + renderButton() + expect(screen.queryByText('slippage')).not.toBeInTheDocument() + }) + it('should render an icon with a custom slippage value', () => { + mocked(useUserSlippageTolerance).mockReturnValue([new Percent(5, 10_000), noop]) + renderButton() + expect(screen.queryByText('0.05% slippage')).toBeInTheDocument() + }) + it('should render without "slippage" with a custom slippage value and compact', () => { + mocked(useUserSlippageTolerance).mockReturnValue([new Percent(5, 10_000), noop]) + renderButton(/* compact= */ true) + expect(screen.queryByText('0.05%')).toBeInTheDocument() + expect(screen.queryByText('slippage')).not.toBeInTheDocument() + }) + it('should render an icon with a custom slippage and a warning when value is out of bounds', () => { + mocked(useUserSlippageTolerance).mockReturnValue([new Percent(1, 10_000), noop]) + renderButton() + expect(screen.getByTestId('settings-icon-with-slippage')).toHaveStyleRule( + 'background-color', + lightDeprecatedTheme.deprecated_accentWarningSoft + ) + }) +}) diff --git a/apps/web/src/components/Settings/MenuButton/index.tsx b/apps/web/src/components/Settings/MenuButton/index.tsx new file mode 100644 index 0000000..2739e53 --- /dev/null +++ b/apps/web/src/components/Settings/MenuButton/index.tsx @@ -0,0 +1,103 @@ +import { t, Trans } from '@lingui/macro' +import { Settings } from 'components/Icons/Settings' +import Row from 'components/Row' +import { InterfaceTrade } from 'state/routing/types' +import { isUniswapXTrade } from 'state/routing/utils' +import { useUserSlippageTolerance } from 'state/user/hooks' +import { SlippageTolerance } from 'state/user/types' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { useFormatter } from 'utils/formatNumbers' +import validateUserSlippageTolerance, { SlippageValidationResult } from 'utils/validateUserSlippageTolerance' + +const Icon = styled(Settings)` + height: 24px; + width: 24px; + > * { + fill: ${({ theme }) => theme.neutral2}; + } +` + +const Button = styled.button<{ isActive: boolean }>` + border: none; + background-color: transparent; + margin: 0; + padding: 0; + cursor: pointer; + outline: none; + + :not([disabled]):hover { + opacity: 0.7; + } + + ${({ isActive }) => isActive && `opacity: 0.7`} +` + +const IconContainer = styled(Row)` + padding: 6px 12px; + border-radius: 16px; +` + +const IconContainerWithSlippage = styled(IconContainer)<{ displayWarning?: boolean }>` + div { + color: ${({ theme, displayWarning }) => (displayWarning ? theme.deprecated_accentWarning : theme.neutral2)}; + } + + background-color: ${({ theme, displayWarning }) => + displayWarning ? theme.deprecated_accentWarningSoft : theme.surface2}; +` + +const ButtonContent = ({ trade, compact }: { trade?: InterfaceTrade; compact: boolean }) => { + const [userSlippageTolerance] = useUserSlippageTolerance() + const { formatPercent } = useFormatter() + + if (userSlippageTolerance === SlippageTolerance.Auto || isUniswapXTrade(trade)) { + return ( + + + + ) + } + + const isInvalidSlippage = validateUserSlippageTolerance(userSlippageTolerance) !== SlippageValidationResult.Valid + + return ( + + + {compact ? ( + formatPercent(userSlippageTolerance) + ) : ( + {formatPercent(userSlippageTolerance)} slippage + )} + + + + ) +} + +export default function MenuButton({ + disabled, + onClick, + isActive, + compact, + trade, +}: { + disabled: boolean + onClick: () => void + isActive: boolean + compact: boolean + trade?: InterfaceTrade +}) { + return ( + + ) +} diff --git a/apps/web/src/components/Settings/RouterPreferenceSettings/index.test.tsx b/apps/web/src/components/Settings/RouterPreferenceSettings/index.test.tsx new file mode 100644 index 0000000..44a0875 --- /dev/null +++ b/apps/web/src/components/Settings/RouterPreferenceSettings/index.test.tsx @@ -0,0 +1,27 @@ +import store from 'state' +import { RouterPreference } from 'state/routing/types' +import { updateUserRouterPreference } from 'state/user/reducer' +import { fireEvent, render, screen } from 'test-utils/render' + +import RouterPreferenceSettings from '.' + +describe('RouterPreferenceSettings', () => { + // Restore to default router preference before each unit test + beforeEach(() => { + store.dispatch(updateUserRouterPreference({ userRouterPreference: RouterPreference.API })) + }) + it('toggles `Uniswap X` router preference', () => { + render() + + const uniswapXToggle = screen.getByTestId('toggle-uniswap-x-button') + + fireEvent.click(uniswapXToggle) + expect(uniswapXToggle).toHaveAttribute('aria-selected', 'true') + expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.X) + + fireEvent.click(uniswapXToggle) + + expect(uniswapXToggle).toHaveAttribute('aria-selected', 'false') + expect(store.getState().user.userRouterPreference).toEqual(RouterPreference.API) + }) +}) diff --git a/apps/web/src/components/Settings/RouterPreferenceSettings/index.tsx b/apps/web/src/components/Settings/RouterPreferenceSettings/index.tsx new file mode 100644 index 0000000..cdac951 --- /dev/null +++ b/apps/web/src/components/Settings/RouterPreferenceSettings/index.tsx @@ -0,0 +1,47 @@ +import { Trans } from '@lingui/macro' +import Column from 'components/Column' +import UniswapXBrandMark from 'components/Logo/UniswapXBrandMark' +import { RowBetween, RowFixed } from 'components/Row' +import Toggle from 'components/Toggle' +import { RouterPreference } from 'state/routing/types' +import { useRouterPreference } from 'state/user/hooks' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' + +const InlineLink = styled(ThemedText.BodySmall)` + color: ${({ theme }) => theme.accent1}; + display: inline; + cursor: pointer; + &:hover { + opacity: 0.8; + } +` + +export default function RouterPreferenceSettings() { + const [routerPreference, setRouterPreference] = useRouterPreference() + + return ( + + + + + + + + When available, aggregates liquidity sources for better prices and gas free swaps.{' '} + + Learn more + + + + + { + setRouterPreference(routerPreference === RouterPreference.X ? RouterPreference.API : RouterPreference.X) + }} + /> + + ) +} diff --git a/apps/web/src/components/Settings/TransactionDeadlineSettings/index.test.tsx b/apps/web/src/components/Settings/TransactionDeadlineSettings/index.test.tsx new file mode 100644 index 0000000..433a155 --- /dev/null +++ b/apps/web/src/components/Settings/TransactionDeadlineSettings/index.test.tsx @@ -0,0 +1,73 @@ +import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc' +import store from 'state' +import { updateUserDeadline } from 'state/user/reducer' +import { fireEvent, render, screen } from 'test-utils/render' + +import TransactionDeadlineSettings from '.' + +const renderTransactionDeadlineSettings = () => { + render() +} + +const getDeadlineInput = () => screen.queryByTestId('deadline-input') as HTMLInputElement + +describe('TransactionDeadlineSettings', () => { + describe('input', () => { + // Restore to default transaction deadline before each unit test + beforeEach(() => { + store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW })) + }) + it('is not expanded by default', () => { + renderTransactionDeadlineSettings() + expect(getDeadlineInput()).not.toBeVisible() + }) + it('is expanded by default when custom deadline is set', () => { + store.dispatch(updateUserDeadline({ userDeadline: DEFAULT_DEADLINE_FROM_NOW * 2 })) + renderTransactionDeadlineSettings() + expect(getDeadlineInput()).toBeVisible() + }) + it('does not render default deadline as a value, but a placeholder', () => { + renderTransactionDeadlineSettings() + expect(getDeadlineInput().value).toBe('') + }) + it('renders custom deadline above the input', () => { + renderTransactionDeadlineSettings() + + fireEvent.change(getDeadlineInput(), { target: { value: '50' } }) + + expect(screen.queryAllByText('50m').length).toEqual(1) + }) + it('marks deadline as invalid if it is greater than 4320m (3 days) or 0m', () => { + renderTransactionDeadlineSettings() + + const input = getDeadlineInput() + fireEvent.change(input, { target: { value: '4321' } }) + fireEvent.change(input, { target: { value: '0' } }) + fireEvent.blur(input) + + expect(input.value).toBe('') + }) + it('clears errors on blur and overwrites incorrect value with the latest correct value', () => { + renderTransactionDeadlineSettings() + + const input = getDeadlineInput() + fireEvent.change(input, { target: { value: '5' } }) + fireEvent.change(input, { target: { value: '4321' } }) + + // Label renders latest correct value, at this point input is higlighted as invalid + expect(screen.queryAllByText('5m').length).toEqual(1) + + fireEvent.blur(input) + + expect(input.value).toBe('5') + }) + it('does not accept non-numerical values', () => { + renderTransactionDeadlineSettings() + + const input = getDeadlineInput() + fireEvent.change(input, { target: { value: 'c' } }) + + expect(input.value).toBe('') + }) + }) +}) diff --git a/apps/web/src/components/Settings/TransactionDeadlineSettings/index.tsx b/apps/web/src/components/Settings/TransactionDeadlineSettings/index.tsx new file mode 100644 index 0000000..317e57d --- /dev/null +++ b/apps/web/src/components/Settings/TransactionDeadlineSettings/index.tsx @@ -0,0 +1,97 @@ +import { Trans } from '@lingui/macro' +import Expand from 'components/Expand' +import QuestionHelper from 'components/QuestionHelper' +import Row from 'components/Row' +import { Input, InputContainer } from 'components/Settings/Input' +import { DEFAULT_DEADLINE_FROM_NOW } from 'constants/misc' +import ms from 'ms' +import { useState } from 'react' +import { useUserTransactionTTL } from 'state/user/hooks' +import { ThemedText } from 'theme/components' + +enum DeadlineError { + InvalidInput = 'InvalidInput', +} + +const THREE_DAYS_IN_SECONDS = ms(`3d`) / 1000 +const NUMBERS_ONLY = /^[0-9\b]+$/ + +export default function TransactionDeadlineSettings() { + const [deadline, setDeadline] = useUserTransactionTTL() + + const defaultInputValue = deadline && deadline !== DEFAULT_DEADLINE_FROM_NOW ? (deadline / 60).toString() : '' + + // If user has previously entered a custom deadline, we want to show that value in the input field + // instead of a placeholder by defualt + const [deadlineInput, setDeadlineInput] = useState(defaultInputValue) + const [deadlineError, setDeadlineError] = useState(false) + + // If user has previously entered a custom deadline, we want to show the settings expanded by default. + const [isOpen, setIsOpen] = useState(defaultInputValue.length > 0) + + function parseCustomDeadline(value: string) { + // Do not allow non-numerical characters in the input field + if (value.length > 0 && !NUMBERS_ONLY.test(value)) { + return + } + + setDeadlineInput(value) + setDeadlineError(false) + + // If the input is empty, set the deadline to the default + if (value.length === 0) { + setDeadline(DEFAULT_DEADLINE_FROM_NOW) + return + } + + // Parse user input and set the deadline if valid, error otherwise + try { + const parsed: number = Number.parseInt(value) * 60 + if (parsed === 0 || parsed > THREE_DAYS_IN_SECONDS) { + setDeadlineError(DeadlineError.InvalidInput) + } else { + setDeadline(parsed) + } + } catch (error) { + setDeadlineError(DeadlineError.InvalidInput) + } + } + + return ( + setIsOpen(!isOpen)} + testId="transaction-deadline-settings" + header={ + + + Transaction deadline + + Your transaction will revert if it is pending for more than this period of time.} + /> + + } + button={{deadline / 60}m} + > + + + parseCustomDeadline(e.target.value)} + onBlur={() => { + // When the input field is blurred, reset the input field to the current deadline + setDeadlineInput(defaultInputValue) + setDeadlineError(false) + }} + /> + + minutes + + + + + ) +} diff --git a/apps/web/src/components/Settings/index.test.tsx b/apps/web/src/components/Settings/index.test.tsx new file mode 100644 index 0000000..587384b --- /dev/null +++ b/apps/web/src/components/Settings/index.test.tsx @@ -0,0 +1,52 @@ +import { Percent } from '@uniswap/sdk-core' +import { isSupportedChain, isUniswapXSupportedChain } from 'constants/chains' +import { mocked } from 'test-utils/mocked' +import { fireEvent, render, screen, waitFor } from 'test-utils/render' + +import SettingsTab from './index' + +const slippage = new Percent(75, 10_000) +jest.mock('constants/chains') + +describe('Settings Tab', () => { + describe('showRoutingSettings', () => { + beforeEach(() => { + mocked(isSupportedChain).mockReturnValue(true) + }) + + it('renders routing settings when hideRoutingSettings is false', async () => { + mocked(isUniswapXSupportedChain).mockReturnValue(true) + render() + + const settingsButton = screen.getByTestId('open-settings-dialog-button') + fireEvent.click(settingsButton) + + await waitFor(() => { + expect(screen.getByTestId('toggle-uniswap-x-button')).toBeInTheDocument() + }) + }) + + it('does not render routing settings when hideRoutingSettings is true', async () => { + render() + + const settingsButton = screen.getByTestId('open-settings-dialog-button') + fireEvent.click(settingsButton) + + await waitFor(() => { + expect(screen.queryByTestId('toggle-uniswap-x-button')).not.toBeInTheDocument() + }) + }) + + it('does not render routing settings when uniswapx is not enabled', async () => { + mocked(isUniswapXSupportedChain).mockReturnValue(false) + render() + + const settingsButton = screen.getByTestId('open-settings-dialog-button') + fireEvent.click(settingsButton) + + await waitFor(() => { + expect(screen.queryByTestId('toggle-uniswap-x-button')).not.toBeInTheDocument() + }) + }) + }) +}) diff --git a/apps/web/src/components/Settings/index.tsx b/apps/web/src/components/Settings/index.tsx new file mode 100644 index 0000000..d9d3d79 --- /dev/null +++ b/apps/web/src/components/Settings/index.tsx @@ -0,0 +1,190 @@ +import { Trans } from '@lingui/macro' +import { Percent } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { Scrim } from 'components/AccountDrawer' +import AnimatedDropdown from 'components/AnimatedDropdown' +import Column, { AutoColumn } from 'components/Column' +import Row from 'components/Row' +import { isSupportedChain, isUniswapXSupportedChain, L2_CHAIN_IDS } from 'constants/chains' +import useDisableScrolling from 'hooks/useDisableScrolling' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { Portal } from 'nft/components/common/Portal' +import { useIsMobile } from 'nft/hooks' +import { useCallback, useMemo, useRef } from 'react' +import { X } from 'react-feather' +import { useCloseModal, useModalIsOpen, useToggleSettingsMenu } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import { InterfaceTrade } from 'state/routing/types' +import { isUniswapXTrade } from 'state/routing/utils' +import styled from 'styled-components' +import { Divider, ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' + +import MaxSlippageSettings from './MaxSlippageSettings' +import MenuButton from './MenuButton' +import RouterPreferenceSettings from './RouterPreferenceSettings' +import TransactionDeadlineSettings from './TransactionDeadlineSettings' + +const CloseButton = styled.button` + background: transparent; + border: none; + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + height: 24px; + padding: 0; + width: 24px; +` + +const Menu = styled.div` + position: relative; +` + +const MenuFlyout = styled(AutoColumn)` + min-width: 20.125rem; + background-color: ${({ theme }) => theme.surface1}; + border: 1px solid ${({ theme }) => theme.surface3}; + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.01), 0px 4px 8px rgba(0, 0, 0, 0.04), 0px 16px 24px rgba(0, 0, 0, 0.04), + 0px 24px 32px rgba(0, 0, 0, 0.01); + border-radius: 12px; + position: absolute; + top: 100%; + margin-top: 10px; + right: 0; + z-index: 100; + color: ${({ theme }) => theme.neutral1}; + ${({ theme }) => theme.deprecated_mediaWidth.deprecated_upToMedium` + min-width: 18.125rem; + `}; + user-select: none; + padding: 16px; +` + +const ExpandColumn = styled(AutoColumn)<{ $padTop: boolean }>` + gap: 16px; + padding-top: ${({ $padTop }) => ($padTop ? '16px' : '0')}; +` + +const MobileMenuContainer = styled(Row)` + overflow: visible; + position: fixed; + height: 100%; + top: 100vh; + left: 0; + right: 0; + width: 100%; + z-index: ${Z_INDEX.fixed}; +` + +const MobileMenuWrapper = styled(Column)<{ $open: boolean }>` + height: min-content; + width: 100%; + padding: 8px 16px 24px; + background-color: ${({ theme }) => theme.surface1}; + overflow: hidden; + position: absolute; + bottom: ${({ $open }) => ($open ? `100vh` : 0)}; + transition: bottom ${({ theme }) => theme.transition.duration.medium}; + border: ${({ theme }) => `1px solid ${theme.surface3}`}; + border-radius: 12px; + border-bottom-right-radius: 0px; + border-bottom-left-radius: 0px; + font-size: 16px; + box-shadow: unset; + z-index: ${Z_INDEX.modal}; +` + +const MobileMenuHeader = styled(Row)` + margin-bottom: 16px; +` + +export default function SettingsTab({ + autoSlippage, + chainId, + trade, + compact = false, + hideRoutingSettings = false, +}: { + autoSlippage: Percent + chainId?: number + trade?: InterfaceTrade + compact?: boolean + hideRoutingSettings?: boolean +}) { + const { chainId: connectedChainId } = useWeb3React() + const showDeadlineSettings = Boolean(chainId && !L2_CHAIN_IDS.includes(chainId)) + const node = useRef(null) + const isOpen = useModalIsOpen(ApplicationModal.SETTINGS) + + const closeModal = useCloseModal() + const closeMenu = useCallback(() => closeModal(ApplicationModal.SETTINGS), [closeModal]) + const toggleMenu = useToggleSettingsMenu() + + const isMobile = useIsMobile() + const isOpenMobile = isOpen && isMobile + const isOpenDesktop = isOpen && !isMobile + + useOnClickOutside(node, isOpenDesktop ? closeMenu : undefined) + useDisableScrolling(isOpen) + + const uniswapXEnabled = chainId && isUniswapXSupportedChain(chainId) + const showRoutingSettings = Boolean(uniswapXEnabled && !hideRoutingSettings) + + const isChainSupported = isSupportedChain(chainId) + const Settings = useMemo( + () => ( + <> + {showRoutingSettings && ( + + + + )} + + + {showRoutingSettings && } + + {showDeadlineSettings && ( + <> + + + + )} + + + + ), + [autoSlippage, showDeadlineSettings, showRoutingSettings, trade] + ) + + return ( + + + {isOpenDesktop && {Settings}} + {isOpenMobile && ( + + + + + + + + + + + Settings + + + + {Settings} + + + + )} + + ) +} diff --git a/apps/web/src/components/Slider/index.tsx b/apps/web/src/components/Slider/index.tsx new file mode 100644 index 0000000..60333ba --- /dev/null +++ b/apps/web/src/components/Slider/index.tsx @@ -0,0 +1,129 @@ +import { ChangeEvent, useCallback } from 'react' +import styled from 'styled-components' + +const StyledRangeInput = styled.input<{ size: number }>` + -webkit-appearance: none; /* Hides the slider so that custom slider can be made */ + width: 100%; /* Specific width is required for Firefox. */ + background: transparent; /* Otherwise white in Chrome */ + cursor: pointer; + + &:focus { + outline: none; + } + + &::-moz-focus-outer { + border: 0; + } + + &::-webkit-slider-thumb { + -webkit-appearance: none; + height: ${({ size }) => size}px; + width: ${({ size }) => size}px; + background-color: ${({ theme }) => theme.accent1}; + border-radius: 100%; + border: none; + transform: translateY(-50%); + color: ${({ theme }) => theme.surface1}; + + &:hover, + &:focus { + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), + 0px 24px 32px rgba(0, 0, 0, 0.04); + } + } + + &::-moz-range-thumb { + height: ${({ size }) => size}px; + width: ${({ size }) => size}px; + background-color: #565a69; + border-radius: 100%; + border: none; + color: ${({ theme }) => theme.surface1}; + + &:hover, + &:focus { + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), + 0px 24px 32px rgba(0, 0, 0, 0.04); + } + } + + &::-ms-thumb { + height: ${({ size }) => size}px; + width: ${({ size }) => size}px; + background-color: #565a69; + border-radius: 100%; + color: ${({ theme }) => theme.surface1}; + + &:hover, + &:focus { + box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.1), 0px 4px 8px rgba(0, 0, 0, 0.08), 0px 16px 24px rgba(0, 0, 0, 0.06), + 0px 24px 32px rgba(0, 0, 0, 0.04); + } + } + + &::-webkit-slider-runnable-track { + background: linear-gradient(90deg, ${({ theme }) => theme.accent1}, ${({ theme }) => theme.accent1}); + height: 2px; + } + + &::-moz-range-track { + background: linear-gradient(90deg, ${({ theme }) => theme.surface4}, ${({ theme }) => theme.surface2}); + height: 2px; + } + + &::-ms-track { + width: 100%; + border-color: transparent; + color: transparent; + + background: ${({ theme }) => theme.surface4}; + height: 2px; + } + &::-ms-fill-lower { + background: ${({ theme }) => theme.surface4}; + } + &::-ms-fill-upper { + background: ${({ theme }) => theme.surface2}; + } +` + +interface InputSliderProps { + value: number + onChange: (value: number) => void + step?: number + min?: number + max?: number + size?: number +} + +export default function Slider({ + value, + onChange, + min = 0, + step = 1, + max = 100, + size = 28, + ...rest +}: InputSliderProps) { + const changeCallback = useCallback( + (e: ChangeEvent) => { + onChange(parseInt(e.target.value)) + }, + [onChange] + ) + + return ( + + ) +} diff --git a/apps/web/src/components/Suspense/SuspendConditionally.tsx b/apps/web/src/components/Suspense/SuspendConditionally.tsx new file mode 100644 index 0000000..5989584 --- /dev/null +++ b/apps/web/src/components/Suspense/SuspendConditionally.tsx @@ -0,0 +1,20 @@ +import React, { useState } from 'react' + +export const SuspendConditionally = (props: { if: boolean; children: React.ReactNode }) => { + useSuspendIf(props.if) + return <>{props.children} +} + +function useSuspendIf(shouldSuspend = false) { + const [resolve, setResolve] = useState<((val?: unknown) => void) | undefined>() + + if (!resolve && shouldSuspend) { + const promise = new Promise((res) => { + setResolve(res) + }) + throw promise + } else if (resolve && !shouldSuspend) { + resolve() + setResolve(undefined) + } +} diff --git a/apps/web/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx b/apps/web/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx new file mode 100644 index 0000000..97964ba --- /dev/null +++ b/apps/web/src/components/Suspense/SuspenseWithPreviousRenderAsFallback.tsx @@ -0,0 +1,14 @@ +import React, { Suspense, useDeferredValue } from 'react' + +/** + * This is useful for keeping the "last rendered" components on-screen while any suspense + * is triggered below this component. + * + * It stores a reference to the current children, and then returns them as the fallback. + */ + +export const SuspenseWithPreviousRenderAsFallback = (props: { children: React.ReactNode }) => { + const previousChildren = useDeferredValue(props.children) + + return {props.children} +} diff --git a/apps/web/src/components/SwitchLocaleLink/index.tsx b/apps/web/src/components/SwitchLocaleLink/index.tsx new file mode 100644 index 0000000..a8a8e5a --- /dev/null +++ b/apps/web/src/components/SwitchLocaleLink/index.tsx @@ -0,0 +1,49 @@ +import { Trans } from '@lingui/macro' +import { useLocationLinkProps } from 'hooks/useLocationLinkProps' +import { useMemo } from 'react' +import styled from 'styled-components' +import { StyledInternalLink, ThemedText } from 'theme/components' + +import { DEFAULT_LOCALE, LOCALE_LABEL, SupportedLocale } from '../../constants/locales' +import { navigatorLocale, useActiveLocale } from '../../hooks/useActiveLocale' + +const Container = styled(ThemedText.DeprecatedSmall)` + opacity: ${({ theme }) => theme.opacity.hover}; + :hover { + opacity: 1; + } + margin-top: 1rem !important; +` + +const useTargetLocale = (activeLocale: SupportedLocale) => { + const browserLocale = useMemo(() => navigatorLocale(), []) + + if (browserLocale && (browserLocale !== DEFAULT_LOCALE || activeLocale !== DEFAULT_LOCALE)) { + if (activeLocale === browserLocale) { + return DEFAULT_LOCALE + } else { + return browserLocale + } + } + return null +} + +export function SwitchLocaleLink() { + const activeLocale = useActiveLocale() + const targetLocale = useTargetLocale(activeLocale) + + const { to, onClick } = useLocationLinkProps(targetLocale) + + if (!targetLocale || !to) return null + + return ( + + + Uniswap available in:{' '} + + {LOCALE_LABEL[targetLocale]} + + + + ) +} diff --git a/apps/web/src/components/Table/Cell.test.tsx b/apps/web/src/components/Table/Cell.test.tsx new file mode 100644 index 0000000..75d82ca --- /dev/null +++ b/apps/web/src/components/Table/Cell.test.tsx @@ -0,0 +1,17 @@ +import { render, screen } from '@testing-library/react' + +import { Cell } from './Cell' + +describe('Table Cell', () => { + it('shows loading bubble', () => { + render(TestData) + const testDataElements = screen.queryAllByText('TestData') + expect(testDataElements.length).toBe(0) + }) + + it('shows data', () => { + render(TestData) + const testDataElements = screen.queryAllByText('TestData') + expect(testDataElements.length).toBeGreaterThan(0) + }) +}) diff --git a/apps/web/src/components/Table/Cell.tsx b/apps/web/src/components/Table/Cell.tsx new file mode 100644 index 0000000..432adcd --- /dev/null +++ b/apps/web/src/components/Table/Cell.tsx @@ -0,0 +1,57 @@ +import { LoadingBubble } from 'components/Tokens/loading' +import { PropsWithChildren } from 'react' +import styled from 'styled-components' + +const Container = styled.div<{ + $width?: number + $minWidth?: number + $maxWidth?: number + $justifyContent?: string + $grow?: boolean +}>` + ${({ $width }) => $width && `width: ${$width}px`}; + ${({ $minWidth }) => $minWidth && `min-width: ${$minWidth}px`}; + ${({ $maxWidth }) => $maxWidth && `max-width: ${$maxWidth}px`}; + flex: ${({ $grow }) => ($grow ? '1' : '0')}; + display: flex; + justify-content: ${({ $justifyContent }) => $justifyContent ?? 'flex-end'}; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +` +const LoadingDataBubble = styled(LoadingBubble)` + width: 75%; + height: 16px; +` +export function Cell({ + loading, + width, + minWidth, + maxWidth, + justifyContent, + grow, + children, + testId, +}: PropsWithChildren<{ + loading?: boolean + width?: number + minWidth?: number + maxWidth?: number + grow?: boolean + justifyContent?: string + testId?: string +}>) { + return ( + + {loading ? : children} + + ) +} diff --git a/apps/web/src/components/Table/ErrorBox.tsx b/apps/web/src/components/Table/ErrorBox.tsx new file mode 100644 index 0000000..06747ed --- /dev/null +++ b/apps/web/src/components/Table/ErrorBox.tsx @@ -0,0 +1,38 @@ +import Column from 'components/Column' +import { MissingDataIcon } from 'components/Table/icons' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const ErrorModalContainer = styled.div` + display: flex; + align-items: flex-start; + justify-content: flex-start; + + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + + width: 320px; + padding: 12px; + gap: 12px; + + background-color: ${({ theme }) => theme.surface5}; + backdrop-filter: blur(24px); + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + + border: 1px ${({ theme }) => theme.surface3} solid; + border-radius: 20px; +` + +export const ErrorModal = ({ header, subtitle }: { header: React.ReactNode; subtitle: React.ReactNode }) => ( + +
+ +
+ + {header} + {subtitle} + +
+) diff --git a/apps/web/src/components/Table/Filter.tsx b/apps/web/src/components/Table/Filter.tsx new file mode 100644 index 0000000..292abe5 --- /dev/null +++ b/apps/web/src/components/Table/Filter.tsx @@ -0,0 +1,110 @@ +import Column from 'components/Column' +import Row from 'components/Row' +import { DropdownIcon } from 'components/Table/icons' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { useScreenSize } from 'hooks/useScreenSize' +import { Portal } from 'nft/components/common/Portal' +import { Checkbox } from 'nft/components/layout/Checkbox' +import { Fragment, useCallback, useRef, useState } from 'react' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' + +const StyledDropdownIcon = styled(DropdownIcon)` + position: relative; +` +const FilterDropdown = styled(Column)<{ isSticky?: boolean }>` + position: absolute; + top: ${({ isSticky }) => (isSticky ? 64 : 42)}px; + padding: 8px; + border-radius: 12px; + background: ${({ theme }) => theme.surface2}; + gap: 8px; + width: 240px; + border-radius: 12px; + border: ${({ theme }) => `1px solid ${theme.surface3}`}; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + opacity: 1 !important; + z-index: ${Z_INDEX.modal}; + + @media only screen and (max-width: ${({ theme }) => `${theme.breakpoint.sm}px`}) { + position: fixed; + bottom: 0; + left: 0; + top: unset; + width: 100vw; + } +` + +const FilterRow = styled(Row)` + padding: 10px 8px; + justify-content: space-between; + border-radius: 8px; + &:hover { + background: ${({ theme }) => theme.surface3}; + } +` + +interface FilterProps { + allFilters: T[] + activeFilter: T[] + setFilters: (filter: T[]) => void + isOpen: boolean + toggleFilterModal: () => void + isSticky?: boolean +} + +export function Filter({ + allFilters, + activeFilter, + setFilters, + isOpen, + toggleFilterModal, + isSticky, +}: FilterProps) { + const [hoveredRow, setHoveredRow] = useState(-1) + const isScreenSize = useScreenSize() + const isMobile = !isScreenSize['sm'] + const filterModalRef = useRef(null) + useOnClickOutside(filterModalRef, isOpen ? toggleFilterModal : undefined) + + const handleFilterOptionClick = useCallback( + (filter: T) => { + if (activeFilter.includes(filter)) { + setFilters(activeFilter.filter((f) => f !== filter)) + } else { + setFilters([...activeFilter, filter]) + } + }, + [activeFilter, setFilters] + ) + // Need to put the modal in a Portal when on mobile to show over promo banner + const Wrapper = isMobile ? Portal : Fragment + + return ( + <> + + {isOpen && ( + + + {allFilters.map((filter, index) => ( + { + e.stopPropagation() + e.preventDefault() + handleFilterOptionClick(filter) + }} + onMouseEnter={() => setHoveredRow(index)} + onMouseLeave={() => setHoveredRow(-1)} + > + {filter} + + + ))} + + + )} + + ) +} diff --git a/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap b/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap new file mode 100644 index 0000000..d5e09c3 --- /dev/null +++ b/apps/web/src/components/Table/__snapshots__/styled.test.tsx.snap @@ -0,0 +1,396 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenLinkCell renders known token on a different chain 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; + max-width: 68px; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c6 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c4 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c5 { + position: absolute; + border-radius: 4px; + left: 65%; + top: 65%; + outline: 1.5px solid #FFFFFF; + width: 40%; + height: 40%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; +} + +.c0 { + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #222222; +} + +.c0:hover { + opacity: 0.6; +} + +.c0:active { + opacity: 0.4; +} + +.c7 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + + + +
+
+ +
+ + + Polygon logo + + + + + polygon.svg + + +
+
+
+ USDC +
+
+
+
+
+
+`; + +exports[`TokenLinkCell renders known token on mainnet 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; + max-width: 68px; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c5 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c4 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c0 { + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #222222; +} + +.c0:hover { + opacity: 0.6; +} + +.c0:active { + opacity: 0.4; +} + +.c6 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + + + +
+
+ +
+
+ USDC +
+
+
+
+
+
+`; + +exports[`TokenLinkCell renders unknown token 1`] = ` + + .c1 { + box-sizing: border-box; + margin: 0; + min-width: 0; + max-width: 68px; +} + +.c2 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 4px; +} + +.c5 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + position: relative; + top: 0; + left: 0; +} + +.c4 { + width: 16px; + height: 16px; + border-radius: 50%; +} + +.c0 { + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #222222; +} + +.c0:hover { + opacity: 0.6; +} + +.c0:active { + opacity: 0.4; +} + +.c6 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + + + + +
+
+ +
+
+ UNKNOWN +
+
+
+
+
+
+`; diff --git a/apps/web/src/components/Table/icons.tsx b/apps/web/src/components/Table/icons.tsx new file mode 100644 index 0000000..7b0d053 --- /dev/null +++ b/apps/web/src/components/Table/icons.tsx @@ -0,0 +1,44 @@ +type SVGProps = React.SVGProps & { + fill?: string + height?: string | number + width?: string | number + gradientId?: string +} + +export const DropdownIcon = (props: SVGProps) => ( + + + + +) + +export const MissingDataBars = ({ color }: { color: string }) => ( + + + + +) + +export const MissingDataIcon = (props: SVGProps) => ( + + + + + +) diff --git a/apps/web/src/components/Table/index.tsx b/apps/web/src/components/Table/index.tsx new file mode 100644 index 0000000..e16ebfe --- /dev/null +++ b/apps/web/src/components/Table/index.tsx @@ -0,0 +1,240 @@ +import { ApolloError } from '@apollo/client' +import { Trans } from '@lingui/macro' +import { + CellContext, + ColumnDef, + RowData, + Table as TanstackTable, + flexRender, + getCoreRowModel, + useReactTable, +} from '@tanstack/react-table' +import Loader from 'components/Icons/LoadingSpinner' +import { ErrorModal } from 'components/Table/ErrorBox' +import useDebounce from 'hooks/useDebounce' +import { useEffect, useRef, useState } from 'react' +import { ScrollSync, ScrollSyncPane } from 'react-scroll-sync' +import { ThemedText } from 'theme/components' +import { FadePresence } from 'theme/components/FadePresence' +import { Z_INDEX } from 'theme/zIndex' +import { + CellContainer, + DataRow, + HeaderRow, + LOAD_MORE_BOTTOM_OFFSET, + LoadingIndicator, + LoadingIndicatorContainer, + NoDataFoundTableRow, + ReturnButton, + ReturnButtonContainer, + ReturnIcon, + SHOW_RETURN_TO_TOP_OFFSET, + TableBodyContainer, + TableContainer, + TableHead, + TableRowLink, +} from './styled' + +function TableBody({ + table, + loading, + error, +}: { + table: TanstackTable + loading?: boolean + error?: ApolloError +}) { + if (loading || error) { + // loading and error states + return ( + <> + {Array.from({ length: 7 }, (_, rowIndex) => ( + + {table.getAllColumns().map((column, columnIndex) => ( + + {flexRender(column.columnDef.cell, {} as CellContext)} + + ))} + + ))} + {error && ( + Error loading data} + subtitle={Data is unavailable at the moment; we're working on a fix} + /> + )} + + ) + } + + if (!table.getRowModel()?.rows.length) { + // no errors, but no data round + return ( + + + No data found + + + ) + } + + return ( + // data found + <> + {table.getRowModel().rows.map((row) => { + const cells = row + .getVisibleCells() + .map((cell) => ( + {flexRender(cell.column.columnDef.cell, cell.getContext())} + )) + const rowOriginal = row.original as any + const linkState = rowOriginal.linkState // optional data passed to linked page, accessible via useLocation().state + const rowTestId = rowOriginal.testId + return 'link' in rowOriginal && typeof rowOriginal.link === 'string' ? ( + + {cells} + + ) : ( + + {cells} + + ) + })} + + ) +} + +export function Table({ + columns, + data, + loading, + error, + loadMore, + maxWidth, + maxHeight, +}: { + columns: ColumnDef[] + data: Data[] + loading?: boolean + error?: ApolloError + loadMore?: ({ onComplete }: { onComplete?: () => void }) => void + maxWidth?: number + maxHeight?: number +}) { + const [showReturn, setShowReturn] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) + + const [scrollPosition, setScrollPosition] = useState<{ + distanceFromTop: number + distanceToBottom: number + }>({ + distanceFromTop: 0, + distanceToBottom: LOAD_MORE_BOTTOM_OFFSET, + }) + const { distanceFromTop, distanceToBottom } = useDebounce(scrollPosition, 125) + const tableBodyRef = useRef(null) + const lastLoadedLengthRef = useRef(data?.length ?? 0) + const canLoadMore = useRef(true) + + useEffect(() => { + const scrollableElement = maxHeight ? tableBodyRef.current : window + if (scrollableElement === null) { + return + } + const updateScrollPosition = () => { + if (scrollableElement instanceof HTMLDivElement) { + const { scrollTop, scrollHeight, clientHeight } = scrollableElement + setScrollPosition({ + distanceFromTop: scrollTop, + distanceToBottom: scrollHeight - scrollTop - clientHeight, + }) + } else { + setScrollPosition({ + distanceFromTop: scrollableElement.scrollY, + distanceToBottom: document.body.scrollHeight - scrollableElement.scrollY - scrollableElement.innerHeight, + }) + } + } + scrollableElement.addEventListener('scroll', updateScrollPosition) + return () => scrollableElement.removeEventListener('scroll', updateScrollPosition) + }, [loadMore, maxHeight, loadingMore]) + + useEffect(() => { + setShowReturn(!loading && !error && distanceFromTop >= SHOW_RETURN_TO_TOP_OFFSET) + if (distanceToBottom < LOAD_MORE_BOTTOM_OFFSET && !loadingMore && loadMore && canLoadMore.current && !error) { + setLoadingMore(true) + // Manually update scroll position to prevent re-triggering + setScrollPosition({ + distanceFromTop: SHOW_RETURN_TO_TOP_OFFSET, + distanceToBottom: LOAD_MORE_BOTTOM_OFFSET, + }) + loadMore({ + onComplete: () => { + setLoadingMore(false) + if (data?.length === lastLoadedLengthRef.current) { + canLoadMore.current = false + } else { + lastLoadedLengthRef.current = data?.length ?? 0 + } + }, + }) + } + }, [data?.length, distanceFromTop, distanceToBottom, error, loadMore, loading, loadingMore]) + + const table = useReactTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + }) + + return ( +
+ + + + + + {table.getFlatHeaders().map((header) => ( + + {flexRender(header.column.columnDef.header, header.getContext())} + + ))} + + + {showReturn && ( + + + { + setShowReturn(false) + const scrollableElement = maxHeight ? tableBodyRef.current : window + scrollableElement?.scrollTo({ + top: 0, + behavior: 'smooth', + }) + }} + > + + Return to top + + + + )} + + + + + + + + + + Loading + + + + +
+ ) +} diff --git a/apps/web/src/components/Table/styled.test.tsx b/apps/web/src/components/Table/styled.test.tsx new file mode 100644 index 0000000..4150e93 --- /dev/null +++ b/apps/web/src/components/Table/styled.test.tsx @@ -0,0 +1,26 @@ +import { TokenLinkCell } from 'components/Table/styled' +import { Chain } from 'graphql/data/__generated__/types-and-hooks' +import { validBEPoolToken0 } from 'test-utils/pools/fixtures' +import { render, screen } from 'test-utils/render' + +jest.mock('hooks/Tokens') + +describe('TokenLinkCell', () => { + it('renders unknown token', () => { + const { asFragment } = render() + expect(screen.getByText('UNKNOWN')).toBeDefined() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders known token on mainnet', () => { + const { asFragment } = render() + expect(screen.getByText('USDC')).toBeDefined() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders known token on a different chain', () => { + const { asFragment } = render() + expect(screen.getByText('Polygon logo')).toBeDefined() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/Table/styled.tsx b/apps/web/src/components/Table/styled.tsx new file mode 100644 index 0000000..84569c8 --- /dev/null +++ b/apps/web/src/components/Table/styled.tsx @@ -0,0 +1,262 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import { ButtonLight } from 'components/Button' +import Column from 'components/Column' +import { HideScrollBarStyles } from 'components/Common' +import Row from 'components/Row' +import { getAbbreviatedTimeString } from 'components/Table/utils' +import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' +import { NATIVE_CHAIN_ID, nativeOnChain } from 'constants/tokens' +import { Token } from 'graphql/data/__generated__/types-and-hooks' +import { OrderDirection, getTokenDetailsURL, supportedChainIdFromGQLChain, unwrapToken } from 'graphql/data/util' +import { OrderDirection as TheGraphOrderDirection } from 'graphql/thegraph/__generated__/types-and-hooks' +import { useActiveLocale } from 'hooks/useActiveLocale' +import { ArrowDown, CornerLeftUp, ExternalLink as ExternalLinkIcon } from 'react-feather' +import { Link } from 'react-router-dom' +import styled, { css } from 'styled-components' +import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' + +export const SHOW_RETURN_TO_TOP_OFFSET = 500 +export const LOAD_MORE_BOTTOM_OFFSET = 50 + +export const TableContainer = styled(Column)<{ $maxWidth?: number; $maxHeight?: number }>` + max-width: ${({ $maxWidth }) => $maxWidth}px; + max-height: ${({ $maxHeight }) => $maxHeight}px; + // Center layout + justify-content: center; + align-items: center; + margin: 0px auto 24px auto; +` +const StickyStyles = css` + top: 73px; + position: sticky; + position: -webkit-sticky; + z-index: ${Z_INDEX.under_dropdown}; +` +export const TableHead = styled.div<{ $isSticky?: boolean }>` + width: 100%; + position: relative; + ${({ $isSticky }) => ($isSticky ? StickyStyles : '')} + // Place header at bottom of container (top of container used to add distance from nav / hide rows) + display: flex; + flex-direction: column; + justify-content: flex-end; + // Solid background that matches surface, in order to hide rows as they scroll behind header + background: ${({ theme }) => theme.surface1}; +` +export const TableBodyContainer = styled(Column)` + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid ${({ theme }) => theme.surface3}; + border-bottom: 1px solid ${({ theme }) => theme.surface3}; + border-left: 1px solid ${({ theme }) => theme.surface3}; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + ${HideScrollBarStyles} +` +export const ReturnButton = styled(ButtonLight)` + font-size: 16px; + border-radius: 900px; + width: fit-content; + margin-top: 8px; +` +export const ReturnIcon = styled(CornerLeftUp)` + width: 16px; + height: 16px; + margin-right: 8px; +` +export const ReturnButtonContainer = styled(Row)<{ $top?: number }>` + position: absolute; + justify-content: center; + top: ${({ $top }) => $top}px; + left: 50%; + transform: translateX(-50%); + width: max-content; +` +export const LoadingIndicatorContainer = styled(Row)<{ show: boolean }>` + position: sticky; + justify-content: center; + margin-top: -48px; + visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; +` +export const LoadingIndicator = styled(Row)` + background: ${({ theme }) => theme.accent2}; + border-radius: 8px; + width: fit-content; + padding: 8px; + color: ${({ theme }) => theme.accent1}; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: ${Z_INDEX.under_dropdown}; +` + +const TableRow = styled(Row)` + padding: 0px 12px; + width: fit-content; + min-width: 100%; + display: flex; + min-height: 64px; +` +export const DataRow = styled(TableRow)` + @media not all and (hover: none) { + :hover { + background: ${({ theme }) => theme.surface3}; + } + } +` +export const NoDataFoundTableRow = styled(TableRow)` + justify-content: center; +` + +export const HeaderRow = styled(TableRow)<{ $dimmed?: boolean }>` + border: 1px solid ${({ theme }) => theme.surface3}; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: ${({ theme }) => theme.surface2}; + ${HideScrollBarStyles} + overscroll-behavior: none; + + ${({ $dimmed }) => $dimmed && 'opacity: 0.4;'} +` +export const CellContainer = styled.div` + display: flex; + flex-grow: 1; + + &:last-child { + justify-content: flex-end; + } + + &:first-child { + flex-grow: 0; + } +` +export const StyledExternalLink = styled(ExternalLink)` + text-decoration: none; + ${ClickableStyle} + color: ${({ theme }) => theme.neutral1} +` +const StyledInternalLink = styled(Link)` + text-decoration: none; + ${ClickableStyle} + color: ${({ theme }) => theme.neutral1} +` + +export const TableRowLink = styled(Link)` + color: none; + text-decoration: none; + cursor: pointer; +` + +export const ClickableHeaderRow = styled(Row)<{ $justify?: string }>` + justify-content: ${({ $justify }) => $justify ?? 'flex-end'}; + cursor: pointer; + width: 100%; + gap: 4px; + ${ClickableStyle} +` +export const HeaderArrow = styled(ArrowDown)<{ direction: OrderDirection | TheGraphOrderDirection }>` + height: 16px; + width: 16px; + color: ${({ theme }) => theme.neutral1}; + transform: ${({ direction }) => (direction === 'asc' ? 'rotate(180deg)' : 'rotate(0deg)')}; +` +export const HeaderSortText = styled(ThemedText.BodySecondary)<{ $active?: boolean }>` + ${({ $active, theme }) => $active && `color: ${theme.neutral1};`} +` + +export const FilterHeaderRow = styled(Row)<{ modalOpen?: boolean }>` + ${({ modalOpen }) => !modalOpen && ClickableStyle} + cursor: pointer; + user-select: none; + gap: 4px; +` +const StyledTimestampRow = styled(StyledExternalLink)` + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + width: 100%; +` +const StyledExternalLinkIcon = styled(ExternalLinkIcon)` + display: none; + height: 16px; + width: 16px; + color: ${({ theme }) => theme.neutral2}; + ${StyledTimestampRow}:hover & { + display: block; + } +` + +/** + * Converts the given timestamp to an abbreviated format (s,m,h) for timestamps younger than 1 day + * and a full discreet format for timestamps older than 1 day (e.g. DD/MM HH:MMam/pm). + * Hovering on the timestamp will display the full discreet format. (e.g. DD/MM/YYYY HH:MMam/pm) + * Clicking on the timestamp will open the given link in a new tab + * @param timestamp: unix timestamp in SECONDS + * @param link: link to open on click + * @returns JSX.Element containing the formatted timestamp + */ +export const TimestampCell = ({ timestamp, link }: { timestamp: number; link: string }) => { + const locale = useActiveLocale() + const options: Intl.DateTimeFormatOptions = { + year: '2-digit', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + } + const fullDate = new Date(timestamp * 1000) + .toLocaleString(locale, options) + .toLocaleLowerCase(locale) + .replace(/\s(am|pm)/, '$1') + return ( + + + {getAbbreviatedTimeString(timestamp * 1000)} + + + + ) +} + +const TokenSymbolText = styled(ThemedText.BodyPrimary)` + ${EllipsisStyle} +` +/** + * Given a token displays the Token's Logo and Symbol with a link to its TDP + * @param token + * @returns JSX.Element showing the Token's Logo, Chain logo if non-mainnet, and Token Symbol + */ +export const TokenLinkCell = ({ token }: { token: Token }) => { + const chainId = supportedChainIdFromGQLChain(token.chain) ?? ChainId.MAINNET + const unwrappedToken = unwrapToken(chainId, token) + const isNative = unwrappedToken.address === NATIVE_CHAIN_ID + const nativeCurrency = nativeOnChain(chainId) + return ( + + + + {unwrappedToken?.symbol ?? UNKNOWN} + + + ) +} diff --git a/apps/web/src/components/Table/utils.ts b/apps/web/src/components/Table/utils.ts new file mode 100644 index 0000000..2fc8b76 --- /dev/null +++ b/apps/web/src/components/Table/utils.ts @@ -0,0 +1,30 @@ +import { t } from '@lingui/macro' + +/** + * Displays the time as a human-readable string. + * + * @param {number} timestamp - Transaction timestamp in milliseconds. + * @param {number} locale - BCP 47 language tag (e.g. en-US). + * @returns {string} Message to display. + */ +export function getAbbreviatedTimeString(timestamp: number) { + const now = Date.now() + const timeSince = now - timestamp + const secondsPassed = Math.floor(timeSince / 1000) + const minutesPassed = Math.floor(secondsPassed / 60) + const hoursPassed = Math.floor(minutesPassed / 60) + const daysPassed = Math.floor(hoursPassed / 24) + const monthsPassed = Math.floor(daysPassed / 30) + + if (monthsPassed > 0) { + return t`${monthsPassed}mo ago` + } else if (daysPassed > 0) { + return t`${daysPassed}d ago` + } else if (hoursPassed > 0) { + return t`${hoursPassed}h ago` + } else if (minutesPassed > 0) { + return t`${minutesPassed}m ago` + } else { + return t`${secondsPassed}s ago` + } +} diff --git a/apps/web/src/components/TextInput/__snapshots__/index.test.tsx.snap b/apps/web/src/components/TextInput/__snapshots__/index.test.tsx.snap new file mode 100644 index 0000000..55328e5 --- /dev/null +++ b/apps/web/src/components/TextInput/__snapshots__/index.test.tsx.snap @@ -0,0 +1,146 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ResizableTextArea renders correctly 1`] = ` + + .c0 { + font-size: 12; + outline: none; + border: none; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + resize: none; + background-color: #FFFFFF; + -webkit-transition: color 300ms step-start; + transition: color 300ms step-start; + color: #222222; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 535; + width: 100%; + line-height: 1.2; + padding: 0px; + -webkit-appearance: textfield; +} + +.c0::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c0::-webkit-outer-spin-button, +.c0::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.c0::-webkit-input-placeholder { + color: #7D7D7D; +} + +.c0::-moz-placeholder { + color: #7D7D7D; +} + +.c0:-ms-input-placeholder { + color: #7D7D7D; +} + +.c0::placeholder { + color: #7D7D7D; +} + + + + + + + +`; + +exports[`TextInput renders correctly 1`] = ` + + .c0 { + font-size: 12; + outline: none; + border: none; + -webkit-flex: 1 1 auto; + -ms-flex: 1 1 auto; + flex: 1 1 auto; + width: 0; + background-color: #FFFFFF; + -webkit-transition: color 300ms step-start; + transition: color 300ms step-start; + color: #222222; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 535; + width: 100%; + padding: 0px; + -webkit-appearance: textfield; +} + +.c0::-webkit-search-decoration { + -webkit-appearance: none; +} + +.c0::-webkit-outer-spin-button, +.c0::-webkit-inner-spin-button { + -webkit-appearance: none; +} + +.c0::-webkit-input-placeholder { + color: #CECECE; +} + +.c0::-moz-placeholder { + color: #CECECE; +} + +.c0:-ms-input-placeholder { + color: #CECECE; +} + +.c0::placeholder { + color: #CECECE; +} + + + +
+ +
+
+
+
+`; diff --git a/apps/web/src/components/TextInput/index.test.tsx b/apps/web/src/components/TextInput/index.test.tsx new file mode 100644 index 0000000..e4c0f98 --- /dev/null +++ b/apps/web/src/components/TextInput/index.test.tsx @@ -0,0 +1,70 @@ +import { fireEvent, render, screen } from 'test-utils/render' +import noop from 'utilities/src/react/noop' + +import { ResizingTextArea, TextInput } from './' + +describe('TextInput', () => { + it('renders correctly', () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + }) + + it('calls the handler on user input', () => { + const onUserInputSpy = jest.fn() + render( + + ) + + fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } }) + + expect(onUserInputSpy).toHaveBeenCalledWith('New value') + expect(onUserInputSpy).toHaveBeenCalledTimes(1) + }) +}) + +describe('ResizableTextArea', () => { + it('renders correctly', () => { + const { asFragment } = render( + + ) + expect(asFragment()).toMatchSnapshot() + }) + + it('calls the handler on user input', () => { + const onUserInputSpy = jest.fn() + render( + + ) + + fireEvent.change(screen.getByPlaceholderText('Test Placeholder'), { target: { value: 'New value' } }) + + expect(onUserInputSpy).toHaveBeenCalledWith('New value') + expect(onUserInputSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/web/src/components/TextInput/index.tsx b/apps/web/src/components/TextInput/index.tsx new file mode 100644 index 0000000..3b8d6ce --- /dev/null +++ b/apps/web/src/components/TextInput/index.tsx @@ -0,0 +1,146 @@ +import { ChangeEvent, memo, useCallback, useRef } from 'react' +import styled from 'styled-components' + +const Input = styled.input<{ error?: boolean; fontSize?: string }>` + font-size: ${({ fontSize }) => fontSize || '1.25rem'}; + outline: none; + border: none; + flex: 1 1 auto; + width: 0; + background-color: ${({ theme }) => theme.surface1}; + transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}; + color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 535; + width: 100%; + padding: 0px; + -webkit-appearance: textfield; + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-outer-spin-button, + ::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } +` + +const TextAreaInput = styled.textarea<{ error?: boolean; fontSize?: string }>` + font-size: ${({ fontSize }) => fontSize || '1.25rem'}; + outline: none; + border: none; + flex: 1 1 auto; + width: 0; + resize: none; + background-color: ${({ theme }) => theme.surface1}; + transition: color 300ms ${({ error }) => (error ? 'step-end' : 'step-start')}; + color: ${({ error, theme }) => (error ? theme.critical : theme.neutral1)}; + overflow: hidden; + text-overflow: ellipsis; + font-weight: 535; + width: 100%; + line-height: 1.2; + padding: 0px; + -webkit-appearance: textfield; + + ::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-outer-spin-button, + ::-webkit-inner-spin-button { + -webkit-appearance: none; + } + + ::placeholder { + color: ${({ theme }) => theme.neutral2}; + } +` + +export const TextInput = ({ + className, + value, + onUserInput, + placeholder, + fontSize, +}: { + className?: string + value: string + onUserInput: (value: string) => void + placeholder: string + fontSize: string +}) => { + const handleInput = useCallback( + (event: ChangeEvent) => { + onUserInput(event.target.value) + }, + [onUserInput] + ) + + return ( +
+ +
+ ) +} + +export const ResizingTextArea = memo( + ({ + className, + value, + onUserInput, + placeholder, + fontSize, + }: { + className?: string + value: string + onUserInput: (value: string) => void + placeholder: string + fontSize: string + }) => { + const inputRef = useRef(document.createElement('textarea')) + + const handleInput = useCallback( + (event: ChangeEvent) => { + inputRef.current.style.height = 'auto' + inputRef.current.style.height = inputRef.current.scrollHeight + 'px' + onUserInput(event.target.value) + }, + [onUserInput] + ) + + return ( + + ) + } +) + +ResizingTextArea.displayName = 'ResizingTextArea' diff --git a/apps/web/src/components/Toggle/MultiToggle.tsx b/apps/web/src/components/Toggle/MultiToggle.tsx new file mode 100644 index 0000000..30985af --- /dev/null +++ b/apps/web/src/components/Toggle/MultiToggle.tsx @@ -0,0 +1,32 @@ +import styled from 'styled-components' + +export const ToggleWrapper = styled.button<{ width?: string }>` + display: flex; + align-items: center; + width: ${({ width }) => width ?? '100%'}; + padding: 1px; + background: ${({ theme }) => theme.surface2}; + border-radius: 8px; + border: ${({ theme }) => '1px solid ' + theme.surface3}; + cursor: pointer; + outline: none; +` + +export const ToggleElement = styled.span<{ isActive?: boolean; fontSize?: string }>` + display: flex; + align-items: center; + width: 100%; + padding: 4px 0.5rem; + border-radius: 6px; + justify-content: center; + height: 100%; + background: ${({ theme, isActive }) => (isActive ? theme.surface1 : 'none')}; + color: ${({ theme, isActive }) => (isActive ? theme.neutral1 : theme.neutral3)}; + font-size: ${({ fontSize }) => fontSize ?? '1rem'}; + font-weight: 535; + white-space: nowrap; + :hover { + user-select: initial; + color: ${({ theme, isActive }) => (isActive ? theme.neutral2 : theme.neutral3)}; + } +` diff --git a/apps/web/src/components/Toggle/PillMultiToggle.tsx b/apps/web/src/components/Toggle/PillMultiToggle.tsx new file mode 100644 index 0000000..beba73c --- /dev/null +++ b/apps/web/src/components/Toggle/PillMultiToggle.tsx @@ -0,0 +1,104 @@ +import { t } from '@lingui/macro' +import { createRef, useMemo, useState } from 'react' +import styled from 'styled-components' +import { Z_INDEX } from 'theme/zIndex' + +const togglePadding = 4 + +const OptionsSelector = styled.div` + display: flex; + position: relative; + justify-content: flex-end; + gap: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + border-radius: 20px; + height: 36px; + padding: ${togglePadding}px; + width: 100%; +` + +const ActivePill = styled.div` + position: absolute; + height: 28px; + top: 3px; + background-color: ${({ theme }) => theme.surface3}; + border-radius: 16px; + transition: left 0.3s ease, width 0.3s ease; +` +const OptionButton = styled.button<{ active: boolean }>` + flex: 1; + display: flex; + align-items: center; + justify-content: center; + background-color: transparent; + font-weight: 535; + font-size: 16px; + padding: 8px 12px; + border-radius: 15px; + line-height: 20px; + border: none; + cursor: pointer; + color: ${({ theme, active }) => (active ? theme.neutral1 : theme.neutral2)}; + transition-duration: ${({ theme }) => theme.transition.duration.fast}; + z-index: ${Z_INDEX.active}; + :hover { + ${({ active, theme }) => !active && `opacity: ${theme.opacity.hover};`} + } +` + +export interface PillMultiToggleOption { + value: string // Value to be selected/stored, used as default display value + display?: JSX.Element // Optional custom display element +} + +function getPillMultiToggleOption(option: PillMultiToggleOption | string): PillMultiToggleOption { + if (typeof option === 'string') { + return { value: option } + } + return option +} + +export default function PillMultiToggle({ + options, + currentSelected, + onSelectOption, +}: { + options: readonly (PillMultiToggleOption | string)[] + currentSelected: string + onSelectOption: (option: string) => void +}) { + const buttonRefs = useMemo(() => options.map(() => createRef()), [options]) + + const [activeIndex, setActiveIndex] = useState( + options.map((o) => getPillMultiToggleOption(o).value).indexOf(currentSelected) + ) + + return ( + + + {options.map((option, i) => { + const { value, display } = getPillMultiToggleOption(option) + const ref = buttonRefs[i] + + return ( + { + setActiveIndex(i) + onSelectOption(value) + }} + > + {display ?? <>{t`${value}`}} + + ) + })} + + ) +} diff --git a/apps/web/src/components/Toggle/index.tsx b/apps/web/src/components/Toggle/index.tsx new file mode 100644 index 0000000..12dffba --- /dev/null +++ b/apps/web/src/components/Toggle/index.tsx @@ -0,0 +1,85 @@ +import { darken } from 'polished' +import { useState } from 'react' +import styled, { keyframes } from 'styled-components' + +const Wrapper = styled.button<{ isActive?: boolean; activeElement?: boolean }>` + align-items: center; + background: ${({ isActive, theme }) => (isActive ? theme.accent2 : 'transparent')}; + border: ${({ theme, isActive }) => (isActive ? '1px solid transparent' : `1px solid ${theme.surface3}`)}; + border-radius: 20px; + cursor: pointer; + display: flex; + outline: none; + padding: 4px; + width: fit-content; +` + +const turnOnToggle = keyframes` + from { + margin-left: 0em; + margin-right: 2.2em; + } + to { + margin-left: 2.2em; + margin-right: 0em; + } +` + +const turnOffToggle = keyframes` + from { + margin-left: 2.2em; + margin-right: 0em; + } + to { + margin-left: 0em; + margin-right: 2.2em; + } +` + +const ToggleElementHoverStyle = (hasBgColor: boolean, theme: any, isActive?: boolean) => + hasBgColor + ? { + opacity: '0.8', + } + : { + background: isActive ? darken(0.05, theme.accent1) : darken(0.05, theme.surface3), + color: isActive ? theme.white : theme.neutral3, + } + +const ToggleElement = styled.span<{ isActive?: boolean; bgColor?: string; isInitialToggleLoad?: boolean }>` + animation: 0.1s + ${({ isActive, isInitialToggleLoad }) => (isInitialToggleLoad ? 'none' : isActive ? turnOnToggle : turnOffToggle)} + ease-in; + background: ${({ theme, bgColor, isActive }) => + isActive ? bgColor ?? theme.accent1 : bgColor ? theme.surface3 : theme.neutral3}; + border-radius: 50%; + height: 24px; + :hover { + ${({ bgColor, theme, isActive }) => ToggleElementHoverStyle(!!bgColor, theme, isActive)} + } + margin-left: ${({ isActive }) => isActive && '2.2em'}; + margin-right: ${({ isActive }) => !isActive && '2.2em'}; + width: 24px; +` + +interface ToggleProps { + id?: string + bgColor?: string + isActive: boolean + toggle: () => void +} + +export default function Toggle({ id, bgColor, isActive, toggle }: ToggleProps) { + const [isInitialToggleLoad, setIsInitialToggleLoad] = useState(true) + + const switchToggle = () => { + toggle() + if (isInitialToggleLoad) setIsInitialToggleLoad(false) + } + + return ( + + + + ) +} diff --git a/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx b/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx new file mode 100644 index 0000000..a63b242 --- /dev/null +++ b/apps/web/src/components/TokenSafety/TokenSafetyIcon.tsx @@ -0,0 +1,43 @@ +import { Warning, WARNING_LEVEL } from 'constants/tokenSafety' +import { AlertTriangle, Slash } from 'react-feather' +import styled, { css } from 'styled-components' + +const WarningContainer = styled.div` + margin-left: 4px; + display: flex; + justify-content: center; +` + +const WarningIconStyle = css<{ size?: string }>` + width: ${({ size }) => size ?? '1em'}; + height: ${({ size }) => size ?? '1em'}; +` + +const WarningIcon = styled(AlertTriangle)` + ${WarningIconStyle}; + color: ${({ theme }) => theme.neutral3}; +` + +export const BlockedIcon = styled(Slash)` + ${WarningIconStyle} + color: ${({ theme }) => theme.neutral2}; +` + +export default function TokenSafetyIcon({ warning }: { warning: Warning | null }) { + switch (warning?.level) { + case WARNING_LEVEL.BLOCKED: + return ( + + + + ) + case WARNING_LEVEL.UNKNOWN: + return ( + + + + ) + default: + return null + } +} diff --git a/apps/web/src/components/TokenSafety/TokenSafetyLabel.tsx b/apps/web/src/components/TokenSafety/TokenSafetyLabel.tsx new file mode 100644 index 0000000..661a97e --- /dev/null +++ b/apps/web/src/components/TokenSafety/TokenSafetyLabel.tsx @@ -0,0 +1,36 @@ +import { WARNING_LEVEL } from 'constants/tokenSafety' +import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor' +import { ReactNode } from 'react' +import { AlertTriangle, Slash } from 'react-feather' +import { Text } from 'rebass' +import styled from 'styled-components' + +const Label = styled.div<{ color: string; backgroundColor: string }>` + padding: 4px 4px; + font-size: 12px; + background-color: ${({ backgroundColor }) => backgroundColor}; + border-radius: 8px; + color: ${({ color }) => color}; + display: inline-flex; + align-items: center; +` + +const Title = styled(Text)` + margin-right: 5px; + font-weight: 535; + font-size: 12px; +` + +type TokenWarningLabelProps = { + level: WARNING_LEVEL + canProceed: boolean + children: ReactNode +} +export default function TokenSafetyLabel({ level, canProceed, children }: TokenWarningLabelProps) { + return ( + + ) +} diff --git a/apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx b/apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx new file mode 100644 index 0000000..316c871 --- /dev/null +++ b/apps/web/src/components/TokenSafety/TokenSafetyMessage.tsx @@ -0,0 +1,83 @@ +import { Trans } from '@lingui/macro' +import { displayWarningLabel, getWarningCopy, TOKEN_SAFETY_ARTICLE, Warning } from 'constants/tokenSafety' +import { useTokenWarningColor, useTokenWarningTextColor } from 'hooks/useTokenWarningColor' +import { AlertTriangle, Slash } from 'react-feather' +import { Text } from 'rebass' +import styled from 'styled-components' +import { ExternalLink } from 'theme/components' + +const Label = styled.div<{ color: string; backgroundColor: string }>` + width: 100%; + padding: 12px 20px 16px; + background-color: ${({ backgroundColor }) => backgroundColor}; + border-radius: 16px; + border: 1px solid ${({ theme }) => theme.surface3}; + color: ${({ color }) => color}; +` + +const TitleRow = styled.div` + align-items: center; + font-weight: 535; + display: inline-flex; +` + +const Title = styled(Text)` + font-weight: 535; + font-size: 16px; + line-height: 24px; + margin-left: 7px; +` + +const DetailsRow = styled.div` + margin-top: 8px; + font-size: 12px; + line-height: 16px; + color: ${({ theme }) => theme.neutral2}; +` + +const StyledLink = styled(ExternalLink)` + color: ${({ theme }) => theme.accent1}; + + font-weight: 535; +` + +type TokenSafetyMessageProps = { + warning: Warning + tokenAddress: string + plural?: boolean + tokenSymbol?: string +} + +export default function TokenSafetyMessage({ + warning, + tokenAddress, + plural = false, + tokenSymbol, +}: TokenSafetyMessageProps) { + const backgroundColor = useTokenWarningColor(warning.level) + const textColor = useTokenWarningTextColor(warning.level) + const { heading, description } = getWarningCopy(warning, plural, tokenSymbol) + + return ( + + ) +} diff --git a/apps/web/src/components/TokenSafety/TokenSafetyModal.tsx b/apps/web/src/components/TokenSafety/TokenSafetyModal.tsx new file mode 100644 index 0000000..e3e2d36 --- /dev/null +++ b/apps/web/src/components/TokenSafety/TokenSafetyModal.tsx @@ -0,0 +1,29 @@ +import TokenSafety, { TokenSafetyProps } from '.' +import Modal from '../Modal' + +interface TokenSafetyModalProps extends TokenSafetyProps { + isOpen: boolean +} + +export default function TokenSafetyModal({ + isOpen, + tokenAddress, + secondTokenAddress, + onContinue, + onCancel, + onBlocked, + showCancel, +}: TokenSafetyModalProps) { + return ( + + + + ) +} diff --git a/apps/web/src/components/TokenSafety/index.tsx b/apps/web/src/components/TokenSafety/index.tsx new file mode 100644 index 0000000..b74dae0 --- /dev/null +++ b/apps/web/src/components/TokenSafety/index.tsx @@ -0,0 +1,303 @@ +import { Trans } from '@lingui/macro' +import { Token } from '@uniswap/sdk-core' +import { ButtonPrimary } from 'components/Button' +import { AutoColumn } from 'components/Column' +import CurrencyLogo from 'components/Logo/CurrencyLogo' +import TokenSafetyLabel from 'components/TokenSafety/TokenSafetyLabel' +import { + checkWarning, + displayWarningLabel, + getWarningCopy, + NotFoundWarning, + TOKEN_SAFETY_ARTICLE, + Warning, +} from 'constants/tokenSafety' +import { useToken } from 'hooks/Tokens' +import { ExternalLink as LinkIconFeather } from 'react-feather' +import { Text } from 'rebass' +import { useAddUserToken } from 'state/user/hooks' +import styled from 'styled-components' +import { ButtonText, CopyLinkIcon, ExternalLink } from 'theme/components' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const Wrapper = styled.div` + width: 100%; + position: relative; + display: flex; + flex-flow: column; + align-items: center; +` + +const Container = styled.div` + width: 100%; + padding: 32px 40px; + display: flex; + flex-flow: column; + align-items: center; +` + +const LogoContainer = styled.div` + display: flex; + gap: 16px; +` + +const ShortColumn = styled(AutoColumn)` + margin-top: 10px; +` + +const InfoText = styled(Text)` + padding: 0 12px 0 12px; + font-size: 14px; + line-height: 20px; + text-align: center; +` + +const StyledButton = styled(ButtonPrimary)` + margin-top: 24px; + width: 100%; + font-weight: 535; +` + +const StyledCancelButton = styled(ButtonText)` + margin-top: 16px; + color: ${({ theme }) => theme.neutral2}; + font-weight: 535; + font-size: 14px; +` + +const StyledCloseButton = styled(StyledButton)` + background-color: ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral1}; + + &:hover { + background-color: ${({ theme }) => theme.surface3}; + opacity: ${({ theme }) => theme.opacity.hover}; + transition: opacity 250ms ease; + } +` + +const Buttons = ({ + warning, + onContinue, + onCancel, + onBlocked, + showCancel, +}: { + warning: Warning + onContinue?: () => void + onCancel: () => void + onBlocked?: () => void + showCancel?: boolean +}) => { + return warning.canProceed ? ( + <> + + {!displayWarningLabel(warning) ? Continue : I understand} + + {showCancel && Cancel} + + ) : ( + + Close + + ) +} + +const SafetyLabel = ({ warning }: { warning: Warning }) => { + return ( + + {warning.message} + + ) +} + +// TODO: Replace color with stylesheet color +const LinkColumn = styled(AutoColumn)` + width: 100%; + margin-top: 16px; + position: relative; +` + +const ExplorerContainer = styled.div` + width: 100%; + height: 32px; + margin-top: 10px; + font-size: 20px; + background-color: ${({ theme }) => theme.accent2}; + color: ${({ theme }) => theme.accent1}; + border-radius: 8px; + padding: 2px 12px; + display: flex; + align-items: center; + overflow: hidden; +` + +const ExplorerLinkWrapper = styled.div` + display: flex; + overflow: hidden; + align-items: center; + cursor: pointer; + + :hover { + opacity: ${({ theme }) => theme.opacity.hover}; + } + :active { + opacity: ${({ theme }) => theme.opacity.click}; + } +` + +const ExplorerLink = styled.div` + display: block; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` +const ExplorerLinkIcon = styled(LinkIconFeather)` + height: 16px; + width: 18px; + margin-left: 8px; +` + +const LinkIconWrapper = styled.div` + justify-content: center; + display: flex; +` + +function ExternalLinkIcon() { + return ( + + + + ) +} + +function ExplorerView({ token }: { token: Token }) { + if (token) { + const explorerLink = getExplorerLink(token?.chainId, token?.address, ExplorerDataType.TOKEN) + return ( + + window.open(explorerLink, '_blank')}> + {explorerLink} + + + + + ) + } else { + return null + } +} + +const StyledExternalLink = styled(ExternalLink)` + color: ${({ theme }) => theme.accent1}; + stroke: currentColor; + font-weight: 535; +` + +export interface TokenSafetyProps { + tokenAddress: string | null + secondTokenAddress?: string + onContinue: () => void + onCancel: () => void + onBlocked?: () => void + showCancel?: boolean +} + +export default function TokenSafety({ + tokenAddress, + secondTokenAddress, + onContinue, + onCancel, + onBlocked, + showCancel, +}: TokenSafetyProps) { + const logos = [] + const urls = [] + + const token1Warning = tokenAddress ? checkWarning(tokenAddress) : null + const token1 = useToken(tokenAddress) + const token2Warning = secondTokenAddress ? checkWarning(secondTokenAddress) : null + const token2 = useToken(secondTokenAddress) + + const token1Unsupported = !token1Warning?.canProceed + const token2Unsupported = !token2Warning?.canProceed + + // Logic for only showing the 'unsupported' warning if one is supported and other isn't + if (token1 && token1Warning && (token1Unsupported || !(token2Warning && token2Unsupported))) { + logos.push() + urls.push() + } + if (token2 && token2Warning && (token2Unsupported || !(token1Warning && token1Unsupported))) { + logos.push() + urls.push() + } + + const plural = logos.length > 1 + // Show higher level warning if two are present + let displayWarning = token1Warning + if (!token1Warning || (token2Warning && token2Unsupported && !token1Unsupported)) { + displayWarning = token2Warning + } + + // If a warning is acknowledged, import these tokens + const addToken = useAddUserToken() + const acknowledge = () => { + if (token1) { + addToken(token1) + } + if (token2) { + addToken(token2) + } + onContinue() + } + + const { heading, description } = getWarningCopy(displayWarning, plural) + const learnMoreUrl = ( + + Learn more + + ) + + return displayWarning ? ( + + + + {logos} + + {displayWarningLabel(displayWarning) && ( + + + + )} + + + {heading} {description} {learnMoreUrl} + + + {urls} + + + + ) : ( + + + + + + + + {heading} {description} {learnMoreUrl} + + + + + + ) +} diff --git a/apps/web/src/components/TokenSafety/verified.svg b/apps/web/src/components/TokenSafety/verified.svg new file mode 100644 index 0000000..69d4c91 --- /dev/null +++ b/apps/web/src/components/TokenSafety/verified.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/web/src/components/Tokens/TokenDetails/About.tsx b/apps/web/src/components/Tokens/TokenDetails/About.tsx new file mode 100644 index 0000000..8f9aec4 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/About.tsx @@ -0,0 +1,12 @@ +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' + +export const AboutContainer = styled.div` + gap: 16px; + padding: 24px 0px; + ${textFadeIn} +` +export const AboutHeader = styled(ThemedText.MediumHeader)` + font-size: 28px !important; +` diff --git a/apps/web/src/components/Tokens/TokenDetails/ActivitySection.test.tsx b/apps/web/src/components/Tokens/TokenDetails/ActivitySection.test.tsx new file mode 100644 index 0000000..5d7d0c3 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ActivitySection.test.tsx @@ -0,0 +1,24 @@ +import { screen } from '@testing-library/react' +import { Token } from '@uniswap/sdk-core' +import { render } from 'test-utils/render' + +import { ActivitySection } from './ActivitySection' + +const mockToken = new Token(1, '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 18, 'WETH', 'Wrapped Ether') + +jest.mock('pages/TokenDetails/TDPContext', () => ({ + useTDPContext: () => { + return { + currency: mockToken, + } + }, +})) + +describe('ActivitySection', () => { + it('has Pools and Transactions tabs', () => { + render() + + expect(screen.getByText('Pools')).toBeInTheDocument() + expect(screen.getByText('Transactions')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/Tokens/TokenDetails/ActivitySection.tsx b/apps/web/src/components/Tokens/TokenDetails/ActivitySection.tsx new file mode 100644 index 0000000..7863a82 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ActivitySection.tsx @@ -0,0 +1,49 @@ +import { Trans } from '@lingui/macro' +import Row from 'components/Row' +import { TokenDetailsPoolsTable } from 'components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable' +import { useState } from 'react' +import styled from 'styled-components' +import { ClickableStyle, ThemedText } from 'theme/components' + +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { TransactionsTable } from './tables/TransactionsTable' + +const Container = styled.div` + width: 100%; + display: flex; + flex-direction: column; +` +const Tab = styled(ThemedText.HeadlineMedium)<{ isActive?: boolean }>` + cursor: pointer; + color: ${({ isActive, theme }) => (isActive ? theme.neutral1 : theme.neutral2)}; + ${ClickableStyle}; +` +enum ActivityTab { + Txs, + Pools, +} +export function ActivitySection() { + const { wrapped: referenceToken, chainId } = useTDPContext().currency + + const [activityInView, setActivityInView] = useState(ActivityTab.Txs) + + if (!referenceToken) { + return null + } + return ( + + + setActivityInView(ActivityTab.Txs)}> + Transactions + + setActivityInView(ActivityTab.Pools)}> + Pools + + + {activityInView === ActivityTab.Txs && } + {activityInView === ActivityTab.Pools && ( + + )} + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx new file mode 100644 index 0000000..986a98c --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/BalanceSummary.tsx @@ -0,0 +1,173 @@ +import { Trans } from '@lingui/macro' +import { ChainId, Currency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import { Chain, PortfolioTokenBalancePartsFragment } from 'graphql/data/__generated__/types-and-hooks' +import { getTokenDetailsURL, gqlToCurrency, supportedChainIdFromGQLChain } from 'graphql/data/util' +import { useMemo } from 'react' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { useTDPContext } from 'pages/TokenDetails/TDPContext' + +const BalancesCard = styled.div` + color: ${({ theme }) => theme.neutral1}; + display: flex; + flex-direction: column; + gap: 24px; + height: fit-content; + width: 100%; + + // 768 hardcoded to match NFT-redesign navbar breakpoints + // src/nft/css/sprinkles.css.ts + // change to match theme breakpoints when this navbar is updated + @media screen and (min-width: 768px) { + display: flex; + } +` +const BalanceSection = styled.div` + height: fit-content; + width: 100%; +` +const BalanceRow = styled.div` + align-items: center; + display: flex; + flex-direction: row; + margin-top: 12px; +` +const BalanceItem = styled.div` + display: flex; + align-items: center; +` + +const BalanceAmountsContainer = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + width: 100%; + margin-left: 12px; +` + +interface BalanceProps { + currency?: Currency + chainId?: ChainId + gqlBalance?: PortfolioTokenBalancePartsFragment + onClick?: () => void +} +const Balance = ({ currency, chainId = ChainId.MAINNET, gqlBalance, onClick }: BalanceProps) => { + const { formatNumber } = useFormatter() + const currencies = useMemo(() => [currency], [currency]) + + const formattedGqlBalance = formatNumber({ + input: gqlBalance?.quantity, + type: NumberType.TokenNonTx, + }) + const formattedUsdGqlValue = formatNumber({ + input: gqlBalance?.denominatedValue?.value, + type: NumberType.PortfolioBalance, + }) + + return ( + + + + + {formattedUsdGqlValue} + + + {formattedGqlBalance} + + + + ) +} + +const PageChainBalanceSummary = ({ pageChainBalance }: { pageChainBalance?: PortfolioTokenBalancePartsFragment }) => { + if (!pageChainBalance || !pageChainBalance.token) return null + const currency = gqlToCurrency(pageChainBalance.token) + return ( + + + Your balance + + + + ) +} + +const OtherChainsBalanceSummary = ({ + otherChainBalances, + hasPageChainBalance, +}: { + otherChainBalances: readonly PortfolioTokenBalancePartsFragment[] + hasPageChainBalance: boolean +}) => { + const navigate = useNavigate() + + if (!otherChainBalances.length) return null + return ( + + {hasPageChainBalance ? ( + + On other networks + + ) : ( + + Balance on other networks + + )} + {otherChainBalances.map((balance) => { + const currency = balance.token && gqlToCurrency(balance.token) + const chainId = (balance.token && supportedChainIdFromGQLChain(balance.token.chain)) ?? ChainId.MAINNET + return ( + + navigate( + getTokenDetailsURL({ + address: balance.token?.address, + chain: balance.token?.chain ?? Chain.Ethereum, + }) + ) + } + /> + ) + })} + + ) +} + +export default function BalanceSummary() { + const { account } = useWeb3React() + const { currencyChain, multiChainMap } = useTDPContext() + + const pageChainBalance = multiChainMap[currencyChain]?.balance + const otherChainBalances: PortfolioTokenBalancePartsFragment[] = [] + for (const [key, value] of Object.entries(multiChainMap)) { + if (key !== currencyChain && value?.balance !== undefined) { + otherChainBalances.push(value.balance) + } + } + const hasBalances = pageChainBalance || Boolean(otherChainBalances.length) + + if (!account || !hasBalances) { + return null + } + return ( + + + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ChartSection/AdvancedPriceChartToggle.tsx b/apps/web/src/components/Tokens/TokenDetails/ChartSection/AdvancedPriceChartToggle.tsx new file mode 100644 index 0000000..ca7f4d1 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ChartSection/AdvancedPriceChartToggle.tsx @@ -0,0 +1,61 @@ +import { t } from '@lingui/macro' +import { ReactComponent as CandlestickChartIcon } from 'assets/svg/candlestick-chart-icon.svg' +import { ReactComponent as LineChartIcon } from 'assets/svg/line-chart-icon.svg' +import { CHART_TYPE_LABELS, PriceChartType } from 'components/Charts/utils' +import Row from 'components/Row' +import { useScreenSize } from 'hooks/useScreenSize' +import styled from 'styled-components' +import { EllipsisStyle } from 'theme/components' + +import { ChartTypeDropdown } from './ChartTypeSelector' + +const ChartTypeRow = styled(Row)` + ${EllipsisStyle} +` +const ADVANCED_PRICE_CHART_OPTIONS = [ + { + value: PriceChartType.LINE, + icon: , + display: ( + + + {CHART_TYPE_LABELS[PriceChartType.LINE]} + + ), + }, + { + value: PriceChartType.CANDLESTICK, + icon: , + display: ( + + + {CHART_TYPE_LABELS[PriceChartType.CANDLESTICK]} + + ), + }, +] + +export const AdvancedPriceChartToggle = ({ + currentChartType, + onChartTypeChange, + disableCandlestickUI, +}: { + currentChartType: PriceChartType + onChartTypeChange: (c: PriceChartType) => void + disableCandlestickUI?: boolean +}) => { + const screenSize = useScreenSize() + const isMobileScreen = !screenSize['sm'] + const currentChartTypeDisplayOptions = ADVANCED_PRICE_CHART_OPTIONS.find((o) => o.value === currentChartType) + + return ( + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ChartSection/ChartTypeSelector.tsx b/apps/web/src/components/Tokens/TokenDetails/ChartSection/ChartTypeSelector.tsx new file mode 100644 index 0000000..cc33796 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ChartSection/ChartTypeSelector.tsx @@ -0,0 +1,92 @@ +import { Trans } from '@lingui/macro' +import { CHART_TYPE_LABELS, ChartType, PriceChartType } from 'components/Charts/utils' +import { DropdownSelector, InternalMenuItem } from 'components/DropdownSelector' +import { MouseoverTooltip } from 'components/Tooltip' +import { useReducer } from 'react' +import { Check, Info } from 'react-feather' +import { css, useTheme } from 'styled-components' + +import { isMobile } from 'uniswap/src/utils/platform' + +const StyledDropdownButton = css` + border-radius: 20px; + width: 100%; + height: 36px; +` +const StyledMenuFlyout = css` + min-width: 130px; + border-radius: 16px; + right: 0px; +` + +interface ChartTypeSelectorOption { + value: T // Value to be selected/stored, used as default display value + display?: JSX.Element // Optional custom display element +} + +function getChartTypeSelectorOption( + option: ChartTypeSelectorOption | T +): ChartTypeSelectorOption { + if (typeof option === 'string') { + return { value: option } + } + return option +} + +export function ChartTypeDropdown({ + options, + disabledOption, + menuLabel, + currentChartType, + onSelectOption, + tooltipText, +}: { + options: readonly (ChartTypeSelectorOption | T)[] + disabledOption?: T + menuLabel?: JSX.Element + currentChartType: T + onSelectOption: (option: T) => void + tooltipText?: string +}) { + const theme = useTheme() + const [isMenuOpen, toggleMenu] = useReducer((s) => !s, false) + + return ( + + {options.map((option) => { + const { value: chartType, display } = getChartTypeSelectorOption(option) + const disabled = chartType === disabledOption + return ( + This setting is unavailable for the current chart} + placement={!isMobile ? 'right' : undefined} + > + { + if (disabled) return + onSelectOption(chartType) + toggleMenu() + }} + disabled={disabled} + > + {display ?? CHART_TYPE_LABELS[chartType]} + {chartType === currentChartType && } + {disabled && } + + + ) + })} + + } + tooltipText={tooltipText} + buttonCss={StyledDropdownButton} + menuFlyoutCss={StyledMenuFlyout} + /> + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ChartSection/hooks.ts b/apps/web/src/components/Tokens/TokenDetails/ChartSection/hooks.ts new file mode 100644 index 0000000..5b6a4a2 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ChartSection/hooks.ts @@ -0,0 +1,182 @@ +import { PriceChartData } from 'components/Charts/PriceChart' +import { StackedLineData } from 'components/Charts/StackedLineChart' +import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer' +import { ChartType, PriceChartType } from 'components/Charts/utils' +import { + CandlestickOhlcFragment, + Chain, + HistoryDuration, + PriceHistoryFallbackFragment, + useTokenHistoricalTvlsQuery, + useTokenHistoricalVolumesQuery, + useTokenPriceQuery, +} from 'graphql/data/__generated__/types-and-hooks' +import { UTCTimestamp } from 'lightweight-charts' +import { useMemo, useReducer } from 'react' +import { ChartQueryResult, DataQuality, checkDataQuality, withUTCTimestamp } from './util' + +type TDPChartQueryVariables = { chain: Chain; address?: string; duration: HistoryDuration } + +function fallbackToPriceChartData(priceHistoryEntry: PriceHistoryFallbackFragment): PriceChartData { + const { value, timestamp } = priceHistoryEntry + const time = timestamp as UTCTimestamp + return { time, value, open: value, high: value, low: value, close: value } +} + +function toPriceChartData(ohlc: CandlestickOhlcFragment): PriceChartData { + const { open, high, low, close } = ohlc + const time = ohlc.timestamp as UTCTimestamp + return { time, value: close.value, open: open.value, high: high.value, low: low.value, close: close.value } +} + +const currentTimeSeconds = () => (Date.now() / 1000) as UTCTimestamp + +const CANDLESTICK_FALLBACK_THRESHOLD = 0.1 +export function useTDPPriceChartData( + variables: TDPChartQueryVariables, + skip: boolean, + priceChartType: PriceChartType +): ChartQueryResult & { disableCandlestickUI: boolean } { + const [fallback, enablePriceHistoryFallback] = useReducer(() => true, false) + const { data, loading } = useTokenPriceQuery({ variables: { ...variables, fallback }, skip }) + + return useMemo(() => { + const { ohlc, priceHistory, price } = data?.token?.market ?? {} + let entries = (ohlc ? ohlc?.map(toPriceChartData) : priceHistory?.map(fallbackToPriceChartData)) ?? [] + const currentPrice = price?.value + + if (ohlc) { + // Special case: backend returns invalid OHLC data on some chains. If we detect long series of 0's, return an empty array to trigger fallback. + const zeroCount = entries.filter((x) => x.value === 0).length + if (!ohlc.length || zeroCount / entries.length > CANDLESTICK_FALLBACK_THRESHOLD) { + enablePriceHistoryFallback() // triggers a re-fetch that uses priceHistory instead of OHLC + return { + chartType: ChartType.PRICE, + entries: [], + loading: true, + disableCandlestickUI: true, + dataQuality: DataQuality.INVALID, + } + } + + // For line charts made using ohlc data, the min and max entries should point to their low/high, rather than close, + // to ensure the chart line makes contact with the min/max lines. + if (priceChartType === PriceChartType.LINE) { + let min = entries[0].low + let minIndex = 0 + let max = entries[0].high + let maxIndex = 0 + + entries.forEach((entry, index) => { + if (entry.low < min) { + min = entry.low + minIndex = index + } + if (entry.high > max) { + max = entry.high + maxIndex = index + } + }) + // Avoid modifying the last entry, as it should point to the current price + if (minIndex !== entries.length - 1) entries[minIndex].value = min + if (maxIndex !== entries.length - 1) entries[maxIndex].value = max + } + // Special case: backend data for OHLC data is currently too granular, so points should be combined, halving the data + else if (priceChartType === PriceChartType.CANDLESTICK) { + const combinedEntries = [] + + const startIndex = entries.length % 2 // If the length is odd, start at the second entry + for (let i = startIndex; i < entries.length; i += 2) { + const first = entries[i] + const second = entries[i + 1] + const combined = { + time: first.time, + open: first.open, + high: Math.max(first.high, second.high), + low: Math.min(first.low, second.low), + close: second.close, + value: second.close, + } + combinedEntries.push(combined) + } + entries = combinedEntries + } + } + + // Append current price to end of array to ensure data freshness and that each time period ends with same price + if (currentPrice && entries.length > 1) { + const lastEntry = entries[entries.length - 1] + const secondToLastEntry = entries[entries.length - 2] + const granularity = lastEntry.time - secondToLastEntry.time + + const time = currentTimeSeconds() + // If the current price falls within the last entry's time window, update the last entry's close price + if (time - lastEntry.time < granularity) { + lastEntry.time = time + lastEntry.value = currentPrice + lastEntry.close = currentPrice + } else { + // If the current price falls outside the last entry's time window, add it as a new entry + entries.push({ + time, + value: currentPrice, + open: currentPrice, + high: currentPrice, + low: currentPrice, + close: currentPrice, + }) + } + } + + const dataQuality = checkDataQuality(entries, ChartType.PRICE, variables.duration) + return { chartType: ChartType.PRICE, entries, loading, dataQuality, disableCandlestickUI: fallback } + }, [data?.token?.market, fallback, loading, priceChartType, variables.duration]) +} + +export function useTDPVolumeChartData( + variables: TDPChartQueryVariables, + skip: boolean +): ChartQueryResult { + const { data, loading } = useTokenHistoricalVolumesQuery({ variables, skip }) + return useMemo(() => { + const entries = data?.token?.market?.historicalVolume?.map(withUTCTimestamp) ?? [] + const dataQuality = checkDataQuality(entries, ChartType.VOLUME, variables.duration) + return { chartType: ChartType.VOLUME, entries, loading, dataQuality } + }, [data?.token?.market?.historicalVolume, loading, variables.duration]) +} + +function toStackedLineData(entry: { timestamp: number; value: number }): StackedLineData { + return { values: [entry.value], time: entry.timestamp as UTCTimestamp } +} + +export function useTDPTVLChartData( + variables: TDPChartQueryVariables, + skip: boolean +): ChartQueryResult { + const { data, loading } = useTokenHistoricalTvlsQuery({ variables, skip }) + return useMemo(() => { + const { historicalTvl, totalValueLocked } = data?.token?.market ?? {} + const entries = historicalTvl?.map(toStackedLineData) ?? [] + const currentTvl = totalValueLocked?.value + + // Append current tvl to end of array to ensure data freshness and that each time period ends with same tvl + if (currentTvl && entries.length > 1) { + const lastEntry = entries[entries.length - 1] + const secondToLastEntry = entries[entries.length - 2] + const granularity = lastEntry.time - secondToLastEntry.time + + const time = currentTimeSeconds() + // If the current tvl falls within the last entry's time window, update the last entry's tvl + if (time - lastEntry.time < granularity) { + lastEntry.time = time + lastEntry.values = [currentTvl] + } else { + // If the current tvl falls outside the last entry's time window, add it as a new entry + entries.push({ time, values: [currentTvl] }) + } + } + + const dataQuality = checkDataQuality(entries, ChartType.TVL, variables.duration) + return { chartType: ChartType.TVL, entries, loading, dataQuality } + }, [data?.token?.market, loading, variables.duration]) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ChartSection/index.tsx b/apps/web/src/components/Tokens/TokenDetails/ChartSection/index.tsx new file mode 100644 index 0000000..e800685 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ChartSection/index.tsx @@ -0,0 +1,211 @@ +import { PriceChart, PriceChartData } from 'components/Charts/PriceChart' +import { LineChart, StackedLineData } from 'components/Charts/StackedLineChart' +import { refitChartContentAtom } from 'components/Charts/TimeSelector' +import { ChartType, PriceChartType } from 'components/Charts/utils' +import { VolumeChart } from 'components/Charts/VolumeChart' +import { TimePeriod, toHistoryDuration } from 'graphql/data/util' +import { useMemo, useState } from 'react' +import styled from 'styled-components' + +import { Trans } from '@lingui/macro' +import { ChartSkeleton } from 'components/Charts/LoadingState' +import { SingleHistogramData } from 'components/Charts/VolumeChart/renderer' +import PillMultiToggle, { PillMultiToggleOption } from 'components/Toggle/PillMultiToggle' +import { + DISPLAYS, + getTimePeriodFromDisplay, + ORDERED_TIMES, + TimePeriodDisplay, +} from 'components/Tokens/TokenTable/TimeSelector' +import { Chain } from 'graphql/data/__generated__/types-and-hooks' +import { useAtomValue } from 'jotai/utils' +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { AdvancedPriceChartToggle } from './AdvancedPriceChartToggle' +import { ChartTypeDropdown } from './ChartTypeSelector' +import { useTDPPriceChartData, useTDPTVLChartData, useTDPVolumeChartData } from './hooks' +import { ChartQueryResult, DataQuality } from './util' + +export const TDP_CHART_HEIGHT_PX = 356 +const TDP_CHART_SELECTOR_OPTIONS = [ChartType.PRICE, ChartType.VOLUME, ChartType.TVL] as const +type TokenDetailsChartType = (typeof TDP_CHART_SELECTOR_OPTIONS)[number] + +export const DEFAULT_PILL_TIME_SELECTOR_OPTIONS = ORDERED_TIMES.map((time: TimePeriod) => ({ + value: DISPLAYS[time], +})) as PillMultiToggleOption[] + +export const ChartActionsContainer = styled.div` + display: flex; + flex-direction: row-reverse; + width: 100%; + justify-content: space-between; + align-items: center; + margin-top: 12px; + + @media only screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + flex-direction: column; + gap: 16px; + } +` +const TimePeriodSelectorContainer = styled.div` + @media only screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + width: 100%; + } +` + +const ChartTypeSelectorContainer = styled.div` + display: flex; + gap: 8px; + @media only screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + width: 100%; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + } +` + +/** Represents a variety of query result shapes, discriminated via additional `chartType` field. */ +type ActiveQuery = + | ChartQueryResult + | ChartQueryResult + | ChartQueryResult + +export type TDPChartState = { + /** Time controls for TDP Charts */ + timePeriod: TimePeriod + setTimePeriod: (timePeriod: TimePeriod) => void + /** Selectors for TDP Charts */ + setChartType: (chartType: TokenDetailsChartType) => void + priceChartType: PriceChartType + setPriceChartType: (priceChartType: PriceChartType) => void + activeQuery: ActiveQuery + /** Special-case: flag to disable candlestick toggle on tokens with invalid OHLC data */ + disableCandlestickUI: boolean +} + +const InvalidChartMessage = () => Unable to display historical data for the current token. + +/** Exported to `TDPContext` to fire queries on pageload. `TDPChartState` should be accessed through `useTDPContext` rather than this hook. */ +export function useCreateTDPChartState(tokenDBAddress: string | undefined, currencyChainName: Chain): TDPChartState { + const [timePeriod, setTimePeriod] = useState(TimePeriod.DAY) + + const [chartType, setChartType] = useState(ChartType.PRICE) + const [priceChartType, setPriceChartType] = useState(PriceChartType.LINE) + + const variables = { address: tokenDBAddress, chain: currencyChainName, duration: toHistoryDuration(timePeriod) } + + const priceQuery = useTDPPriceChartData(variables, chartType !== ChartType.PRICE, priceChartType) + const volumeQuery = useTDPVolumeChartData(variables, chartType !== ChartType.VOLUME) + const tvlQuery = useTDPTVLChartData(variables, chartType !== ChartType.TVL) + + return useMemo(() => { + const { disableCandlestickUI } = priceQuery + const activeQuery = (() => { + switch (chartType) { + case ChartType.PRICE: + return priceQuery + case ChartType.VOLUME: + return volumeQuery + case ChartType.TVL: + return tvlQuery + } + })() + + return { + timePeriod, + setTimePeriod, + setChartType, + priceChartType: disableCandlestickUI ? PriceChartType.LINE : priceChartType, + setPriceChartType, + activeQuery, + disableCandlestickUI, + } + }, [chartType, priceQuery, volumeQuery, tvlQuery, timePeriod, priceChartType]) +} + +export default function ChartSection() { + const { activeQuery, timePeriod, priceChartType } = useTDPContext().chartState + + return ( +
+ {(() => { + if (activeQuery.dataQuality === DataQuality.INVALID) { + return ( + } + /> + ) + } + + const stale = activeQuery.dataQuality === DataQuality.STALE + switch (activeQuery.chartType) { + case ChartType.PRICE: + return ( + + ) + case ChartType.VOLUME: + return ( + + ) + case ChartType.TVL: + return + } + })()} + +
+ ) +} + +function ChartControls() { + const { + activeQuery, + timePeriod, + setTimePeriod, + setChartType, + priceChartType, + setPriceChartType, + disableCandlestickUI, + } = useTDPContext().chartState + const refitChartContent = useAtomValue(refitChartContentAtom) + + return ( + + + {activeQuery.chartType === ChartType.PRICE && ( + + )} + { + setChartType(c) + if (c === ChartType.PRICE) setPriceChartType(PriceChartType.LINE) + }} + /> + + + { + const time = getTimePeriodFromDisplay(option as TimePeriodDisplay) + if (time === timePeriod) { + refitChartContent?.() + } else { + setTimePeriod(time) + } + }} + /> + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ChartSection/util.ts b/apps/web/src/components/Tokens/TokenDetails/ChartSection/util.ts new file mode 100644 index 0000000..703eee8 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ChartSection/util.ts @@ -0,0 +1,63 @@ +import { ChartType } from 'components/Charts/utils' +import { HistoryDuration } from 'graphql/data/__generated__/types-and-hooks' +import { UTCTimestamp } from 'lightweight-charts' +import ms from 'ms' + +export type ChartQueryResult = { + chartType: TChartType + entries: TDataType[] + loading: boolean + dataQuality: DataQuality +} + +export enum DataQuality { + VALID, + INVALID, + STALE, +} + +/** Used for expecting the same data freshness regardless of time period, e.g. 1y price chart should still have a recent point */ +const CONSTANT_STALENESS: Partial> = { + [HistoryDuration.Hour]: ms('15m'), + [HistoryDuration.Day]: ms('15m'), + [HistoryDuration.Week]: ms('15m'), + [HistoryDuration.Month]: ms('15m'), + [HistoryDuration.Year]: ms('15m'), +} + +/** Used decreasing freshness regardless of time period, e.g. 1h volume chart has more recent data than 1y volume chart */ +const GRANULAR_STALENESS: Partial> = { + [HistoryDuration.Hour]: ms('15m'), + [HistoryDuration.Day]: ms('4h'), + [HistoryDuration.Week]: ms('1d'), + [HistoryDuration.Month]: ms('4d'), + [HistoryDuration.Year]: ms('30d'), +} + +/** Maps from `ChartType` and `HistoryDuration` to expected data freshness threshold */ +const CHART_DURATION_STALE_THRESHOLD_MAP: Record | undefined>> = { + [ChartType.PRICE]: CONSTANT_STALENESS, + [ChartType.VOLUME]: GRANULAR_STALENESS, + [ChartType.TVL]: CONSTANT_STALENESS, + // Liquidity chart does not have a time axis + [ChartType.LIQUIDITY]: undefined, +} + +export function checkDataQuality( + data: { time: number }[], + chartType: ChartType, + duration: HistoryDuration +): DataQuality { + if (data.length < 3) return DataQuality.INVALID + const timeInMs = data[data.length - 1].time * 1000 + const stalenessThreshold = CHART_DURATION_STALE_THRESHOLD_MAP[chartType]?.[duration] + if (!stalenessThreshold || Date.now() - timeInMs < stalenessThreshold) { + return DataQuality.VALID + } else { + return DataQuality.STALE + } +} + +export function withUTCTimestamp(entry: T): T & { time: UTCTimestamp } { + return { ...entry, time: entry.timestamp as UTCTimestamp } +} diff --git a/apps/web/src/components/Tokens/TokenDetails/Delta.tsx b/apps/web/src/components/Tokens/TokenDetails/Delta.tsx new file mode 100644 index 0000000..c59b605 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/Delta.tsx @@ -0,0 +1,40 @@ +import { ArrowChangeDown } from 'components/Icons/ArrowChangeDown' +import { ArrowChangeUp } from 'components/Icons/ArrowChangeUp' +import styled from 'styled-components' + +const StyledUpArrow = styled(ArrowChangeUp)<{ $noColor?: boolean }>` + color: ${({ theme, $noColor }) => ($noColor ? theme.neutral2 : theme.success)}; +` +const StyledDownArrow = styled(ArrowChangeDown)<{ $noColor?: boolean }>` + color: ${({ theme, $noColor }) => ($noColor ? theme.neutral2 : theme.critical)}; +` + +export function calculateDelta(start: number, current: number) { + return (current / start - 1) * 100 +} + +function isValidDelta(delta: number | null | undefined): delta is number { + // Null-check not including zero + return delta !== null && delta !== undefined && delta !== Infinity && !isNaN(delta) +} + +interface DeltaArrowProps { + delta?: number | null + noColor?: boolean + size?: number +} + +export function DeltaArrow({ delta, noColor = false, size = 16 }: DeltaArrowProps) { + if (!isValidDelta(delta)) return null + + return Math.sign(delta) < 0 ? ( + + ) : ( + + ) +} + +export const DeltaText = styled.span<{ delta?: number }>` + color: ${({ theme, delta }) => + delta !== undefined ? (Math.sign(delta) < 0 ? theme.critical : theme.success) : theme.neutral1}; +` diff --git a/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx b/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx new file mode 100644 index 0000000..4af4e1b --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/InvalidTokenDetails.tsx @@ -0,0 +1,88 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { ButtonPrimary } from 'components/Button' +import { getChainInfo } from 'constants/chainInfo' +import useSelectChain from 'hooks/useSelectChain' +import { useNavigate } from 'react-router-dom' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +import { ReactComponent as EyeIcon } from '../../../assets/svg/eye.svg' + +const InvalidDetailsContainer = styled.div` + padding-top: 128px; + display: flex; + flex-direction: column; + align-items: center; +` + +const InvalidDetailsText = styled.span` + margin-top: 28px; + margin-bottom: 20px; + + text-align: center; + + color: ${({ theme }) => theme.neutral2}; + font-size: 20px; + font-weight: 535; + line-height: 28px; +` + +const TokenExploreButton = styled(ButtonPrimary)` + width: fit-content; + padding: 12px 16px; + border-radius: 12px; + + color: ${({ theme }) => theme.neutral1}; + font-size: 16px; + font-weight: 535; +` + +export default function InvalidTokenDetails({ + pageChainId, + isInvalidAddress, +}: { + pageChainId: ChainId + isInvalidAddress?: boolean +}) { + const { chainId } = useWeb3React() + const navigate = useNavigate() + const selectChain = useSelectChain() + + // if the token's address is valid and the chains match, it's a non-existant token + const isNonExistentToken = !isInvalidAddress && pageChainId === chainId + + const connectedChainLabel = chainId ? getChainInfo(chainId)?.label : undefined + + return ( + + + {isInvalidAddress || isNonExistentToken ? ( + <> + + This token doesn't exist + + navigate('/tokens')}> + + Explore tokens + + + + ) : ( + <> + {connectedChainLabel && ( + + This token doesn't exist on {connectedChainLabel} + + )} + selectChain(pageChainId)}> + + Switch to {getChainInfo(pageChainId).label} + + + + )} + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/MobileBalanceSummaryFooter.tsx b/apps/web/src/components/Tokens/TokenDetails/MobileBalanceSummaryFooter.tsx new file mode 100644 index 0000000..c25d43d --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/MobileBalanceSummaryFooter.tsx @@ -0,0 +1,120 @@ +import { Trans } from '@lingui/macro' +import { useWeb3React } from '@web3-react/core' +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { CHAIN_ID_TO_BACKEND_NAME } from 'graphql/data/util' +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import styled from 'styled-components' +import { StyledInternalLink, ThemedText } from 'theme/components' +import { Z_INDEX } from 'theme/zIndex' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +const Wrapper = styled.div` + align-content: center; + align-items: center; + background-color: ${({ theme }) => theme.surface1}; + border: 1px solid ${({ theme }) => theme.surface3}; + color: ${({ theme }) => theme.neutral2}; + display: none; + flex-direction: row; + font-weight: 535; + font-size: 14px; + height: fit-content; + justify-content: space-between; + left: 0; + line-height: 20px; + position: fixed; + z-index: ${Z_INDEX.sticky}; + border-radius: 20px; + bottom: 56px; + margin: 8px; + padding: 12px 32px; + width: calc(100vw - 16px); + + @media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) { + bottom: 0px; + } + @media screen and (max-width: ${({ theme }) => theme.breakpoint.lg}px) { + display: flex; + } +` +const BalanceValue = styled.div` + color: ${({ theme }) => theme.neutral1}; + font-size: 20px; + line-height: 20px; + display: flex; + gap: 8px; +` +const Balance = styled.div` + align-items: flex-end; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 8px; +` +const BalanceInfo = styled.div` + display: flex; + flex: 10 1 auto; + flex-direction: column; + justify-content: flex-start; + gap: 6px; +` +const FiatValue = styled(ThemedText.Caption)` + font-size: 12px; + line-height: 16px; + + @media screen and (min-width: ${({ theme }) => theme.breakpoint.sm}px) { + line-height: 24px; + } +` +const SwapButton = styled(StyledInternalLink)` + background-color: ${({ theme }) => theme.accent1}; + border: none; + border-radius: 22px; + color: ${({ theme }) => theme.neutralContrast}; + display: flex; + flex: 1 1 auto; + padding: 12px 16px; + font-size: 16px; + font-weight: 535; + height: 44px; + justify-content: center; + margin: auto; + max-width: 100vw; +` + +export default function MobileBalanceSummaryFooter() { + const { currency, multiChainMap, currencyChain } = useTDPContext() + const pageChainBalance = multiChainMap[currencyChain]?.balance + + const { account } = useWeb3React() + const { formatNumber } = useFormatter() + + const formattedGqlBalance = formatNumber({ + input: pageChainBalance?.quantity, + type: NumberType.TokenNonTx, + }) + const formattedUsdGqlValue = formatNumber({ + input: pageChainBalance?.denominatedValue?.value, + type: NumberType.PortfolioBalance, + }) + const chain = CHAIN_ID_TO_BACKEND_NAME[currency.chainId].toLowerCase() + + return ( + + {Boolean(account && pageChainBalance) && ( + + Your balance + + + {formattedGqlBalance} {currency.symbol} + + {formattedUsdGqlValue} + + + )} + + Swap + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/Resource.tsx b/apps/web/src/components/Tokens/TokenDetails/Resource.tsx new file mode 100644 index 0000000..cbeb114 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/Resource.tsx @@ -0,0 +1,27 @@ +import { darken } from 'polished' +import styled from 'styled-components' +import { ExternalLink } from 'theme/components' + +const ResourceLink = styled(ExternalLink)` + display: flex; + color: ${({ theme }) => theme.accent1}; + font-weight: 535; + font-size: 14px; + line-height: 20px; + gap: 4px; + text-decoration: none; + + &:hover, + &:focus { + color: ${({ theme }) => darken(0.1, theme.accent1)}; + text-decoration: none; + } +` +export default function Resource({ name, link }: { name: string; link: string }) { + return ( + + {name} + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/ShareButton.tsx b/apps/web/src/components/Tokens/TokenDetails/ShareButton.tsx new file mode 100644 index 0000000..423b25c --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/ShareButton.tsx @@ -0,0 +1,100 @@ +import { Trans, t } from '@lingui/macro' +import { DropdownSelector } from 'components/DropdownSelector' +import { CheckMark } from 'components/Icons/CheckMark' +import { Share as ShareIcon } from 'components/Icons/Share' +import { TwitterXLogo } from 'components/Icons/TwitterX' +import { ActionButtonStyle, ActionMenuFlyoutStyle } from 'components/Tokens/TokenDetails/shared' +import useCopyClipboard from 'hooks/useCopyClipboard' +import useDisableScrolling from 'hooks/useDisableScrolling' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { useRef } from 'react' +import { Link } from 'react-feather' +import { useModalIsOpen, useToggleModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import styled, { useTheme } from 'styled-components' +import { colors } from 'theme/colors' +import { ThemedText } from 'theme/components' +import { opacify } from 'theme/utils' + +const TWITTER_WIDTH = 560 +const TWITTER_HEIGHT = 480 + +const ShareAction = styled.div` + display: flex; + align-items: center; + padding: 8px; + border-radius: 8px; + font-size: 16px; + font-weight: 485; + gap: 12px; + height: 40px; + color: ${({ theme }) => theme.neutral1}; + cursor: pointer; + :hover { + background-color: ${({ theme }) => opacify(10, theme.darkMode ? colors.gray200 : colors.gray300)}; + } +` + +export function openShareTweetWindow(name: string) { + const currentLocation = window.location.href + const positionX = (window.screen.width - TWITTER_WIDTH) / 2 + const positionY = (window.screen.height - TWITTER_HEIGHT) / 2 + window.open( + `https://twitter.com/intent/tweet?text=Check%20out%20${name}%20${currentLocation}%20via%20@Uniswap`, + 'newwindow', + `left=${positionX}, top=${positionY}, width=${TWITTER_WIDTH}, height=${TWITTER_HEIGHT}` + ) +} + +export default function ShareButton({ name }: { name: string }) { + const theme = useTheme() + const node = useRef(null) + const open = useModalIsOpen(ApplicationModal.SHARE) + const toggleShare = useToggleModal(ApplicationModal.SHARE) + useOnClickOutside(node, open ? toggleShare : undefined) + + useDisableScrolling(open) + + const currentLocation = window.location.href + + const [isCopied, setCopied] = useCopyClipboard() + + return ( +
+ } + tooltipText={t`Share`} + internalMenuItems={ + <> + setCopied(currentLocation)}> + {isCopied ? ( + + ) : ( + + )} + + {isCopied ? Copied : Copy link} + + + { + toggleShare() + openShareTweetWindow(name) + }} + > + + + Share to Twitter + + + + } + hideChevron + buttonCss={ActionButtonStyle} + menuFlyoutCss={ActionMenuFlyoutStyle} + /> +
+ ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx b/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx new file mode 100644 index 0000000..96a3445 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/Skeleton.test.tsx @@ -0,0 +1,23 @@ +import { ChainId } from '@uniswap/sdk-core' +import { USDC_MAINNET } from 'constants/tokens' +import { render } from 'test-utils/render' + +import { getLoadingTitle, TokenDetailsPageSkeleton } from './Skeleton' + +describe('TDP Skeleton', () => { + it('should render correctly', () => { + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + }) +}) + +describe('getLoadingTitle', () => { + it('should return correct title', () => { + const { asFragment } = render( + <>{getLoadingTitle(USDC_MAINNET, USDC_MAINNET.address, ChainId.MAINNET, 'ethereum')} + ) + expect(asFragment()).toMatchSnapshot() + expect(asFragment().textContent).toContain('token data for') + expect(asFragment().textContent).toContain('on Ethereum') + }) +}) diff --git a/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx b/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx new file mode 100644 index 0000000..a841ddb --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/Skeleton.tsx @@ -0,0 +1,313 @@ +import { Trans } from '@lingui/macro' +import { Currency } from '@uniswap/sdk-core' +import { BreadcrumbNavContainer, BreadcrumbNavLink } from 'components/BreadcrumbNav' +import Row from 'components/Row' +import { SwapSkeleton } from 'components/swap/SwapSkeleton' +import { supportedChainIdFromGQLChain, validateUrlChainParam } from 'graphql/data/util' +import { useCurrency } from 'hooks/Tokens' +import { ReactNode } from 'react' +import { ChevronRight } from 'react-feather' +import { useParams } from 'react-router-dom' +import styled, { css } from 'styled-components' +import { BREAKPOINTS } from 'theme' +import { ClickableStyle } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { capitalize } from 'tsafe' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { ChartSkeleton } from 'components/Charts/LoadingState' +import { ChartType } from 'components/Charts/utils' +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { LoadingBubble } from '../loading' +import { AboutContainer, AboutHeader } from './About' +import { TDP_CHART_HEIGHT_PX } from './ChartSection' +import { StatPair, StatWrapper, StatsWrapper } from './StatsSection' +import { Hr } from './shared' + +const SWAP_COMPONENT_WIDTH = 360 + +export const TokenDetailsLayout = styled.div` + display: flex; + padding: 0 16px 52px; + justify-content: center; + width: 100%; + gap: 40px; + + @media screen and (min-width: ${({ theme }) => theme.breakpoint.md}px) { + padding: 48px 20px; + } + @media screen and (max-width: ${({ theme }) => theme.breakpoint.lg}px) { + flex-direction: column; + align-items: center; + } + @media screen and (min-width: ${({ theme }) => theme.breakpoint.xl}px) { + gap: 60px; + } +` + +export const LeftPanel = styled.div` + flex: 1; + max-width: 780px; + overflow: hidden; + width: 100%; +` +export const RightPanel = styled.div` + display: flex; + padding-top: 53px; + flex-direction: column; + gap: 40px; + width: ${SWAP_COMPONENT_WIDTH}px; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.lg}px) { + width: 100%; + max-width: 780px; + } +` + +export const TokenInfoContainer = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 8px; + margin-bottom: 20px; + gap: 20px; + ${textFadeIn}; + animation-duration: ${({ theme }) => theme.transition.duration.medium}; +` +export const TokenNameCell = styled.div` + display: flex; + gap: 12px; + font-size: 20px; + line-height: 28px; + align-items: center; + padding-top: 4px; + min-width: 32px; + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + flex-direction: column; + align-items: flex-start; + } +` +/* Loading state bubbles */ +const DetailBubble = styled(LoadingBubble)` + height: 16px; + width: 180px; +` +const SquaredBubble = styled(DetailBubble)` + height: 32px; + border-radius: 8px; +` +const NavBubble = styled(DetailBubble)` + width: 169px; +` +const TokenLogoBubble = styled(DetailBubble)` + width: 32px; + height: 32px; + border-radius: 50%; +` +const TitleBubble = styled(DetailBubble)` + height: 36px; + width: 136px; +` + +const SectionBubble = styled(SquaredBubble)` + width: 120px; +` +const StatTitleBubble = styled(DetailBubble)` + width: 80px; + margin-bottom: 4px; +` +const StatBubble = styled(SquaredBubble)` + width: 116px; +` +const WideBubble = styled(DetailBubble)` + margin-bottom: 6px; + width: 100%; +` + +const ThinTitleBubble = styled(WideBubble)` + width: 120px; +` + +const HalfWideBubble = styled(WideBubble)` + width: 50%; +` + +const StatsLoadingContainer = styled.div` + width: 100%; + display: flex; + flex-wrap: wrap; +` + +const ExtraDetailsContainer = styled.div` + padding-top: 24px; +` + +const Space = styled.div<{ heightSize: number }>` + height: ${({ heightSize }) => `${heightSize}px`}; +` + +const loadingFooterTextCss = css` + color: ${({ theme }) => theme.neutral3}; + font-size: 12px; + font-weight: 500; + line-height: 16px; + text-decoration: none; +` + +const LoadingFooterHeaderContainer = styled(Row)` + align-items: center; + ${loadingFooterTextCss} + + @media screen and (min-width: ${BREAKPOINTS.md}px) { + padding: 16px 90px 8px 0; + position: fixed; + bottom: 0; + right: 0; + justify-content: flex-end; + } +` + +const LoadingFooterHeader = styled.h1` + ${loadingFooterTextCss} +` + +const LoadingFooterLink = styled.a` + ${loadingFooterTextCss} + ${ClickableStyle} +` + +// exported for testing +export function getLoadingTitle( + token: Currency | undefined, + tokenAddress: string, + chainId: number, + chainName: string | undefined +): ReactNode { + let tokenName = '' + if (token?.name && token?.symbol) { + tokenName = `${token?.name} (${token?.symbol})` + } else if (token?.name) { + tokenName = token?.name + } else if (token?.symbol) { + tokenName = token?.symbol + } else { + tokenName = tokenAddress || '' + } + const chainSuffix = chainName ? ` on ${capitalize(chainName)}` : '' + const tokenLink = token?.isNative ? ( + tokenName + ) : ( + + {tokenName} + + ) + return ( + + token data for {tokenLink} + {chainSuffix} + + ) +} + +export function LoadingChart() { + return +} + +function LoadingStats() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +/* Loading State: row component with loading bubbles */ +function TokenDetailsSkeleton() { + const { chainName, tokenAddress } = useParams<{ chainName?: string; tokenAddress?: string }>() + const chainId = supportedChainIdFromGQLChain(validateUrlChainParam(chainName)) + const token = useCurrency(tokenAddress === NATIVE_CHAIN_ID ? 'ETH' : tokenAddress, chainId) + + return ( + + + + Explore + + + Tokens + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + {tokenAddress && ( + + Loading + {getLoadingTitle(token, tokenAddress, chainId, chainName)} + + )} +
+ ) +} + +export function TokenDetailsPageSkeleton() { + return ( + + + + + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx new file mode 100644 index 0000000..da2c160 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/StatsSection.tsx @@ -0,0 +1,174 @@ +import { Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { MouseoverTooltip } from 'components/Tooltip' +import { getChainInfo } from 'constants/chainInfo' +import { TokenQueryData } from 'graphql/data/Token' +import { ReactNode } from 'react' +import styled from 'styled-components' +import { ExternalLink, ThemedText } from 'theme/components' +import { textFadeIn } from 'theme/styles' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { HEADER_DESCRIPTIONS } from 'components/Tokens/TokenTable' +import { UNSUPPORTED_METADATA_CHAINS } from '../constants' +import { TokenSortMethod } from '../state' + +export const StatWrapper = styled.div` + color: ${({ theme }) => theme.neutral2}; + font-size: 14px; + min-width: 121px; + flex: 1; + padding-top: 24px; + padding-bottom: 0px; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + min-width: 168px; + } +` +const TokenStatsSection = styled.div` + display: flex; + flex-wrap: wrap; +` +export const StatPair = styled.div` + display: flex; + flex: 1; + flex-wrap: wrap; +` + +const Header = styled(ThemedText.MediumHeader)` + font-size: 28px !important; + padding-top: 40px; +` + +const StatPrice = styled.div` + margin-top: 4px; + font-size: 28px; + color: ${({ theme }) => theme.neutral1}; +` +const NoData = styled.div` + color: ${({ theme }) => theme.neutral3}; + padding-top: 40px; +` +export const StatsWrapper = styled.div` + gap: 16px; + ${textFadeIn} +` + +type NumericStat = number | undefined | null + +function Stat({ + dataCy, + value, + title, + description, +}: { + dataCy: string + value: NumericStat + title: ReactNode + description?: ReactNode +}) { + const { formatNumber } = useFormatter() + + return ( + + + {title} + + + {formatNumber({ + input: value, + type: NumberType.FiatTokenStats, + })} + + + ) +} + +type StatsSectionProps = { + chainId: ChainId + address: string + tokenQueryData: TokenQueryData +} +export default function StatsSection(props: StatsSectionProps) { + const { chainId, address, tokenQueryData } = props + const { label, infoLink } = getChainInfo(chainId) + + const tokenMarketInfo = tokenQueryData?.market + const tokenProjectMarketInfo = tokenQueryData?.project?.markets?.[0] // aggregated market price from CoinGecko + + const FDV = tokenProjectMarketInfo?.fullyDilutedValuation?.value + const marketCap = tokenProjectMarketInfo?.marketCap?.value + const TVL = tokenMarketInfo?.totalValueLocked?.value + const volume24H = tokenMarketInfo?.volume24H?.value + + const hasStats = TVL || FDV || marketCap || volume24H + + if (hasStats) { + return ( + +
+ Stats +
+ + + + Total value locked (TVL) is the aggregate amount of the asset available across all Uniswap v3 + liquidity pools. + + } + title={TVL} + /> + Market capitalization is the total market value of an asset's circulating supply. + } + title={Market cap} + /> + + + FDV} + /> + + 1 day volume is the amount of the asset that has been traded on Uniswap v3 during the past 24 hours. + + } + title={1 day volume} + /> + + +
+ ) + } else { + return UNSUPPORTED_METADATA_CHAINS.includes(chainId) ? ( + <> +
+ Stats +
+ + + Token stats and charts for {label} are available on{' '} + + info.uniswap.org + + + + + ) : ( + No stats available + ) + } +} diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.test.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.test.tsx new file mode 100644 index 0000000..6edfe18 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.test.tsx @@ -0,0 +1,83 @@ +import userEvent from '@testing-library/user-event' +import { USDC_MAINNET } from 'constants/tokens' +import { Chain } from 'graphql/data/__generated__/types-and-hooks' +import { useCurrency } from 'hooks/Tokens' +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { mocked } from 'test-utils/mocked' +import { validUSDCCurrency } from 'test-utils/pools/fixtures' +import { act, render, screen } from 'test-utils/render' +import { validTokenProjectResponse } from 'test-utils/tokens/fixtures' +import { TokenDescription } from './TokenDescription' + +jest.mock('hooks/Tokens', () => { + const originalModule = jest.requireActual('hooks/Tokens') + return { + ...originalModule, + useCurrency: jest.fn(), + } +}) +jest.mock('pages/TokenDetails/TDPContext', () => ({ + useTDPContext: jest.fn(), +})) + +describe('TokenDescription', () => { + beforeEach(() => { + mocked(useCurrency).mockReturnValue(validUSDCCurrency) + }) + + it('renders token information correctly with defaults', () => { + mocked(useTDPContext).mockReturnValue({ + address: USDC_MAINNET.address, + currency: USDC_MAINNET, + currencyChainName: Chain.Ethereum, + tokenQuery: validTokenProjectResponse, + } as any) + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText('Info')).toBeVisible() + expect(screen.getByText('Website')).toBeVisible() + expect(screen.getByText('Twitter')).toBeVisible() + expect(screen.getByText('Etherscan')).toBeVisible() + expect(screen.getByText('0xA0b8...eB48')).toBeVisible() + }) + + it('truncates description and shows more', async () => { + mocked(useTDPContext).mockReturnValue({ + address: USDC_MAINNET.address, + currency: USDC_MAINNET, + currencyChainName: Chain.Ethereum, + tokenQuery: validTokenProjectResponse, + } as any) + const { asFragment } = render() + + expect(asFragment()).toMatchSnapshot() + const truncatedDescription = screen.getByTestId('token-description-truncated') + const fullDescription = screen.getByTestId('token-description-full') + + expect(truncatedDescription).toHaveStyleRule('display', 'inline') + expect(fullDescription).toHaveStyleRule('display', 'none') + + await act(() => userEvent.click(screen.getByText('Show more'))) + expect(truncatedDescription).toHaveStyleRule('display', 'none') + expect(fullDescription).toHaveStyleRule('display', 'inline') + expect(screen.getByText('Hide')).toBeVisible() + }) + + it('no description or social buttons shown when not available', async () => { + mocked(useTDPContext).mockReturnValue({ + address: USDC_MAINNET.address, + currency: USDC_MAINNET, + currencyChainName: Chain.Ethereum, + tokenQuery: { data: undefined, loading: false, error: undefined }, + } as any) + const { asFragment } = render() + expect(asFragment()).toMatchSnapshot() + + expect(screen.getByText('No token information available')).toBeVisible() + expect(screen.queryByText('Website')).toBeNull() + expect(screen.queryByText('Twitter')).toBeNull() + expect(screen.getByText('Etherscan')).toBeVisible() + expect(screen.getByText('0xA0b8...eB48')).toBeVisible() + }) +}) diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx new file mode 100644 index 0000000..7158b28 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDescription.tsx @@ -0,0 +1,198 @@ +import { t, Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import Column from 'components/Column' +import { EtherscanLogo } from 'components/Icons/Etherscan' +import { Globe } from 'components/Icons/Globe' +import { TwitterXLogo } from 'components/Icons/TwitterX' +import Row from 'components/Row' +import { FOTTooltipContent } from 'components/swap/SwapLineItem' +import { NoInfoAvailable, truncateDescription, TruncateDescriptionButton } from 'components/Tokens/TokenDetails/shared' +import Tooltip, { MouseoverTooltip, TooltipSize } from 'components/Tooltip' +import useCopyClipboard from 'hooks/useCopyClipboard' +import { useSwapTaxes } from 'hooks/useSwapTaxes' +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { useCallback, useReducer } from 'react' +import { Copy } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' +import { shortenAddress } from 'utilities/src/addresses' +import { useFormatter } from 'utils/formatNumbers' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const TokenInfoSection = styled(Column)` + gap: 16px; + width: 100%; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.lg}px) { + gap: 24px; + } +` + +const InfoSectionHeader = styled(ThemedText.HeadlineSmall)` + @media screen and (max-width: ${({ theme }) => theme.breakpoint.lg}px) { + font-size: 28px !important; + line-height: 36px !important; + } +` + +const TokenNameRow = styled(Row)` + gap: 8px; + width: 100%; +` + +const TokenButtonRow = styled(TokenNameRow)` + flex-wrap: wrap; +` + +const TokenInfoButton = styled(Row)` + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + color: ${({ theme }) => theme.neutral1}; + background-color: ${({ theme }) => theme.surface2}; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: max-content; + ${ClickableStyle} +` + +const TokenDescriptionContainer = styled(ThemedText.BodyPrimary)` + ${EllipsisStyle} + max-width: 100%; + // max-height: fit-content; + line-height: 24px; + white-space: pre-wrap; +` + +const DescriptionVisibilityWrapper = styled.p<{ $visible: boolean }>` + display: ${({ $visible }) => ($visible ? 'inline' : 'none')}; +` + +const TRUNCATE_CHARACTER_COUNT = 200 + +export function TokenDescription() { + const { address, currency, tokenQuery } = useTDPContext() + const { neutral2 } = useTheme() + + const { description, homepageUrl, twitterName } = tokenQuery.data?.token?.project ?? {} + const explorerUrl = getExplorerLink( + currency.chainId, + address, + currency.isNative ? ExplorerDataType.NATIVE : ExplorerDataType.TOKEN + ) + + const [isCopied, setCopied] = useCopyClipboard() + const copy = useCallback(() => { + setCopied(address) + }, [address, setCopied]) + + const [isDescriptionTruncated, toggleIsDescriptionTruncated] = useReducer((x) => !x, true) + const truncatedDescription = truncateDescription(description ?? '', TRUNCATE_CHARACTER_COUNT) + const shouldTruncate = !!description && description.length > TRUNCATE_CHARACTER_COUNT + const showTruncatedDescription = shouldTruncate && isDescriptionTruncated + const { inputTax: sellFee, outputTax: buyFee } = useSwapTaxes(address, address) + const { formatPercent } = useFormatter() + const { sellFeeString, buyFeeString } = { + sellFeeString: formatPercent(sellFee), + buyFeeString: formatPercent(buyFee), + } + const hasFee = Boolean(parseFloat(sellFeeString)) || Boolean(parseFloat(buyFee.toFixed(2))) + const sameFee = sellFeeString === buyFeeString + + return ( + + + Info + + + {!currency.isNative && ( + + + + {shortenAddress(currency.address)} + + + )} + + + + {currency.chainId === ChainId.MAINNET ? Etherscan : Explorer} + + + {homepageUrl && ( + + + + Website + + + )} + {twitterName && ( + + + + Twitter + + + )} + + + {!description && ( + + No token information available + + )} + {description && ( + <> + + {description} + + + {truncatedDescription} + + + )} + {shouldTruncate && ( + + {isDescriptionTruncated ? Show more : Hide} + + )} + + {hasFee && ( + + + + } + > + + {sameFee ? ( + + {currency.symbol}  + fee: {sellFeeString} + + ) : ( + <> + + {currency.symbol}  + buy fee: {buyFeeString} + {' '} + + {currency.symbol}  + sell fee: {sellFeeString} + {' '} + + )} + + + )} + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx b/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx new file mode 100644 index 0000000..f2b50b4 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/TokenDetailsHeader.tsx @@ -0,0 +1,245 @@ +import { t, Trans } from '@lingui/macro' +import { ChainId } from '@uniswap/sdk-core' +import { ReactComponent as MenuIcon } from 'assets/images/menu.svg' +import { PortfolioLogo } from 'components/AccountDrawer/MiniPortfolio/PortfolioLogo' +import { CheckMark } from 'components/Icons/CheckMark' +import { EtherscanLogo } from 'components/Icons/Etherscan' +import { ExplorerIcon } from 'components/Icons/ExplorerIcon' +import { Globe } from 'components/Icons/Globe' +import { Share as ShareIcon } from 'components/Icons/Share' +import { TwitterXLogo } from 'components/Icons/TwitterX' +import Row from 'components/Row' +import ShareButton, { openShareTweetWindow } from 'components/Tokens/TokenDetails/ShareButton' +import { ActionButtonStyle } from 'components/Tokens/TokenDetails/shared' +import { MouseoverTooltip, TooltipSize } from 'components/Tooltip' +import useCopyClipboard from 'hooks/useCopyClipboard' +import { useOnClickOutside } from 'hooks/useOnClickOutside' +import { useScreenSize } from 'hooks/useScreenSize' +import { useReducer, useRef } from 'react' +import { Link } from 'react-feather' +import styled, { useTheme } from 'styled-components' +import { ClickableStyle, EllipsisStyle, ExternalLink, ThemedText } from 'theme/components' +import { opacify } from 'theme/utils' +import { Z_INDEX } from 'theme/zIndex' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { TokenNameCell } from './Skeleton' + +const HeaderActionsContainer = styled.div` + display: flex; + gap: 8px; + align-items: center; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.xs}px) { + flex-direction: column; + position: fixed; + bottom: 0; + left: 0; + align-items: unset; + width: 100vw; + padding: 8px; + background: ${({ theme }) => theme.surface2}; + border-radius: 12px 12px 0 0; + border: ${({ theme }) => `1px solid ${theme.surface3}`}; + box-shadow: ${({ theme }) => theme.deprecated_deepShadow}; + opacity: 1 !important; + z-index: ${Z_INDEX.modal}; + } +` + +const StyledMenuIcon = styled(MenuIcon)` + padding: 8px 12px; + border-radius: 20px; + color: ${({ theme }) => theme.neutral1}; + background-color: ${({ theme }) => opacify(12, theme.neutral1)}; + width: 40px; + height: 32px; + margin-top: 8px; + ${ClickableStyle} + + path { + stroke: ${({ theme }) => theme.neutral1}; + } +` + +const ActionButton = styled(Row)` + ${ActionButtonStyle} + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.xs}px) { + color: unset; + background-color: unset; + width: unset; + + align-items: center; + text-decoration: none; + cursor: pointer; + gap: 12px; + padding: 10px 8px; + border-radius: 8px; + &:hover { + background: ${({ theme }) => theme.surface3}; + opacity: 1; + } + } +` + +const TokenTitle = styled.div` + display: flex; + gap: 8px; + overflow: hidden; + white-space: nowrap; +` + +const TokenSymbol = styled(ThemedText.SubHeaderSmall)` + font-size: 24px !important; + line-height: inherit; + margin-top: 0; + margin-bottom: 0; + + text-transform: uppercase; + + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + display: none; + } +` + +const TokenName = styled(ThemedText.HeadlineMedium)` + ${EllipsisStyle} + font-size: 24px !important; + min-width: 40px; +` + +export const StyledExternalLink = styled(ExternalLink)` + &:hover { + // Override hover behavior from ExternalLink + opacity: 1; + } +` +export const TokenDetailsHeader = () => { + const { address, currency, tokenQuery } = useTDPContext() + + const theme = useTheme() + const screenSize = useScreenSize() + const isMobileScreen = !screenSize['xs'] + + const [actionsModalIsOpen, toggleActionsModal] = useReducer((s) => !s, false) + const actionsRef = useRef(null) + useOnClickOutside(actionsRef, actionsModalIsOpen ? toggleActionsModal : undefined) + + const [isShareModalOpen, toggleShareModal] = useReducer((s) => !s, false) + const shareMenuRef = useRef(null) + useOnClickOutside(shareMenuRef, isShareModalOpen ? toggleShareModal : undefined) + + const tokenSymbolName = currency.symbol ?? Symbol not found + + const explorerUrl = getExplorerLink( + currency.chainId, + address, + currency.isNative ? ExplorerDataType.NATIVE : ExplorerDataType.TOKEN + ) + + const { homepageUrl, twitterName, logoUrl } = tokenQuery.data?.token?.project ?? {} + const twitterUrl = twitterName && `https://x.com/${twitterName}` + + const currentLocation = window.location.href + + const twitterShareName = + currency.name && currency.symbol + ? `${currency.name} (${currency.symbol})` + : currency?.name || currency?.symbol || '' + + const [isCopied, setCopied] = useCopyClipboard() + + return ( + <> + + + + {currency.name ?? Name not found} + {tokenSymbolName} + + +
+ {isMobileScreen && } + {!isMobileScreen || (isMobileScreen && actionsModalIsOpen) ? ( + + {explorerUrl && ( + + + + {currency.chainId === ChainId.MAINNET ? ( + + ) : ( + + )} + {isMobileScreen && ( + + Explorer + + )} + + + + )} + {homepageUrl && ( + + + + + {isMobileScreen && ( + + Website + + )} + + + + )} + {twitterUrl && ( + + + + + {isMobileScreen && ( + + Twitter + + )} + + + + )} + {isMobileScreen ? ( + <> + setCopied(currentLocation)}> + {isCopied ? ( + + ) : ( + + )} + + {isCopied ? Copied : Copy link} + + + { + toggleActionsModal() + openShareTweetWindow(twitterShareName) + }} + > + + + Share to Twitter + + + + ) : ( + + )} + + ) : null} +
+ + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/__snapshots__/Skeleton.test.tsx.snap b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/Skeleton.test.tsx.snap new file mode 100644 index 0000000..3d24bf5 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/Skeleton.test.tsx.snap @@ -0,0 +1,953 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TDP Skeleton should render correctly 1`] = ` + + .c11 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c12 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c24 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c2 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + color: #7D7D7D; + font-size: 16px; + line-height: 24px; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + gap: 4px; + margin-bottom: 20px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} + +.c3 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + color: #7D7D7D; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + -webkit-text-decoration: none; + text-decoration: none; +} + +.c3:hover { + color: #CECECE; +} + +.c39 { + border-radius: 12px; + height: 40px; + width: 40px; + position: relative; + margin-top: -18px; + margin-bottom: -18px; + margin-left: auto; + margin-right: auto; + background-color: #F9F9F9; + border: 4px solid; + border-color: #FFFFFF; + z-index: 2; +} + +.c41 { + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.c40 { + position: absolute; + left: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + margin: 0; +} + +.c31 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 4px; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + padding: 8px; + border: 1px solid #22222212; + border-radius: 16px; + background-color: #FFFFFF; +} + +.c35 { + background-color: #F9F9F9; + border-radius: 4px; + height: 56px; + width: 60px; +} + +.c37 { + background-color: #F9F9F9; + border-radius: 16px; + height: 56px; + width: 100px; +} + +.c42 { + background-color: #F9F9F9; + border-radius: 16px; + height: 56px; + width: 100%; +} + +.c36 { + background-color: #22222212; + height: 36px; +} + +.c32 { + padding: 8px; +} + +.c34 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} + +.c33 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-flow: column; + -ms-flex-flow: column; + flex-flow: column; + background-color: #F9F9F9; + border-radius: 16px; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 30px; + padding: 48px 12px; +} + +.c38 { + position: relative; +} + +.c4 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c23 { + gap: 16px; + padding: 24px 0px; + -webkit-animation: iAjNNh 125ms ease-in; + animation: iAjNNh 125ms ease-in; +} + +.c25 { + font-size: 28px !important; +} + +.c22 { + background-color: #22222212; + border: none; + height: 0.5px; +} + +.c19 { + color: #7D7D7D; + font-size: 14px; + min-width: 121px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + padding-top: 24px; + padding-bottom: 0px; +} + +.c18 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.c14 { + gap: 16px; + -webkit-animation: iAjNNh 125ms ease-in; + animation: iAjNNh 125ms ease-in; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0 16px 52px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + width: 100%; + gap: 40px; +} + +.c1 { + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + max-width: 780px; + overflow: hidden; + width: 100%; +} + +.c30 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding-top: 53px; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + gap: 40px; + width: 360px; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-top: 8px; + margin-bottom: 20px; + gap: 20px; + -webkit-animation: iAjNNh 125ms ease-in; + animation: iAjNNh 125ms ease-in; + -webkit-animation-duration: 250ms; + animation-duration: 250ms; +} + +.c8 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 12px; + font-size: 20px; + line-height: 28px; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + padding-top: 4px; + min-width: 32px; +} + +.c5 { + height: 16px; + width: 180px; +} + +.c15 { + height: 32px; + border-radius: 8px; +} + +.c6 { + width: 169px; +} + +.c9 { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.c10 { + height: 36px; + width: 136px; +} + +.c16 { + width: 120px; +} + +.c20 { + width: 80px; + margin-bottom: 4px; +} + +.c21 { + width: 116px; +} + +.c26 { + margin-bottom: 6px; + width: 100%; +} + +.c29 { + width: 120px; +} + +.c27 { + width: 50%; +} + +.c17 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.c28 { + padding-top: 24px; +} + +.c13 { + height: 4px; +} + +@media screen and (max-width:640px) { + .c19 { + min-width: 168px; + } +} + +@media screen and (min-width:768px) { + .c0 { + padding: 48px 20px; + } +} + +@media screen and (max-width:1024px) { + .c0 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + } +} + +@media screen and (min-width:1280px) { + .c0 { + gap: 60px; + } +} + +@media screen and (max-width:1024px) { + .c30 { + width: 100%; + max-width: 780px; + } +} + +@media screen and (max-width:640px) { + .c8 { + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + } +} + + + +
+
+ +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Swap +
+
+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+ + + +`; + +exports[`getLoadingTitle should return correct title 1`] = ` + + .c0 { + color: #CECECE; + font-size: 12px; + font-weight: 500; + line-height: 16px; + -webkit-text-decoration: none; + text-decoration: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c0:hover { + opacity: 0.6; +} + +.c0:active { + opacity: 0.4; +} + + + + token data for + + USD//C (USDC) + + on Ethereum + + + +`; diff --git a/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap new file mode 100644 index 0000000..d3e6f1b --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/__snapshots__/TokenDescription.test.tsx.snap @@ -0,0 +1,871 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TokenDescription no description or social buttons shown when not available 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c10 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c10:hover { + opacity: 0.6; +} + +.c10:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c8 { + display: inline-block; + height: inherit; +} + +.c12 { + color: #CECECE; + font-weight: 485; + font-size: 16px; +} + +.c1 { + gap: 16px; + width: 100%; +} + +.c6 { + gap: 8px; + width: 100%; +} + +.c7 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.c9 { + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + color: #222222; + background-color: #F9F9F9; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c9:hover { + opacity: 0.6; +} + +.c9:active { + opacity: 0.4; +} + +.c11 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + line-height: 24px; + white-space: pre-wrap; +} + +@media screen and (max-width:1024px) { + .c1 { + gap: 24px; + } +} + +@media screen and (max-width:1024px) { + .c3 { + font-size: 28px !important; + line-height: 36px !important; + } +} + + + +
+
+ Info +
+
+
+
+ + + + + 0xA0b8...eB48 +
+
+ +
+ + + + + Etherscan +
+
+
+
+

+ No token information available +

+
+
+
+
+
+`; + +exports[`TokenDescription renders token information correctly with defaults 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c10 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c10:hover { + opacity: 0.6; +} + +.c10:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c8 { + display: inline-block; + height: inherit; +} + +.c14 { + color: #7D7D7D; + font-weight: 485; + font-size: 0.85em; + padding-top: 0.5em; +} + +.c14:hover, +.c14:focus { + color: #636363; + cursor: pointer; +} + +.c1 { + gap: 16px; + width: 100%; +} + +.c6 { + gap: 8px; + width: 100%; +} + +.c7 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.c9 { + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + color: #222222; + background-color: #F9F9F9; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c9:hover { + opacity: 0.6; +} + +.c9:active { + opacity: 0.4; +} + +.c11 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + line-height: 24px; + white-space: pre-wrap; +} + +.c12 { + display: none; +} + +.c13 { + display: inline; +} + +@media screen and (max-width:1024px) { + .c1 { + gap: 24px; + } +} + +@media screen and (max-width:1024px) { + .c3 { + font-size: 28px !important; + line-height: 36px !important; + } +} + + + +
+
+ Info +
+ +
+

+ USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value between people, businesses and financial institutions just like email between mail services and texts between SMS providers. We believe by removing artificial economic borders, we can create a more inclusive global economy. +

+

+ USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value... +

+
+ Show more +
+
+
+
+
+
+`; + +exports[`TokenDescription truncates description and shows more 1`] = ` + + .c4 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c5 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c2 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c10 { + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; + color: #FC72FF; + stroke: #FC72FF; + font-weight: 500; +} + +.c10:hover { + opacity: 0.6; +} + +.c10:active { + opacity: 0.4; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c8 { + display: inline-block; + height: inherit; +} + +.c14 { + color: #7D7D7D; + font-weight: 485; + font-size: 0.85em; + padding-top: 0.5em; +} + +.c14:hover, +.c14:focus { + color: #636363; + cursor: pointer; +} + +.c1 { + gap: 16px; + width: 100%; +} + +.c6 { + gap: 8px; + width: 100%; +} + +.c7 { + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; +} + +.c9 { + gap: 8px; + padding: 8px 12px; + border-radius: 20px; + color: #222222; + background-color: #F9F9F9; + font-size: 14px; + font-weight: 535; + line-height: 16px; + width: -webkit-max-content; + width: -moz-max-content; + width: max-content; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c9:hover { + opacity: 0.6; +} + +.c9:active { + opacity: 0.4; +} + +.c11 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + line-height: 24px; + white-space: pre-wrap; +} + +.c12 { + display: none; +} + +.c13 { + display: inline; +} + +@media screen and (max-width:1024px) { + .c1 { + gap: 24px; + } +} + +@media screen and (max-width:1024px) { + .c3 { + font-size: 28px !important; + line-height: 36px !important; + } +} + + + +
+
+ Info +
+ +
+

+ USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value between people, businesses and financial institutions just like email between mail services and texts between SMS providers. We believe by removing artificial economic borders, we can create a more inclusive global economy. +

+

+ USDC is a fully collateralized US dollar stablecoin. USDC is the bridge between dollars and trading on cryptocurrency exchanges. The technology behind CENTRE makes it possible to exchange value... +

+
+ Show more +
+
+
+
+
+
+`; diff --git a/apps/web/src/components/Tokens/TokenDetails/index.tsx b/apps/web/src/components/Tokens/TokenDetails/index.tsx new file mode 100644 index 0000000..bd425a6 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/index.tsx @@ -0,0 +1,213 @@ +import { Trans } from '@lingui/macro' +import { InterfacePageName } from '@uniswap/analytics-events' +import { ChainId, Currency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import { Trace } from 'analytics' +import { BreadcrumbNavContainer, BreadcrumbNavLink, CurrentPageBreadcrumb } from 'components/BreadcrumbNav' +import TokenSafetyMessage from 'components/TokenSafety/TokenSafetyMessage' +import TokenSafetyModal from 'components/TokenSafety/TokenSafetyModal' +import ChartSection from 'components/Tokens/TokenDetails/ChartSection' +import { LeftPanel, RightPanel, TokenDetailsLayout, TokenInfoContainer } from 'components/Tokens/TokenDetails/Skeleton' +import StatsSection from 'components/Tokens/TokenDetails/StatsSection' +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { getTokenDetailsURL } from 'graphql/data/util' +import { useCurrency } from 'hooks/Tokens' +import { getInitialUrl } from 'hooks/useAssetLogoSource' +import useParsedQueryString from 'hooks/useParsedQueryString' +import { useScreenSize } from 'hooks/useScreenSize' +import { Swap } from 'pages/Swap' +import { useTDPContext } from 'pages/TokenDetails/TDPContext' +import { PropsWithChildren, useCallback, useMemo, useState } from 'react' +import { ChevronRight } from 'react-feather' +import { useNavigate } from 'react-router-dom' +import { CurrencyState } from 'state/swap/SwapContext' +import styled from 'styled-components' +import { addressesAreEquivalent } from 'utils/addressesAreEquivalent' +import { ActivitySection } from './ActivitySection' +import BalanceSummary from './BalanceSummary' +import MobileBalanceSummaryFooter from './MobileBalanceSummaryFooter' +import { TokenDescription } from './TokenDescription' +import { TokenDetailsHeader } from './TokenDetailsHeader' +import { Hr } from './shared' + +const DividerLine = styled(Hr)` + margin-top: 40px; + margin-bottom: 40px; + @media screen and (max-width: ${({ theme }) => theme.breakpoint.sm}px) { + opacity: 0; + margin-bottom: 0; + } +` + +function TDPBreadcrumb() { + const { address, currency, currencyChain } = useTDPContext() + + return ( + + + Explore + + + Tokens + + + + ) +} + +function getCurrencyURLAddress(currency?: Currency): string { + if (!currency) return '' + + if (currency.isToken) { + return currency.address + } + return NATIVE_CHAIN_ID +} + +function useSwapInitialInputCurrency() { + const { currency } = useTDPContext() + const parsedQs = useParsedQueryString() + + const inputTokenAddress = useMemo(() => { + return typeof parsedQs.inputCurrency === 'string' ? (parsedQs.inputCurrency as string) : undefined + }, [parsedQs]) + + return useCurrency(inputTokenAddress, currency.chainId) +} + +function TDPSwapComponent() { + const { address, currency, currencyChain, warning } = useTDPContext() + const appChainId = useWeb3React().chainId ?? ChainId.MAINNET + const navigate = useNavigate() + + const handleCurrencyChange = useCallback( + (tokens: CurrencyState) => { + const inputCurrencyURLAddress = getCurrencyURLAddress(tokens.inputCurrency) + const outputCurrencyURLAddress = getCurrencyURLAddress(tokens.outputCurrency) + if ( + addressesAreEquivalent(inputCurrencyURLAddress, address) || + addressesAreEquivalent(outputCurrencyURLAddress, address) + ) { + return + } + + const newDefaultToken = tokens.outputCurrency ?? tokens.inputCurrency + + if (!newDefaultToken) return + + const preloadedLogoSrc = getInitialUrl( + newDefaultToken.wrapped.address, + newDefaultToken.chainId, + newDefaultToken.isNative + ) + const url = getTokenDetailsURL({ + // The function falls back to "NATIVE" if the address is null + address: newDefaultToken.isNative ? null : newDefaultToken.address, + chain: currencyChain, + inputAddress: + // If only one token was selected before we navigate, then it was the default token and it's being replaced. + // On the new page, the *new* default token becomes the output, and we don't have another option to set as the input token. + tokens.inputCurrency && tokens.inputCurrency !== newDefaultToken ? inputCurrencyURLAddress : null, + }) + navigate(url, { state: { preloadedLogoSrc } }) + }, + [address, currencyChain, navigate] + ) + + // Other token to prefill the swap form with + const initialInputCurrency = useSwapInitialInputCurrency() + + const [openTokenSafetyModal, setOpenTokenSafetyModal] = useState(false) + const [continueSwap, setContinueSwap] = useState<{ resolve: (value: boolean | PromiseLike) => void }>() + + const onResolveSwap = useCallback( + (value: boolean) => { + continueSwap?.resolve(value) + setContinueSwap(undefined) + }, + [continueSwap, setContinueSwap] + ) + const isBlockedToken = warning?.canProceed === false + + return ( + <> +
isBlockedToken && setOpenTokenSafetyModal(true)} + > + +
+ {warning && } + onResolveSwap(true)} + onBlocked={() => { + setOpenTokenSafetyModal(false) + }} + onCancel={() => onResolveSwap(false)} + showCancel={true} + /> + + ) +} + +function TDPAnalytics({ children }: PropsWithChildren) { + const { address, currency } = useTDPContext() + return ( + + {children} + + ) +} + +export default function TokenDetails() { + const { address, currency, tokenQuery } = useTDPContext() + const tokenQueryData = tokenQuery.data?.token + + const { lg: isLargeScreenSize } = useScreenSize() + + return ( + + + + + + + + + + + + + + {isLargeScreenSize && ( + <> + + + + )} + + + + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/shared.ts b/apps/web/src/components/Tokens/TokenDetails/shared.ts new file mode 100644 index 0000000..8a02786 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/shared.ts @@ -0,0 +1,69 @@ +import { darken } from 'polished' +import styled, { css } from 'styled-components' +import { ClickableStyle } from 'theme/components' +import { opacify } from 'theme/utils' + +export const ActionButtonStyle = css` + display: flex; + align-items: center; + justify-content: flex-start; + padding: 8px 12px; + border-radius: 20px; + border: none; + background-color: ${({ theme }) => theme.surface2}; + width: max-content; + ${ClickableStyle} + + // Override FilterButton background-color + :hover { + background-color: ${({ theme }) => opacify(12, theme.neutral1)}; + } + :focus { + background-color: ${({ theme }) => opacify(12, theme.neutral1)}; + } +` + +export const ActionMenuFlyoutStyle = css` + width: 200px; + top: 40px; + right: 0px; + overflow: auto; +` + +export const Hr = styled.hr` + background-color: ${({ theme }) => theme.surface3}; + border: none; + height: 0.5px; +` + +export const NoInfoAvailable = styled.p` + color: ${({ theme }) => theme.neutral3}; + font-weight: 485; + font-size: 16px; +` + +export const TruncateDescriptionButton = styled.div` + color: ${({ theme }) => theme.neutral2}; + font-weight: 485; + font-size: 0.85em; + padding-top: 0.5em; + + &:hover, + &:focus { + color: ${({ theme }) => darken(0.1, theme.neutral2)}; + cursor: pointer; + } +` + +export const truncateDescription = (desc: string, maxCharacterCount = TRUNCATE_CHARACTER_COUNT) => { + //trim the string to the maximum length + let tokenDescriptionTruncated = desc.slice(0, maxCharacterCount) + //re-trim if we are in the middle of a word + tokenDescriptionTruncated = `${tokenDescriptionTruncated.slice( + 0, + Math.min(tokenDescriptionTruncated.length, tokenDescriptionTruncated.lastIndexOf(' ')) + )}...` + return tokenDescriptionTruncated +} + +const TRUNCATE_CHARACTER_COUNT = 400 diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx new file mode 100644 index 0000000..51f258b --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.test.tsx @@ -0,0 +1,76 @@ +import { ApolloError } from '@apollo/client' +import { ChainId, Percent, Token } from '@uniswap/sdk-core' +import { TokenDetailsPoolsTable } from 'components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable' +import { ProtocolVersion } from 'graphql/data/__generated__/types-and-hooks' +import { usePoolsFromTokenAddress } from 'graphql/data/pools/usePoolsFromTokenAddress' +import Router from 'react-router-dom' +import { mocked } from 'test-utils/mocked' +import { validBEPoolToken0, validBEPoolToken1, validParams, validPoolToken0 } from 'test-utils/pools/fixtures' +import { render, screen } from 'test-utils/render' + +jest.mock('graphql/data/pools/usePoolsFromTokenAddress') +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useParams: jest.fn(), +})) + +const mockToken = new Token(ChainId.MAINNET, validPoolToken0.id, 18) + +describe('TDPPoolTable', () => { + beforeEach(() => { + jest.spyOn(Router, 'useParams').mockReturnValue(validParams) + }) + + it('renders loading state', () => { + mocked(usePoolsFromTokenAddress).mockReturnValue({ + loading: true, + error: undefined, + pools: [], + loadMore: jest.fn(), + }) + + const { asFragment } = render() + expect(screen.getAllByTestId('cell-loading-bubble')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders error state', () => { + mocked(usePoolsFromTokenAddress).mockReturnValue({ + loading: false, + error: new ApolloError({ errorMessage: 'error fetching data' }), + pools: [], + loadMore: jest.fn(), + }) + + const { asFragment } = render() + expect(screen.getByTestId('table-error-modal')).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) + + it('renders data filled state', () => { + const mockData = [ + { + token0: validBEPoolToken0, + token1: validBEPoolToken1, + feeTier: 10000, + hash: '0x123', + txCount: 200, + tvl: 300, + volume24h: 400, + volumeWeek: 500, + turnover: new Percent(6, 100), + protocolVersion: ProtocolVersion.V3, + }, + ] + mocked(usePoolsFromTokenAddress).mockReturnValue({ + pools: mockData, + loading: false, + error: undefined, + loadMore: jest.fn(), + }) + + const { asFragment } = render() + expect(screen.getByTestId(`tdp-pools-table-${validPoolToken0.id}`)).not.toBeNull() + expect(asFragment()).toMatchSnapshot() + }) +}) diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx new file mode 100644 index 0000000..87f31b1 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TokenDetailsPoolsTable.tsx @@ -0,0 +1,39 @@ +import { ChainId, Token } from '@uniswap/sdk-core' +import { PoolTableColumns, PoolsTable, sortAscendingAtom, sortMethodAtom } from 'components/Pools/PoolTable/PoolTable' +import { usePoolsFromTokenAddress } from 'graphql/data/pools/usePoolsFromTokenAddress' +import { OrderDirection } from 'graphql/data/util' +import { useAtomValue, useResetAtom } from 'jotai/utils' +import { useEffect, useMemo } from 'react' + +const HIDDEN_COLUMNS = [PoolTableColumns.Transactions] + +export function TokenDetailsPoolsTable({ chainId, referenceToken }: { chainId: ChainId; referenceToken: Token }) { + const sortMethod = useAtomValue(sortMethodAtom) + const sortAscending = useAtomValue(sortAscendingAtom) + const sortState = useMemo( + () => ({ sortBy: sortMethod, sortDirection: sortAscending ? OrderDirection.Asc : OrderDirection.Desc }), + [sortAscending, sortMethod] + ) + const { pools, loading, error, loadMore } = usePoolsFromTokenAddress(referenceToken.address, sortState, chainId) + + const resetSortMethod = useResetAtom(sortMethodAtom) + const resetSortAscending = useResetAtom(sortAscendingAtom) + useEffect(() => { + resetSortMethod() + resetSortAscending() + }, [resetSortAscending, resetSortMethod]) + + return ( +
+ +
+ ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx new file mode 100644 index 0000000..bf52109 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/tables/TransactionsTable.tsx @@ -0,0 +1,262 @@ +import { Trans } from '@lingui/macro' +import { createColumnHelper } from '@tanstack/react-table' +import { ChainId, Token } from '@uniswap/sdk-core' +import Row from 'components/Row' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { Filter } from 'components/Table/Filter' +import { + FilterHeaderRow, + HeaderArrow, + HeaderSortText, + StyledExternalLink, + TimestampCell, + TokenLinkCell, +} from 'components/Table/styled' +import { Token as GQLToken } from 'graphql/data/__generated__/types-and-hooks' +import { TokenTransactionType, useTokenTransactions } from 'graphql/data/useTokenTransactions' +import { unwrapToken } from 'graphql/data/util' +import { OrderDirection, Swap_OrderBy } from 'graphql/thegraph/__generated__/types-and-hooks' +import { useActiveLocalCurrency } from 'hooks/useActiveLocalCurrency' +import { useMemo, useReducer, useState } from 'react' +import styled from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { shortenAddress } from 'utilities/src/addresses' +import { useFormatter } from 'utils/formatNumbers' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +const StyledSwapAmount = styled(ThemedText.BodyPrimary)` + display: inline-block; + ${EllipsisStyle} + max-width: 75px; +` + +const TableWrapper = styled.div` + min-height: 158px; +` +interface SwapTransaction { + hash: string + timestamp: number + input: SwapLeg + output: SwapLeg + usdValue: number + makerAddress: string +} + +interface SwapLeg { + address?: string + symbol?: string + amount: number + token: GQLToken +} + +type TokenTxTableSortState = { + sortBy: Swap_OrderBy + sortDirection: OrderDirection +} + +export function TransactionsTable({ chainId, referenceToken }: { chainId: ChainId; referenceToken: Token }) { + const activeLocalCurrency = useActiveLocalCurrency() + const { formatNumber, formatFiatPrice } = useFormatter() + const [filterModalIsOpen, toggleFilterModal] = useReducer((s) => !s, false) + const [filter, setFilters] = useState([TokenTransactionType.BUY, TokenTransactionType.SELL]) + const [sortState] = useState({ + sortBy: Swap_OrderBy.Timestamp, + sortDirection: OrderDirection.Desc, + }) + const { transactions, loading, loadMore, error } = useTokenTransactions(referenceToken.address, chainId, filter) + const unwrappedReferenceToken = unwrapToken(chainId, referenceToken) + + const data = useMemo( + () => + transactions.map((transaction) => { + const swapLeg0 = { + address: transaction.token0.address, + symbol: transaction.token0.symbol, + amount: parseFloat(transaction.token0Quantity), + token: transaction.token0, + } + const swapLeg1 = { + address: transaction.token1.address, + symbol: transaction.token1.symbol, + amount: parseFloat(transaction.token1Quantity), + token: transaction.token1, + } + const token0IsBeingSold = parseFloat(transaction.token0Quantity) < 0 + return { + hash: transaction.hash, + timestamp: transaction.timestamp, + input: token0IsBeingSold ? swapLeg0 : swapLeg1, + output: token0IsBeingSold ? swapLeg1 : swapLeg0, + usdValue: transaction.usdValue.value, + makerAddress: transaction.account, + } + }), + [transactions] + ) + + const showLoadingSkeleton = loading || !!error + // TODO(WEB-3236): once GQL BE Transaction query is supported add usd, token0 amount, and token1 amount sort support + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return [ + columnHelper.accessor((row) => row, { + id: 'timestamp', + header: () => ( + + + {sortState.sortBy === Swap_OrderBy.Timestamp && } + + Time + + + + ), + cell: (row) => ( + + + + ), + }), + columnHelper.accessor((row) => row.output.address, { + id: 'swap-type', + header: () => ( + + + + + Type + + + + ), + cell: (outputTokenAddress) => { + const isBuy = String(outputTokenAddress.getValue?.()).toLowerCase() === referenceToken.address.toLowerCase() + return ( + + + {isBuy ? Buy : Sell} + + + ) + }, + }), + columnHelper.accessor( + (row) => + row.input.address?.toLowerCase() === referenceToken.address.toLowerCase() + ? row.input.amount + : row.output.amount, + { + id: 'reference-amount', + header: () => ( + + ${unwrappedReferenceToken.symbol} + + ), + cell: (inputTokenAmount) => ( + + + {formatNumber({ + input: Math.abs(inputTokenAmount.getValue?.()) || 0, + })} + + + ), + } + ), + columnHelper.accessor( + (row) => { + const nonReferenceSwapLeg = + row.input.address?.toLowerCase() === referenceToken.address.toLowerCase() ? row.output : row.input + return ( + + + {formatNumber({ + input: Math.abs(nonReferenceSwapLeg.amount) || 0, + })} + + + + ) + }, + { + id: 'non-reference-amount', + header: () => ( + + + For + + + ), + cell: (swapOutput) => ( + + {swapOutput.getValue?.()} + + ), + } + ), + columnHelper.accessor((row) => row.usdValue, { + id: 'fiat-value', + header: () => ( + + + {sortState.sortBy === Swap_OrderBy.AmountUsd && } + + {activeLocalCurrency} + + + + ), + cell: (fiat) => ( + + {formatFiatPrice({ price: fiat.getValue?.() })} + + ), + }), + columnHelper.accessor((row) => row.makerAddress, { + id: 'maker-address', + header: () => ( + + + Wallet + + + ), + cell: (makerAddress) => ( + + + {shortenAddress(makerAddress.getValue?.())} + + + ), + }), + ] + }, [ + sortState.sortBy, + sortState.sortDirection, + showLoadingSkeleton, + chainId, + filterModalIsOpen, + filter, + referenceToken.address, + unwrappedReferenceToken.symbol, + formatNumber, + activeLocalCurrency, + formatFiatPrice, + ]) + + return ( + +
+ + ) +} diff --git a/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap b/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap new file mode 100644 index 0000000..95d5596 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenDetails/tables/__snapshots__/TokenDetailsPoolsTable.test.tsx.snap @@ -0,0 +1,2984 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TDPPoolTable renders data filled state 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c20 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + gap: 8px; +} + +.c12 { + display: inline-block; + height: inherit; +} + +.c9 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c24 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c29 { + height: 16px; + width: 16px; +} + +.c29 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c30 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c1 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c2 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c17 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c17::-webkit-scrollbar { + display: none; +} + +.c27 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c28 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c5 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c6 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c6::-webkit-scrollbar { + display: none; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c7:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c18 { + color: none; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; +} + +.c13 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c13:hover { + opacity: 0.6; +} + +.c13:active { + opacity: 0.4; +} + +.c14 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c15 { + color: #222222; +} + +.c8 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c10 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c16 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c25 { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.c21 { + position: relative; + top: 0; + left: 0; +} + +.c22 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + gap: 2px; + position: relative; + top: 0; + left: 0; +} + +.c22 img { + width: 14px; + height: 28px; + object-fit: cover; +} + +.c22 img:first-child { + border-radius: 14px 0 0 14px; + object-position: 0 0; +} + +.c22 img:last-child { + border-radius: 0 14px 14px 0; + object-position: 100% 0; +} + +.c23 { + width: 14px; + height: 28px; + border-radius: 50%; +} + +.c26 { + padding: 2px 6px; + background: #F9F9F9; + border-radius: 5px; +} + +@media not all and (hover:none) { + .c19:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+ +
+
+ + + + Loading +
+
+
+
+
+
+
+
+`; + +exports[`TDPPoolTable renders error state 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c12 { + display: inline-block; + height: inherit; +} + +.c9 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c22 { + color: #222222; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c19 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c25 { + height: 16px; + width: 16px; +} + +.c25 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c26 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c21 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-align-items: flex-start; + -webkit-box-align: flex-start; + -ms-flex-align: flex-start; + align-items: flex-start; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + position: absolute; + top: 50%; + left: 50%; + -webkit-transform: translate(-50%,-50%); + -ms-transform: translate(-50%,-50%); + transform: translate(-50%,-50%); + width: 320px; + padding: 12px; + gap: 12px; + background-color: #00000004; + -webkit-backdrop-filter: blur(24px); + backdrop-filter: blur(24px); + box-shadow: 0 4px 6px rgba(0,0,0,0.1); + border: 1px #22222212 solid; + border-radius: 20px; +} + +.c1 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c2 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c17 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c17::-webkit-scrollbar { + display: none; +} + +.c23 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c24 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c5 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c6 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; + opacity: 0.4; +} + +.c6::-webkit-scrollbar { + display: none; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c7:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c13 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c13:hover { + opacity: 0.6; +} + +.c13:active { + opacity: 0.4; +} + +.c14 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c15 { + color: #222222; +} + +.c8 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c10 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c16 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c20 { + width: 75%; + height: 16px; +} + +@media not all and (hover:none) { + .c18:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + +
+
+
+ Error loading data +
+
+ Data is unavailable at the moment; we're working on a fix +
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; + +exports[`TDPPoolTable renders loading state 1`] = ` + + .c3 { + box-sizing: border-box; + margin: 0; + min-width: 0; +} + +.c4 { + width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + padding: 0; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c12 { + display: inline-block; + height: inherit; +} + +.c9 { + color: #7D7D7D; + -webkit-letter-spacing: -0.01em; + -moz-letter-spacing: -0.01em; + -ms-letter-spacing: -0.01em; + letter-spacing: -0.01em; +} + +.c0 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; +} + +.c19 { + border-radius: 12px; + border-radius: 12px; + height: 24px; + width: 50%; + width: 50%; + -webkit-animation: fAQEyV 1.5s infinite; + animation: fAQEyV 1.5s infinite; + -webkit-animation-fill-mode: both; + animation-fill-mode: both; + background: linear-gradient( to left, #22222212 25%, rgba(53,53,53,0.07) 50%, #22222212 75% ); + will-change: background-position; + background-size: 400%; +} + +.c23 { + height: 16px; + width: 16px; +} + +.c23 path { + stroke: #FC72FF; + background: #7D7D7D; + fill: none; +} + +.c24 { + -webkit-animation: 2s fvtopB linear infinite; + animation: 2s fvtopB linear infinite; +} + +.c1 { + max-width: px; + max-height: 600px; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + margin: 0px auto 24px auto; +} + +.c2 { + width: 100%; + position: relative; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + background: #FFFFFF; +} + +.c17 { + width: 100%; + position: relative; + overflow-x: auto; + overscroll-behavior-x: none; + border-right: 1px solid #22222212; + border-bottom: 1px solid #22222212; + border-left: 1px solid #22222212; + border-bottom-right-radius: 20px; + border-bottom-left-radius: 20px; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; +} + +.c17::-webkit-scrollbar { + display: none; +} + +.c21 { + position: -webkit-sticky; + position: sticky; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + margin-top: -48px; + visibility: hidden; +} + +.c22 { + background: #FFEFFF; + border-radius: 8px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + padding: 8px; + color: #FC72FF; + font-size: 16px; + font-weight: 535; + gap: 8px; + height: 34px; + z-index: 990; +} + +.c5 { + padding: 0px 12px; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; + min-width: 100%; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + min-height: 64px; +} + +.c6 { + border: 1px solid #22222212; + border-top-right-radius: 20px; + border-top-left-radius: 20px; + overflow: auto; + width: unset; + min-height: 52px; + background: #F9F9F9; + -ms-overflow-style: none; + -webkit-scrollbar-width: none; + -moz-scrollbar-width: none; + -ms-scrollbar-width: none; + scrollbar-width: none; + overscroll-behavior: none; +} + +.c6::-webkit-scrollbar { + display: none; +} + +.c7 { + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-flex: 1; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; +} + +.c7:last-child { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; +} + +.c7:first-child { + -webkit-box-flex: 0; + -webkit-flex-grow: 0; + -ms-flex-positive: 0; + flex-grow: 0; +} + +.c13 { + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + cursor: pointer; + width: 100%; + gap: 4px; + -webkit-text-decoration: none; + text-decoration: none; + cursor: pointer; + -webkit-transition-duration: 125ms; + transition-duration: 125ms; +} + +.c13:hover { + opacity: 0.6; +} + +.c13:active { + opacity: 0.4; +} + +.c14 { + height: 16px; + width: 16px; + color: #222222; + -webkit-transform: rotate(0deg); + -ms-transform: rotate(0deg); + transform: rotate(0deg); +} + +.c15 { + color: #222222; +} + +.c8 { + min-width: 44px; + -webkit-flex: 0; + -ms-flex: 0; + flex: 0; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: center; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c10 { + width: 240px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: start; + -webkit-justify-content: flex-start; + -ms-flex-pack: start; + justify-content: flex-start; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c11 { + min-width: 120px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c16 { + min-width: 100px; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + display: -webkit-box; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-box-pack: end; + -webkit-justify-content: flex-end; + -ms-flex-pack: end; + justify-content: flex-end; + -webkit-align-items: center; + -webkit-box-align: center; + -ms-flex-align: center; + align-items: center; + font-variant-numeric: lining-nums tabular-nums; + overflow: hidden; + padding: 12px 8px; +} + +.c20 { + width: 75%; + height: 16px; +} + +@media not all and (hover:none) { + .c18:hover { + background: #22222212; + } +} + + + +
+
+
+
+
+
+
+
+ # +
+
+
+
+
+
+ Pool +
+
+
+
+
+
+
+
+ + + + +
+ TVL +
+
+
+
+
+
+
+
+
+
+
+
+ 1 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ 7 day volume +
+
+
+
+
+
+
+
+
+
+
+
+ Turnover +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + Loading +
+
+
+
+
+ + + +`; diff --git a/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx new file mode 100644 index 0000000..2b75de3 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/NetworkFilter.tsx @@ -0,0 +1,103 @@ +import Badge from 'components/Badge' +import { DropdownSelector, InternalMenuItem } from 'components/DropdownSelector' +import { ChainLogo } from 'components/Logo/ChainLogo' +import { getChainInfo } from 'constants/chainInfo' +import { + BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS, + BACKEND_SUPPORTED_CHAINS, + supportedChainIdFromGQLChain, + validateUrlChainParam, +} from 'graphql/data/util' +import { ExploreTab } from 'pages/Explore' +import { useExploreParams } from 'pages/Explore/redirects' +import { useReducer } from 'react' +import { Check } from 'react-feather' +import { useNavigate } from 'react-router-dom' +import styled, { css, useTheme } from 'styled-components' +import { EllipsisStyle } from 'theme/components' + +const NetworkLabel = styled.div` + ${EllipsisStyle} + display: flex; + gap: 8px; + align-items: center; +` + +const Tag = styled(Badge)` + background-color: ${({ theme }) => theme.surface2}; + color: ${({ theme }) => theme.neutral2}; + font-size: 10px; + opacity: 1; + padding: 4px 6px; +` +const StyledButton = css` + height: 40px; +` +const StyledMenuFlyout = css` + max-height: 350px; + min-width: 240px; + right: 0px; + @media screen and (max-width: ${({ theme }) => `${theme.breakpoint.lg}px`}) { + left: 0px; + } +` +export default function NetworkFilter() { + const theme = useTheme() + const navigate = useNavigate() + const [isMenuOpen, toggleMenu] = useReducer((s) => !s, false) + + const exploreParams = useExploreParams() + const tab = exploreParams.tab + const currentChainName = validateUrlChainParam(exploreParams.chainName) + const chainId = supportedChainIdFromGQLChain(currentChainName) + + return ( +
+ + + + } + internalMenuItems={ + <> + {BACKEND_SUPPORTED_CHAINS.map((network) => { + const chainId = supportedChainIdFromGQLChain(network) + const chainInfo = getChainInfo(chainId) + return ( + { + navigate(`/explore/${tab ?? ExploreTab.Tokens}/${network.toLowerCase()}`) + toggleMenu() + }} + > + + {chainInfo.label} + + {network === currentChainName && } + + ) + })} + {BACKEND_NOT_YET_SUPPORTED_CHAIN_IDS.map((network) => { + const chainInfo = getChainInfo(network) + return ( + + + {chainInfo.label} + + Coming soon + + ) + })} + + } + buttonCss={StyledButton} + menuFlyoutCss={StyledMenuFlyout} + /> +
+ ) +} diff --git a/apps/web/src/components/Tokens/TokenTable/SearchBar.tsx b/apps/web/src/components/Tokens/TokenTable/SearchBar.tsx new file mode 100644 index 0000000..616f885 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/SearchBar.tsx @@ -0,0 +1,116 @@ +import { Trans } from '@lingui/macro' +import { BrowserEvent, InterfaceElementName, InterfaceEventName } from '@uniswap/analytics-events' +import { TraceEvent } from 'analytics' +import searchIcon from 'assets/svg/search.svg' +import xIcon from 'assets/svg/x.svg' +import useDebounce from 'hooks/useDebounce' +import { useAtomValue, useUpdateAtom } from 'jotai/utils' +import { useEffect, useState } from 'react' +import styled from 'styled-components' + +import { MEDIUM_MEDIA_BREAKPOINT } from '../constants' +import { exploreSearchStringAtom } from '../state' +const ICON_SIZE = '20px' + +const SearchBarContainer = styled.div` + display: flex; + flex: 1; +` +const SearchInput = styled.input<{ isOpen?: boolean }>` + background: no-repeat scroll 7px 7px; + background-image: url(${searchIcon}); + background-size: 20px 20px; + background-position: 12px center; + background-color: ${({ theme }) => theme.surface1}; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + height: 100%; + width: ${({ isOpen }) => (isOpen ? '200px' : '0')}; + font-size: 16px; + font-weight: 485; + padding-left: 40px; + color: ${({ theme }) => theme.neutral2}; + transition-duration: ${({ theme }) => theme.transition.duration.fast}; + text-overflow: ellipsis; + + :hover { + background-color: ${({ theme }) => theme.surface1}; + } + + :focus { + outline: none; + background-color: ${({ theme }) => theme.surface1}; + border-color: ${({ theme }) => theme.accent1}; + color: ${({ theme }) => theme.neutral1}; + } + + ::placeholder { + color: ${({ theme }) => theme.neutral3}; + } + ::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; + height: ${ICON_SIZE}; + width: ${ICON_SIZE}; + background-image: url(${xIcon}); + margin-right: 10px; + background-size: ${ICON_SIZE} ${ICON_SIZE}; + cursor: pointer; + } + + @media only screen and (max-width: ${MEDIUM_MEDIA_BREAKPOINT}) { + width: ${({ isOpen }) => (isOpen ? 'min(100%, 200px)' : '0')}; + } +` + +export default function SearchBar({ tab }: { tab?: string }) { + const currentString = useAtomValue(exploreSearchStringAtom) + const [localFilterString, setLocalFilterString] = useState(currentString) + const setFilterString = useUpdateAtom(exploreSearchStringAtom) + const debouncedLocalFilterString = useDebounce(localFilterString, 300) + const [isOpen, setIsOpen] = useState(false) + + useEffect(() => { + setLocalFilterString(currentString) + if (currentString) setIsOpen(true) + }, [currentString]) + + useEffect(() => { + setFilterString(debouncedLocalFilterString) + }, [debouncedLocalFilterString, setFilterString]) + + const handleFocus = () => setIsOpen(true) + + const handleBlur = () => { + if (localFilterString === '') setIsOpen(false) + } + + return ( + + ( + + setLocalFilterString(value)} + isOpen={isOpen} + onFocus={handleFocus} + onBlur={handleBlur} + /> + + )} + > + {tab === 'tokens' ? 'Search tokens' : 'Search pools'} + + + ) +} diff --git a/apps/web/src/components/Tokens/TokenTable/TimeSelector.tsx b/apps/web/src/components/Tokens/TokenTable/TimeSelector.tsx new file mode 100644 index 0000000..e20912a --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/TimeSelector.tsx @@ -0,0 +1,101 @@ +import { Trans } from '@lingui/macro' +import { DropdownSelector, InternalMenuItem } from 'components/DropdownSelector' +import { TimePeriod } from 'graphql/data/util' +import { useAtom } from 'jotai' +import { useReducer } from 'react' +import { Check } from 'react-feather' +import { css, useTheme } from 'styled-components' + +import { useScreenSize } from 'hooks/useScreenSize' +import { filterTimeAtom } from '../state' + +export enum TimePeriodDisplay { + HOUR = '1H', + DAY = '1D', + WEEK = '1W', + MONTH = '1M', + YEAR = '1Y', +} + +export const DISPLAYS: Record = { + [TimePeriod.HOUR]: TimePeriodDisplay.HOUR, + [TimePeriod.DAY]: TimePeriodDisplay.DAY, + [TimePeriod.WEEK]: TimePeriodDisplay.WEEK, + [TimePeriod.MONTH]: TimePeriodDisplay.MONTH, + [TimePeriod.YEAR]: TimePeriodDisplay.YEAR, +} + +export function getTimePeriodFromDisplay(display: TimePeriodDisplay): TimePeriod { + switch (display) { + case TimePeriodDisplay.HOUR: + return TimePeriod.HOUR + case TimePeriodDisplay.DAY: + return TimePeriod.DAY + case TimePeriodDisplay.WEEK: + return TimePeriod.WEEK + case TimePeriodDisplay.MONTH: + return TimePeriod.MONTH + case TimePeriodDisplay.YEAR: + return TimePeriod.YEAR + } +} + +export const ORDERED_TIMES: TimePeriod[] = [ + TimePeriod.HOUR, + TimePeriod.DAY, + TimePeriod.WEEK, + TimePeriod.MONTH, + TimePeriod.YEAR, +] + +const StyledMenuFlyout = css` + max-height: 300px; + left: 0px; +` +// TODO: change this to reflect data pipeline +export default function TimeSelector() { + const theme = useTheme() + const [isMenuOpen, toggleMenu] = useReducer((s) => !s, false) + const [activeTime, setTime] = useAtom(filterTimeAtom) + + const screenSize = useScreenSize() + const isLargeScreen = screenSize['lg'] + + return ( +
+ + {DISPLAYS[activeTime]} {isLargeScreen && volume} + + } + internalMenuItems={ + <> + {ORDERED_TIMES.map((time) => ( + { + setTime(time) + toggleMenu() + }} + > +
+ {DISPLAYS[time]} volume +
+ {time === activeTime && } +
+ ))} + + } + dataTestId="time-selector" + buttonCss={css` + height: 40px; + `} + menuFlyoutCss={StyledMenuFlyout} + /> +
+ ) +} diff --git a/apps/web/src/components/Tokens/TokenTable/index.tsx b/apps/web/src/components/Tokens/TokenTable/index.tsx new file mode 100644 index 0000000..7188881 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/index.tsx @@ -0,0 +1,353 @@ +import { Trans } from '@lingui/macro' +import { createColumnHelper } from '@tanstack/react-table' +import { ChainId } from '@uniswap/sdk-core' +import { ParentSize } from '@visx/responsive' +import QueryTokenLogo from 'components/Logo/QueryTokenLogo' +import Row from 'components/Row' +import { Table } from 'components/Table' +import { Cell } from 'components/Table/Cell' +import { MAX_WIDTH_MEDIA_BREAKPOINT } from 'components/Tokens/constants' +import { SparklineMap, TopToken, useTopTokens } from 'graphql/data/TopTokens' +import { + OrderDirection, + chainIdToBackendName, + getTokenDetailsURL, + supportedChainIdFromGQLChain, + validateUrlChainParam, +} from 'graphql/data/util' +import { ReactElement, ReactNode, useMemo } from 'react' +import styled from 'styled-components' +import { EllipsisStyle, ThemedText } from 'theme/components' +import { NumberType, useFormatter } from 'utils/formatNumbers' + +import { ApolloError } from '@apollo/client' +import SparklineChart from 'components/Charts/SparklineChart' +import { ClickableHeaderRow, HeaderArrow, HeaderSortText } from 'components/Table/styled' +import { TokenSortMethod, sortAscendingAtom, sortMethodAtom, useSetSortMethod } from 'components/Tokens/state' +import { MouseoverTooltip } from 'components/Tooltip' +import { NATIVE_CHAIN_ID } from 'constants/tokens' +import { useAtomValue } from 'jotai/utils' +import { useExploreParams } from 'pages/Explore/redirects' +import { DeltaArrow, DeltaText } from '../TokenDetails/Delta' + +const TableWrapper = styled.div` + margin: 0 auto; + max-width: ${MAX_WIDTH_MEDIA_BREAKPOINT}; +` + +export const NameText = styled(ThemedText.BodyPrimary)` + ${EllipsisStyle} +` +const ValueText = styled(ThemedText.BodyPrimary)` + ${EllipsisStyle} +` + +const SparklineContainer = styled.div` + width: 124px; + height: 40px; +` + +interface TokenTableValue { + index: number + tokenDescription: ReactElement + price: number + percentChange1hr: ReactElement + percentChange1d: ReactElement + fdv: number + volume: number + sparkline: ReactElement + link: string + /** Used for pre-loading TDP with logo to extract color from */ + linkState: { preloadedLogoSrc?: string } +} + +function TokenDescription({ token }: { token: TopToken }) { + return ( + + + {token.name} + {token.symbol} + + ) +} + +export function TopTokensTable() { + const chainName = validateUrlChainParam(useExploreParams().chainName) + const chainId = supportedChainIdFromGQLChain(chainName) + const { tokens, tokenSortRank, loadingTokens, sparklines, error } = useTopTokens(chainName) + + return ( + + + + ) +} + +const HEADER_TEXT: Record = { + [TokenSortMethod.FULLY_DILUTED_VALUATION]: FDV, + [TokenSortMethod.PRICE]: Price, + [TokenSortMethod.VOLUME]: Volume, + [TokenSortMethod.HOUR_CHANGE]: 1 hour, + [TokenSortMethod.DAY_CHANGE]: 1 day, +} + +export const HEADER_DESCRIPTIONS: Record = { + [TokenSortMethod.PRICE]: undefined, + [TokenSortMethod.DAY_CHANGE]: undefined, + [TokenSortMethod.HOUR_CHANGE]: undefined, + [TokenSortMethod.FULLY_DILUTED_VALUATION]: ( + + Fully diluted valuation (FDV) calculates the total market value assuming all tokens are in circulation. + + ), + [TokenSortMethod.VOLUME]: ( + Volume is the amount of the asset that has been traded on Uniswap v3 during the selected time frame. + ), +} + +function TokenTableHeader({ + category, + isCurrentSortMethod, + direction, +}: { + category: TokenSortMethod + isCurrentSortMethod: boolean + direction: OrderDirection +}) { + const handleSortCategory = useSetSortMethod(category) + + return ( + + + {isCurrentSortMethod && } + {HEADER_TEXT[category]} + + + ) +} + +function TokenTable({ + tokens, + tokenSortRank, + sparklines, + loading, + error, + loadMore, + chainId, +}: { + tokens?: readonly TopToken[] + tokenSortRank: Record + sparklines: SparklineMap + loading: boolean + error?: ApolloError + loadMore?: ({ onComplete }: { onComplete?: () => void }) => void + chainId: ChainId +}) { + const { formatFiatPrice, formatNumber, formatDelta } = useFormatter() + const sortAscending = useAtomValue(sortAscendingAtom) + const orderDirection = sortAscending ? OrderDirection.Asc : OrderDirection.Desc + const sortMethod = useAtomValue(sortMethodAtom) + + const tokenTableValues: TokenTableValue[] | undefined = useMemo( + () => + tokens?.map((token) => { + const delta1hr = token.market?.pricePercentChange1Hour?.value + const delta1d = token.market?.pricePercentChange1Day?.value + return { + index: tokenSortRank[token.address ?? NATIVE_CHAIN_ID], + tokenDescription: , + price: token.market?.price?.value ?? 0, + testId: `token-table-row-${token.address}`, + percentChange1hr: ( + <> + + {formatDelta(delta1hr)} + + ), + percentChange1d: ( + <> + + {formatDelta(delta1d)} + + ), + fdv: token?.project?.markets?.[0]?.fullyDilutedValuation?.value ?? 0, + volume: token.market?.volume?.value ?? 0, + sparkline: ( + + + {({ width, height }) => + sparklines && ( + + ) + } + + + ), + link: getTokenDetailsURL({ + address: token.address, + chain: chainIdToBackendName(chainId), + }), + linkState: { preloadedLogoSrc: token.project?.logoUrl }, + } + }) ?? [], + [chainId, formatDelta, sparklines, tokenSortRank, tokens] + ) + + const showLoadingSkeleton = loading || !!error + const columns = useMemo(() => { + const columnHelper = createColumnHelper() + return [ + columnHelper.accessor((row) => row.index, { + id: 'index', + header: () => ( + + # + + ), + cell: (index) => ( + + {index.getValue?.()} + + ), + }), + columnHelper.accessor((row) => row.tokenDescription, { + id: 'tokenDescription', + header: () => ( + + + Token name + + + ), + cell: (tokenDescription) => ( + + {tokenDescription.getValue?.()} + + ), + }), + columnHelper.accessor((row) => row.price, { + id: 'price', + header: () => ( + + + + ), + cell: (price) => ( + + + {/* A simple 0 price indicates the price is not currently available from the api */} + {price.getValue?.() === 0 + ? '-' + : formatFiatPrice({ price: price.getValue?.(), type: NumberType.FiatTokenPrice })} + + + ), + }), + columnHelper.accessor((row) => row.percentChange1hr, { + id: 'percentChange1hr', + header: () => ( + + + + ), + cell: (percentChange1hr) => ( + + {percentChange1hr.getValue?.()} + + ), + }), + columnHelper.accessor((row) => row.percentChange1d, { + id: 'percentChange1d', + header: () => ( + + + + ), + cell: (percentChange1d) => ( + + {percentChange1d.getValue?.()} + + ), + }), + columnHelper.accessor((row) => row.fdv, { + id: 'fdv', + header: () => ( + + + + ), + cell: (fdv) => ( + + {formatNumber({ input: fdv.getValue?.(), type: NumberType.FiatTokenStats })} + + ), + }), + columnHelper.accessor((row) => row.volume, { + id: 'volume', + header: () => ( + + + + ), + cell: (volume) => ( + + {formatNumber({ input: volume.getValue?.(), type: NumberType.FiatTokenStats })} + + ), + }), + columnHelper.accessor((row) => row.sparkline, { + id: 'sparkline', + header: () => , + cell: (sparkline) => ( + + {sparkline.getValue?.()} + + ), + }), + ] + }, [formatFiatPrice, formatNumber, orderDirection, showLoadingSkeleton, sortMethod]) + + return ( +
+ ) +} diff --git a/apps/web/src/components/Tokens/TokenTable/search.svg b/apps/web/src/components/Tokens/TokenTable/search.svg new file mode 100644 index 0000000..f3603d4 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/components/Tokens/TokenTable/x.svg b/apps/web/src/components/Tokens/TokenTable/x.svg new file mode 100644 index 0000000..bee7cd4 --- /dev/null +++ b/apps/web/src/components/Tokens/TokenTable/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/components/Tokens/constants.ts b/apps/web/src/components/Tokens/constants.ts new file mode 100644 index 0000000..c0fa023 --- /dev/null +++ b/apps/web/src/components/Tokens/constants.ts @@ -0,0 +1,12 @@ +import { ChainId } from '@uniswap/sdk-core' + +// Breakpoints specifically for the token pages +// TODO(WEB-2968): Deprecate these in the new .info project +export const MAX_WIDTH_MEDIA_BREAKPOINT = '1200px' +export const XLARGE_MEDIA_BREAKPOINT = '960px' +export const MEDIUM_MEDIA_BREAKPOINT = '720px' +export const SMALL_MEDIA_BREAKPOINT = '540px' +export const MOBILE_MEDIA_BREAKPOINT = '420px' + +// includes chains that the backend does not current source off-chain metadata for +export const UNSUPPORTED_METADATA_CHAINS = [ChainId.BNB, ChainId.AVALANCHE] diff --git a/apps/web/src/components/Tokens/loading.tsx b/apps/web/src/components/Tokens/loading.tsx new file mode 100644 index 0000000..a2f3765 --- /dev/null +++ b/apps/web/src/components/Tokens/loading.tsx @@ -0,0 +1,31 @@ +import { loadingAnimation } from 'components/Loader/styled' +import { lighten } from 'polished' +import styled from 'styled-components' + +/* Loading state bubbles (animation style from: src/components/Loader/styled.tsx) */ +export const LoadingBubble = styled.div<{ + height?: string + width?: string + round?: boolean + delay?: string + margin?: string +}>` + border-radius: 12px; + border-radius: ${({ round }) => (round ? '50%' : '12px')}; + ${({ margin }) => margin && `margin: ${margin}`}; + height: ${({ height }) => height ?? '24px'}; + width: 50%; + width: ${({ width }) => width ?? '50%'}; + animation: ${loadingAnimation} 1.5s infinite; + ${({ delay }) => delay && `animation-delay: ${delay};`} + animation-fill-mode: both; + background: linear-gradient( + to left, + ${({ theme }) => theme.surface3} 25%, + // Default color values prevent undefined argument error + ${({ theme }) => lighten(0.075, theme.surface3 ?? '#FFFFFF12')} 50%, + ${({ theme }) => theme.surface3} 75% + ); + will-change: background-position; + background-size: 400%; +` diff --git a/apps/web/src/components/Tokens/state.ts b/apps/web/src/components/Tokens/state.ts new file mode 100644 index 0000000..656ebc4 --- /dev/null +++ b/apps/web/src/components/Tokens/state.ts @@ -0,0 +1,32 @@ +import { TimePeriod } from 'graphql/data/util' +import { atom, useAtom } from 'jotai' +import { atomWithReset, useUpdateAtom } from 'jotai/utils' +import { useCallback } from 'react' + +export enum TokenSortMethod { + FULLY_DILUTED_VALUATION = 'FDV', + PRICE = 'Price', + VOLUME = 'Volume', + HOUR_CHANGE = '1 hour', + DAY_CHANGE = '1 day', +} + +export const exploreSearchStringAtom = atomWithReset('') +export const filterTimeAtom = atom(TimePeriod.DAY) +export const sortMethodAtom = atom(TokenSortMethod.VOLUME) +export const sortAscendingAtom = atom(false) + +/* keep track of sort category for token table */ +export function useSetSortMethod(newSortMethod: TokenSortMethod) { + const [sortMethod, setSortMethod] = useAtom(sortMethodAtom) + const setSortAscending = useUpdateAtom(sortAscendingAtom) + + return useCallback(() => { + if (sortMethod === newSortMethod) { + setSortAscending((sortAscending) => !sortAscending) + } else { + setSortMethod(newSortMethod) + setSortAscending(false) + } + }, [sortMethod, setSortMethod, setSortAscending, newSortMethod]) +} diff --git a/apps/web/src/components/Tooltip/index.tsx b/apps/web/src/components/Tooltip/index.tsx new file mode 100644 index 0000000..efe7844 --- /dev/null +++ b/apps/web/src/components/Tooltip/index.tsx @@ -0,0 +1,121 @@ +import { transparentize } from 'polished' +import { PropsWithChildren, ReactNode, useEffect, useState } from 'react' +import styled from 'styled-components' +import noop from 'utilities/src/react/noop' + +import Popover, { PopoverProps } from '../Popover' + +export enum TooltipSize { + ExtraSmall = '200px', + Small = '256px', + Large = '400px', + Max = 'max-content', +} + +const getPaddingForSize = (size: TooltipSize) => { + switch (size) { + case TooltipSize.ExtraSmall: + case TooltipSize.Max: + return '8px' + case TooltipSize.Small: + return '12px' + case TooltipSize.Large: + return '16px 20px' + } +} + +const TooltipContainer = styled.div<{ size: TooltipSize }>` + max-width: ${({ size }) => size}; + width: calc(100vw - 16px); + cursor: default; + padding: ${({ size }) => getPaddingForSize(size)}; + pointer-events: auto; + + color: ${({ theme }) => theme.neutral1}; + font-weight: 485; + font-size: 12px; + line-height: 16px; + word-break: break-word; + + background: ${({ theme }) => theme.surface1}; + border-radius: 12px; + border: 1px solid ${({ theme }) => theme.surface3}; + box-shadow: 0 4px 8px 0 ${({ theme }) => transparentize(0.9, theme.shadow1)}; +` + +type TooltipProps = Omit & { + text: ReactNode + open?: () => void + close?: () => void + size?: TooltipSize + disabled?: boolean + timeout?: number +} + +// TODO(WEB-2024) +// Migrate to MouseoverTooltip and move this component inline to MouseoverTooltip +export default function Tooltip({ text, open, close, disabled, size = TooltipSize.Small, ...rest }: TooltipProps) { + return ( + + {text} + + ) + } + {...rest} + /> + ) +} + +// TODO(WEB-2024) +// Do not pass through PopoverProps. Prefer higher-level interface to control MouseoverTooltip. +type MouseoverTooltipProps = Omit & + PropsWithChildren<{ + text: ReactNode + size?: TooltipSize + disabled?: boolean + timeout?: number + placement?: PopoverProps['placement'] + onOpen?: () => void + forceShow?: boolean + }> + +export function MouseoverTooltip(props: MouseoverTooltipProps) { + const { text, disabled, children, onOpen, forceShow, timeout, ...rest } = props + const [show, setShow] = useState(false) + const open = () => { + setShow(true) + onOpen?.() + } + const close = () => setShow(false) + + useEffect(() => { + if (show && timeout) { + const tooltipTimer = setTimeout(() => { + setShow(false) + }, timeout) + + return () => { + clearTimeout(tooltipTimer) + } + } + return + }, [timeout, show]) + + return ( + +
+ {children} +
+
+ ) +} diff --git a/apps/web/src/components/TopLevelModals/index.tsx b/apps/web/src/components/TopLevelModals/index.tsx new file mode 100644 index 0000000..e5e9e2b --- /dev/null +++ b/apps/web/src/components/TopLevelModals/index.tsx @@ -0,0 +1,48 @@ +import { useWeb3React } from '@web3-react/core' +import { OffchainActivityModal } from 'components/AccountDrawer/MiniPortfolio/Activity/OffchainActivityModal' +import UniwalletModal from 'components/AccountDrawer/UniwalletModal' +import { Banners } from 'components/Banner/shared/Banners' +import AddressClaimModal from 'components/claim/AddressClaimModal' +import ConnectedAccountBlocked from 'components/ConnectedAccountBlocked' +import FeatureFlagModal from 'components/FeatureFlagModal/FeatureFlagModal' +import FiatOnrampModal from 'components/FiatOnrampModal' +import { UkDisclaimerModal } from 'components/NavBar/UkDisclaimerModal' +import { PrivacyPolicyModal } from 'components/PrivacyPolicy' +import DevFlagsBox from 'dev/DevFlagsBox' +import useAccountRiskCheck from 'hooks/useAccountRiskCheck' +import Bag from 'nft/components/bag/Bag' +import TransactionCompleteModal from 'nft/components/collection/TransactionCompleteModal' +import { GetTheAppModal } from 'pages/Landing/components/DownloadApp/GetTheAppModal' +import { useModalIsOpen, useToggleModal } from 'state/application/hooks' +import { ApplicationModal } from 'state/application/reducer' +import { isDevelopmentEnv, isStagingEnv } from 'utils/env' + +export default function TopLevelModals() { + const addressClaimOpen = useModalIsOpen(ApplicationModal.ADDRESS_CLAIM) + const addressClaimToggle = useToggleModal(ApplicationModal.ADDRESS_CLAIM) + const blockedAccountModalOpen = useModalIsOpen(ApplicationModal.BLOCKED_ACCOUNT) + const { account } = useWeb3React() + useAccountRiskCheck(account) + const accountBlocked = Boolean(blockedAccountModalOpen && account) + const shouldShowDevFlags = isDevelopmentEnv() || isStagingEnv() + + return ( + <> + + + + + + + + + + + + + + + {shouldShowDevFlags && } + + ) +} diff --git a/apps/web/src/components/TransactionConfirmationModal/AnimatedConfirmation.tsx b/apps/web/src/components/TransactionConfirmationModal/AnimatedConfirmation.tsx new file mode 100644 index 0000000..84e0743 --- /dev/null +++ b/apps/web/src/components/TransactionConfirmationModal/AnimatedConfirmation.tsx @@ -0,0 +1,69 @@ +import styled, { keyframes, useTheme } from 'styled-components' + +const Wrapper = styled.div<{ size?: string }>` + height: 90px; + width: 90px; +` + +const dash = keyframes` + 0% { + stroke-dashoffset: 1000; + } + 100% { + stroke-dashoffset: 0; + } +` + +const dashCheck = keyframes` + 0% { + stroke-dashoffset: -100; + } + 100% { + stroke-dashoffset: 900; + } +` + +const Circle = styled.circle` + stroke-dasharray: 1000; + stroke-dashoffset: 0; + -webkit-animation: ${dash} 0.9s ease-in-out; + animation: ${dash} 0.9s ease-in-out; +` + +const PolyLine = styled.polyline` + stroke-dasharray: 1000; + stroke-dashoffset: 0; + stroke-dashoffset: -100; + -webkit-animation: ${dashCheck} 0.9s 0.35s ease-in-out forwards; + animation: ${dashCheck} 0.9s 0.35s ease-in-out forwards; +` + +export default function AnimatedConfirmation({ className }: { className?: string }) { + const theme = useTheme() + + return ( + + + + + + + ) +} diff --git a/apps/web/src/components/TransactionConfirmationModal/ConfirmationModalContent.test.tsx b/apps/web/src/components/TransactionConfirmationModal/ConfirmationModalContent.test.tsx new file mode 100644 index 0000000..c100c1f --- /dev/null +++ b/apps/web/src/components/TransactionConfirmationModal/ConfirmationModalContent.test.tsx @@ -0,0 +1,31 @@ +import { render, screen } from 'test-utils/render' +import noop from 'utilities/src/react/noop' + +import { ConfirmationModalContent } from '.' + +describe('ConfirmationModalContent', () => { + it('should render the L2 icon for optimism', () => { + render( +
topContent
} + bottomContent={() =>
bottomContent
} + headerContent={() =>
headerContent
} + /> + ) + expect(screen.getByTestId('confirmation-modal-chain-icon')).toBeInTheDocument() + }) + + it('should not render a chain icon', () => { + render( +
topContent
} + bottomContent={() =>
bottomContent
} + /> + ) + expect(screen.queryByTestId('confirmation-modal-chain-icon')).not.toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/TransactionConfirmationModal/index.tsx b/apps/web/src/components/TransactionConfirmationModal/index.tsx new file mode 100644 index 0000000..abd0b55 --- /dev/null +++ b/apps/web/src/components/TransactionConfirmationModal/index.tsx @@ -0,0 +1,339 @@ +import { t, Trans } from '@lingui/macro' +import { ChainId, Currency } from '@uniswap/sdk-core' +import { useWeb3React } from '@web3-react/core' +import Badge from 'components/Badge' +import { ChainLogo } from 'components/Logo/ChainLogo' +import { getChainInfo } from 'constants/chainInfo' +import { SupportedL2ChainId } from 'constants/chains' +import useCurrencyLogoURIs from 'lib/hooks/useCurrencyLogoURIs' +import { ReactNode, useCallback, useState } from 'react' +import { AlertCircle, ArrowUpCircle, CheckCircle } from 'react-feather' +import { useIsTransactionConfirmed, useTransaction } from 'state/transactions/hooks' +import styled, { useTheme } from 'styled-components' +import { CloseIcon, CustomLightSpinner, ExternalLink, ThemedText } from 'theme/components' +import { isL2ChainId } from 'utils/chains' +import { ExplorerDataType, getExplorerLink } from 'utils/getExplorerLink' + +import Circle from '../../assets/images/blue-loader.svg' +import { TransactionSummary } from '../AccountDetails/TransactionSummary' +import { ButtonLight, ButtonPrimary } from '../Button' +import { AutoColumn, ColumnCenter } from '../Column' +import Modal from '../Modal' +import Row, { RowBetween, RowFixed } from '../Row' +import AnimatedConfirmation from './AnimatedConfirmation' + +const Wrapper = styled.div` + background-color: ${({ theme }) => theme.surface1}; + border-radius: 20px; + outline: 1px solid ${({ theme }) => theme.surface3}; + width: 100%; + padding: 16px; +` + +const BottomSection = styled(AutoColumn)` + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; +` + +const ConfirmedIcon = styled(ColumnCenter)<{ inline?: boolean }>` + padding: ${({ inline }) => (inline ? '20px 0' : '32px 0;')}; +` + +const ConfirmationModalContentWrapper = styled(AutoColumn)` + padding-bottom: 12px; +` + +function ConfirmationPendingContent({ + onDismiss, + pendingText, + inline, +}: { + onDismiss: () => void + pendingText: ReactNode + inline?: boolean // not in modal +}) { + return ( + + + {!inline && ( + +
+ + + )} + + + + + + Waiting for confirmation + + + {pendingText} + + + Confirm this transaction in your wallet + + + + + ) +} +function TransactionSubmittedContent({ + onDismiss, + chainId, + hash, + currencyToAdd, + inline, +}: { + onDismiss: () => void + hash?: string + chainId: number + currencyToAdd?: Currency + inline?: boolean // not in modal +}) { + const theme = useTheme() + + const { connector } = useWeb3React() + + const token = currencyToAdd?.wrapped + const logoURL = useCurrencyLogoURIs(token)[0] + + const [success, setSuccess] = useState() + + const addToken = useCallback(() => { + if (!token?.symbol || !connector.watchAsset) return + connector + .watchAsset({ + address: token.address, + symbol: token.symbol, + decimals: token.decimals, + image: logoURL, + }) + .then(() => setSuccess(true)) + .catch(() => setSuccess(false)) + }, [connector, logoURL, token]) + + const explorerText = chainId === ChainId.MAINNET ? t`View on Etherscan` : t`View on Block Explorer` + + return ( + + + {!inline && ( + +
+ + + )} + + + + + + Transaction submitted + + {currencyToAdd && connector.watchAsset && ( + + {!success ? ( + + Add {currencyToAdd.symbol} + + ) : ( + + Added {currencyToAdd.symbol} + + + )} + + )} + + + {inline ? Return : Close} + + + {chainId && hash && ( + + {explorerText} + + )} + + + + ) +} + +export function ConfirmationModalContent({ + title, + bottomContent, + onDismiss, + topContent, + headerContent, +}: { + title: ReactNode + onDismiss: () => void + topContent: () => ReactNode + bottomContent?: () => ReactNode + headerContent?: () => ReactNode +}) { + return ( + + + + {headerContent?.()} + + {title} + + + + {topContent()} + + {bottomContent && {bottomContent()}} + + ) +} + +const StyledL2Badge = styled(Badge)` + padding: 6px 8px; +` + +function L2Content({ + onDismiss, + chainId, + hash, + pendingText, + inline, +}: { + onDismiss: () => void + hash?: string + chainId: SupportedL2ChainId + currencyToAdd?: Currency + pendingText: ReactNode + inline?: boolean // not in modal +}) { + const theme = useTheme() + + const transaction = useTransaction(hash) + const confirmed = useIsTransactionConfirmed(hash) + const transactionSuccess = transaction?.receipt?.status === 1 + + // convert unix time difference to seconds + const secondsToConfirm = transaction?.confirmedTime + ? (transaction.confirmedTime - transaction.addedTime) / 1000 + : undefined + + const info = getChainInfo(chainId) + + return ( + + + {!inline && ( + + + + + {info.label} + + + + + )} + + {confirmed ? ( + transactionSuccess ? ( + // + + ) : ( + + ) + ) : ( + + )} + + + + {!hash ? ( + Confirm transaction in wallet + ) : !confirmed ? ( + Transaction submitted + ) : transactionSuccess ? ( + Success + ) : ( + Error + )} + + + {transaction ? : pendingText} + + {chainId && hash ? ( + + + View on Explorer + + + ) : ( +
+ )} + + {!secondsToConfirm ? ( +
+ ) : ( +
+ Transaction completed in + + {secondsToConfirm} seconds 🎉 + +
+ )} + + + {inline ? Return : Close} + + + + + ) +} + +interface ConfirmationModalProps { + isOpen: boolean + onDismiss: () => void + hash?: string + reviewContent: () => ReactNode + attemptingTxn: boolean + pendingText: ReactNode + currencyToAdd?: Currency +} + +export default function TransactionConfirmationModal({ + isOpen, + onDismiss, + attemptingTxn, + hash, + pendingText, + reviewContent, + currencyToAdd, +}: ConfirmationModalProps) { + const { chainId } = useWeb3React() + + if (!chainId) return null + + // confirmation screen + return ( + + {isL2ChainId(chainId) && (hash || attemptingTxn) ? ( + + ) : attemptingTxn ? ( + + ) : hash ? ( + + ) : ( + reviewContent() + )} + + ) +} diff --git a/apps/web/src/components/UniTag/UniTagProfilePicture.tsx b/apps/web/src/components/UniTag/UniTagProfilePicture.tsx new file mode 100644 index 0000000..1b6480b --- /dev/null +++ b/apps/web/src/components/UniTag/UniTagProfilePicture.tsx @@ -0,0 +1,27 @@ +import styled from 'styled-components' +import { useUnitagByAddressWithoutFlag } from 'uniswap/src/features/unitags/hooksWithoutFlags' + +const Container = styled.div<{ $iconSize: number }>` + height: ${({ $iconSize: iconSize }) => `${iconSize}px`}; + width: ${({ $iconSize: iconSize }) => `${iconSize}px`}; + border-radius: 50%; + background-color: ${({ theme }) => theme.surface3}; + font-size: initial; +` + +const Profile = styled.img` + height: inherit; + width: inherit; + border-radius: inherit; + object-fit: cover; +` + +export function UniTagProfilePicture({ account, size }: { account: string; size: number }) { + const { unitag } = useUnitagByAddressWithoutFlag(account, Boolean(account)) + + return ( + + {unitag?.metadata?.avatar && } + + ) +} diff --git a/apps/web/src/components/Unicon/Container.ts b/apps/web/src/components/Unicon/Container.ts new file mode 100644 index 0000000..b87579f --- /dev/null +++ b/apps/web/src/components/Unicon/Container.ts @@ -0,0 +1,223 @@ +export const svgPaths: React.SVGProps[][] = [ + [ + { + d: 'M35.9146 19.765L29.9428 19.1838C29.9805 18.796 30 18.4012 30 18C30 17.5988 29.9805 17.204 29.9428 16.8162L35.9146 16.235C35.9711 16.8157 36 17.4045 36 18C36 18.5955 35.9711 19.1843 35.9146 19.765ZM35.2294 12.7731L29.4871 14.5128C29.2582 13.7572 28.9562 13.0324 28.5887 12.3463L33.8778 9.51335C34.4298 10.544 34.8845 11.6347 35.2294 12.7731ZM31.9147 6.58074L27.2788 10.3897C26.7784 9.78072 26.2193 9.22156 25.6103 8.7212L29.4193 4.08528C30.3302 4.8337 31.1663 5.66983 31.9147 6.58074ZM26.4867 2.12222L23.6537 7.41131C22.9676 7.04381 22.2428 6.74181 21.4872 6.51288L23.2269 0.770639C24.3653 1.11554 25.456 1.57016 26.4867 2.12222ZM19.765 0.0854343L19.1838 6.05722C18.796 6.01948 18.4012 6 18 6C17.5988 6 17.204 6.01948 16.8162 6.05722L16.235 0.0854339C16.8157 0.0289206 17.4045 0 18 0C18.5955 0 19.1843 0.0289207 19.765 0.0854343ZM12.7731 0.770638L14.5128 6.51288C13.7572 6.74181 13.0324 7.04381 12.3463 7.41131L9.51335 2.12222C10.544 1.57016 11.6347 1.11553 12.7731 0.770638ZM6.58074 4.08528L10.3897 8.7212C9.78072 9.22156 9.22156 9.78072 8.7212 10.3897L4.08528 6.58074C4.8337 5.66983 5.66983 4.8337 6.58074 4.08528ZM2.12222 9.51335L7.41131 12.3463C7.04381 13.0324 6.74181 13.7572 6.51288 14.5128L0.770639 12.7731C1.11554 11.6347 1.57016 10.544 2.12222 9.51335ZM0.0854342 16.235C0.0289207 16.8157 0 17.4045 0 18C0 18.5955 0.0289206 19.1843 0.0854339 19.765L6.05722 19.1838C6.01948 18.796 6 18.4012 6 18C6 17.5988 6.01948 17.204 6.05722 16.8162L0.0854342 16.235ZM0.770638 23.2269L6.51288 21.4872C6.74181 22.2428 7.04381 22.9676 7.41131 23.6537L2.12222 26.4867C1.57016 25.456 1.11554 24.3653 0.770638 23.2269ZM4.08528 29.4193L8.7212 25.6103C9.22156 26.2193 9.78072 26.7784 10.3897 27.2788L6.58074 31.9147C5.66983 31.1663 4.8337 30.3302 4.08528 29.4193ZM9.51335 33.8778L12.3463 28.5887C13.0324 28.9562 13.7572 29.2582 14.5128 29.4871L12.7731 35.2294C11.6347 34.8845 10.544 34.4298 9.51335 33.8778ZM16.235 35.9146L16.8162 29.9428C17.204 29.9805 17.5988 30 18 30C18.4012 30 18.796 29.9805 19.1838 29.9428L19.765 35.9146C19.1843 35.9711 18.5955 36 18 36C17.4045 36 16.8157 35.9711 16.235 35.9146ZM23.2269 35.2294C24.3653 34.8845 25.456 34.4298 26.4867 33.8778L23.6537 28.5887C22.9676 28.9562 22.2428 29.2582 21.4872 29.4871L23.2269 35.2294ZM29.4193 31.9147L25.6103 27.2788C26.2193 26.7784 26.7784 26.2193 27.2788 25.6103L31.9147 29.4193C31.1663 30.3302 30.3302 31.1663 29.4193 31.9147ZM33.8778 26.4867L28.5887 23.6537C28.9562 22.9676 29.2582 22.2428 29.4871 21.4872L35.2294 23.2269C34.8845 24.3653 34.4298 25.456 33.8778 26.4867Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M35.993 18.5062L30.9949 18.3682C30.9983 18.246 31 18.1233 31 18C31 17.8767 30.9983 17.754 30.9949 17.6318L35.993 17.4938C35.9977 17.662 36 17.8307 36 18C36 18.1693 35.9977 18.338 35.993 18.5062ZM35.9372 16.4858L30.9545 16.9008C30.9341 16.6567 30.9071 16.4149 30.8736 16.1755L35.8253 15.4825C35.8717 15.8141 35.9091 16.1486 35.9372 16.4858ZM35.6574 14.4871L30.7524 15.4574C30.7052 15.2185 30.6514 14.9821 30.5913 14.7483L35.4336 13.5028C35.5171 13.8274 35.5918 14.1556 35.6574 14.4871ZM35.1545 12.532L30.3902 14.0493C30.3165 13.8176 30.2363 13.5888 30.15 13.3629L34.8209 11.5788C34.9407 11.8924 35.052 12.2103 35.1545 12.532ZM34.4341 10.646L29.8714 12.6909C29.7719 12.469 29.6664 12.2504 29.555 12.0352L33.9953 9.73658C34.1498 10.0349 34.2961 10.3382 34.4341 10.646ZM33.5061 8.85332L29.202 11.3977C29.0781 11.1882 28.9485 10.9824 28.8133 10.7805L32.9682 7.99892C33.1552 8.27829 33.3346 8.56319 33.5061 8.85332ZM32.3832 7.17587L28.3904 10.1854C28.2437 9.99086 28.0917 9.80047 27.9345 9.6145L31.7531 6.38676C31.9703 6.64373 32.1804 6.90687 32.3832 7.17587ZM31.0799 5.63405L27.4472 9.06966C27.2798 8.8926 27.1074 8.72022 26.9303 8.55276L30.3659 4.92005C30.6105 5.15137 30.8486 5.38947 31.0799 5.63405ZM29.6132 4.24694L26.3855 8.06554C26.1995 7.90834 26.0091 7.7563 25.8146 7.60964L28.8241 3.61683C29.0931 3.8196 29.3563 4.02973 29.6132 4.24694ZM28.0011 3.03182L25.2195 7.18666C25.0176 7.05149 24.8118 6.92188 24.6023 6.79804L27.1467 2.49385C27.4368 2.66536 27.7217 2.84478 28.0011 3.03182ZM26.2634 2.00466L23.9648 6.44498C23.7496 6.33357 23.531 6.22806 23.3091 6.12863L25.354 1.5659C25.6618 1.70386 25.965 1.85021 26.2634 2.00466ZM24.4212 1.17909L22.6371 5.84995C22.4112 5.76367 22.1823 5.68354 21.9507 5.60976L23.4679 0.845535C23.7897 0.948011 24.1076 1.05929 24.4212 1.17909ZM22.4972 0.566362L21.2517 5.40875C21.0179 5.34861 20.7815 5.29482 20.5426 5.24755L21.5129 0.342612C21.8444 0.408185 22.1726 0.482859 22.4972 0.566362ZM20.5175 0.174659L19.8245 5.1264C19.5851 5.09289 19.3433 5.06587 19.0992 5.04554L19.5142 0.0627905C19.8514 0.090873 20.1859 0.128248 20.5175 0.174659ZM18.5062 0.00698034L18.3682 5.00508C18.246 5.0017 18.1233 5 18 5C17.8767 5 17.754 5.0017 17.6318 5.00508L17.4938 0.0069805C17.662 0.002337 17.8307 0 18 0C18.1693 0 18.338 0.00233695 18.5062 0.00698034ZM16.4858 0.0627908L16.9008 5.04554C16.6567 5.06587 16.4149 5.09289 16.1755 5.1264L15.4825 0.17466C15.8141 0.128249 16.1486 0.0908734 16.4858 0.0627908ZM14.4871 0.342613L15.4574 5.24755C15.2185 5.29482 14.9821 5.34862 14.7483 5.40875L13.5028 0.566363C13.8274 0.48286 14.1556 0.408186 14.4871 0.342613ZM12.532 0.845537L14.0493 5.60977C13.8176 5.68355 13.5888 5.76367 13.3629 5.84995L11.5788 1.17909C11.8924 1.05929 12.2103 0.948013 12.532 0.845537ZM10.646 1.56591L12.6909 6.12863C12.469 6.22806 12.2504 6.33357 12.0352 6.44499L9.73658 2.00466C10.0349 1.85021 10.3382 1.70386 10.646 1.56591ZM8.85332 2.49385L11.3977 6.79804C11.1882 6.92188 10.9824 7.05149 10.7805 7.18667L7.99892 3.03182C8.27829 2.84479 8.56319 2.66537 8.85332 2.49385ZM7.17587 3.61684L10.1854 7.60964C9.99086 7.75631 9.80047 7.90834 9.6145 8.06554L6.38676 4.24694C6.64373 4.02974 6.90686 3.8196 7.17587 3.61684ZM5.63406 4.92005L9.06966 8.55276C8.8926 8.72022 8.72022 8.8926 8.55276 9.06967L4.92005 5.63406C5.15137 5.38947 5.38947 5.15137 5.63406 4.92005ZM4.24694 6.38676L8.06554 9.6145C7.90834 9.80048 7.7563 9.99086 7.60964 10.1854L3.61683 7.17588C3.8196 6.90687 4.02973 6.64373 4.24694 6.38676ZM3.03182 7.99892L7.18666 10.7805C7.05149 10.9824 6.92188 11.1882 6.79804 11.3977L2.49385 8.85333C2.66536 8.5632 2.84478 8.27829 3.03182 7.99892ZM2.00466 9.73659L6.44498 12.0352C6.33357 12.2504 6.22806 12.469 6.12863 12.6909L1.5659 10.646C1.70386 10.3382 1.85021 10.035 2.00466 9.73659ZM1.17909 11.5788L5.84995 13.3629C5.76367 13.5888 5.68354 13.8177 5.60976 14.0493L0.845535 12.5321C0.948011 12.2103 1.05929 11.8924 1.17909 11.5788ZM0.566362 13.5028L5.40875 14.7483C5.34861 14.9821 5.29482 15.2185 5.24755 15.4574L0.342612 14.4871C0.408185 14.1556 0.482859 13.8274 0.566362 13.5028ZM0.174659 15.4825L5.1264 16.1755C5.09289 16.4149 5.06587 16.6567 5.04554 16.9008L0.0627905 16.4858C0.090873 16.1486 0.128248 15.8141 0.174659 15.4825ZM0.00698034 17.4938C0.00233695 17.662 0 17.8307 0 18C0 18.1693 0.002337 18.338 0.0069805 18.5062L5.00508 18.3682C5.0017 18.246 5 18.1233 5 18C5 17.8767 5.0017 17.754 5.00508 17.6318L0.00698034 17.4938ZM0.0627908 19.5142L5.04554 19.0992C5.06587 19.3433 5.09289 19.5851 5.1264 19.8245L0.17466 20.5175C0.128249 20.1859 0.0908734 19.8514 0.0627908 19.5142ZM0.342613 21.5129L5.24755 20.5426C5.29482 20.7815 5.34862 21.0179 5.40875 21.2517L0.566363 22.4972C0.48286 22.1726 0.408186 21.8444 0.342613 21.5129ZM0.845537 23.468L5.60977 21.9507C5.68355 22.1824 5.76367 22.4112 5.84995 22.6371L1.17909 24.4212C1.05929 24.1076 0.948013 23.7897 0.845537 23.468ZM1.56591 25.354L6.12863 23.3091C6.22806 23.531 6.33357 23.7496 6.44499 23.9648L2.00466 26.2634C1.85021 25.9651 1.70386 25.6618 1.56591 25.354ZM2.49385 27.1467L6.79804 24.6023C6.92188 24.8118 7.05149 25.0176 7.18667 25.2195L3.03182 28.0011C2.84479 27.7217 2.66537 27.4368 2.49385 27.1467ZM3.61684 28.8241L7.60964 25.8146C7.75631 26.0091 7.90834 26.1995 8.06554 26.3855L4.24694 29.6132C4.02974 29.3563 3.8196 29.0931 3.61684 28.8241ZM4.92005 30.3659L8.55276 26.9303C8.72022 27.1074 8.8926 27.2798 9.06967 27.4472L5.63406 31.08C5.38947 30.8486 5.15137 30.6105 4.92005 30.3659ZM6.38676 31.7531L9.6145 27.9345C9.80048 28.0917 9.99086 28.2437 10.1854 28.3904L7.17588 32.3832C6.90687 32.1804 6.64373 31.9703 6.38676 31.7531ZM7.99892 32.9682L10.7805 28.8133C10.9824 28.9485 11.1882 29.0781 11.3977 29.202L8.85333 33.5061C8.5632 33.3346 8.27829 33.1552 7.99892 32.9682ZM9.73659 33.9953L12.0352 29.555C12.2504 29.6664 12.469 29.7719 12.6909 29.8714L10.646 34.4341C10.3382 34.2961 10.035 34.1498 9.73659 33.9953ZM11.5788 34.8209L13.3629 30.15C13.5888 30.2363 13.8177 30.3165 14.0493 30.3902L12.5321 35.1545C12.2103 35.052 11.8924 34.9407 11.5788 34.8209ZM13.5028 35.4336L14.7483 30.5913C14.9821 30.6514 15.2185 30.7052 15.4574 30.7524L14.4871 35.6574C14.1556 35.5918 13.8274 35.5171 13.5028 35.4336ZM15.4825 35.8253L16.1755 30.8736C16.4149 30.9071 16.6567 30.9341 16.9008 30.9545L16.4858 35.9372C16.1486 35.9091 15.8141 35.8717 15.4825 35.8253ZM17.4938 35.993L17.6318 30.9949C17.754 30.9983 17.8767 31 18 31C18.1233 31 18.246 30.9983 18.3682 30.9949L18.5062 35.993C18.338 35.9977 18.1693 36 18 36C17.8307 36 17.662 35.9977 17.4938 35.993ZM19.5142 35.9372L19.0992 30.9545C19.3433 30.9341 19.5851 30.9071 19.8245 30.8736L20.5175 35.8253C20.1859 35.8717 19.8514 35.9091 19.5142 35.9372ZM21.5129 35.6574L20.5426 30.7524C20.7815 30.7052 21.0179 30.6514 21.2517 30.5913L22.4972 35.4336C22.1726 35.5171 21.8444 35.5918 21.5129 35.6574ZM23.468 35.1545L21.9507 30.3902C22.1824 30.3165 22.4112 30.2363 22.6371 30.15L24.4212 34.8209C24.1076 34.9407 23.7897 35.052 23.468 35.1545ZM25.354 34.4341L23.3091 29.8714C23.531 29.7719 23.7496 29.6664 23.9648 29.555L26.2634 33.9953C25.9651 34.1498 25.6618 34.2961 25.354 34.4341ZM27.1467 33.5061L24.6023 29.202C24.8118 29.0781 25.0176 28.9485 25.2195 28.8133L28.0011 32.9682C27.7217 33.1552 27.4368 33.3346 27.1467 33.5061ZM28.8241 32.3832L25.8146 28.3904C26.0091 28.2437 26.1995 28.0917 26.3855 27.9345L29.6132 31.7531C29.3563 31.9703 29.0931 32.1804 28.8241 32.3832ZM30.3659 31.0799L26.9303 27.4472C27.1074 27.2798 27.2798 27.1074 27.4472 26.9303L31.08 30.3659C30.8486 30.6105 30.6105 30.8486 30.3659 31.0799ZM31.7531 29.6132L27.9345 26.3855C28.0917 26.1995 28.2437 26.0091 28.3904 25.8146L32.3832 28.8241C32.1804 29.0931 31.9703 29.3563 31.7531 29.6132ZM32.9682 28.0011L28.8133 25.2195C28.9485 25.0176 29.0781 24.8118 29.202 24.6023L33.5061 27.1467C33.3346 27.4368 33.1552 27.7217 32.9682 28.0011ZM33.9953 26.2634L29.555 23.9648C29.6664 23.7496 29.7719 23.531 29.8714 23.3091L34.4341 25.354C34.2961 25.6618 34.1498 25.965 33.9953 26.2634ZM34.8209 24.4212L30.15 22.6371C30.2363 22.4112 30.3165 22.1823 30.3902 21.9507L35.1545 23.4679C35.052 23.7897 34.9407 24.1076 34.8209 24.4212ZM35.4336 22.4972L30.5913 21.2517C30.6514 21.0179 30.7052 20.7815 30.7524 20.5426L35.6574 21.5129C35.5918 21.8444 35.5171 22.1726 35.4336 22.4972ZM35.8253 20.5175L30.8736 19.8245C30.9071 19.5851 30.9341 19.3433 30.9545 19.0992L35.9372 19.5142C35.9091 19.8514 35.8717 20.1859 35.8253 20.5175Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M16.6962 0.411464C17.4776 -0.137155 18.5224 -0.137155 19.3038 0.411465L25.4698 4.74043C25.5828 4.81972 25.7028 4.88854 25.8284 4.94603L32.6866 8.08442C33.5558 8.48216 34.0782 9.38069 33.9904 10.327L33.2983 17.7944C33.2856 17.9312 33.2856 18.0688 33.2983 18.2056L33.9904 25.673C34.0782 26.6193 33.5558 27.5178 32.6866 27.9156L25.8284 31.054C25.7028 31.1115 25.5828 31.1803 25.4698 31.2596L19.3038 35.5885C18.5224 36.1372 17.4776 36.1372 16.6962 35.5885L10.5302 31.2596C10.4172 31.1803 10.2972 31.1115 10.1716 31.054L3.31338 27.9156C2.44423 27.5178 1.92184 26.6193 2.00956 25.673L2.70174 18.2056C2.71442 18.0688 2.71442 17.9312 2.70174 17.7944L2.00956 10.327C1.92184 9.38068 2.44423 8.48216 3.31338 8.08442L10.1716 4.94603C10.2972 4.88854 10.4172 4.81972 10.5302 4.74043L16.6962 0.411464Z', + }, + ], + [ + { + d: 'M16.79 0.854044C17.1949 -0.2846 18.8052 -0.2846 19.2101 0.854043L21.9319 8.50777L29.2685 5.02035C30.3599 4.50153 31.4986 5.64018 30.9798 6.73164L27.4923 14.0682L35.1461 16.79C36.2847 17.1949 36.2847 18.8052 35.1461 19.2101L27.4923 21.9319L30.9798 29.2685C31.4986 30.3599 30.3599 31.4986 29.2685 30.9798L21.9319 27.4923L19.2101 35.1461C18.8052 36.2847 17.1949 36.2847 16.79 35.1461L14.0682 27.4923L6.73164 30.9798C5.64018 31.4986 4.50153 30.3599 5.02035 29.2685L8.50777 21.9319L0.854043 19.2101C-0.2846 18.8052 -0.2846 17.1949 0.854044 16.79L8.50777 14.0682L5.02035 6.73164C4.50153 5.64018 5.64018 4.50153 6.73164 5.02035L14.0682 8.50777L16.79 0.854044Z', + }, + ], + [ + { + d: 'M5.27208 5.27208C5.32789 6.2816 5.28704 7.28498 5.15454 8.2701C4.78652 11.0064 3.71144 13.6019 2.03681 15.797C1.43391 16.5872 0.753302 17.3256 0 18C0.753302 18.6744 1.43391 19.4128 2.03681 20.203C3.71144 22.3981 4.78652 24.9936 5.15454 27.7299C5.28704 28.715 5.32789 29.7184 5.27208 30.7279C6.2816 30.6721 7.28498 30.713 8.2701 30.8455C11.0064 31.2135 13.6019 32.2886 15.797 33.9632C16.5872 34.5661 17.3256 35.2467 18 36C18.6744 35.2467 19.4128 34.5661 20.203 33.9632C22.3981 32.2886 24.9936 31.2135 27.7299 30.8455C28.715 30.713 29.7184 30.6721 30.7279 30.7279C30.6721 29.7184 30.713 28.715 30.8455 27.7299C31.2135 24.9936 32.2886 22.3981 33.9632 20.203C34.5661 19.4128 35.2467 18.6744 36 18C35.2467 17.3256 34.5661 16.5872 33.9632 15.797C32.2886 13.6019 31.2135 11.0064 30.8455 8.2701C30.713 7.28498 30.6721 6.2816 30.7279 5.27208C29.7184 5.32789 28.715 5.28704 27.7299 5.15454C24.9936 4.78652 22.3981 3.71144 20.203 2.03681C19.4128 1.43391 18.6744 0.753302 18 0C17.3256 0.753302 16.5872 1.43391 15.797 2.03681C13.6019 3.71144 11.0064 4.78652 8.2701 5.15454C7.28498 5.28704 6.2816 5.32789 5.27208 5.27208ZM8.18649 8.18649C7.79745 11.7778 6.38596 15.1854 4.1216 18C6.38596 20.8146 7.79745 24.2222 8.18649 27.8135C11.7778 28.2026 15.1854 29.614 18 31.8784C20.8146 29.614 24.2222 28.2026 27.8135 27.8135C28.2026 24.2222 29.614 20.8146 31.8784 18C29.614 15.1854 28.2026 11.7778 27.8135 8.18649C24.2222 7.79745 20.8146 6.38596 18 4.1216C15.1854 6.38596 11.7778 7.79745 8.18649 8.18649Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M16.951 2.48991C17.349 1.65123 18.651 1.65123 19.049 2.48991C22.058 8.83114 27.1689 13.942 33.5101 16.951C34.3488 17.349 34.3488 18.651 33.5101 19.049C27.1689 22.058 22.058 27.1689 19.049 33.5101C18.651 34.3488 17.349 34.3488 16.951 33.5101C13.942 27.1689 8.83114 22.058 2.48991 19.049C1.65123 18.651 1.65123 17.349 2.48991 16.951C8.83114 13.942 13.942 8.83114 16.951 2.48991Z', + }, + ], + [ + { + d: 'M18 0C21.2219 3.599 25.9048 5.53872 30.7279 5.27208C30.4613 10.0952 32.401 14.7781 36 18C32.401 21.2219 30.4613 25.9048 30.7279 30.7279C25.9048 30.4613 21.2219 32.401 18 36C14.7781 32.401 10.0952 30.4613 5.27208 30.7279C5.53872 25.9048 3.599 21.2219 0 18C3.599 14.7781 5.53872 10.0952 5.27208 5.27208C10.0952 5.53872 14.7781 3.599 18 0Z', + }, + ], + [ + { + d: 'M28 3H8C5.23858 3 3 5.23858 3 8V28C3 30.7614 5.23858 33 8 33H28C30.7614 33 33 30.7614 33 28V8C33 5.23858 30.7614 3 28 3ZM8 0C3.58172 0 0 3.58172 0 8V28C0 32.4183 3.58172 36 8 36H28C32.4183 36 36 32.4183 36 28V8C36 3.58172 32.4183 0 28 0H8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M14.8848 1.29034C16.6053 -0.430117 19.3947 -0.430113 21.1152 1.29035L34.7097 14.8848C36.4301 16.6053 36.4301 19.3947 34.7097 21.1152L21.1152 34.7097C19.3947 36.4301 16.6053 36.4301 14.8848 34.7097L1.29034 21.1152C-0.430117 19.3947 -0.430113 16.6053 1.29035 14.8848L14.8848 1.29034Z', + }, + ], + [ + { + d: 'M18 3.16162L12.254 7.19574C11.991 7.38034 11.7118 7.54039 11.4199 7.67396L5.04683 10.5904L5.68893 17.5175C5.71869 17.8385 5.71869 18.1615 5.68893 18.4825L5.04683 25.4096L11.4199 28.326C11.7118 28.4596 11.991 28.6197 12.254 28.8043L18 32.8384L23.746 28.8043C24.009 28.6197 24.2882 28.4596 24.5801 28.326L30.9532 25.4096L30.3111 18.4825C30.2813 18.1615 30.2813 17.8385 30.3111 17.5175L30.9532 10.5904L24.5801 7.67396C24.2882 7.54038 24.009 7.38033 23.746 7.19574L18 3.16162ZM19.3038 0.411465C18.5224 -0.137155 17.4776 -0.137155 16.6962 0.411464L10.5302 4.74043C10.4172 4.81972 10.2972 4.88854 10.1716 4.94603L3.31338 8.08442C2.44423 8.48216 1.92184 9.38068 2.00956 10.327L2.70174 17.7944C2.71442 17.9312 2.71442 18.0688 2.70174 18.2056L2.00956 25.673C1.92184 26.6193 2.44423 27.5178 3.31338 27.9156L10.1716 31.054C10.2972 31.1115 10.4172 31.1803 10.5302 31.2596L16.6962 35.5885C17.4776 36.1372 18.5224 36.1372 19.3038 35.5885L25.4698 31.2596C25.5828 31.1803 25.7028 31.1115 25.8284 31.054L32.6866 27.9156C33.5558 27.5178 34.0782 26.6193 33.9904 25.673L33.2983 18.2056C33.2856 18.0688 33.2856 17.9312 33.2983 17.7944L33.9904 10.327C34.0782 9.38069 33.5558 8.48216 32.6866 8.08442L25.8284 4.94603C25.7028 4.88854 25.5828 4.81972 25.4698 4.74043L19.3038 0.411465Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M15.4282 2.57149C15.4281 1.15133 16.5794 3.65802e-05 17.9995 0H18.0005C19.4206 3.67846e-05 20.5719 1.15133 20.5718 2.5715C20.5718 3.99166 19.4205 5.14289 18.0003 5.14286C16.5802 5.14289 15.4282 3.99166 15.4282 2.57149ZM23.4894 3.34828C24.2006 2.11903 25.7736 1.69906 27.0029 2.41025L27.0037 2.41072C28.2329 3.12198 28.6528 4.69504 27.9416 5.92426C27.2303 7.15347 25.6572 7.57336 24.428 6.8621C23.1988 6.15091 22.7782 4.57753 23.4894 3.34828ZM12.5106 3.34828C13.2218 4.57753 12.8018 6.15057 11.5726 6.86177C10.3433 7.57302 8.7697 7.15347 8.05844 5.92426C7.34719 4.69504 7.76707 3.12198 8.99629 2.41072L8.9971 2.41025C10.2264 1.69906 11.7994 2.11903 12.5106 3.34828ZM30.0757 8.05844C31.305 7.34719 32.878 7.76707 33.5893 8.99629L33.5898 8.9971C34.3009 10.2264 33.881 11.7994 32.6517 12.5106C31.4225 13.2218 29.8494 12.8018 29.1382 11.5726C28.427 10.3433 28.8465 8.7697 30.0757 8.05844ZM5.92426 8.05844C7.15347 8.7697 7.57336 10.3428 6.8621 11.572C6.15091 12.8012 4.57753 13.2218 3.34828 12.5106C2.11903 11.7994 1.69906 10.2264 2.41025 8.9971L2.41072 8.99629C3.12198 7.76707 4.69504 7.34719 5.92426 8.05844ZM2.5715 15.4282C3.99166 15.4282 5.14289 16.5795 5.14286 17.9997C5.14289 19.4198 3.99166 20.5718 2.57149 20.5718C1.15133 20.5719 3.65802e-05 19.4206 0 18.0005V17.9995C3.67846e-05 16.5794 1.15133 15.4281 2.5715 15.4282ZM33.4285 15.4282C34.8487 15.4281 36 16.5794 36 17.9995V18.0005C36 19.4206 34.8487 20.5719 33.4285 20.5718C32.0083 20.5718 30.8571 19.4205 30.8571 18.0003C30.8571 16.5802 32.0083 15.4282 33.4285 15.4282ZM3.34828 23.4894C4.57753 22.7782 6.15057 23.1982 6.86177 24.4274C7.57302 25.6567 7.15347 27.2303 5.92426 27.9416C4.69504 28.6528 3.12198 28.2329 2.41072 27.0037L2.41025 27.0029C1.69906 25.7736 2.11903 24.2006 3.34828 23.4894ZM32.6517 23.4894C33.881 24.2006 34.3009 25.7736 33.5898 27.0029L33.5893 27.0037C32.878 28.2329 31.305 28.6528 30.0757 27.9416C28.8465 27.2303 28.4266 25.6572 29.1379 24.428C29.8491 23.1988 31.4225 22.7782 32.6517 23.4894ZM27.9416 30.0757C28.6528 31.305 28.2329 32.878 27.0037 33.5893L27.0029 33.5898C25.7736 34.3009 24.2006 33.881 23.4894 32.6517C22.7782 31.4225 23.1982 29.8494 24.4274 29.1382C25.6567 28.427 27.2303 28.8465 27.9416 30.0757ZM8.05844 30.0757C8.7697 28.8465 10.3428 28.4266 11.572 29.1379C12.8012 29.8491 13.2218 31.4225 12.5106 32.6517C11.7994 33.881 10.2264 34.3009 8.9971 33.5898L8.99629 33.5893C7.76707 32.878 7.34719 31.305 8.05844 30.0757ZM15.4282 33.4285C15.4282 32.0083 16.5795 30.8571 17.9997 30.8571C19.4198 30.8571 20.5718 32.0083 20.5718 33.4285C20.5719 34.8487 19.4206 36 18.0005 36H17.9995C16.5794 36 15.4281 34.8487 15.4282 33.4285Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M31.3333 31.3333H18C10.6362 31.3333 4.66667 25.3638 4.66667 18C4.66667 10.6362 10.6362 4.66667 18 4.66667C25.3638 4.66667 31.3333 10.6362 31.3333 18V31.3333ZM18 2C26.8366 2 34 9.16344 34 18V34H18C9.16345 34 2 26.8366 2 18C2 9.16344 9.16345 2 18 2Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M3 18C3 26.2843 9.71573 33 18 33C26.2843 33 33 26.2843 33 18C33 9.71573 26.2843 3 18 3H3V18Z', + }, + ], + [ + { + d: 'M18.1309 3.25957C9.91898 3.40293 3.14567 10.1762 3.00231 18.3882C2.85896 26.6001 9.39985 33.141 17.6118 32.9977C25.8238 32.8543 32.5971 26.081 32.7404 17.8691L33 3L18.1309 3.25957Z', + }, + ], + [ + { + d: 'M31.3333 4.66667H18C10.6362 4.66667 4.66667 10.6362 4.66667 18C4.66667 25.3638 10.6362 31.3333 18 31.3333C25.3638 31.3333 31.3333 25.3638 31.3333 18V4.66667ZM18 34C26.8366 34 34 26.8366 34 18V2H18C9.16345 2 2 9.16345 2 18C2 26.8366 9.16345 34 18 34Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M5.5 30.5H18C24.9036 30.5 30.5 24.9036 30.5 18C30.5 11.0964 24.9036 5.5 18 5.5C11.0964 5.5 5.5 11.0964 5.5 18V30.5ZM18 3C9.71573 3 3 9.71573 3 18V33H18C26.2843 33 33 26.2843 33 18C33 9.71573 26.2843 3 18 3Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M18.1309 32.7404C9.91898 32.5971 3.14567 25.8238 3.00231 17.6118C2.85896 9.39985 9.39985 2.85896 17.6118 3.00231C25.8238 3.14567 32.5971 9.91898 32.7404 18.1309L33 33L18.1309 32.7404Z', + }, + ], + [ + { + d: 'M3 18C3 9.71573 9.71573 3 18 3C26.2843 3 33 9.71573 33 18C33 26.2843 26.2843 33 18 33H3V18Z', + }, + ], + [ + { + d: 'M31.3333 4.66667H18C10.6362 4.66667 4.66667 10.6362 4.66667 18C4.66667 25.3638 10.6362 31.3333 18 31.3333C25.3638 31.3333 31.3333 25.3638 31.3333 18V4.66667ZM18 34C26.8366 34 34 26.8366 34 18V2H18C9.16345 2 2 9.16345 2 18C2 26.8366 9.16345 34 18 34Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M22.3256 8.9814C22.8579 7.58563 23.219 6.3422 23.219 5.53768C23.219 2.47931 20.8825 0 18.0003 0C15.1181 0 12.7816 2.4793 12.7816 5.53768C12.7816 6.34216 13.1426 7.5855 13.6748 8.98118C13.3006 9.16098 12.9393 9.36339 12.5926 9.58663C11.6988 8.42573 10.8474 7.49306 10.1797 7.09422C7.61962 5.56503 4.37602 6.41884 2.93491 9.00126C1.4938 11.5837 2.4009 14.9168 4.96097 16.446C5.59209 16.823 6.70445 17.1085 8.02244 17.3248C8.00756 17.5479 8 17.7731 8 18C8 18.2269 8.00756 18.4521 8.02244 18.6752C6.70444 18.8915 5.59208 19.177 4.96097 19.554C2.4009 21.0832 1.4938 24.4163 2.93491 26.9987C4.37602 29.5812 7.61962 30.435 10.1797 28.9058C10.8474 28.5069 11.6988 27.5743 12.5926 26.4134C12.9393 26.6366 13.3006 26.839 13.6748 27.0188C13.1426 28.4145 12.7816 29.6578 12.7816 30.4623C12.7816 33.5207 15.1181 36 18.0003 36C20.8825 36 23.219 33.5207 23.219 30.4623C23.219 29.6578 22.8579 28.4144 22.3256 27.0186C22.6997 26.8388 23.0609 26.6365 23.4074 26.4133C24.3012 27.5742 25.1526 28.5069 25.8203 28.9057C28.3804 30.4349 31.624 29.5811 33.0651 26.9987C34.5062 24.4163 33.5991 21.0831 31.0391 19.554C30.4079 19.177 29.2956 18.8914 27.9776 18.6751C27.9924 18.452 28 18.2269 28 18C28 17.7731 27.9924 17.548 27.9776 17.3249C29.2956 17.1086 30.4079 16.823 31.0391 16.446C33.5991 14.9169 34.5062 11.5837 33.0651 9.00132C31.624 6.4189 28.3804 5.56509 25.8203 7.09428C25.1526 7.49311 24.3013 8.42577 23.4074 9.58668C23.0609 9.3635 22.6997 9.16115 22.3256 8.9814Z', + }, + ], + [ + { + d: 'M23.3288 6.69196C22.9015 6.79905 22.4544 6.61386 22.228 6.23596L21.4653 4.96334C19.8967 2.34555 16.1033 2.34555 14.5347 4.96334L13.772 6.23597C13.5456 6.61386 13.0985 6.79905 12.6712 6.69196L11.2321 6.33132C8.27179 5.58948 5.58948 8.27179 6.33132 11.2321L6.69196 12.6712C6.79905 13.0985 6.61386 13.5456 6.23597 13.772L4.96334 14.5347C2.34555 16.1033 2.34555 19.8967 4.96334 21.4653L6.23596 22.228C6.61386 22.4544 6.79905 22.9015 6.69196 23.3288L6.33132 24.7679C5.58948 27.7282 8.27179 30.4105 11.2321 29.6687L12.6712 29.308C13.0985 29.2009 13.5456 29.3861 13.772 29.764L14.5347 31.0367C16.1033 33.6544 19.8967 33.6544 21.4653 31.0367L22.228 29.764C22.4544 29.3861 22.9015 29.2009 23.3288 29.308L24.7679 29.6687C27.7282 30.4105 30.4105 27.7282 29.6687 24.7679L29.308 23.3288C29.2009 22.9015 29.3861 22.4544 29.764 22.228L31.0367 21.4653C33.6544 19.8967 33.6544 16.1033 31.0367 14.5347L29.764 13.772C29.3861 13.5456 29.2009 13.0985 29.308 12.6712L29.6687 11.2321C30.4105 8.27179 27.7282 5.58948 24.7679 6.33132L23.3288 6.69196ZM24.0387 3.4213C21.3051 -1.14043 14.6949 -1.14043 11.9613 3.4213C6.80276 2.12858 2.12858 6.80276 3.4213 11.9613C-1.14043 14.6949 -1.14043 21.3051 3.4213 24.0387C2.12858 29.1972 6.80276 33.8714 11.9613 32.5787C14.6948 37.1404 21.3051 37.1404 24.0387 32.5787C29.1972 33.8714 33.8714 29.1972 32.5787 24.0387C37.1404 21.3051 37.1404 14.6948 32.5787 11.9613C33.8714 6.80276 29.1972 2.12858 24.0387 3.4213Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M36 18C36 27.9411 27.9411 36 18 36C8.05887 36 0 27.9411 0 18C0 8.05887 8.05887 0 18 0C27.9411 0 36 8.05887 36 18Z', + }, + ], + [ + { + d: 'M5 3L5 20C5 27.1797 10.8203 33 18 33C25.1797 33 31 27.1797 31 20V3L5 3ZM18 36C26.8366 36 34 28.8366 34 20V0.193028C34 0.0864212 33.9136 -1.0416e-06 33.807 -1.05092e-06L2.19303 -3.8147e-06C2.08642 -3.8147e-06 2 0.08642 2 0.193027V20C2 28.8366 9.16344 36 18 36Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M32.5883 17.0062L18.9938 3.41167C18.445 2.86278 17.555 2.86278 17.0062 3.41166L3.41167 17.0061C2.86278 17.555 2.86278 18.445 3.41166 18.9938L17.0061 32.5883C17.555 33.1372 18.445 33.1372 18.9938 32.5883L32.5883 18.9938C33.1372 18.445 33.1372 17.555 32.5883 17.0062ZM21.1152 1.29035C19.3947 -0.430113 16.6053 -0.430117 14.8848 1.29034L1.29035 14.8848C-0.430113 16.6053 -0.430117 19.3947 1.29034 21.1152L14.8848 34.7097C16.6053 36.4301 19.3947 36.4301 21.1152 34.7097L34.7097 21.1152C36.4301 19.3947 36.4301 16.6053 34.7097 14.8848L21.1152 1.29035Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M2.42518 14.0533C7.4872 11.5821 11.5821 7.4872 14.0533 2.42518C14.8966 0.697682 16.5947 5.0071e-07 18 0C19.4053 -5.00709e-07 21.1034 0.69768 21.9467 2.42518C24.4179 7.4872 28.5128 11.5821 33.5748 14.0533C35.3023 14.8966 36 16.5947 36 18C36 19.4053 35.3023 21.1034 33.5748 21.9467C28.5128 24.4179 24.4179 28.5128 21.9467 33.5748C21.1034 35.3023 19.4053 36 18 36C16.5947 36 14.8966 35.3023 14.0533 33.5748C11.5821 28.5128 7.4872 24.4179 2.42518 21.9467C0.697682 21.1034 5.0071e-07 19.4053 0 18C-5.00709e-07 16.5947 0.69768 14.8966 2.42518 14.0533ZM19.1159 2.80717C18.6882 1.9312 17.3118 1.9312 16.8842 2.80717C14.1049 8.50011 8.50011 14.1049 2.80717 16.8842C1.9312 17.3118 1.9312 18.6882 2.80717 19.1159C8.50011 21.8951 14.1049 27.4999 16.8842 33.1928C17.3118 34.0688 18.6882 34.0688 19.1159 33.1928C21.8951 27.4999 27.4999 21.8951 33.1928 19.1159C34.0688 18.6882 34.0688 17.3118 33.1928 16.8842C27.4999 14.1049 21.8951 8.50011 19.1159 2.80717Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M18 36C27.9411 36 36 27.9411 36 18C36 8.05887 27.9411 0 18 0C8.05887 0 0 8.05887 0 18C0 27.9411 8.05887 36 18 36ZM18 33C26.2843 33 33 26.2843 33 18C33 9.71573 26.2843 3 18 3C9.71573 3 3 9.71573 3 18C3 26.2843 9.71573 33 18 33Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M11.9613 3.4213C14.6949 -1.14043 21.3051 -1.14043 24.0387 3.4213C29.1972 2.12858 33.8714 6.80276 32.5787 11.9613C37.1404 14.6948 37.1404 21.3051 32.5787 24.0387C33.8714 29.1972 29.1972 33.8714 24.0387 32.5787C21.3051 37.1404 14.6948 37.1404 11.9613 32.5787C6.80276 33.8714 2.12858 29.1972 3.4213 24.0387C-1.14043 21.3051 -1.14043 14.6949 3.4213 11.9613C2.12858 6.80276 6.80276 2.12858 11.9613 3.4213Z', + }, + ], + [ + { + d: 'M27 9C27 4.02944 22.9706 0 18 0C13.0294 0 9 4.02944 9 9C4.02944 9 0 13.0294 0 18C0 22.9706 4.02944 27 9 27C9 31.9706 13.0294 36 18 36C22.9706 36 27 31.9706 27 27C31.9706 27 36 22.9706 36 18C36 13.0294 31.9706 9 27 9Z', + }, + ], + [ + { + d: 'M0 8C0 3.58172 3.58172 0 8 0H28C32.4183 0 36 3.58172 36 8V28C36 32.4183 32.4183 36 28 36H8C3.58172 36 0 32.4183 0 28V8Z', + }, + ], + [ + { + d: 'M34 20C34 28.8366 26.8366 36 18 36C9.16344 36 2 28.8366 2 20L2 -3.8147e-06L34 -1.01717e-06V20Z', + }, + ], + [ + { + d: 'M13.4991 4.50029C13.499 2.01501 15.5135 0.000160128 17.9988 0H18.0012C20.4865 0.000160128 22.501 2.01501 22.5009 4.50029C22.5008 5.26131 22.3119 5.97819 21.9784 6.6066C21.787 6.96725 21.9316 7.44815 22.3087 7.60464V7.60464C22.6861 7.76121 23.1238 7.52106 23.2435 7.13046C23.4519 6.45025 23.8251 5.80975 24.3631 5.27166C26.1204 3.51419 28.9696 3.514 30.7271 5.27125L30.7288 5.27291C32.486 7.03038 32.4858 9.87963 30.7283 11.6369C30.1902 12.1749 29.5497 12.5482 28.8695 12.7568C28.479 12.8765 28.2389 13.3142 28.3954 13.6915V13.6915C28.5519 14.0686 29.0329 14.2133 29.3936 14.0218C30.022 13.6882 30.7388 13.4992 31.4997 13.4991C33.985 13.499 35.9998 15.5135 36 17.9988V18.0012C35.9998 20.4865 33.985 22.501 31.4997 22.5009C30.7387 22.5008 30.0218 22.3119 29.3934 21.9784C29.0328 21.787 28.5518 21.9316 28.3954 22.3087V22.3087C28.2388 22.6861 28.4789 23.1238 28.8695 23.2435C29.5497 23.4519 30.1903 23.8251 30.7283 24.3631C32.4858 26.1204 32.486 28.9696 30.7288 30.7271L30.7271 30.7288C28.9696 32.486 26.1204 32.4858 24.3631 30.7283C23.8251 30.1902 23.4518 29.5497 23.2432 28.8695C23.1235 28.479 22.6858 28.2389 22.3085 28.3954V28.3954C21.9314 28.5519 21.7867 29.0329 21.9782 29.3936C22.3118 30.022 22.5008 30.7388 22.5009 31.4997C22.501 33.985 20.4865 35.9998 18.0012 36H17.9988C15.5135 35.9998 13.499 33.985 13.4991 31.4997C13.4992 30.7387 13.6881 30.0218 14.0216 29.3934C14.213 29.0328 14.0684 28.5518 13.6913 28.3954V28.3954C13.3139 28.2388 12.8762 28.4789 12.7565 28.8695C12.5481 29.5497 12.1749 30.1903 11.6369 30.7283C9.87963 32.4858 7.03038 32.486 5.27291 30.7288L5.27125 30.7271C3.514 28.9696 3.51419 26.1204 5.27166 24.3631C5.8098 23.8251 6.45031 23.4518 7.13048 23.2432C7.52103 23.1235 7.76111 22.6858 7.60456 22.3085V22.3085C7.44806 21.9314 6.96706 21.7867 6.6064 21.9782C5.97803 22.3118 5.26122 22.5008 4.50029 22.5009C2.01501 22.501 0.000160128 20.4865 0 18.0012V17.9988C0.000160128 15.5135 2.01501 13.499 4.50029 13.4991C5.26131 13.4992 5.97819 13.6881 6.6066 14.0216C6.96725 14.213 7.44815 14.0684 7.60464 13.6913V13.6913C7.76121 13.3139 7.52106 12.8762 7.13046 12.7565C6.45025 12.5481 5.80975 12.1749 5.27166 11.6369C3.53614 9.90157 3.5381 7.00605 5.27291 5.27125C7.03038 3.514 9.87963 3.51419 11.6369 5.27166C12.1749 5.8098 12.5482 6.45031 12.7568 7.13048C12.8765 7.52102 13.3142 7.76111 13.6915 7.60456V7.60456C14.0686 7.44806 14.2133 6.96706 14.0218 6.6064C13.6882 5.97804 13.4992 5.26122 13.4991 4.50029Z', + }, + ], + [ + { + d: 'M16.9393 5.80583C13.8649 2.73139 8.88026 2.73139 5.80583 5.80583C2.73139 8.88026 2.73139 13.8649 5.80583 16.9393L6.86649 18L5.80583 19.0607C2.73139 22.1351 2.73139 27.1197 5.80583 30.1942C8.88026 33.2686 13.8649 33.2686 16.9393 30.1942L18 29.1335L19.0607 30.1942C22.1351 33.2686 27.1197 33.2686 30.1942 30.1942C33.2686 27.1197 33.2686 22.1351 30.1942 19.0607L29.1335 18L30.1942 16.9393C33.2686 13.8649 33.2686 8.88026 30.1942 5.80583C27.1197 2.73139 22.1351 2.73139 19.0607 5.80583L18 6.86649L16.9393 5.80583ZM18 2.75187C13.7362 -0.534636 7.59277 -0.223757 3.68451 3.68451C-0.223756 7.59277 -0.534636 13.7362 2.75187 18C-0.534636 22.2638 -0.223757 28.4072 3.68451 32.3155C7.59277 36.2238 13.7362 36.5346 18 33.2481C22.2638 36.5346 28.4072 36.2238 32.3155 32.3155C36.2238 28.4072 36.5346 22.2638 33.2481 18C36.5346 13.7362 36.2238 7.59277 32.3155 3.68451C28.4072 -0.223757 22.2638 -0.534636 18 2.75187Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M31 33V16C31 8.8203 25.1797 3 18 3C10.8203 3 5 8.8203 5 16V33H31ZM18 0C9.16344 0 2 7.16344 2 16V35.807C2 35.9136 2.08642 36 2.19303 36H33.807C33.9136 36 34 35.9136 34 35.807V16C34 7.16344 26.8366 0 18 0Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M32.9117 18C37.0295 13.8823 37.0295 7.20606 32.9117 3.08831C28.794 -1.02944 22.1178 -1.02944 18 3.08831C13.8823 -1.02944 7.20606 -1.02944 3.08831 3.08831C-1.02944 7.20606 -1.02944 13.8823 3.08831 18C-1.02944 22.1178 -1.02944 28.794 3.08831 32.9117C7.20606 37.0295 13.8823 37.0295 18 32.9117C22.1178 37.0295 28.794 37.0295 32.9117 32.9117C37.0295 28.794 37.0295 22.1178 32.9117 18Z', + }, + ], + [ + { + d: 'M6.46299 17.9895L7.23763 16.705L6.46299 17.9895C6.46882 17.993 6.47468 17.9965 6.48055 18C6.47468 18.0035 6.46883 18.007 6.46299 18.0105L7.23763 19.295L6.46299 18.0105C3.66342 19.6988 2.67064 23.3627 4.25251 26.2246C5.83226 29.0827 9.37566 30.0547 12.193 28.447C12.2242 31.6989 14.7179 34.5 18.0002 34.5C21.2824 34.5 23.7761 31.699 23.8074 28.4472C26.6247 30.0545 30.1678 29.0825 31.7475 26.2245C33.3294 23.3626 32.3366 19.6988 29.537 18.0105C29.5312 18.007 29.5254 18.0035 29.5195 18C29.5254 17.9965 29.5312 17.993 29.537 17.9895C32.3366 16.3012 33.3294 12.6374 31.7475 9.77547C30.1678 6.91754 26.6247 5.94548 23.8074 7.55278C23.7761 4.30101 21.2824 1.5 18.0002 1.5C14.7179 1.5 12.2242 4.30112 12.193 7.55297C9.37566 5.94533 5.83226 6.91734 4.25251 9.77542C2.67064 12.6373 3.66342 16.3012 6.46299 17.9895Z', + }, + ], + [ + { + d: 'M2 16C2 7.16344 9.16344 0 18 0C26.8366 0 34 7.16344 34 16V36H2V16Z', + }, + ], + [ + { + d: 'M18 2.76923C14.1765 2.76923 11.0769 5.8688 11.0769 9.69231V11.0769H9.69231C5.8688 11.0769 2.76923 14.1765 2.76923 18C2.76923 21.8235 5.8688 24.9231 9.69231 24.9231H11.0769V26.3077C11.0769 30.1312 14.1765 33.2308 18 33.2308C21.8235 33.2308 24.9231 30.1312 24.9231 26.3077V24.9231H26.3077C30.1312 24.9231 33.2308 21.8235 33.2308 18C33.2308 14.1765 30.1312 11.0769 26.3077 11.0769H24.9231V9.69231C24.9231 5.8688 21.8235 2.76923 18 2.76923ZM8.39391 8.39391C9.02837 3.65493 13.0874 0 18 0C22.9126 0 26.9716 3.65493 27.6061 8.39391C32.3451 9.02837 36 13.0874 36 18C36 22.9126 32.3451 26.9716 27.6061 27.6061C26.9716 32.3451 22.9126 36 18 36C13.0874 36 9.02837 32.3451 8.39391 27.6061C3.65493 26.9716 0 22.9126 0 18C0 13.0874 3.65493 9.02837 8.39391 8.39391Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], +] diff --git a/apps/web/src/components/Unicon/Emblem.ts b/apps/web/src/components/Unicon/Emblem.ts new file mode 100644 index 0000000..52ac10f --- /dev/null +++ b/apps/web/src/components/Unicon/Emblem.ts @@ -0,0 +1,626 @@ +export const svgPaths: React.SVGProps[][] = [ + [ + { + d: 'M0 8C0 3.58172 3.58172 0 8 0V4C5.79086 4 4 5.79086 4 8H0ZM8 8L4 8C4 10.2091 5.79086 12 8 12V16C12.4183 16 16 12.4183 16 8H12C12 5.79086 10.2091 4 8 4L8 8ZM8 8L12 8C12 10.2091 10.2091 12 8 12L8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M6.61962 1.57177C7.38198 0.809409 8.61802 0.80941 9.38038 1.57177L14.4282 6.61962C15.1906 7.38198 15.1906 8.61802 14.4282 9.38038L9.38038 14.4282C8.61802 15.1906 7.38198 15.1906 6.61962 14.4282L1.57177 9.38038C0.809409 8.61802 0.80941 7.38198 1.57177 6.61962L6.61962 1.57177Z', + }, + ], + [ + { + d: 'M8 4.33253L4.33253 8L8 11.6675L11.6675 8L8 4.33253ZM9.38038 1.57177C8.61802 0.80941 7.38198 0.809409 6.61962 1.57177L1.57177 6.61962C0.80941 7.38198 0.809409 8.61802 1.57177 9.38038L6.61962 14.4282C7.38198 15.1906 8.61802 15.1906 9.38038 14.4282L14.4282 9.38038C15.1906 8.61802 15.1906 7.38198 14.4282 6.61962L9.38038 1.57177Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z', + }, + { + d: 'M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM4 5C4 4.44772 4.44772 4 5 4H11C11.5523 4 12 4.44772 12 5V11C12 11.5523 11.5523 12 11 12H5C4.44772 12 4 11.5523 4 11V5Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M16 8C12.2937 6.62854 9.37146 3.70632 8 0C6.62854 3.70632 3.70632 6.62854 0 8C3.70632 9.37146 6.62854 12.2937 8 16C9.37146 12.2937 12.2937 9.37146 16 8ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M9.38038 1.57177C8.61802 0.80941 7.38198 0.809409 6.61962 1.57177L1.57177 6.61962C0.80941 7.38198 0.809409 8.61802 1.57177 9.38038L6.61962 14.4282C7.38198 15.1906 8.61802 15.1906 9.38038 14.4282L14.4282 9.38038C15.1906 8.61802 15.1906 7.38198 14.4282 6.61962L9.38038 1.57177ZM8 11.1716C9.65685 11.1716 11 9.82842 11 8.17157C11 6.51472 9.65685 5.17157 8 5.17157C6.34315 5.17157 5 6.51472 5 8.17157C5 9.82842 6.34315 11.1716 8 11.1716Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M15 2C15 1.44772 14.5523 1 14 1H9C8.44772 1 8 1.44772 8 2V8H14C14.5523 8 15 7.55228 15 7V2Z', + }, + { + d: 'M8 8H2C1.44771 8 1 8.44772 1 9V14C1 14.5523 1.44772 15 2 15H7C7.55228 15 8 14.5523 8 14V8Z', + }, + ], + [ + { + d: 'M0 8C0 3.58172 3.58172 0 8 0L8 4.41421C7.74408 4.41421 7.48816 4.51184 7.29289 4.70711L4.70711 7.29289C4.51184 7.48816 4.41421 7.74408 4.41421 8L0 8ZM8 8H4.41421C4.41421 8.25592 4.51184 8.51184 4.70711 8.70711L7.29289 11.2929C7.48816 11.4882 7.74408 11.5858 8 11.5858L8 16C12.4183 16 16 12.4183 16 8L11.5858 8C11.5858 7.74408 11.4882 7.48816 11.2929 7.29289L8.70711 4.70711C8.51184 4.51184 8.25592 4.41421 8 4.41421L8 8ZM8 8H11.5858C11.5858 8.25592 11.4882 8.51184 11.2929 8.70711L8.70711 11.2929C8.51184 11.4882 8.25592 11.5858 8 11.5858L8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M1 2C1 1.44772 1.44772 1 2 1H7C7.55228 1 8 1.44772 8 2V8H2C1.44772 8 1 7.55228 1 7V2Z', + }, + { + d: 'M8 8H14C14.5523 8 15 8.44772 15 9V14C15 14.5523 14.5523 15 14 15H9C8.44772 15 8 14.5523 8 14V8Z', + }, + ], + [ + { + d: 'M13.6569 2.34315C10.5327 -0.781049 5.46734 -0.781049 2.34315 2.34315L8 8L2.34315 13.6569C5.46734 16.781 10.5327 16.781 13.6569 13.6569L8 8L13.6569 2.34315Z', + }, + ], + [ + { + d: 'M8 0C3.58172 0 0 3.58172 0 8L8 8L8 16C12.4183 16 16 12.4183 16 8L8 8L8 0Z', + }, + ], + [ + { + d: 'M16 8C12.2937 6.62854 9.37146 3.70632 8 0C6.62854 3.70632 3.70632 6.62854 0 8C3.70632 9.37146 6.62854 12.2937 8 16C9.37146 12.2937 12.2937 9.37146 16 8ZM11 8L8 5L5 8L8 11L11 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M2 4.4C2 3.07452 3.07452 2 4.4 2H11.6C12.9255 2 14 3.07452 14 4.4V11.6C14 12.9255 12.9255 14 11.6 14H4.4C3.07452 14 2 12.9255 2 11.6V4.4Z', + }, + ], + [ + { + d: 'M5.17158 2.34315C4.39053 1.5621 3.1242 1.5621 2.34315 2.34315C1.5621 3.12419 1.5621 4.39052 2.34315 5.17157L4.46447 7.29289C4.85499 7.68342 4.85499 8.31658 4.46447 8.70711L2.34315 10.8284C1.5621 11.6095 1.5621 12.8758 2.34315 13.6569C3.1242 14.4379 4.39053 14.4379 5.17158 13.6569L7.2929 11.5355C7.68342 11.145 8.31659 11.145 8.70711 11.5355L10.8284 13.6569C11.6095 14.4379 12.8758 14.4379 13.6569 13.6569C14.4379 12.8758 14.4379 11.6095 13.6569 10.8284L11.5355 8.70711C11.145 8.31658 11.145 7.68342 11.5355 7.29289L13.6569 5.17157C14.4379 4.39052 14.4379 3.12419 13.6569 2.34315C12.8758 1.5621 11.6095 1.5621 10.8284 2.34315L8.70711 4.46447C8.31659 4.85499 7.68342 4.85499 7.2929 4.46447L5.17158 2.34315Z', + }, + ], + [ + { + d: 'M7 2C7 1.44772 7.44772 1 8 1C8.55228 1 9 1.44772 9 2V5C9 5.55228 8.55228 6 8 6C7.44772 6 7 5.55228 7 5V2Z', + }, + { + d: 'M7 11C7 10.4477 7.44772 10 8 10C8.55228 10 9 10.4477 9 11V14C9 14.5523 8.55228 15 8 15C7.44772 15 7 14.5523 7 14V11Z', + }, + { + d: 'M14 7C14.5523 7 15 7.44772 15 8C15 8.55228 14.5523 9 14 9H11C10.4477 9 10 8.55228 10 8C10 7.44772 10.4477 7 11 7H14Z', + }, + { + d: 'M5 7C5.55228 7 6 7.44772 6 8C6 8.55228 5.55228 9 5 9H2C1.44772 9 1 8.55228 1 8C1 7.44772 1.44772 7 2 7H5Z', + }, + ], + [ + { + d: 'M15.9585 8.81981C15.9166 9.23189 15.5183 9.49613 15.112 9.41574L11.7175 8.74421C10.9132 8.5851 10.4734 9.64707 11.1547 10.1032L14.0293 12.0277C14.3735 12.2581 14.4683 12.7266 14.2068 13.0478C13.861 13.4725 13.4725 13.861 13.0478 14.2068C12.7266 14.4683 12.2581 14.3735 12.0277 14.0293L10.1032 11.1547C9.64707 10.4734 8.5851 10.9132 8.74421 11.7175L9.41574 15.112C9.49613 15.5183 9.23189 15.9166 8.81981 15.9585C8.55027 15.9859 8.27678 16 8 16C7.72322 16 7.44973 15.9859 7.18019 15.9585C6.76811 15.9166 6.50387 15.5183 6.58426 15.112L7.25579 11.7175C7.4149 10.9132 6.35293 10.4734 5.89682 11.1547L3.97232 14.0293C3.74188 14.3735 3.27344 14.4683 2.95224 14.2068C2.52755 13.861 2.13902 13.4725 1.79322 13.0478C1.53168 12.7266 1.62651 12.2581 1.9707 12.0277L4.84532 10.1032C5.52661 9.64707 5.08681 8.5851 4.28253 8.74421L0.888013 9.41574C0.481675 9.49613 0.0834376 9.23189 0.0414916 8.81981C0.0140555 8.55027 0 8.27678 0 8C0 7.72322 0.0140554 7.44973 0.0414915 7.18019C0.0834375 6.76811 0.481675 6.50387 0.888013 6.58426L4.28253 7.25579C5.08681 7.4149 5.52661 6.35293 4.84532 5.89683L1.9707 3.97232C1.6265 3.74188 1.53168 3.27344 1.79322 2.95224C2.13902 2.52755 2.52755 2.13902 2.95224 1.79322C3.27344 1.53168 3.74188 1.62651 3.97231 1.9707L5.89683 4.84533C6.35293 5.52661 7.4149 5.08681 7.25579 4.28253L6.58426 0.888013C6.50387 0.481675 6.76811 0.0834376 7.18019 0.0414915C7.44973 0.0140555 7.72322 0 8 0C8.27678 0 8.55027 0.0140554 8.81981 0.0414915C9.23189 0.0834375 9.49613 0.481675 9.41574 0.888013L8.74421 4.28253C8.5851 5.08681 9.64707 5.52661 10.1032 4.84532L12.0277 1.9707C12.2581 1.6265 12.7266 1.53168 13.0478 1.79322C13.4725 2.13902 13.861 2.52755 14.2068 2.95223C14.4683 3.27344 14.3735 3.74188 14.0293 3.97231L11.1547 5.89683C10.4734 6.35294 10.9132 7.4149 11.7175 7.25579L15.112 6.58426C15.5183 6.50387 15.9166 6.76811 15.9585 7.18019C15.9859 7.44973 16 7.72322 16 8C16 8.27678 15.9859 8.55027 15.9585 8.81981Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M10 2C10 0.895431 9.10457 0 8 0C6.89543 0 6 0.89543 6 2V5C6 5.55228 5.55228 6 5 6H2C0.895431 6 0 6.89543 0 8C0 9.10457 0.895431 10 2 10H5C5.55228 10 6 10.4477 6 11V14C6 15.1046 6.89543 16 8 16C9.10457 16 10 15.1046 10 14V11C10 10.4477 10.4477 10 11 10H14C15.1046 10 16 9.10457 16 8C16 6.89543 15.1046 6 14 6H11C10.4477 6 10 5.55228 10 5V2Z', + }, + ], + [ + { + d: 'M12 1.07026C9.60879 2.4535 8 5.03887 8 8V0C5.03887 0 2.4535 1.60879 1.07026 4C2.4535 6.39121 5.03887 8 8 8H0C0 10.9611 1.6088 13.5465 4 14.9297C6.39121 13.5465 8 10.9611 8 8L8 16C10.9611 16 13.5465 14.3912 14.9297 12C13.5469 9.60955 10.9628 8.00101 8.00279 8H16C16 5.03887 14.3912 2.4535 12 1.07026Z', + }, + ], + [ + { + d: 'M15.9831 8.52372L12.9894 8.33026C12.9964 8.22148 13 8.11137 13 8C13 7.88863 12.9964 7.77852 12.9894 7.66974L15.9831 7.47627C15.9943 7.64941 16 7.82404 16 8C16 8.17596 15.9943 8.35059 15.9831 8.52372ZM15.8477 6.4387L12.9048 7.02091C12.8622 6.80567 12.8058 6.59565 12.7366 6.39168L15.5774 5.42754C15.6887 5.75556 15.7794 6.09313 15.8477 6.4387ZM15.1766 4.46092L12.487 5.78982C12.3902 5.59393 12.2808 5.40497 12.1596 5.22403L14.6525 3.55507C14.8461 3.84425 15.0214 4.14676 15.1766 4.46092ZM14.0148 2.72519L11.7601 4.70411C11.6156 4.5395 11.4605 4.3844 11.2959 4.23992L13.2748 1.98518C13.5372 2.21551 13.7845 2.46275 14.0148 2.72519ZM12.4449 1.34748L10.776 3.84038C10.595 3.71924 10.4061 3.60981 10.2102 3.51302L11.5391 0.823408C11.8532 0.978631 12.1557 1.15388 12.4449 1.34748ZM10.5725 0.422581L9.60832 3.26343C9.40435 3.19421 9.19433 3.13782 8.97909 3.09524L9.5613 0.152272C9.90687 0.220637 10.2444 0.311259 10.5725 0.422581ZM8.52373 0.0168691L8.33026 3.01062C8.22148 3.0036 8.11137 3 8 3C7.88863 3 7.77852 3.0036 7.66974 3.01062L7.47627 0.0168691C7.64941 0.00568084 7.82404 0 8 0C8.17596 0 8.35059 0.00568083 8.52373 0.0168691ZM6.4387 0.152272L7.02091 3.09524C6.80567 3.13782 6.59565 3.19421 6.39168 3.26343L5.42754 0.422581C5.75556 0.311259 6.09313 0.220638 6.4387 0.152272ZM4.46092 0.823408L5.78982 3.51302C5.59393 3.60981 5.40497 3.71925 5.22403 3.84038L3.55507 1.34748C3.84425 1.15388 4.14676 0.978631 4.46092 0.823408ZM2.72519 1.98518L4.70411 4.23992C4.5395 4.3844 4.3844 4.5395 4.23992 4.70412L1.98517 2.7252C2.21551 2.46275 2.46275 2.21551 2.72519 1.98518ZM1.34748 3.55508L3.84038 5.22403C3.71924 5.40497 3.60981 5.59393 3.51302 5.78982L0.823408 4.46092C0.978631 4.14676 1.15388 3.84425 1.34748 3.55508ZM0.422581 5.42754L3.26343 6.39168C3.19421 6.59565 3.13782 6.80567 3.09524 7.02091L0.152272 6.4387C0.220637 6.09313 0.311259 5.75556 0.422581 5.42754ZM0.0168691 7.47627C0.00568083 7.64941 0 7.82404 0 8C0 8.17596 0.00568084 8.35059 0.0168691 8.52373L3.01062 8.33026C3.0036 8.22148 3 8.11137 3 8C3 7.88863 3.0036 7.77852 3.01062 7.66974L0.0168691 7.47627ZM0.152272 9.5613L3.09524 8.97909C3.13782 9.19433 3.19421 9.40435 3.26343 9.60832L0.422581 10.5725C0.311259 10.2444 0.220637 9.90687 0.152272 9.5613ZM0.823408 11.5391L3.51302 10.2102C3.60981 10.4061 3.71925 10.595 3.84038 10.776L1.34748 12.4449C1.15388 12.1557 0.978631 11.8532 0.823408 11.5391ZM1.98518 13.2748L4.23992 11.2959C4.3844 11.4605 4.5395 11.6156 4.70412 11.7601L2.7252 14.0148C2.46275 13.7845 2.21551 13.5372 1.98518 13.2748ZM3.55508 14.6525L5.22403 12.1596C5.40497 12.2808 5.59393 12.3902 5.78982 12.487L4.46092 15.1766C4.14676 15.0214 3.84425 14.8461 3.55508 14.6525ZM5.42754 15.5774L6.39168 12.7366C6.59565 12.8058 6.80567 12.8622 7.02091 12.9048L6.4387 15.8477C6.09313 15.7794 5.75556 15.6887 5.42754 15.5774ZM7.47628 15.9831L7.66974 12.9894C7.77852 12.9964 7.88863 13 8 13C8.11137 13 8.22148 12.9964 8.33026 12.9894L8.52373 15.9831C8.35059 15.9943 8.17596 16 8 16C7.82404 16 7.64941 15.9943 7.47628 15.9831ZM9.5613 15.8477L8.97909 12.9048C9.19433 12.8622 9.40435 12.8058 9.60832 12.7366L10.5725 15.5774C10.2444 15.6887 9.90687 15.7794 9.5613 15.8477ZM11.5391 15.1766L10.2102 12.487C10.4061 12.3902 10.595 12.2808 10.776 12.1596L12.4449 14.6525C12.1557 14.8461 11.8532 15.0214 11.5391 15.1766ZM13.2748 14.0148L11.2959 11.7601C11.4605 11.6156 11.6156 11.4605 11.7601 11.2959L14.0148 13.2748C13.7845 13.5372 13.5372 13.7845 13.2748 14.0148ZM14.6525 12.4449L12.1596 10.776C12.2808 10.595 12.3902 10.4061 12.487 10.2102L15.1766 11.5391C15.0214 11.8532 14.8461 12.1557 14.6525 12.4449ZM15.5774 10.5725L12.7366 9.60832C12.8058 9.40435 12.8622 9.19433 12.9048 8.97909L15.8477 9.5613C15.7794 9.90687 15.6887 10.2444 15.5774 10.5725Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + { + d: 'M11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8Z', + }, + ], + [ + { + d: 'M15.9026 9.25189L12.939 8.78615C12.979 8.53177 13 8.26937 13 8C13 7.73063 12.979 7.46823 12.939 7.21385L15.9026 6.74811C15.9667 7.15599 16 7.57411 16 8C16 8.42589 15.9667 8.84401 15.9026 9.25189ZM15.4706 5.13214L12.6701 6.20789C12.4812 5.7162 12.2158 5.26012 11.8871 4.85468L14.2174 2.96533C14.7416 3.61188 15.167 4.34182 15.4706 5.13214ZM13.0347 1.78258L11.1453 4.11289C10.7399 3.78417 10.2838 3.51876 9.79211 3.32989L10.8679 0.529397C11.6582 0.832983 12.3881 1.25837 13.0347 1.78258ZM9.25189 0.0973803L8.78615 3.06101C8.53177 3.02103 8.26937 3 8 3C7.73063 3 7.46823 3.02103 7.21385 3.06101L6.74811 0.0973805C7.15599 0.0332797 7.57411 0 8 0C8.42589 0 8.84401 0.0332797 9.25189 0.0973803ZM5.13214 0.529397L6.20789 3.32989C5.7162 3.51876 5.26012 3.78417 4.85468 4.11289L2.96533 1.78258C3.61188 1.25837 4.34182 0.832983 5.13214 0.529397ZM1.78258 2.96533L4.11289 4.85468C3.78417 5.26012 3.51876 5.7162 3.32989 6.20789L0.529397 5.13214C0.832983 4.34182 1.25837 3.61188 1.78258 2.96533ZM0.0973804 6.74811C0.0332797 7.15599 0 7.57411 0 8C0 8.42589 0.0332797 8.84401 0.0973805 9.25189L3.06101 8.78615C3.02103 8.53177 3 8.26937 3 8C3 7.73063 3.02103 7.46823 3.06101 7.21386L0.0973804 6.74811ZM0.529397 10.8679L3.32989 9.79211C3.51876 10.2838 3.78417 10.7399 4.11289 11.1453L1.78258 13.0347C1.25837 12.3881 0.832983 11.6582 0.529397 10.8679ZM2.96533 14.2174L4.85468 11.8871C5.26012 12.2158 5.7162 12.4812 6.20789 12.6701L5.13214 15.4706C4.34182 15.167 3.61188 14.7416 2.96533 14.2174ZM6.74811 15.9026L7.21386 12.939C7.46823 12.979 7.73063 13 8 13C8.26937 13 8.53177 12.979 8.78615 12.939L9.25189 15.9026C8.84401 15.9667 8.42589 16 8 16C7.57411 16 7.15599 15.9667 6.74811 15.9026ZM10.8679 15.4706L9.79211 12.6701C10.2838 12.4812 10.7399 12.2158 11.1453 11.8871L13.0347 14.2174C12.3881 14.7416 11.6582 15.167 10.8679 15.4706ZM14.2174 13.0347L11.8871 11.1453C12.2158 10.7399 12.4812 10.2838 12.6701 9.79211L15.4706 10.8679C15.167 11.6582 14.7416 12.3881 14.2174 13.0347Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + { + d: 'M11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8Z', + }, + ], + [ + { + d: 'M2 14C2 10.6863 4.68629 8 8 8C4.68629 8 2 5.31371 2 2H5C6.65685 2 8 3.34315 8 5C8 3.34315 9.34315 2 11 2H14C14 5.31352 11.3134 7.99969 8 8C11.3134 8.00031 14 10.6865 14 14H11C9.34315 14 8 12.6569 8 11C8 12.6569 6.65685 14 5 14L2 14Z', + }, + ], + [ + { + d: 'M8 8C11.3137 8 14 4.86599 14 1H2C2 4.86567 4.68667 7.99948 8 8C4.68667 8.00053 2 11.1343 2 15H14C14 11.134 11.3137 8 8 8Z', + }, + ], + [ + { + d: 'M15.9026 9.25189L12.939 8.78615C12.979 8.53177 13 8.26937 13 8C13 7.73063 12.979 7.46823 12.939 7.21385L15.9026 6.74811C15.9667 7.15599 16 7.57411 16 8C16 8.42589 15.9667 8.84401 15.9026 9.25189ZM15.4706 5.13214L12.6701 6.20789C12.4812 5.7162 12.2158 5.26012 11.8871 4.85468L14.2174 2.96533C14.7416 3.61188 15.167 4.34182 15.4706 5.13214ZM13.0347 1.78258L11.1453 4.11289C10.7399 3.78417 10.2838 3.51876 9.79211 3.32989L10.8679 0.529397C11.6582 0.832983 12.3881 1.25837 13.0347 1.78258ZM9.25189 0.0973803L8.78615 3.06101C8.53177 3.02103 8.26937 3 8 3C7.73063 3 7.46823 3.02103 7.21385 3.06101L6.74811 0.0973805C7.15599 0.0332797 7.57411 0 8 0C8.42589 0 8.84401 0.0332797 9.25189 0.0973803ZM5.13214 0.529397L6.20789 3.32989C5.7162 3.51876 5.26012 3.78417 4.85468 4.11289L2.96533 1.78258C3.61188 1.25837 4.34182 0.832983 5.13214 0.529397ZM1.78258 2.96533L4.11289 4.85468C3.78417 5.26012 3.51876 5.7162 3.32989 6.20789L0.529397 5.13214C0.832983 4.34182 1.25837 3.61188 1.78258 2.96533ZM0.0973804 6.74811C0.0332797 7.15599 0 7.57411 0 8C0 8.42589 0.0332797 8.84401 0.0973805 9.25189L3.06101 8.78615C3.02103 8.53177 3 8.26937 3 8C3 7.73063 3.02103 7.46823 3.06101 7.21386L0.0973804 6.74811ZM0.529397 10.8679L3.32989 9.79211C3.51876 10.2838 3.78417 10.7399 4.11289 11.1453L1.78258 13.0347C1.25837 12.3881 0.832983 11.6582 0.529397 10.8679ZM2.96533 14.2174L4.85468 11.8871C5.26012 12.2158 5.7162 12.4812 6.20789 12.6701L5.13214 15.4706C4.34182 15.167 3.61188 14.7416 2.96533 14.2174ZM6.74811 15.9026L7.21386 12.939C7.46823 12.979 7.73063 13 8 13C8.26937 13 8.53177 12.979 8.78615 12.939L9.25189 15.9026C8.84401 15.9667 8.42589 16 8 16C7.57411 16 7.15599 15.9667 6.74811 15.9026ZM10.8679 15.4706L9.79211 12.6701C10.2838 12.4812 10.7399 12.2158 11.1453 11.8871L13.0347 14.2174C12.3881 14.7416 11.6582 15.167 10.8679 15.4706ZM14.2174 13.0347L11.8871 11.1453C12.2158 10.7399 12.4812 10.2838 12.6701 9.79211L15.4706 10.8679C15.167 11.6582 14.7416 12.3881 14.2174 13.0347Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + { + d: 'M8 5C8.64911 6.29822 9.70178 7.35089 11 8C9.70178 8.64911 8.64911 9.70178 8 11C7.35089 9.70178 6.29822 8.64911 5 8C6.29822 7.35089 7.35089 6.29822 8 5Z', + }, + ], + [ + { + d: 'M15.9831 8.52372L12.9894 8.33026C12.9964 8.22148 13 8.11137 13 8C13 7.88863 12.9964 7.77852 12.9894 7.66974L15.9831 7.47627C15.9943 7.64941 16 7.82404 16 8C16 8.17596 15.9943 8.35059 15.9831 8.52372ZM15.8477 6.4387L12.9048 7.02091C12.8622 6.80567 12.8058 6.59565 12.7366 6.39168L15.5774 5.42754C15.6887 5.75556 15.7794 6.09313 15.8477 6.4387ZM15.1766 4.46092L12.487 5.78982C12.3902 5.59393 12.2808 5.40497 12.1596 5.22403L14.6525 3.55507C14.8461 3.84425 15.0214 4.14676 15.1766 4.46092ZM14.0148 2.72519L11.7601 4.70411C11.6156 4.5395 11.4605 4.3844 11.2959 4.23992L13.2748 1.98518C13.5372 2.21551 13.7845 2.46275 14.0148 2.72519ZM12.4449 1.34748L10.776 3.84038C10.595 3.71924 10.4061 3.60981 10.2102 3.51302L11.5391 0.823408C11.8532 0.978631 12.1557 1.15388 12.4449 1.34748ZM10.5725 0.422581L9.60832 3.26343C9.40435 3.19421 9.19433 3.13782 8.97909 3.09524L9.5613 0.152272C9.90687 0.220637 10.2444 0.311259 10.5725 0.422581ZM8.52373 0.0168691L8.33026 3.01062C8.22148 3.0036 8.11137 3 8 3C7.88863 3 7.77852 3.0036 7.66974 3.01062L7.47627 0.0168691C7.64941 0.00568084 7.82404 0 8 0C8.17596 0 8.35059 0.00568083 8.52373 0.0168691ZM6.4387 0.152272L7.02091 3.09524C6.80567 3.13782 6.59565 3.19421 6.39168 3.26343L5.42754 0.422581C5.75556 0.311259 6.09313 0.220638 6.4387 0.152272ZM4.46092 0.823408L5.78982 3.51302C5.59393 3.60981 5.40497 3.71925 5.22403 3.84038L3.55507 1.34748C3.84425 1.15388 4.14676 0.978631 4.46092 0.823408ZM2.72519 1.98518L4.70411 4.23992C4.5395 4.3844 4.3844 4.5395 4.23992 4.70412L1.98517 2.7252C2.21551 2.46275 2.46275 2.21551 2.72519 1.98518ZM1.34748 3.55508L3.84038 5.22403C3.71924 5.40497 3.60981 5.59393 3.51302 5.78982L0.823408 4.46092C0.978631 4.14676 1.15388 3.84425 1.34748 3.55508ZM0.422581 5.42754L3.26343 6.39168C3.19421 6.59565 3.13782 6.80567 3.09524 7.02091L0.152272 6.4387C0.220637 6.09313 0.311259 5.75556 0.422581 5.42754ZM0.0168691 7.47627C0.00568083 7.64941 0 7.82404 0 8C0 8.17596 0.00568084 8.35059 0.0168691 8.52373L3.01062 8.33026C3.0036 8.22148 3 8.11137 3 8C3 7.88863 3.0036 7.77852 3.01062 7.66974L0.0168691 7.47627ZM0.152272 9.5613L3.09524 8.97909C3.13782 9.19433 3.19421 9.40435 3.26343 9.60832L0.422581 10.5725C0.311259 10.2444 0.220637 9.90687 0.152272 9.5613ZM0.823408 11.5391L3.51302 10.2102C3.60981 10.4061 3.71925 10.595 3.84038 10.776L1.34748 12.4449C1.15388 12.1557 0.978631 11.8532 0.823408 11.5391ZM1.98518 13.2748L4.23992 11.2959C4.3844 11.4605 4.5395 11.6156 4.70412 11.7601L2.7252 14.0148C2.46275 13.7845 2.21551 13.5372 1.98518 13.2748ZM3.55508 14.6525L5.22403 12.1596C5.40497 12.2808 5.59393 12.3902 5.78982 12.487L4.46092 15.1766C4.14676 15.0214 3.84425 14.8461 3.55508 14.6525ZM5.42754 15.5774L6.39168 12.7366C6.59565 12.8058 6.80567 12.8622 7.02091 12.9048L6.4387 15.8477C6.09313 15.7794 5.75556 15.6887 5.42754 15.5774ZM7.47628 15.9831L7.66974 12.9894C7.77852 12.9964 7.88863 13 8 13C8.11137 13 8.22148 12.9964 8.33026 12.9894L8.52373 15.9831C8.35059 15.9943 8.17596 16 8 16C7.82404 16 7.64941 15.9943 7.47628 15.9831ZM9.5613 15.8477L8.97909 12.9048C9.19433 12.8622 9.40435 12.8058 9.60832 12.7366L10.5725 15.5774C10.2444 15.6887 9.90687 15.7794 9.5613 15.8477ZM11.5391 15.1766L10.2102 12.487C10.4061 12.3902 10.595 12.2808 10.776 12.1596L12.4449 14.6525C12.1557 14.8461 11.8532 15.0214 11.5391 15.1766ZM13.2748 14.0148L11.2959 11.7601C11.4605 11.6156 11.6156 11.4605 11.7601 11.2959L14.0148 13.2748C13.7845 13.5372 13.5372 13.7845 13.2748 14.0148ZM14.6525 12.4449L12.1596 10.776C12.2808 10.595 12.3902 10.4061 12.487 10.2102L15.1766 11.5391C15.0214 11.8532 14.8461 12.1557 14.6525 12.4449ZM15.5774 10.5725L12.7366 9.60832C12.8058 9.40435 12.8622 9.19433 12.9048 8.97909L15.8477 9.5613C15.7794 9.90687 15.6887 10.2444 15.5774 10.5725Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + { + d: 'M8 5C8.64911 6.29822 9.70178 7.35089 11 8C9.70178 8.64911 8.64911 9.70178 8 11C7.35089 9.70178 6.29822 8.64911 5 8C6.29822 7.35089 7.35089 6.29822 8 5Z', + }, + ], + [ + { + d: 'M15.8477 9.5613L8.03733 8.01617L14.6525 12.4449C14.0685 13.3173 13.3173 14.0685 12.4449 14.6525L8.01617 8.03733L9.5613 15.8477C9.05633 15.9476 8.53428 16 8 16C7.46572 16 6.94367 15.9476 6.4387 15.8477L7.98383 8.03733L3.55508 14.6525C2.68266 14.0685 1.93154 13.3173 1.34748 12.4449L7.96267 8.01617L0.152272 9.5613C0.0523756 9.05633 0 8.53428 0 8C0 7.46572 0.0523755 6.94367 0.152272 6.4387L7.96267 7.98383L1.34748 3.55508C1.93154 2.68266 2.68266 1.93154 3.55507 1.34748L7.98383 7.96267L6.4387 0.152272C6.94367 0.0523755 7.46572 0 8 0C8.53428 0 9.05633 0.0523755 9.5613 0.152272L8.01617 7.96267L12.4449 1.34748C13.3173 1.93154 14.0685 2.68266 14.6525 3.55507L8.03733 7.98383L15.8477 6.4387C15.9476 6.94367 16 7.46572 16 8C16 8.53428 15.9476 9.05633 15.8477 9.5613Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M2.34314 2.34314C3.12419 1.5621 4.39052 1.5621 5.17157 2.34314L7.29289 4.46447C7.68342 4.85499 8.31658 4.85499 8.70711 4.46447L10.8284 2.34314C11.6095 1.5621 12.8758 1.5621 13.6569 2.34314C14.4379 3.12419 14.4379 4.39052 13.6569 5.17157L11.5355 7.29289C11.145 7.68342 11.145 8.31658 11.5355 8.70711L13.6569 10.8284C14.4379 11.6095 14.4379 12.8758 13.6569 13.6569C12.8758 14.4379 11.6095 14.4379 10.8284 13.6569L8.70711 11.5355C8.31658 11.145 7.68342 11.145 7.29289 11.5355L5.17157 13.6569C4.39052 14.4379 3.12419 14.4379 2.34314 13.6569C1.5621 12.8758 1.5621 11.6095 2.34314 10.8284L4.46447 8.70711C4.85499 8.31658 4.85499 7.68342 4.46447 7.29289L2.34314 5.17157C1.5621 4.39052 1.5621 3.12419 2.34314 2.34314ZM8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8 8C8 8 5.5 3.82101 5.5 2.46154C5.5 1.10207 6.61929 0 8 0C9.38071 0 10.5 1.10207 10.5 2.46154C10.5 3.82101 8 8 8 8Z', + }, + { + d: 'M8 8C8 8 12.179 5.5 13.5385 5.5C14.8979 5.5 16 6.61929 16 8C16 9.38071 14.8979 10.5 13.5385 10.5C12.1803 10.5 8.00807 8.00483 8 8Z', + }, + { + d: 'M8 8C8 8 10.5 12.179 10.5 13.5385C10.5 14.8979 9.38071 16 8 16C6.61929 16 5.5 14.8979 5.5 13.5385C5.5 12.1808 7.99339 8.01105 8 8Z', + }, + { + d: 'M2.46154 5.5C3.82101 5.5 8 8 8 8C8 8 3.82101 10.5 2.46154 10.5C1.10207 10.5 0 9.38071 0 8C0 6.61929 1.10207 5.5 2.46154 5.5Z', + }, + ], + [ + { + d: 'M10.3195 2.46119C10.3195 3.82047 8.00012 7.99888 8.00012 7.99888C8.00012 7.99888 5.68069 3.82047 5.68069 2.46119C5.68069 1.10191 6.71913 0 8.00012 0C9.28111 0 10.3195 1.10191 10.3195 2.46119Z', + }, + { + d: 'M2.20488 7.30932C3.34269 7.98896 8.00002 8 8.00002 8C7.97235 8.00007 3.33929 8.01307 2.20488 8.69068C1.06707 9.37032 0.66391 10.8517 1.3044 11.9994C1.9449 13.1472 3.3865 13.5267 4.52431 12.847C5.66058 12.1683 7.9937 8.01126 8.00002 8C8.0094 8.01672 10.3402 12.1687 11.4757 12.847C12.6135 13.5266 14.0551 13.1472 14.6956 11.9994C15.3361 10.8517 14.9329 9.37029 13.7951 8.69065C12.6833 8.02652 8.21072 8.00083 8.00717 8C8.21072 7.99917 12.6833 7.97348 13.7951 7.30935C14.9329 6.62971 15.3361 5.14833 14.6956 4.00059C14.0551 2.85285 12.6135 2.47337 11.4757 3.15301C10.3434 3.82938 8.02234 7.96026 8.00002 8C7.99376 7.98884 5.66059 3.83171 4.52431 3.15299C3.3865 2.47335 1.9449 2.85282 1.3044 4.00056C0.66391 5.1483 1.06707 6.62968 2.20488 7.30932Z', + }, + { + d: 'M8.00012 8.00112C8.00012 8.00112 10.3195 12.1795 10.3195 13.5388C10.3195 14.8981 9.28111 16 8.00012 16C6.71913 16 5.68069 14.8981 5.68069 13.5388C5.68069 12.1795 8.00012 8.00112 8.00012 8.00112Z', + }, + ], + [ + { + d: 'M8 0C9.10457 0 10 0.895431 10 2V5C10 5.55228 10.4477 6 11 6H14C15.1046 6 16 6.89543 16 8C16 9.10457 15.1046 10 14 10H11C10.4477 10 10 10.4477 10 11V14C10 15.1046 9.10457 16 8 16C6.89543 16 6 15.1046 6 14V11C6 10.4477 5.55228 10 5 10H2C0.895431 10 0 9.10457 0 8C0 6.89543 0.895431 6 2 6H5C5.55228 6 6 5.55228 6 5V2C6 0.89543 6.89543 0 8 0ZM8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M15.9026 9.25189L12.939 8.78615C12.979 8.53177 13 8.26937 13 8C13 7.73063 12.979 7.46823 12.939 7.21385L15.9026 6.74811C15.9667 7.15599 16 7.57411 16 8C16 8.42589 15.9667 8.84401 15.9026 9.25189ZM15.4706 5.13214L12.6701 6.20789C12.4812 5.7162 12.2158 5.26012 11.8871 4.85468L14.2174 2.96533C14.7416 3.61188 15.167 4.34182 15.4706 5.13214ZM13.0347 1.78258L11.1453 4.11289C10.7399 3.78417 10.2838 3.51876 9.79211 3.32989L10.8679 0.529397C11.6582 0.832983 12.3881 1.25837 13.0347 1.78258ZM9.25189 0.0973803L8.78615 3.06101C8.53177 3.02103 8.26937 3 8 3C7.73063 3 7.46823 3.02103 7.21385 3.06101L6.74811 0.0973805C7.15599 0.0332797 7.57411 0 8 0C8.42589 0 8.84401 0.0332797 9.25189 0.0973803ZM5.13214 0.529397L6.20789 3.32989C5.7162 3.51876 5.26012 3.78417 4.85468 4.11289L2.96533 1.78258C3.61188 1.25837 4.34182 0.832983 5.13214 0.529397ZM1.78258 2.96533L4.11289 4.85468C3.78417 5.26012 3.51876 5.7162 3.32989 6.20789L0.529397 5.13214C0.832983 4.34182 1.25837 3.61188 1.78258 2.96533ZM0.0973804 6.74811C0.0332797 7.15599 0 7.57411 0 8C0 8.42589 0.0332797 8.84401 0.0973805 9.25189L3.06101 8.78615C3.02103 8.53177 3 8.26937 3 8C3 7.73063 3.02103 7.46823 3.06101 7.21386L0.0973804 6.74811ZM0.529397 10.8679L3.32989 9.79211C3.51876 10.2838 3.78417 10.7399 4.11289 11.1453L1.78258 13.0347C1.25837 12.3881 0.832983 11.6582 0.529397 10.8679ZM2.96533 14.2174L4.85468 11.8871C5.26012 12.2158 5.7162 12.4812 6.20789 12.6701L5.13214 15.4706C4.34182 15.167 3.61188 14.7416 2.96533 14.2174ZM6.74811 15.9026L7.21386 12.939C7.46823 12.979 7.73063 13 8 13C8.26937 13 8.53177 12.979 8.78615 12.939L9.25189 15.9026C8.84401 15.9667 8.42589 16 8 16C7.57411 16 7.15599 15.9667 6.74811 15.9026ZM10.8679 15.4706L9.79211 12.6701C10.2838 12.4812 10.7399 12.2158 11.1453 11.8871L13.0347 14.2174C12.3881 14.7416 11.6582 15.167 10.8679 15.4706ZM14.2174 13.0347L11.8871 11.1453C12.2158 10.7399 12.4812 10.2838 12.6701 9.79211L15.4706 10.8679C15.167 11.6582 14.7416 12.3881 14.2174 13.0347Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M11.923 8.78487L15.8477 9.5613C15.9476 9.05633 16 8.53428 16 8C16 7.46572 15.9476 6.94367 15.8477 6.4387L11.923 7.21513C11.8192 6.6932 11.6138 6.20786 11.3283 5.78059L14.6525 3.55507C14.0685 2.68266 13.3173 1.93154 12.4449 1.34748L10.2194 4.6717C9.79214 4.38621 9.3068 4.18081 8.78487 4.07697L9.5613 0.152272C9.05633 0.0523755 8.53428 0 8 0C7.46572 0 6.94367 0.0523755 6.4387 0.152272L7.21513 4.07697C6.6932 4.18082 6.20786 4.38621 5.78059 4.6717L3.55507 1.34748C2.68266 1.93154 1.93154 2.68266 1.34748 3.55508L4.6717 5.78059C4.38621 6.20786 4.18081 6.6932 4.07697 7.21513L0.152272 6.4387C0.0523755 6.94367 0 7.46572 0 8C0 8.53428 0.0523756 9.05633 0.152272 9.5613L4.07697 8.78487C4.18082 9.3068 4.38621 9.79214 4.6717 10.2194L1.34748 12.4449C1.93154 13.3173 2.68266 14.0685 3.55508 14.6525L5.78059 11.3283C6.20786 11.6138 6.6932 11.8192 7.21513 11.923L6.4387 15.8477C6.94367 15.9476 7.46572 16 8 16C8.53428 16 9.05633 15.9476 9.5613 15.8477L8.78487 11.923C9.3068 11.8192 9.79214 11.6138 10.2194 11.3283L12.4449 14.6525C13.3173 14.0685 14.0685 13.3173 14.6525 12.4449L11.3283 10.2194C11.6138 9.79214 11.8192 9.3068 11.923 8.78487ZM11.923 8.78487C11.9735 8.53108 12 8.26863 12 8C12 7.73137 11.9735 7.46892 11.923 7.21513L8.03733 7.98383L11.3283 5.78059C11.0353 5.34206 10.6579 4.9647 10.2194 4.6717L8.01617 7.96267L8.78487 4.07697C8.53108 4.02648 8.26863 4 8 4C7.73137 4 7.46892 4.02648 7.21513 4.07697L7.98383 7.96267L5.78059 4.6717C5.34206 4.9647 4.9647 5.34207 4.6717 5.78059L7.96267 7.98383L4.07697 7.21513C4.02648 7.46892 4 7.73137 4 8C4 8.26863 4.02648 8.53108 4.07697 8.78487L7.96267 8.01617L4.6717 10.2194C4.9647 10.6579 5.34207 11.0353 5.78059 11.3283L7.98383 8.03733L7.21513 11.923C7.46892 11.9735 7.73137 12 8 12C8.26863 12 8.53108 11.9735 8.78487 11.923L8.01617 8.03733L10.2194 11.3283C10.6579 11.0353 11.0353 10.6579 11.3283 10.2194L8.03733 8.01617L11.923 8.78487Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M10.9418 8.59077L15.8477 9.5613C15.9476 9.05633 16 8.53428 16 8C16 7.46572 15.9476 6.94367 15.8477 6.4387L10.9418 7.40923C10.98 7.60024 11 7.79778 11 8C11 8.20222 10.98 8.39977 10.9418 8.59077ZM10.4972 9.66303C10.277 9.99308 9.99308 10.277 9.66303 10.4972L12.4449 14.6525C13.3173 14.0685 14.0685 13.3173 14.6525 12.4449L10.4972 9.66303ZM8.59077 10.9418C8.39977 10.98 8.20222 11 8 11C7.79778 11 7.60024 10.98 7.40923 10.9418L6.4387 15.8477C6.94367 15.9476 7.46572 16 8 16C8.53428 16 9.05633 15.9476 9.5613 15.8477L8.59077 10.9418ZM6.33697 10.4972C6.00692 10.277 5.72299 9.99308 5.50276 9.66303L1.34748 12.4449C1.93154 13.3173 2.68266 14.0685 3.55508 14.6525L6.33697 10.4972ZM5.05815 8.59077C5.02001 8.39977 5 8.20222 5 8C5 7.79778 5.02001 7.60024 5.05815 7.40923L0.152272 6.4387C0.0523755 6.94367 0 7.46572 0 8C0 8.53428 0.0523756 9.05633 0.152272 9.5613L5.05815 8.59077ZM5.50276 6.33697C5.72299 6.00692 6.00692 5.72299 6.33697 5.50276L3.55507 1.34748C2.68266 1.93154 1.93154 2.68266 1.34748 3.55508L5.50276 6.33697ZM7.40923 5.05815C7.60024 5.02001 7.79778 5 8 5C8.20222 5 8.39977 5.02001 8.59077 5.05815L9.5613 0.152272C9.05633 0.0523755 8.53428 0 8 0C7.46572 0 6.94367 0.0523755 6.4387 0.152272L7.40923 5.05815ZM9.66303 5.50276L12.4449 1.34748C13.3173 1.93154 14.0685 2.68266 14.6525 3.55507L10.4972 6.33697C10.277 6.00692 9.99308 5.72299 9.66303 5.50276Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M6.65494 5.34506L8 4L9.34522 5.34522C9.85712 4.23711 10.3195 3.06813 10.3195 2.46119C10.3195 1.10191 9.28111 0 8.00012 0C6.71913 0 5.68069 1.10191 5.68069 2.46119C5.68069 3.0681 6.14307 4.237 6.65494 5.34506Z', + }, + { + d: 'M6.48343 5.51657L4.20992 7.79008C3.35262 7.68776 2.58369 7.5356 2.20488 7.30932C1.06707 6.62968 0.66391 5.1483 1.3044 4.00056C1.9449 2.85282 3.3865 2.47335 4.52431 3.15299C5.04047 3.4613 5.80362 4.48739 6.48343 5.51657Z', + }, + { + d: 'M4.20992 8.20992C3.35262 8.31224 2.58369 8.4644 2.20488 8.69068C1.06707 9.37032 0.66391 10.8517 1.3044 11.9994C1.9449 13.1472 3.3865 13.5267 4.52431 12.847C5.04047 12.5387 5.80362 11.5126 6.48343 10.4834L4.20992 8.20992Z', + }, + { + d: 'M6.65494 10.6549C6.14307 11.763 5.68069 12.9319 5.68069 13.5388C5.68069 14.8981 6.71913 16 8.00012 16C9.28111 16 10.3195 14.8981 10.3195 13.5388C10.3195 12.9319 9.85712 11.7629 9.34522 10.6548L8 12L6.65494 10.6549Z', + }, + { + d: 'M9.51659 10.4834C10.1964 11.5126 10.9595 12.5387 11.4757 12.847C12.6135 13.5266 14.0551 13.1472 14.6956 11.9994C15.3361 10.8517 14.9329 9.37029 13.7951 8.69065C13.4163 8.46437 12.6474 8.31221 11.7901 8.20989L9.51659 10.4834Z', + }, + { + d: 'M11.7901 7.79011L9.5166 5.5166C10.1964 4.4874 10.9596 3.46132 11.4757 3.15301C12.6135 2.47337 14.0551 2.85285 14.6956 4.00059C15.3361 5.14833 14.9329 6.62971 13.7951 7.30935C13.4163 7.53563 12.6474 7.68779 11.7901 7.79011Z', + }, + { + d: 'M8 6L10 8L8 10L6 8L8 6Z', + }, + ], + [ + { + d: 'M15.4055 4.96811C14.5941 2.98825 13.0118 1.40593 11.0319 0.5945C10.9114 0.68356 10.796 0.782639 10.6869 0.891746C10.2922 1.28636 9.86718 2.18783 9.47419 3.22087C9.00818 3.07729 8.51312 3 8 3C7.48688 3 6.99182 3.07729 6.52581 3.22087C6.13282 2.18783 5.70776 1.28636 5.31315 0.891746C5.20404 0.782638 5.08865 0.683558 4.96811 0.594498C2.98825 1.40593 1.40593 2.98825 0.594501 4.96812C0.68356 5.08865 0.78264 5.20404 0.891747 5.31315C1.28636 5.70776 2.18783 6.13282 3.22087 6.52581C3.07729 6.99182 3 7.48688 3 8C3 8.51312 3.07729 9.00818 3.22087 9.47419C2.18783 9.86718 1.28636 10.2922 0.891747 10.6869C0.782642 10.796 0.683564 10.9113 0.594506 11.0319C1.40594 13.0117 2.98825 14.5941 4.96812 15.4055C5.08866 15.3164 5.20404 15.2174 5.31315 15.1083C5.70776 14.7136 6.13282 13.8122 6.52581 12.7791C6.99182 12.9227 7.48688 13 8 13C8.51312 13 9.00818 12.9227 9.47419 12.7791C9.86718 13.8122 10.2922 14.7136 10.6869 15.1083C10.796 15.2174 10.9113 15.3164 11.0319 15.4055C13.0117 14.5941 14.5941 13.0117 15.4055 11.0319C15.3164 10.9113 15.2174 10.796 15.1083 10.6869C14.7136 10.2922 13.8122 9.86718 12.7791 9.47419C12.9227 9.00818 13 8.51312 13 8C13 7.48688 12.9227 6.99182 12.7791 6.52581C13.8122 6.13282 14.7136 5.70776 15.1083 5.31315C15.2174 5.20404 15.3164 5.08865 15.4055 4.96811ZM8 8C8 8 8.66996 5.33492 9.47419 3.22087C11.0501 3.70641 12.2936 4.94995 12.7791 6.52581C10.6651 7.33004 8 8 8 8ZM8 8C8 8 5.33492 8.66996 3.22087 9.47419C3.70641 11.0501 4.94995 12.2936 6.52581 12.7791C7.33004 10.6651 8 8 8 8ZM8 8C8 8 10.6651 8.66996 12.7791 9.47419C12.2936 11.0501 11.0501 12.2936 9.47419 12.7791C8.66996 10.6651 8 8 8 8ZM8 8C8 8 5.33492 7.33004 3.22087 6.52581C3.70641 4.94994 4.94995 3.7064 6.52581 3.22087C7.33004 5.33492 8 8 8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8 8C8 8 12.7852 6.79708 13.7592 5.82309C13.8476 5.73469 13.9278 5.64121 14 5.54355C13.3426 3.93944 12.0606 2.65743 10.4565 2C10.3588 2.07216 10.2653 2.15243 10.1769 2.24083C9.20292 3.21483 8 8 8 8ZM8 8C8 8 9.20292 12.7852 10.1769 13.7592C10.2653 13.8476 10.3588 13.9278 10.4565 14C12.0606 13.3426 13.3426 12.0606 14 10.4565C13.9278 10.3588 13.8476 10.2653 13.7592 10.1769C12.7852 9.20292 8 8 8 8ZM8 8C8 8 6.79708 12.7852 5.82309 13.7592C5.73469 13.8476 5.64121 13.9278 5.54355 14C3.93944 13.3426 2.65743 12.0606 2 10.4565C2.07216 10.3588 2.15243 10.2653 2.24083 10.1769C3.21483 9.20292 8 8 8 8ZM8 8C8 8 3.21483 6.79708 2.24083 5.82309C2.15243 5.73469 2.07216 5.6412 2 5.54354C2.65743 3.93944 3.93944 2.65743 5.54354 2C5.6412 2.07216 5.73469 2.15243 5.82309 2.24083C6.79708 3.21483 8 8 8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M9.17157 1.17157C7.60948 2.73367 7.60948 5.26633 9.17157 6.82843C10.7337 8.39052 13.2663 8.39052 14.8284 6.82843C16.3905 5.26633 16.3905 2.73367 14.8284 1.17157C13.2663 -0.390524 10.7337 -0.390524 9.17157 1.17157Z', + }, + { + d: 'M1.17157 14.8284C2.73367 16.3905 5.26633 16.3905 6.82843 14.8284C8.39052 13.2663 8.39052 10.7337 6.82843 9.17157C5.26633 7.60948 2.73367 7.60948 1.17157 9.17157C-0.390524 10.7337 -0.390524 13.2663 1.17157 14.8284Z', + }, + { + d: 'M6.82843 1.17157C8.39052 2.73367 8.39052 5.26633 6.82843 6.82843C5.26633 8.39052 2.73367 8.39052 1.17157 6.82843C-0.390524 5.26633 -0.390524 2.73367 1.17157 1.17157C2.73367 -0.390524 5.26633 -0.390524 6.82843 1.17157Z', + }, + { + d: 'M9.17157 14.8284C10.7337 16.3905 13.2663 16.3905 14.8284 14.8284C16.3905 13.2663 16.3905 10.7337 14.8284 9.17157C13.2663 7.60948 10.7337 7.60948 9.17157 9.17157C7.60948 10.7337 7.60948 13.2663 9.17157 14.8284Z', + }, + ], + [ + { + d: 'M13.6569 2.34313C12.0059 5.93367 12.0059 10.0663 13.6569 13.6568C10.0663 12.0058 5.93368 12.0058 2.34315 13.6568C3.99414 10.0663 3.99414 5.93367 2.34315 2.34313C5.93368 3.99413 10.0663 3.99413 13.6569 2.34313Z', + }, + ], + [ + { + d: 'M15.1083 5.31314C13.9061 6.5153 8 8 8 8C8 8 9.4847 2.09389 10.6869 0.891741C10.796 0.782636 10.9113 0.683558 11.0319 0.594499C13.0117 1.40593 14.5941 2.98825 15.4055 4.96812C15.3164 5.08865 15.2174 5.20404 15.1083 5.31314Z', + }, + { + d: 'M15.4055 11.0319C15.3164 10.9113 15.2174 10.796 15.1083 10.6868C13.9061 9.48469 8 8 8 8C8 8 6.5153 2.09389 5.31315 0.891741C5.20404 0.782635 5.08866 0.683557 4.96812 0.594498C2.98825 1.40593 1.40593 2.98824 0.594501 4.96812C0.683561 5.08865 0.782639 5.20404 0.891746 5.31314C2.0939 6.5153 8 8 8 8C8 8 2.0939 9.48469 0.891746 10.6868C0.78264 10.796 0.683561 10.9113 0.594502 11.0319C1.40593 13.0117 2.98825 14.5941 4.96812 15.4055C5.08866 15.3164 5.20404 15.2174 5.31315 15.1082C6.5153 13.9061 8 8 8 8C8 8 9.4847 13.9061 10.6869 15.1082C10.796 15.2174 10.9113 15.3164 11.0319 15.4055C13.0117 14.5941 14.5941 13.0117 15.4055 11.0319Z', + }, + ], + [ + { + d: 'M8 0C8.33735 2.71168 11.5009 4.02205 13.6569 2.34315C11.9779 4.49913 13.2883 7.66266 16 8C13.2883 8.33735 11.9779 11.5009 13.6569 13.6569C11.5009 11.9779 8.33735 13.2883 8 16C7.66266 13.2883 4.49913 11.9779 2.34315 13.6569C4.02205 11.5009 2.71168 8.33735 0 8C2.71168 7.66266 4.02205 4.49913 2.34315 2.34315C4.49913 4.02205 7.66266 2.71168 8 0Z', + }, + ], + [ + { + d: 'M6.24244 4.40581C6.77293 4.14591 7.36943 4 8 4C8.63065 4 9.22722 4.14595 9.75776 4.40591C10.0814 3.6217 10.3195 2.89673 10.3195 2.46119C10.3195 1.10191 9.28111 0 8.00012 0C6.71913 0 5.68069 1.10191 5.68069 2.46119C5.68069 2.89671 5.9188 3.62164 6.24244 4.40581Z', + }, + { + d: 'M5.86339 4.61784C4.80568 5.28742 4.08369 6.43949 4.00681 7.76466C3.23255 7.66304 2.55351 7.51757 2.20488 7.30932C1.06707 6.62968 0.66391 5.1483 1.3044 4.00056C1.9449 2.85282 3.3865 2.47335 4.52431 3.15299C4.88694 3.3696 5.37149 3.94049 5.86339 4.61784Z', + }, + { + d: 'M4.00681 8.23534C3.23255 8.33696 2.5535 8.48244 2.20488 8.69068C1.06707 9.37032 0.66391 10.8517 1.3044 11.9994C1.9449 13.1472 3.3865 13.5267 4.52431 12.847C4.88694 12.6304 5.37149 12.0595 5.86339 11.3822C4.80568 10.7126 4.0837 9.56051 4.00681 8.23534Z', + }, + { + d: 'M6.24244 11.5942C5.9188 12.3784 5.68069 13.1033 5.68069 13.5388C5.68069 14.8981 6.71913 16 8.00012 16C9.28111 16 10.3195 14.8981 10.3195 13.5388C10.3195 13.1033 10.0814 12.3783 9.75776 11.5941C9.22722 11.8541 8.63065 12 8 12C7.36943 12 6.77293 11.8541 6.24244 11.5942Z', + }, + { + d: 'M10.1366 11.3821C10.6285 12.0595 11.1131 12.6304 11.4757 12.847C12.6135 13.5266 14.0551 13.1472 14.6956 11.9994C15.3361 10.8517 14.9329 9.37029 13.7951 8.69065C13.4465 8.4824 12.7675 8.33693 11.9932 8.23531C11.9163 9.56048 11.1943 10.7126 10.1366 11.3821Z', + }, + { + d: 'M11.9932 7.76469C11.9163 6.43952 11.1943 5.28745 10.1366 4.61786C10.6285 3.94051 11.1131 3.36962 11.4757 3.15301C12.6135 2.47337 14.0551 2.85285 14.6956 4.00059C15.3361 5.14833 14.9329 6.62971 13.7951 7.30935C13.4465 7.5176 12.7675 7.66307 11.9932 7.76469Z', + }, + ], + [ + { + d: 'M8 0C9.37146 3.70632 12.2937 6.62854 16 8C12.2937 9.37146 9.37146 12.2937 8 16C6.62854 12.2937 3.70632 9.37146 0 8C3.70632 6.62854 6.62854 3.70632 8 0Z', + }, + ], + [ + { + d: 'M12 4C12 6.20914 10.2091 8 8 8C5.79086 8 4 6.20914 4 4C4 1.79086 5.79086 0 8 0C10.2091 0 12 1.79086 12 4Z', + }, + { + d: 'M8 11C8 13.2091 6.20914 15 4 15C1.79086 15 0 13.2091 0 11C0 8.79086 1.79086 7 4 7C6.20914 7 8 8.79086 8 11Z', + }, + { + d: 'M8 11C8 8.79086 9.79086 7 12 7C14.2091 7 16 8.79086 16 11C16 13.2091 14.2091 15 12 15C9.79086 15 8 13.2091 8 11Z', + }, + ], + [ + { + d: 'M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM5 4C4.44772 4 4 4.44772 4 5V11C4 11.5523 4.44772 12 5 12H11C11.5523 12 12 11.5523 12 11V5C12 4.44772 11.5523 4 11 4H5Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM12.7207 7.32562L8.67438 3.27934C8.30193 2.90689 7.69807 2.90689 7.32562 3.27934L3.27934 7.32562C2.90689 7.69807 2.90689 8.30193 3.27934 8.67438L7.32562 12.7207C7.69807 13.0931 8.30193 13.0931 8.67438 12.7207L12.7207 8.67438C13.0931 8.30193 13.0931 7.69807 12.7207 7.32562Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M10.3438 10.3438C10.9434 10.9431 11.7715 11.3137 12.6863 11.3137C14.5164 11.3137 16 9.83012 16 8.00001C16 6.1699 14.5164 4.6863 12.6863 4.6863C10.8562 4.6863 9.37259 6.1699 9.37259 8.00001C9.37259 8.91475 9.74323 9.74292 10.3425 10.3425C9.74292 9.74323 8.91475 9.37258 8 9.37258C7.08525 9.37258 6.25708 9.74323 5.65746 10.3425C6.25677 9.74292 6.62741 8.91475 6.62741 8.00001C6.62741 6.1699 5.14382 4.6863 3.31371 4.6863C1.4836 4.6863 -3.70335e-07 6.1699 0 8.00001C-4.9378e-07 9.83012 1.4836 11.3137 3.31371 11.3137C4.22845 11.3137 5.05663 10.9431 5.65625 10.3438C5.05695 10.9434 4.68629 11.7715 4.68629 12.6863C4.68629 14.5164 6.16989 16 8 16C9.83011 16 11.3137 14.5164 11.3137 12.6863C11.3137 11.7715 10.9431 10.9434 10.3438 10.3438Z', + }, + { + d: 'M8 6.62742C6.16989 6.62742 4.68629 5.14382 4.68629 3.31371C4.68629 1.4836 6.16989 -7.40671e-08 8 0C9.83011 -7.40671e-08 11.3137 1.4836 11.3137 3.31371C11.3137 5.14382 9.83011 6.62742 8 6.62742Z', + }, + ], + [ + { + d: 'M13.6569 2.34315C11.5009 4.02205 8.33735 2.71168 8 0C7.66266 2.71168 4.49913 4.02205 2.34315 2.34315C4.02205 4.49913 2.71168 7.66266 0 8C2.71168 8.33735 4.02205 11.5009 2.34315 13.6569C4.49913 11.9779 7.66266 13.2883 8 16C8.33735 13.2883 11.5009 11.9779 13.6569 13.6569C11.9779 11.5009 13.2883 8.33735 16 8C13.2883 7.66266 11.9779 4.49913 13.6569 2.34315ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M11.9916 2.34459C12.451 1.88514 13.1959 1.88514 13.6554 2.34459C14.1149 2.80405 14.1149 3.54899 13.6554 4.00845L11.1596 6.50422C10.7002 6.96368 9.95524 6.96368 9.49578 6.50422C9.03632 6.04476 9.03632 5.29983 9.49578 4.84037L11.9916 2.34459Z', + }, + { + d: 'M4.84037 9.49578C5.29983 9.03632 6.04476 9.03632 6.50422 9.49578C6.96368 9.95524 6.96368 10.7002 6.50422 11.1596L4.00845 13.6554C3.54899 14.1149 2.80405 14.1149 2.3446 13.6554C1.88514 13.1959 1.88514 12.451 2.3446 11.9916L4.84037 9.49578Z', + }, + { + d: 'M13.6554 11.9916C14.1149 12.451 14.1149 13.1959 13.6554 13.6554C13.1959 14.1149 12.451 14.1149 11.9916 13.6554L9.49578 11.1596C9.03632 10.7002 9.03632 9.95524 9.49578 9.49578C9.95524 9.03632 10.7002 9.03632 11.1596 9.49578L13.6554 11.9916Z', + }, + { + d: 'M6.50422 4.84037C6.96368 5.29983 6.96368 6.04476 6.50422 6.50422C6.04476 6.96368 5.29983 6.96368 4.84037 6.50422L2.34459 4.00845C1.88513 3.54899 1.88514 2.80405 2.3446 2.34459C2.80405 1.88513 3.54899 1.88514 4.00845 2.34459L6.50422 4.84037Z', + }, + ], + [ + { + d: 'M15 2C15 5.86599 11.866 9 8 9C4.13401 9 1 5.86599 1 2H4.5C6.433 2 8 3.567 8 5.5C8 3.567 9.567 2 11.5 2H15Z', + }, + { + d: 'M1 9C1 12.866 4.13401 16 8 16C11.866 16 15 12.866 15 9H11.5C9.567 9 8 10.567 8 12.5C8 10.567 6.433 9 4.5 9H1Z', + }, + ], + [ + { + d: 'M7.99948 0C6.89491 7.11679e-05 5.99954 0.895559 5.99961 2.00013C5.99968 3.1047 6.8957 4.00007 8.00027 4C9.10484 4.00007 10.0003 3.1047 10.0004 2.00013C10.0005 0.895559 9.1051 7.11679e-05 8.00053 0H7.99948Z', + }, + { + d: 'M4.00001 7.99973C4.00008 6.89516 3.10471 5.99968 2.00014 5.9996C0.895567 5.99953 7.87973e-05 6.89491 7.62939e-06 7.99948V8.00052C7.87973e-05 9.10509 0.895567 10.0005 2.00014 10.0004C3.10471 10.0003 4.00008 9.1043 4.00001 7.99973Z', + }, + { + d: 'M16 7.99948C15.9999 6.89491 15.1044 5.99953 13.9999 5.99961C12.8953 5.99968 11.9999 6.8957 12 8.00027C11.9999 9.10484 12.8953 10.0003 13.9999 10.0004C15.1044 10.0005 15.9999 9.10509 16 8.00052V7.99948Z', + }, + { + d: 'M7.99974 12C6.89517 11.9999 5.99968 12.8953 5.99961 13.9999C5.99954 15.1044 6.89491 15.9999 7.99948 16H8.00053C9.1051 15.9999 10.0005 15.1044 10.0004 13.9999C10.0003 12.8953 9.10431 11.9999 7.99974 12Z', + }, + { + d: 'M8.00001 11C9.65686 11 11 9.65685 11 8C11 6.34315 9.65686 5 8.00001 5C6.34315 5 5.00001 6.34315 5.00001 8C5.00001 9.65685 6.34315 11 8.00001 11Z', + }, + ], + [ + { + d: 'M13.6569 2.34315C11.5009 4.02205 8.33735 2.71168 8 0C7.66266 2.71168 4.49913 4.02205 2.34315 2.34315C4.02205 4.49913 2.71168 7.66266 0 8C2.71168 8.33735 4.02205 11.5009 2.34315 13.6569C4.49913 11.9779 7.66266 13.2883 8 16C8.33735 13.2883 11.5009 11.9779 13.6569 13.6569C11.9779 11.5009 13.2883 8.33735 16 8C13.2883 7.66266 11.9779 4.49913 13.6569 2.34315ZM12 8L8 4L4 8L8 12L12 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8Z', + }, + ], + [ + { + d: 'M13.6569 13.6568C12.0059 10.0663 12.0059 5.93368 13.6569 2.34314C10.0663 3.99414 5.93368 3.99414 2.34315 2.34314C3.99414 5.93368 3.99414 10.0663 2.34315 13.6568C5.93368 12.0059 10.0663 12.0059 13.6569 13.6568ZM11 8L8 5L5 8L8 11L11 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M7.99948 0C6.89491 7.11679e-05 5.99954 0.895559 5.99961 2.00013C5.99968 3.1047 6.8957 4.00007 8.00027 4C9.10484 4.00007 10.0003 3.1047 10.0004 2.00013C10.0005 0.895559 9.1051 7.11679e-05 8.00053 0H7.99948Z', + }, + { + d: 'M13.6565 2.34278C12.8754 1.56178 11.6091 1.56186 10.8281 2.34296C10.0471 3.12406 10.0475 4.39076 10.8286 5.17176C11.6096 5.95286 12.8759 5.95294 13.657 5.17194C14.4381 4.39094 14.4382 3.12461 13.6572 2.34352L13.6565 2.34278Z', + }, + { + d: 'M5.17177 5.17139C5.95287 4.39039 5.95295 3.12406 5.17195 2.34296C4.39095 1.56186 3.12462 1.56178 2.34352 2.34278L2.34278 2.34352C1.56179 3.12462 1.56187 4.39094 2.34297 5.17194C3.12407 5.95294 4.39077 5.95249 5.17177 5.17139Z', + }, + { + d: 'M4.00001 7.99973C4.00008 6.89516 3.10471 5.99968 2.00014 5.9996C0.895567 5.99953 7.87973e-05 6.89491 7.62939e-06 7.99948V8.00052C7.87973e-05 9.10509 0.895567 10.0005 2.00014 10.0004C3.10471 10.0003 4.00008 9.1043 4.00001 7.99973Z', + }, + { + d: 'M16 7.99948C15.9999 6.89491 15.1044 5.99953 13.9999 5.99961C12.8953 5.99968 11.9999 6.8957 12 8.00027C11.9999 9.10484 12.8953 10.0003 13.9999 10.0004C15.1044 10.0005 15.9999 9.10509 16 8.00052V7.99948Z', + }, + { + d: 'M5.1714 10.8282C4.3904 10.0471 3.12407 10.0471 2.34297 10.8281C1.56187 11.6091 1.56179 12.8754 2.34278 13.6565L2.34352 13.6572C3.12462 14.4382 4.39095 14.4381 5.17195 13.657C5.95295 12.8759 5.95249 11.6092 5.1714 10.8282Z', + }, + { + d: 'M13.6572 13.6565C14.4382 12.8754 14.4381 11.6091 13.657 10.8281C12.8759 10.0471 11.6092 10.0475 10.8282 10.8286C10.0471 11.6096 10.0471 12.8759 10.8281 13.657C11.6091 14.4381 12.8754 14.4382 13.6565 13.6572L13.6572 13.6565Z', + }, + { + d: 'M7.99974 12C6.89517 11.9999 5.99968 12.8953 5.99961 13.9999C5.99954 15.1044 6.89491 15.9999 7.99948 16H8.00053C9.1051 15.9999 10.0005 15.1044 10.0004 13.9999C10.0003 12.8953 9.10431 11.9999 7.99974 12Z', + }, + { + d: 'M8.00001 11C9.65686 11 11 9.65685 11 8C11 6.34315 9.65686 5 8.00001 5C6.34315 5 5.00001 6.34315 5.00001 8C5.00001 9.65685 6.34315 11 8.00001 11Z', + }, + ], + [ + { + d: 'M8 8C8 8 13.1678 6.70089 14.2197 5.649C15.2716 4.59712 15.2583 2.87836 14.19 1.81004C13.1216 0.741719 11.4029 0.728395 10.351 1.78028C9.29911 2.83216 8 8 8 8Z', + }, + { + d: 'M8 8C8 8 13.1678 9.29911 14.2197 10.351C15.2716 11.4029 15.2583 13.1216 14.19 14.19C13.1216 15.2583 11.4029 15.2716 10.351 14.2197C9.29911 13.1678 8 8 8 8Z', + }, + { + d: 'M1.78028 10.351C2.83216 9.29911 8 8 8 8C8 8 6.70089 13.1678 5.649 14.2197C4.59712 15.2716 2.87836 15.2583 1.81004 14.19C0.741719 13.1216 0.728395 11.4029 1.78028 10.351Z', + }, + { + d: 'M8 8C8 8 2.83216 6.70089 1.78028 5.649C0.728395 4.59712 0.741719 2.87836 1.81004 1.81004C2.87836 0.741719 4.59712 0.728395 5.649 1.78028C6.70089 2.83216 8 8 8 8Z', + }, + ], + [ + { + d: 'M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M13.6569 13.6568C12.0059 10.0663 12.0059 5.93368 13.6569 2.34314C10.0663 3.99414 5.93368 3.99414 2.34315 2.34314C3.99414 5.93368 3.99414 10.0663 2.34315 13.6568C5.93368 12.0059 10.0663 12.0059 13.6569 13.6568ZM8 11C9.65685 11 11 9.65686 11 8.00001C11 6.34315 9.65685 5.00001 8 5.00001C6.34315 5.00001 5 6.34315 5 8.00001C5 9.65686 6.34315 11 8 11Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M5.34749 1.59629C5.85108 0.646843 6.8496 0.000111103 7.99922 0H8.00078C9.1504 0.000111103 10.1489 0.646843 10.6525 1.59628C11.68 1.28102 12.8433 1.52977 13.6563 2.34259L13.6574 2.3437C14.4702 3.15668 14.719 4.32005 14.4037 5.34749C15.3532 5.85108 15.9999 6.8496 16 7.99922V8.00078C15.9999 9.1504 15.3532 10.1489 14.4037 10.6525C14.719 11.68 14.4702 12.8433 13.6574 13.6563L13.6563 13.6574C12.8433 14.4702 11.68 14.719 10.6525 14.4037C10.1489 15.3532 9.1504 15.9999 8.00078 16H7.99922C6.8496 15.9999 5.85108 15.3532 5.3475 14.4037C4.32005 14.719 3.15668 14.4702 2.3437 13.6574L2.34259 13.6563C1.52977 12.8433 1.28102 11.68 1.59629 10.6525C0.646843 10.1489 0.000111103 9.1504 0 8.00078V7.99922C0.000111103 6.8496 0.646843 5.85108 1.59628 5.34749C1.28102 4.32005 1.52977 3.15668 2.34259 2.3437L2.3437 2.34259C3.15668 1.52977 4.32005 1.28102 5.34749 1.59629ZM7.11693 5.86807C6.98022 6.12611 6.80322 6.3684 6.58593 6.58565C6.36868 6.80294 6.12611 6.98022 5.86807 7.11693C5.95387 7.39605 6.00003 7.69253 6 7.9998C6.00003 8.30706 5.95387 8.60395 5.86807 8.88307C6.12611 9.01978 6.3684 9.19678 6.58565 9.41407C6.80294 9.63132 6.98022 9.87389 7.11693 10.1319C7.39605 10.0461 7.69253 9.99997 7.9998 10C8.30706 9.99997 8.60394 10.0461 8.88307 10.1319C9.01978 9.87389 9.19678 9.6316 9.41407 9.41435C9.63132 9.19706 9.87389 9.01978 10.1319 8.88307C10.0461 8.60395 9.99997 8.30747 10 8.0002C9.99997 7.69294 10.0461 7.39605 10.1319 7.11693C9.87389 6.98022 9.6316 6.80322 9.41435 6.58593C9.19706 6.36868 9.01978 6.12611 8.88307 5.86807C8.60395 5.95387 8.30747 6.00003 8.0002 6C7.69294 6.00003 7.39605 5.95387 7.11693 5.86807Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M12.6045 11.8471C13.4757 10.8056 14 9.46402 14 8C14 6.53598 13.4756 5.19442 12.6045 4.15289L13.6832 2.76772C13.9157 2.46917 13.5308 2.08434 13.2323 2.31683L11.8471 3.39548C10.8056 2.52434 9.46402 2 8 2C6.53598 2 5.19443 2.52434 4.1529 3.39548L2.76773 2.31682C2.46918 2.08434 2.08435 2.46917 2.31684 2.76772L3.39549 4.15289C2.52435 5.19442 2 6.53598 2 8C2 9.46402 2.52435 10.8056 3.39548 11.8471L2.31684 13.2323C2.08435 13.5308 2.46918 13.9156 2.76773 13.6832L4.15289 12.6045C5.19442 13.4756 6.53598 14 8 14C9.46402 14 10.8056 13.4756 11.8471 12.6045L13.2323 13.6832C13.5308 13.9156 13.9156 13.5308 13.6832 13.2323L12.6045 11.8471ZM12.6045 11.8471L10.3747 8.9836L12.89 8.31882C13.2654 8.2721 13.2654 7.72787 12.8899 7.68116L10.3747 7.01637L12.6045 4.15289C12.375 3.87852 12.1215 3.62497 11.8471 3.39548L8.98361 5.62533L8.31883 3.11004C8.27212 2.73454 7.72788 2.73454 7.68117 3.11004L7.01639 5.62533L4.1529 3.39548C3.87853 3.62497 3.62498 3.87852 3.39549 4.15289L5.62535 7.01637L3.11005 7.68116C2.73455 7.72787 2.73455 8.27211 3.11005 8.31882L5.62535 8.9836L3.39548 11.8471C3.62497 12.1215 3.87852 12.375 4.15289 12.6045L7.01639 10.3746L7.68117 12.8899C7.72788 13.2654 8.27212 13.2654 8.31883 12.8899L8.98361 10.3746L11.8471 12.6045C12.1215 12.375 12.375 12.1215 12.6045 11.8471Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8.53781 0.379548C8.35784 -0.126516 7.64216 -0.126516 7.4622 0.379548L6.25252 3.78121L2.99181 2.23124C2.50672 2.00065 2.00065 2.50672 2.23124 2.99181L3.78121 6.25252L0.379548 7.4622C-0.126516 7.64216 -0.126516 8.35784 0.379548 8.53781L3.78121 9.74748L2.23124 13.0082C2.00065 13.4933 2.50672 13.9993 2.99181 13.7688L6.25252 12.2188L7.4622 15.6205C7.64216 16.1265 8.35784 16.1265 8.53781 15.6205L9.74748 12.2188L13.0082 13.7688C13.4933 13.9993 13.9993 13.4933 13.7688 13.0082L12.2188 9.74748L15.6205 8.53781C16.1265 8.35784 16.1265 7.64216 15.6205 7.4622L12.2188 6.25252L13.7688 2.99181C13.9993 2.50672 13.4933 2.00065 13.0082 2.23124L9.74748 3.78121L8.53781 0.379548ZM7.99994 10.9999C9.65679 10.9999 10.9999 9.65679 10.9999 7.99994C10.9999 6.34308 9.65679 4.99994 7.99994 4.99994C6.34308 4.99994 4.99994 6.34308 4.99994 7.99994C4.99994 9.65679 6.34308 10.9999 7.99994 10.9999Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M7.4622 0.379548C7.64216 -0.126516 8.35784 -0.126516 8.53781 0.379548L9.74748 3.78121L13.0082 2.23124C13.4933 2.00065 13.9993 2.50672 13.7688 2.99181L12.2188 6.25252L15.6205 7.4622C16.1265 7.64216 16.1265 8.35784 15.6205 8.53781L12.2188 9.74748L13.7688 13.0082C13.9993 13.4933 13.4933 13.9993 13.0082 13.7688L9.74748 12.2188L8.53781 15.6205C8.35784 16.1265 7.64216 16.1265 7.4622 15.6205L6.25252 12.2188L2.99181 13.7688C2.50672 13.9993 2.00065 13.4933 2.23124 13.0082L3.78121 9.74748L0.379548 8.53781C-0.126516 8.35784 -0.126516 7.64216 0.379548 7.4622L3.78121 6.25252L2.23124 2.99181C2.00065 2.50672 2.50672 2.00065 2.99181 2.23124L6.25252 3.78121L7.4622 0.379548Z', + }, + ], + [ + { + d: 'M4.4 2C3.07452 2 2 3.07452 2 4.4V11.6C2 12.9255 3.07452 14 4.4 14H11.6C12.9255 14 14 12.9255 14 11.6V4.4C14 3.07452 12.9255 2 11.6 2H4.4ZM8.70711 4.70711C8.31658 4.31658 7.68342 4.31658 7.29289 4.70711L4.70711 7.29289C4.31658 7.68342 4.31658 8.31658 4.70711 8.70711L7.29289 11.2929C7.68342 11.6834 8.31658 11.6834 8.70711 11.2929L11.2929 8.70711C11.6834 8.31658 11.6834 7.68342 11.2929 7.29289L8.70711 4.70711Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M15 1C11.134 1 8 4.13401 8 8H15V1Z', + }, + { + d: 'M1 15C4.86599 15 8 11.866 8 8H1V15Z', + }, + { + d: 'M15 15C15 11.134 11.866 8 8 8L8 15H15Z', + }, + { + d: 'M1 1C1 4.86599 4.13401 8 8 8V1L1 1Z', + }, + ], + [ + { + d: 'M8.31883 0.281623C8.27212 -0.0938759 7.72788 -0.0938737 7.68117 0.281626L7.46446 2.02358C4.57807 2.27888 2.27889 4.57807 2.02358 7.46446L0.281623 7.68117C-0.0938759 7.72788 -0.0938737 8.27212 0.281626 8.31883L2.02358 8.53554C2.27889 11.4219 4.57807 13.7211 7.46446 13.9764L7.68117 15.7184C7.72788 16.0939 8.27212 16.0939 8.31883 15.7184L8.53554 13.9764C11.4219 13.7211 13.7211 11.4219 13.9764 8.53554L15.7184 8.31883C16.0939 8.27212 16.0939 7.72788 15.7184 7.68117L13.9764 7.46446C13.7211 4.57806 11.4219 2.27888 8.53554 2.02357L8.31883 0.281623ZM8.53554 2.02357C8.35911 2.00797 8.18049 2 8.00001 2C7.81952 2 7.64089 2.00797 7.46446 2.02358L7.01639 5.62535L4.76773 4.31684C4.46918 4.08435 4.08435 4.46918 4.31684 4.76773L5.62535 7.01639L2.02358 7.46446C2.00798 7.64089 2.00001 7.81951 2.00001 8C2.00001 8.18048 2.00798 8.35911 2.02358 8.53554L5.62535 8.98361L4.31684 11.2323C4.08435 11.5308 4.46918 11.9157 4.76773 11.6832L7.01639 10.3747L7.46446 13.9764C7.64089 13.992 7.81952 14 8.00001 14C8.18049 14 8.35911 13.992 8.53554 13.9764L8.98361 10.3747L11.2323 11.6832C11.5308 11.9157 11.9157 11.5308 11.6832 11.2323L10.3747 8.98361L13.9764 8.53554C13.992 8.35911 14 8.18048 14 8C14 7.81952 13.992 7.64089 13.9764 7.46446L10.3747 7.01639L11.6832 4.76773C11.9157 4.46918 11.5308 4.08435 11.2323 4.31684L8.98361 5.62535L8.53554 2.02357Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M2.34315 2.34315C5.46734 -0.781049 10.5327 -0.781049 13.6569 2.34315L10 6L8.70711 4.70711C8.31658 4.31658 7.68342 4.31658 7.2929 4.70711L6 6L2.34315 2.34315ZM8 8L6 6L4.70711 7.29289C4.31658 7.68342 4.31658 8.31658 4.70711 8.70711L6 10L2.34315 13.6569C5.46734 16.781 10.5327 16.781 13.6569 13.6569L10 10L11.2929 8.70711C11.6834 8.31658 11.6834 7.68342 11.2929 7.29289L10 6L8 8ZM8 8L10 10L8.70711 11.2929C8.31658 11.6834 7.68342 11.6834 7.2929 11.2929L6 10L8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8.53781 0.379548C8.35784 -0.126516 7.64216 -0.126516 7.4622 0.379548L6.25252 3.78121L2.99181 2.23124C2.50672 2.00065 2.00065 2.50672 2.23124 2.99181L3.78121 6.25252L0.379548 7.4622C-0.126516 7.64216 -0.126516 8.35784 0.379548 8.53781L3.78121 9.74748L2.23124 13.0082C2.00065 13.4933 2.50672 13.9993 2.99181 13.7688L6.25252 12.2188L7.4622 15.6205C7.64216 16.1265 8.35784 16.1265 8.53781 15.6205L9.74748 12.2188L13.0082 13.7688C13.4933 13.9993 13.9993 13.4933 13.7688 13.0082L12.2188 9.74748L15.6205 8.53781C16.1265 8.35784 16.1265 7.64216 15.6205 7.4622L12.2188 6.25252L13.7688 2.99181C13.9993 2.50672 13.4933 2.00065 13.0082 2.23124L9.74748 3.78121L8.53781 0.379548ZM10.9999 7.99994L7.99994 4.99994L4.99994 7.99994L7.99994 10.9999L10.9999 7.99994Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M2.34315 2.34315C5.46734 -0.781049 10.5327 -0.781049 13.6569 2.34315L10.8284 5.17157C10.1046 4.44772 9.10457 4 8 4C6.89543 4 5.89543 4.44772 5.17157 5.17157L2.34315 2.34315ZM8 8L5.17157 5.17157C4.44772 5.89543 4 6.89543 4 8C4 9.10457 4.44772 10.1046 5.17157 10.8284L2.34315 13.6569C5.46734 16.781 10.5327 16.781 13.6569 13.6569L10.8284 10.8284C11.5523 10.1046 12 9.10457 12 8C12 6.89543 11.5523 5.89543 10.8284 5.17157L8 8ZM8 8L10.8284 10.8284C10.1046 11.5523 9.10457 12 8 12C6.89543 12 5.89543 11.5523 5.17157 10.8284L8 8Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8.31882 0.281623C8.27211 -0.0938759 7.72788 -0.0938737 7.68116 0.281626L7.34298 2.99999H3V7.34299L0.281616 7.68117C-0.0938835 7.72788 -0.0938814 8.27212 0.281618 8.31883L3 8.65701V13H7.34298L7.68116 15.7184C7.72788 16.0939 8.27211 16.0939 8.31882 15.7184L8.657 13H13V8.65701L15.7184 8.31883C16.0939 8.27212 16.0939 7.72788 15.7184 7.68117L13 7.34299V2.99999H8.657L8.31882 0.281623ZM8.657 2.99999H7.34298L7.01638 5.62535L4.76772 4.31684C4.46917 4.08435 4.08434 4.46918 4.31683 4.76773L5.62534 7.01639L3 7.34299V8.65701L5.62534 8.98361L4.31683 11.2323C4.08434 11.5308 4.46917 11.9157 4.76772 11.6832L7.01638 10.3747L7.34298 13H8.657L8.98361 10.3747L11.2323 11.6832C11.5308 11.9157 11.9156 11.5308 11.6832 11.2323L10.3746 8.98361L13 8.65701V7.34299L10.3746 7.01639L11.6832 4.76773C11.9156 4.46918 11.5308 4.08435 11.2323 4.31684L8.98361 5.62535L8.657 2.99999Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M4.4 2C3.07452 2 2 3.07452 2 4.4V11.6C2 12.9255 3.07452 14 4.4 14H11.6C12.9255 14 14 12.9255 14 11.6V4.4C14 3.07452 12.9255 2 11.6 2H4.4ZM8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + { + d: 'M11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8Z', + }, + ], + [ + { + d: 'M2 3C5.31371 3 8 5.68629 8 9C8 5.68629 10.6863 3 14 3L14 9C14 12.3137 11.3137 15 8 15C4.68629 15 2 12.3137 2 9V3Z', + }, + ], + [ + { + d: 'M7.46226 0.379609C7.64222 -0.126455 8.3579 -0.126455 8.53787 0.379609L9.74754 3.78127L13.0082 2.2313C13.4933 2.00071 13.9994 2.50678 13.7688 2.99187L12.2189 6.25258L15.6205 7.46226C16.1266 7.64222 16.1266 8.3579 15.6205 8.53787L12.2189 9.74754L13.7688 13.0082C13.9994 13.4933 13.4933 13.9994 13.0082 13.7688L9.74754 12.2189L8.53787 15.6205C8.3579 16.1266 7.64222 16.1266 7.46226 15.6205L6.25258 12.2189L2.99187 13.7688C2.50678 13.9994 2.00071 13.4933 2.2313 13.0082L3.78127 9.74754L0.379609 8.53787C-0.126455 8.3579 -0.126455 7.64222 0.379609 7.46226L3.78127 6.25258L2.2313 2.99187C2.00071 2.50678 2.50678 2.00071 2.99187 2.2313L6.25258 3.78127L7.46226 0.379609Z', + }, + ], + [ + { + d: 'M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z', + }, + { + d: 'M16 8C16 12.4183 12.4183 16 8 16C3.58172 16 0 12.4183 0 8C0 3.58172 3.58172 0 8 0C12.4183 0 16 3.58172 16 8ZM8.67438 3.27934L12.7207 7.32562C13.0931 7.69807 13.0931 8.30193 12.7207 8.67438L8.67438 12.7207C8.30193 13.0931 7.69807 13.0931 7.32562 12.7207L3.27934 8.67438C2.90689 8.30193 2.90689 7.69807 3.27934 7.32562L7.32562 3.27934C7.69807 2.90689 8.30193 2.90689 8.67438 3.27934Z', + fillRule: 'evenodd', + clipRule: 'evenodd', + }, + ], + [ + { + d: 'M13.2323 2.31683C13.5308 2.08434 13.9156 2.46917 13.6832 2.76772L10.3747 7.01637L12.8899 7.68116C13.2654 7.72787 13.2654 8.2721 12.89 8.31882L10.3747 8.9836L13.6832 13.2323C13.9156 13.5308 13.5308 13.9156 13.2323 13.6832L8.98361 10.3746L8.31883 12.8899C8.27212 13.2654 7.72788 13.2654 7.68117 12.8899L7.01639 10.3746L2.76773 13.6832C2.46918 13.9156 2.08435 13.5308 2.31684 13.2323L5.62535 8.9836L3.11005 8.31882C2.73455 8.27211 2.73455 7.72787 3.11005 7.68116L5.62535 7.01637L2.31684 2.76772C2.08435 2.46917 2.46918 2.08434 2.76773 2.31682L7.01639 5.62533L7.68117 3.11004C7.72788 2.73454 8.27212 2.73454 8.31883 3.11004L8.98361 5.62533L13.2323 2.31683Z', + }, + ], + [ + { + d: 'M7.68116 0.281626C7.72788 -0.0938737 8.27211 -0.0938759 8.31882 0.281623L8.98361 5.62535L11.2323 4.31684C11.5308 4.08435 11.9156 4.46918 11.6832 4.76773L10.3746 7.01639L15.7184 7.68117C16.0939 7.72788 16.0939 8.27212 15.7184 8.31883L10.3746 8.98361L11.6832 11.2323C11.9156 11.5308 11.5308 11.9157 11.2323 11.6832L8.98361 10.3747L8.31882 15.7184C8.27211 16.0939 7.72788 16.0939 7.68116 15.7184L7.01638 10.3747L4.76772 11.6832C4.46917 11.9157 4.08434 11.5308 4.31683 11.2323L5.62534 8.98361L0.281618 8.31883C-0.0938814 8.27212 -0.0938835 7.72788 0.281616 7.68117L5.62534 7.01639L4.31683 4.76773C4.08434 4.46918 4.46917 4.08435 4.76772 4.31684L7.01638 5.62535L7.68116 0.281626Z', + }, + ], + [ + { + d: 'M14 13C10.6863 13 8 10.3137 8 7C8 10.3137 5.31371 13 2 13L2 7C2 3.68629 4.68629 0.999999 8 1C11.3137 0.999999 14 3.68629 14 7L14 13Z', + }, + ], +] diff --git a/apps/web/src/components/Unicon/index.tsx b/apps/web/src/components/Unicon/index.tsx new file mode 100644 index 0000000..60ec6e6 --- /dev/null +++ b/apps/web/src/components/Unicon/index.tsx @@ -0,0 +1,162 @@ +import React, { memo, useMemo } from 'react' +import { useIsDarkMode } from 'theme/components/ThemeToggle' + +import { blurs, UniconAttributeData, UniconAttributes, UniconAttributesToIndices } from './types' +import { deriveUniconAttributeIndices, getUniconAttributeData, isEthAddress } from './utils' + +const ORIGINAL_CONTAINER_SIZE = 36 +const EMBLEM_XY_SHIFT = 10 + +function PathMask({ + id, + paths, + scale, + shift = 0, +}: { + id: string + paths: React.SVGProps[] + scale: number + shift?: number +}) { + return ( + + + + {paths.map((pathProps) => ( + + ))} + + + ) +} + +type UniconMaskProps = { maskId: string; attributeData: UniconAttributeData; size: number } +function UniconMask({ maskId, attributeData, size }: UniconMaskProps) { + const shapeMaskId = `shape-${maskId}` + const containerMaskId = `container-${maskId}` + + return ( + + + + + + + + {attributeData[UniconAttributes.Container].map((pathProps) => ( + + ))} + + + + + {attributeData[UniconAttributes.Shape].map((pathProps) => ( + + ))} + + + + + + ) +} + +type UniconGradientProps = { gradientId: string; attributeData: UniconAttributeData } +function UniconGradient({ gradientId, attributeData }: UniconGradientProps) { + return ( + + + + + ) +} + +function UniconBlur({ blurId, size }: { blurId: string; size: number }) { + return ( + + + + ) +} + +function UniconSvg({ + attributeIndices, + size, + address, +}: { + attributeIndices: UniconAttributesToIndices + size: number + address: string + mobile?: boolean +}) { + const isDarkMode = useIsDarkMode() + const attributeData = useMemo(() => getUniconAttributeData(attributeIndices), [attributeIndices]) + + const gradientId = `gradient${address + size}` + const maskId = `mask${address + size}` + const blurId = `blur${address + size}` + const svgProps = { + viewBox: `0 0 ${size} ${size}`, + } + + if (!attributeIndices || !attributeData) return null + + return ( + + + + + + + + + + {!isDarkMode && } + + + + ) +} + +interface Props { + address: string + size?: number + randomSeed?: number + border?: boolean + mobile?: boolean +} + +function _Unicon({ address, size = 24, randomSeed = 0, mobile }: Props) { + const attributeIndices = useMemo(() => deriveUniconAttributeIndices(address, randomSeed), [address, randomSeed]) + + if (!address || !isEthAddress(address) || !attributeIndices) return null + + return ( +
+
+ +
+
+ ) +} + +export const Unicon = memo(_Unicon) diff --git a/apps/web/src/components/Unicon/types.ts b/apps/web/src/components/Unicon/types.ts new file mode 100644 index 0000000..749ed5c --- /dev/null +++ b/apps/web/src/components/Unicon/types.ts @@ -0,0 +1,76 @@ +import { svgPaths as containerPaths } from './Container' +import { svgPaths as emblemPaths } from './Emblem' + +export enum UniconAttributes { + GradientStart = 0, + GradientEnd = 1, + Container = 2, + Shape = 3, +} + +export const UniconAttributesArray: UniconAttributes[] = [ + UniconAttributes.GradientStart, + UniconAttributes.GradientEnd, + UniconAttributes.Container, + UniconAttributes.Shape, +] + +export interface UniconAttributesToIndices { + [UniconAttributes.GradientStart]: number + [UniconAttributes.GradientEnd]: number + [UniconAttributes.Container]: number + [UniconAttributes.Shape]: number +} + +export interface UniconAttributeData { + [UniconAttributes.GradientStart]: string + [UniconAttributes.GradientEnd]: string + [UniconAttributes.Container]: React.SVGProps[] + [UniconAttributes.Shape]: React.SVGProps[] +} + +export const gradientStarts = [ + '#6100FF', + '#5065FD', + '#36DBFF', + '#5CFE9D', + '#B1F13C', + '#F9F40B', + '#FF6F1E', + '#F14544', + '#FC72FF', + '#C0C0C0', +] + +export const blurs = [ + '#D3EBA3', + '#F06DF3', + '#9D99F5', + '#EDE590', + '#B0EDFE', + '#FBAA7F', + '#C8BB9B', + '#9D99F5', + '#A26AF3', + '#D3EBA3', +] + +export const gradientEnds = [ + '#D0B2F3', + '#BDB8FA', + '#63CDE8', + '#76D191', + '#9BCD46', + '#EDE590', + '#FBAA7F', + '#FEA79B', + '#F5A1F5', + '#B8C3B7', +] + +export const UniconNumOptions = { + [UniconAttributes.GradientStart]: gradientStarts.length, + [UniconAttributes.GradientEnd]: gradientEnds.length, + [UniconAttributes.Container]: containerPaths.length, + [UniconAttributes.Shape]: emblemPaths.length, +} diff --git a/apps/web/src/components/Unicon/utils.ts b/apps/web/src/components/Unicon/utils.ts new file mode 100644 index 0000000..2ff7bf9 --- /dev/null +++ b/apps/web/src/components/Unicon/utils.ts @@ -0,0 +1,50 @@ +import { isAddress } from 'ethers/lib/utils' + +import { svgPaths as containerPaths } from './Container' +import { svgPaths as emblemPaths } from './Emblem' +import { + gradientEnds, + gradientStarts, + UniconAttributeData, + UniconAttributes, + UniconAttributesArray, + UniconAttributesToIndices, + UniconNumOptions, +} from './types' + +const NUM_CHARS_TO_USE_PER_ATTRIBUTE = 2 + +export const isEthAddress = (address: string) => { + return address.startsWith('0x') && isAddress(address.toLowerCase()) +} + +export const deriveUniconAttributeIndices = ( + address: string, + randomSeed = 0 +): UniconAttributesToIndices | undefined => { + if (!isEthAddress(address)) return + + const hexAddr = address.slice(-40) + const newIndices = { + [UniconAttributes.GradientStart]: 0, + [UniconAttributes.GradientEnd]: 0, + [UniconAttributes.Container]: 0, + [UniconAttributes.Shape]: 0, + } as UniconAttributesToIndices + for (const a of UniconAttributesArray) { + const optionHex = hexAddr.slice(NUM_CHARS_TO_USE_PER_ATTRIBUTE * a, NUM_CHARS_TO_USE_PER_ATTRIBUTE * (a + 1)) + const optionDec = parseInt(optionHex, 16) + randomSeed + const optionIndex = optionDec % UniconNumOptions[a] + newIndices[a] = optionIndex + } + return newIndices +} + +export const getUniconAttributeData = (attributeIndices: UniconAttributesToIndices): UniconAttributeData => { + return { + [UniconAttributes.GradientStart]: gradientStarts[attributeIndices[UniconAttributes.GradientStart]], + [UniconAttributes.GradientEnd]: gradientEnds[attributeIndices[UniconAttributes.GradientEnd]], + [UniconAttributes.Container]: containerPaths[attributeIndices[UniconAttributes.Container]], + [UniconAttributes.Shape]: emblemPaths[attributeIndices[UniconAttributes.Shape]], + } as UniconAttributeData +} diff --git a/apps/web/src/components/V2Unsupported/index.tsx b/apps/web/src/components/V2Unsupported/index.tsx new file mode 100644 index 0000000..aa5280c --- /dev/null +++ b/apps/web/src/components/V2Unsupported/index.tsx @@ -0,0 +1,28 @@ +import { Trans } from '@lingui/macro' +import { AutoColumn } from 'components/Column' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' + +const TextWrapper = styled.div` + border: 1px solid ${({ theme }) => theme.neutral3}; + padding: 16px 12px; + border-radius: 12px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +` + +export function V2Unsupported() { + return ( + + + + + Uniswap V2 is not available on this network. + + + + + ) +} diff --git a/apps/web/src/components/WalletModal/ConnectionErrorView.tsx b/apps/web/src/components/WalletModal/ConnectionErrorView.tsx new file mode 100644 index 0000000..cec5047 --- /dev/null +++ b/apps/web/src/components/WalletModal/ConnectionErrorView.tsx @@ -0,0 +1,55 @@ +import { Trans } from '@lingui/macro' +import { useCloseAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { ButtonEmpty, ButtonPrimary } from 'components/Button' +import { ActivationStatus, useActivationState } from 'connection/activate' +import { AlertTriangle } from 'react-feather' +import styled from 'styled-components' +import { ThemedText } from 'theme/components' +import { flexColumnNoWrap } from 'theme/styles' + +const Wrapper = styled.div` + ${flexColumnNoWrap}; + align-items: center; + justify-content: center; + width: 100%; +` + +const AlertTriangleIcon = styled(AlertTriangle)` + width: 90px; + height: 90px; + stroke-width: 1; + margin: 36px; + color: ${({ theme }) => theme.critical}; +` + +// TODO(cartcrom): move this to a top level modal, rather than inline in the drawer +export default function ConnectionErrorView() { + const { activationState, tryActivation, cancelActivation } = useActivationState() + const closeDrawer = useCloseAccountDrawer() + + if (activationState.status !== ActivationStatus.ERROR) return null + + const retry = () => tryActivation(activationState.connection, closeDrawer) + + return ( + + + + Error connecting + + + + The connection attempt failed. Please click try again and follow the steps to connect in your wallet. + + + + Try again + + + + Back to wallet selection + + + + ) +} diff --git a/apps/web/src/components/WalletModal/Option.test.tsx b/apps/web/src/components/WalletModal/Option.test.tsx new file mode 100644 index 0000000..27e00d3 --- /dev/null +++ b/apps/web/src/components/WalletModal/Option.test.tsx @@ -0,0 +1,81 @@ +import { Connector } from '@web3-react/types' +import UNIWALLET_ICON from 'assets/images/uniwallet.png' +import { useToggleAccountDrawer } from 'components/AccountDrawer/MiniPortfolio/hooks' +import { Connection, ConnectionType } from 'connection/types' +import { mocked } from 'test-utils/mocked' +import { createDeferredPromise } from 'test-utils/promise' +import { act, render } from 'test-utils/render' + +import Option from './Option' + +const mockToggleDrawer = jest.fn() +jest.mock('components/AccountDrawer/MiniPortfolio/hooks') + +beforeEach(() => { + jest.spyOn(console, 'debug').mockReturnValue() + mocked(useToggleAccountDrawer).mockReturnValue(mockToggleDrawer) +}) + +const mockConnection1: Connection = { + getProviderInfo: () => ({ name: 'Mock Connection 1', icon: UNIWALLET_ICON }), + connector: { + activate: jest.fn(), + deactivate: jest.fn(), + } as unknown as Connector, + type: ConnectionType.UNISWAP_WALLET_V2, +} as unknown as Connection + +const mockConnection2: Connection = { + getProviderInfo: () => ({ name: 'Mock Connection 2', icon: UNIWALLET_ICON }), + connector: { + activate: jest.fn(), + deactivate: jest.fn(), + } as unknown as Connector, + type: ConnectionType.INJECTED, +} as unknown as Connection + +describe('Wallet Option', () => { + it('renders default state', () => { + const component = render(
+ ) +} diff --git a/apps/web/src/pages/Landing/LandingV2.tsx b/apps/web/src/pages/Landing/LandingV2.tsx new file mode 100644 index 0000000..1c79b83 --- /dev/null +++ b/apps/web/src/pages/Landing/LandingV2.tsx @@ -0,0 +1,64 @@ +import { memo, useRef } from 'react' +import styled from 'styled-components' + +import { DirectToDefi } from './sections/DirectToDefi' +import { Footer } from './sections/Footer' +import { Hero } from './sections/Hero' +import { NewsletterEtc } from './sections/NewsletterEtc' +import { Stats } from './sections/Stats' + +const Container = styled.div` + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 120px; + @media (max-width: 1024px) { + gap: 80px; + } + margin-top: -72px; + min-width: 100%; + max-width: 1280px; + z-index: 1; +` + +const Grain = styled.div` + display: flex; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url(/images/noise-color.png); + opacity: 0.018; + z-index: 0; +` + +function LandingV2({ transition }: { transition?: boolean }) { + const scrollAnchor = useRef(null) + const scrollToRef = () => { + if (scrollAnchor.current) { + window.scrollTo({ + top: scrollAnchor.current.offsetTop - 120, + behavior: 'smooth', + }) + } + } + + return ( + <> + + + +
+ +
+ + +