diff --git a/.env.example b/.env.example index 3538fb224..097322764 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,9 @@ VITE_XAHAU_TESTNET_LINK= VITE_CUSTOMNETWORK_LINK= VITE_VALIDATOR=vl.ripple.com +#External data source - XRPLMeta node for token search results +XRPL_META_URL=s1.xrplmeta.org + #XRPL Environment: mainnet, testnet, devnet, amm, hooks_testnet, custom VITE_ENVIRONMENT=mainnet diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 56af795c6..aaf9a0a1e 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -5,7 +5,7 @@ name: Node.js CI on: push: - branches: [ main, staging ] + branches: [main, staging] pull_request: workflow_dispatch: @@ -18,15 +18,15 @@ jobs: node-version: [18.12] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache node modules id: cache-nodemodules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -51,15 +51,15 @@ jobs: node-version: [18.12] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache node modules id: cache-nodemodules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -91,15 +91,15 @@ jobs: node-version: [18.12] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache node modules id: cache-nodemodules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: @@ -124,15 +124,15 @@ jobs: node-version: [18.12] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Cache node modules id: cache-nodemodules - uses: actions/cache@v3 + uses: actions/cache@v4 env: cache-name: cache-node-modules with: diff --git a/package-lock.json b/package-lock.json index 0c886fb87..482783fdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@xrplf/isomorphic": "^1.0.0-beta.1", "@xrplf/prettier-config": "^1.9.1", "assert": "^2.1.0", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.20", "axios": "^1.6.5", "body-parser": "^1.20.2", "bunyan": "^1.8.15", @@ -56,7 +56,7 @@ "usehooks-ts": "^3.1.0", "vite": "^5.4.8", "vite-plugin-environment": "^1.1.3", - "vite-plugin-html": "^3.2.0", + "vite-plugin-html": "^3.2.2", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.2.0", "xrpl-client": "^2.4.0" @@ -84,7 +84,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.2", @@ -100,7 +100,7 @@ "react-error-overlay": "6.0.11", "react-test-renderer": "^17.0.2", "redux-mock-store": "^1.5.1", - "sass": "^1.76.0", + "sass": "^1.80.5", "source-map-explorer": "^2.5.3", "stylelint": "^15.11.0", "stylelint-config-idiomatic-order": "^10.0.0", @@ -5428,6 +5428,337 @@ "node": ">= 8" } }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "devOptional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "devOptional": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@parcel/watcher/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@parcel/watcher/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "devOptional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/@parcel/watcher/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/@paystring/utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@paystring/utils/-/utils-2.0.0.tgz", @@ -5723,6 +6054,12 @@ "win32" ] }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "node_modules/@rushstack/eslint-patch": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz", @@ -7447,16 +7784,17 @@ } }, "node_modules/array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7642,9 +7980,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "funding": [ { "type": "opencollective", @@ -7660,11 +7998,11 @@ } ], "dependencies": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -8539,7 +8877,7 @@ }, "node_modules/binary-extensions": { "version": "2.2.0", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8995,7 +9333,7 @@ }, "node_modules/chokidar": { "version": "3.5.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -9015,7 +9353,7 @@ }, "node_modules/chokidar/node_modules/anymatch": { "version": "3.1.2", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -9027,7 +9365,7 @@ }, "node_modules/chokidar/node_modules/braces": { "version": "3.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.0.1" @@ -9038,7 +9376,7 @@ }, "node_modules/chokidar/node_modules/fill-range": { "version": "7.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -9061,7 +9399,7 @@ }, "node_modules/chokidar/node_modules/is-number": { "version": "7.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -9069,7 +9407,7 @@ }, "node_modules/chokidar/node_modules/normalize-path": { "version": "3.0.0", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -9077,7 +9415,7 @@ }, "node_modules/chokidar/node_modules/to-regex-range": { "version": "5.0.1", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -10350,7 +10688,7 @@ }, "node_modules/detect-libc": { "version": "1.0.3", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" @@ -11450,9 +11788,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -11494,34 +11832,36 @@ } }, "node_modules/eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "dependencies": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "node_modules/eslint-plugin-import/node_modules/debug": { @@ -14116,7 +14456,7 @@ }, "node_modules/is-binary-path": { "version": "2.1.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -14166,12 +14506,15 @@ } }, "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -22388,6 +22731,12 @@ "tslib": "^2.0.3" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "devOptional": true + }, "node_modules/node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -22796,15 +23145,17 @@ } }, "node_modules/object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/object.hasown": { @@ -23864,7 +24215,7 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -24578,12 +24929,13 @@ } }, "node_modules/sass": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz", - "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==", + "version": "1.80.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.5.tgz", + "integrity": "sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==", "devOptional": true, "dependencies": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, @@ -24594,6 +24946,34 @@ "node": ">=14.0.0" } }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/sax": { "version": "1.2.4", "dev": true, @@ -27648,9 +28028,9 @@ } }, "node_modules/vite-plugin-html": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.0.tgz", - "integrity": "sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", + "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==", "dependencies": { "@rollup/pluginutils": "^4.2.0", "colorette": "^2.0.16", @@ -32163,6 +32543,159 @@ "fastq": "^1.6.0" } }, + "@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "devOptional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "requires": { + "fill-range": "^7.1.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "devOptional": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "devOptional": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "dev": true, + "optional": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "dev": true, + "optional": true + }, "@paystring/utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@paystring/utils/-/utils-2.0.0.tgz", @@ -32322,6 +32855,12 @@ "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", "optional": true }, + "@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true + }, "@rushstack/eslint-patch": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.3.0.tgz", @@ -33493,16 +34032,17 @@ } }, "array.prototype.findlastindex": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", - "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" } }, "array.prototype.flat": { @@ -33638,15 +34178,15 @@ "dev": true }, "autoprefixer": { - "version": "10.4.17", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz", - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "requires": { - "browserslist": "^4.22.2", - "caniuse-lite": "^1.0.30001578", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" } }, @@ -34290,7 +34830,7 @@ }, "binary-extensions": { "version": "2.2.0", - "devOptional": true + "dev": true }, "bindings": { "version": "1.5.0", @@ -34591,7 +35131,7 @@ }, "chokidar": { "version": "3.5.2", - "devOptional": true, + "dev": true, "requires": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -34605,7 +35145,7 @@ "dependencies": { "anymatch": { "version": "3.1.2", - "devOptional": true, + "dev": true, "requires": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -34613,14 +35153,14 @@ }, "braces": { "version": "3.0.2", - "devOptional": true, + "dev": true, "requires": { "fill-range": "^7.0.1" } }, "fill-range": { "version": "7.0.1", - "devOptional": true, + "dev": true, "requires": { "to-regex-range": "^5.0.1" } @@ -34632,15 +35172,15 @@ }, "is-number": { "version": "7.0.0", - "devOptional": true + "dev": true }, "normalize-path": { "version": "3.0.0", - "devOptional": true + "dev": true }, "to-regex-range": { "version": "5.0.1", - "devOptional": true, + "dev": true, "requires": { "is-number": "^7.0.0" } @@ -35545,7 +36085,7 @@ }, "detect-libc": { "version": "1.0.3", - "dev": true + "devOptional": true }, "detect-newline": { "version": "3.1.0", @@ -36535,9 +37075,9 @@ } }, "eslint-module-utils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", - "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.0.tgz", + "integrity": "sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==", "dev": true, "requires": { "debug": "^3.2.7" @@ -36565,27 +37105,29 @@ } }, "eslint-plugin-import": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", - "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "version": "2.31.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz", + "integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==", "dev": true, "requires": { - "array-includes": "^3.1.7", - "array.prototype.findlastindex": "^1.2.3", + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.8", + "array.prototype.findlastindex": "^1.2.5", "array.prototype.flat": "^1.3.2", "array.prototype.flatmap": "^1.3.2", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", - "eslint-module-utils": "^2.8.0", - "hasown": "^2.0.0", - "is-core-module": "^2.13.1", + "eslint-module-utils": "^2.12.0", + "hasown": "^2.0.2", + "is-core-module": "^2.15.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", - "object.fromentries": "^2.0.7", - "object.groupby": "^1.0.1", - "object.values": "^1.1.7", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.0", "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.8", "tsconfig-paths": "^3.15.0" }, "dependencies": { @@ -38117,7 +38659,7 @@ }, "is-binary-path": { "version": "2.1.0", - "devOptional": true, + "dev": true, "requires": { "binary-extensions": "^2.0.0" } @@ -38144,12 +38686,12 @@ } }, "is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "requires": { - "hasown": "^2.0.0" + "hasown": "^2.0.2" } }, "is-data-descriptor": { @@ -43834,6 +44376,12 @@ "tslib": "^2.0.3" } }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "devOptional": true + }, "node-fetch": { "version": "2.6.12", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", @@ -44107,15 +44655,14 @@ } }, "object.groupby": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", - "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" } }, "object.hasown": { @@ -44770,7 +45317,7 @@ }, "readdirp": { "version": "3.6.0", - "devOptional": true, + "dev": true, "requires": { "picomatch": "^2.2.1" } @@ -45260,14 +45807,32 @@ } }, "sass": { - "version": "1.76.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.76.0.tgz", - "integrity": "sha512-nc3LeqvF2FNW5xGF1zxZifdW3ffIz5aBb7I7tSvOoNu7z1RQ6pFt9MBuiPtjgaI62YWrM/txjWlOCFiGtf2xpw==", + "version": "1.80.5", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.5.tgz", + "integrity": "sha512-TQd2aoQl/+zsxRMEDSxVdpPIqeq9UFc6pr7PzkugiTx3VYCFPUaa3P4RrBQsqok4PO200Vkz0vXQBNlg7W907g==", "devOptional": true, "requires": { - "chokidar": ">=3.0.0 <4.0.0", + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" + }, + "dependencies": { + "chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "devOptional": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "devOptional": true + } } }, "sax": { @@ -47510,9 +48075,9 @@ "requires": {} }, "vite-plugin-html": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.0.tgz", - "integrity": "sha512-2VLCeDiHmV/BqqNn5h2V+4280KRgQzCFN47cst3WiNK848klESPQnzuC3okH5XHtgwHH/6s1Ho/YV6yIO0pgoQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/vite-plugin-html/-/vite-plugin-html-3.2.2.tgz", + "integrity": "sha512-vb9C9kcdzcIo/Oc3CLZVS03dL5pDlOFuhGlZYDCJ840BhWl/0nGeZWf3Qy7NlOayscY4Cm/QRgULCQkEZige5Q==", "requires": { "@rollup/pluginutils": "^4.2.0", "colorette": "^2.0.16", diff --git a/package.json b/package.json index f04822efe..f042929bf 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@xrplf/isomorphic": "^1.0.0-beta.1", "@xrplf/prettier-config": "^1.9.1", "assert": "^2.1.0", - "autoprefixer": "^10.4.17", + "autoprefixer": "^10.4.20", "axios": "^1.6.5", "body-parser": "^1.20.2", "bunyan": "^1.8.15", @@ -51,7 +51,7 @@ "usehooks-ts": "^3.1.0", "vite": "^5.4.8", "vite-plugin-environment": "^1.1.3", - "vite-plugin-html": "^3.2.0", + "vite-plugin-html": "^3.2.2", "vite-plugin-svgr": "^4.2.0", "vite-tsconfig-paths": "^4.2.0", "xrpl-client": "^2.4.0" @@ -79,7 +79,7 @@ "eslint-config-prettier": "^9.1.0", "eslint-config-react-app": "^7.0.1", "eslint-plugin-flowtype": "^8.0.3", - "eslint-plugin-import": "^2.29.1", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.9.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-react": "^7.34.2", @@ -95,7 +95,7 @@ "react-error-overlay": "6.0.11", "react-test-renderer": "^17.0.2", "redux-mock-store": "^1.5.1", - "sass": "^1.76.0", + "sass": "^1.80.5", "source-map-explorer": "^2.5.3", "stylelint": "^15.11.0", "stylelint-config-idiomatic-order": "^10.0.0", diff --git a/public/locales/ca-CA/translations.json b/public/locales/ca-CA/translations.json index a39916bf5..02d1367b3 100644 --- a/public/locales/ca-CA/translations.json +++ b/public/locales/ca-CA/translations.json @@ -531,6 +531,9 @@ "asset_class": null, "trading_pairs": null, "deleted": null, + "holders": null, + "trustlines": null, + "website": null, "assets.mpt_tab_title": null, "assets.no_mpts_message": null, "transaction_type_name_MPTokenIssuanceCreate": null, @@ -553,5 +556,9 @@ "can_escrow": null, "can_trade": null, "can_transfer": null, - "can_clawback": null + "can_clawback": null, + "search_results_banner": null, + "enable_amendment_name": null, + "amendment_status": null, + "expected_date": null } diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json index 58453bb65..83fb30d98 100644 --- a/public/locales/en-US/translations.json +++ b/public/locales/en-US/translations.json @@ -21,7 +21,7 @@ "explorer": "Explorer", "xrpl_org": "XRPL.org", "github": "GitHub", - "header.search.placeholder": "Search by Address, Ledger or Txn", + "header.search.placeholder": "Search by Token, Address, Ledger or Txn", "xrp": "XRP", "xrpl_explorer": "XRPL Explorer", "ledgers": "Ledgers", @@ -540,6 +540,9 @@ "asset_class": "Asset Class", "trading_pairs": "Trading Pairs", "deleted": "Deleted", + "holders": "HOLDERS: {{holders}}", + "trustlines": " TRUSTLINES: {{trustlines}}", + "website": "Wesbite", "mpt_issuance_id": "MPT Issuance ID", "asset_scale": "Asset Scale", "metadata": "Metadata", @@ -553,5 +556,10 @@ "can_escrow": "Can Escrow", "can_trade": "Can Trade", "can_transfer": "Can Transfer", - "can_clawback": "Can Clawback" + "can_clawback": "Can Clawback", + "search_results_banner": "Token search by name and account is now available! Try searching for USD", + "enable_amendment_name": "Amendment Name", + "amendment_status": "Amendment Status", + "expected_date": "Expected Date" + } diff --git a/public/locales/es-ES/translations.json b/public/locales/es-ES/translations.json index d694f1db3..17965bd0a 100644 --- a/public/locales/es-ES/translations.json +++ b/public/locales/es-ES/translations.json @@ -527,6 +527,9 @@ "asset_class": null, "trading_pairs": null, "deleted": null, + "holders": null, + "trustlines": null, + "website": null, "assets.mpt_tab_title": null, "assets.no_mpts_message": null, "transaction_type_name_MPTokenIssuanceCreate": null, @@ -549,5 +552,9 @@ "can_escrow": null, "can_trade": null, "can_transfer": null, - "can_clawback": null + "can_clawback": null, + "search_results_banner": null, + "enable_amendment_name": null, + "amendment_status": null, + "expected_date": null } diff --git a/public/locales/fr-FR/translations.json b/public/locales/fr-FR/translations.json index d6683c855..33ac67e2d 100644 --- a/public/locales/fr-FR/translations.json +++ b/public/locales/fr-FR/translations.json @@ -528,6 +528,9 @@ "asset_class": null, "trading_pairs": null, "deleted": null, + "holders": null, + "trustlines": null, + "website": null, "assets.mpt_tab_title": null, "assets.no_mpts_message": null, "transaction_type_name_MPTokenIssuanceCreate": null, @@ -550,5 +553,9 @@ "can_escrow": null, "can_trade": null, "can_transfer": null, - "can_clawback": null + "can_clawback": null, + "search_results_banner": null, + "enable_amendment_name": null, + "amendment_status": null, + "expected_date": null } diff --git a/public/locales/ja-JP/translations.json b/public/locales/ja-JP/translations.json index 9ed8133ee..e5b667cac 100644 --- a/public/locales/ja-JP/translations.json +++ b/public/locales/ja-JP/translations.json @@ -527,6 +527,9 @@ "asset_class": null, "trading_pairs": null, "deleted": null, + "holders": null, + "trustlines": null, + "website": null, "assets.mpt_tab_title": null, "assets.no_mpts_message": null, "transaction_type_name_MPTokenIssuanceCreate": null, @@ -549,5 +552,9 @@ "can_escrow": null, "can_trade": null, "can_transfer": null, - "can_clawback": null + "can_clawback": null, + "search_results_banner": null, + "enable_amendment_name": null, + "amendment_status": null, + "expected_date": null } diff --git a/public/locales/ko-KR/translations.json b/public/locales/ko-KR/translations.json index 148714cd5..0f5406903 100644 --- a/public/locales/ko-KR/translations.json +++ b/public/locales/ko-KR/translations.json @@ -525,6 +525,9 @@ "asset_class": null, "trading_pairs": null, "deleted": null, + "holders": null, + "trustlines": null, + "website": null, "assets.mpt_tab_title": null, "assets.no_mpts_message": null, "transaction_type_name_MPTokenIssuanceCreate": null, @@ -547,5 +550,9 @@ "can_escrow": null, "can_trade": null, "can_transfer": null, - "can_clawback": null + "can_clawback": null, + "search_results_banner": null, + "enable_amendment_name": null, + "amendment_status": null, + "expected_date": null } diff --git a/server/lib/rippled.js b/server/lib/rippled.js index 7e90d8c27..7d1c27467 100644 --- a/server/lib/rippled.js +++ b/server/lib/rippled.js @@ -4,12 +4,14 @@ const utils = require('./utils') const streams = require('./streams') const RIPPLEDS = [] -process.env.VITE_RIPPLED_HOST?.split(',').forEach((d) => { - const rippled = d.split(':') - RIPPLEDS.push( - `wss://${rippled[0]}:${rippled[1] || process.env.VITE_RIPPLED_WS_PORT}`, - `wss://${rippled[0]}`, - ) +process.env.VITE_RIPPLED_HOST?.split(',').forEach((host) => { + if (host?.includes(':')) { + RIPPLEDS.push(`wss://${host}`) + } else if (process.env.VITE_RIPPLED_WS_PORT) { + RIPPLEDS.push(`wss://${host}:${process.env.VITE_RIPPLED_WS_PORT}`) + } else { + RIPPLEDS.push(`wss://${host}`) + } }) const RIPPLED_CLIENT = new XrplClient(RIPPLEDS, { tryAllNodes: true }) diff --git a/server/routes/v1/index.js b/server/routes/v1/index.js index 41fe59622..70428e99c 100644 --- a/server/routes/v1/index.js +++ b/server/routes/v1/index.js @@ -2,6 +2,7 @@ const api = require('express').Router() const getTokenDiscovery = require('./tokenDiscovery') const getHealth = require('./health') const getCurrentMetrics = require('./currentMetrics') +const getTokensSearch = require('./tokens') if (process.env.VITE_ENVIRONMENT === 'mainnet') { api.use('/token/top', getTokenDiscovery) @@ -13,6 +14,7 @@ if (process.env.VITE_ENVIRONMENT !== 'custom') { // these require a single hardcoded rippled node to connect to api.use('/health', getHealth) api.use('/metrics', getCurrentMetrics) + api.use('/tokens/search/:query', getTokensSearch) } module.exports = api diff --git a/server/routes/v1/tokens.js b/server/routes/v1/tokens.js new file mode 100644 index 000000000..0fcadbe09 --- /dev/null +++ b/server/routes/v1/tokens.js @@ -0,0 +1,125 @@ +const axios = require('axios') +const log = require('../../lib/logger')({ name: 'tokens search' }) + +const REFETCH_INTERVAL = 60 * 60 * 1000 // 1 hour +const XRPLMETA_QUERY_LIMIT = 1000 +const cachedTokenSearchList = { tokens: [], last_updated: null } + +const parseCurrency = (currency) => { + const NON_STANDARD_CODE_LENGTH = 40 + const LP_TOKEN_IDENTIFIER = '03' + + const hexToString = (hex) => { + let string = '' + for (let i = 0; i < hex.length; i += 2) { + const part = hex.substring(i, i + 2) + const code = parseInt(part, 16) + if (!isNaN(code) && code !== 0) { + string += String.fromCharCode(code) + } + } + return string + } + + return currency.length === NON_STANDARD_CODE_LENGTH && + currency?.substring(0, 2) !== LP_TOKEN_IDENTIFIER + ? hexToString(currency) + : currency +} + +async function fetchXRPLMetaTokens(offset) { + log.info(`caching tokens from ${process.env.XRPL_META_URL}`) + return axios + .get( + `https://${process.env.XRPL_META_URL}/tokens?trust_level=1&trust_level=2&trust_level=3`, + { + params: { + sort_by: 'holders', + offset, + limit: XRPLMETA_QUERY_LIMIT, + }, + }, + ) + .then((resp) => resp.data) + .catch((e) => { + log.error(e) + return { count: 0 } + }) +} + +async function cacheXRPLMetaTokens() { + let offset = 0 + let tokensDataBatch = {} + const allTokensFetched = [] + + tokensDataBatch = await fetchXRPLMetaTokens(0) + const { count } = tokensDataBatch + while (offset < count) { + allTokensFetched.push(...tokensDataBatch.tokens) + offset += XRPLMETA_QUERY_LIMIT + // eslint-disable-next-line no-await-in-loop + tokensDataBatch = await fetchXRPLMetaTokens(offset) + } + + cachedTokenSearchList.tokens = allTokensFetched.filter( + (result) => + (result.metrics.trustlines > 50 && + result.metrics.holders > 50 && + result.metrics.marketcap > 0 && + result.metrics.volume_7d > 0) || + result.meta.issuer.trust_level === 3, + ) + cachedTokenSearchList.last_updated = Date.now() + + // nonstandard from XRPLMeta, check for hex codes in currencies and store parsed + cachedTokenSearchList.tokens.map((token) => ({ + ...token, + currency: parseCurrency(token.currency), + })) +} + +function startCaching() { + if (process.env.VITE_ENVIRONMENT !== 'mainnet') { + return + } + cacheXRPLMetaTokens() + setInterval(() => cacheXRPLMetaTokens(), REFETCH_INTERVAL) +} + +startCaching() + +function queryTokens(tokenList, query) { + const sanitizedQuery = query.toLowerCase() + + return tokenList.filter( + (token) => + token.currency?.toLowerCase().includes(sanitizedQuery) || + token.meta?.token?.name?.toLowerCase().includes(sanitizedQuery) || + token.meta?.issuer?.name?.toLowerCase().includes(sanitizedQuery) || + token.issuer?.toLowerCase().startsWith(sanitizedQuery), + ) +} + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +module.exports = async (req, res) => { + try { + log.info('getting tokens list for search') + const { query } = req.params + while (cachedTokenSearchList.tokens.length === 0) { + // eslint-disable-next-line no-await-in-loop -- necessary here to wait for cache to be filled + await sleep(1000) + } + const queriedTokens = await queryTokens(cachedTokenSearchList.tokens, query) + return res.status(200).json({ + result: 'success', + updated: cachedTokenSearchList.last_updated, + tokens: queriedTokens, + }) + } catch (error) { + log.error(error) + return res.status(error.code || 500).json({ message: error.message }) + } +} diff --git a/src/containers/Accounts/AccountHeader/BalanceSelector/balance-selector.scss b/src/containers/Accounts/AccountHeader/BalanceSelector/balance-selector.scss index dc6c77018..30860340e 100644 --- a/src/containers/Accounts/AccountHeader/BalanceSelector/balance-selector.scss +++ b/src/containers/Accounts/AccountHeader/BalanceSelector/balance-selector.scss @@ -1,4 +1,4 @@ -@import 'src/containers/shared/css/variables'; +@use 'src/containers/shared/css/variables' as *; .balance-selector { position: relative; diff --git a/src/containers/Accounts/AccountHeader/styles.scss b/src/containers/Accounts/AccountHeader/styles.scss index fc35853f0..d76a9033d 100644 --- a/src/containers/Accounts/AccountHeader/styles.scss +++ b/src/containers/Accounts/AccountHeader/styles.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .account-header { margin-bottom: 16px; diff --git a/src/containers/Accounts/styles.scss b/src/containers/Accounts/styles.scss index 3296b7a74..1b68962d2 100644 --- a/src/containers/Accounts/styles.scss +++ b/src/containers/Accounts/styles.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables'; .accounts-page { .loader { diff --git a/src/containers/Amendment/amendment.scss b/src/containers/Amendment/amendment.scss index 987963d30..2dd439d43 100644 --- a/src/containers/Amendment/amendment.scss +++ b/src/containers/Amendment/amendment.scss @@ -1,5 +1,5 @@ -@import '../shared/css/variables'; -@import '../shared/css/table'; +@use '../shared/css/variables' as *; +@use '../shared/css/table'; .amendment-summary { width: 80%; diff --git a/src/containers/Amendments/amendmentsTable.scss b/src/containers/Amendments/amendmentsTable.scss index ee533cffb..15805be26 100644 --- a/src/containers/Amendments/amendmentsTable.scss +++ b/src/containers/Amendments/amendmentsTable.scss @@ -1,5 +1,5 @@ -@import '../shared/css/variables'; -@import '../shared/css/table'; +@use '../shared/css/variables' as *; +@use '../shared/css/table'; .amendments-page{ .summary { diff --git a/src/containers/App/app.scss b/src/containers/App/app.scss index 38316f6ed..aa0fe1b4d 100644 --- a/src/containers/App/app.scss +++ b/src/containers/App/app.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .app { flex-grow: 1; diff --git a/src/containers/CustomNetworkHome/index.scss b/src/containers/CustomNetworkHome/index.scss index 00a97ca2d..180e748a3 100644 --- a/src/containers/CustomNetworkHome/index.scss +++ b/src/containers/CustomNetworkHome/index.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .app { position: relative; diff --git a/src/containers/Footer/footer.scss b/src/containers/Footer/footer.scss index 7af646bb7..fdee0125e 100644 --- a/src/containers/Footer/footer.scss +++ b/src/containers/Footer/footer.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .footer { position: relative; diff --git a/src/containers/Header/LanguagePicker/LanguagePicker.scss b/src/containers/Header/LanguagePicker/LanguagePicker.scss index b3513de99..5cc3a89fe 100644 --- a/src/containers/Header/LanguagePicker/LanguagePicker.scss +++ b/src/containers/Header/LanguagePicker/LanguagePicker.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .language-picker { margin-left: auto; diff --git a/src/containers/Header/NavigationMenu/NavigationMenu.scss b/src/containers/Header/NavigationMenu/NavigationMenu.scss index 2272b0668..f94c69c6d 100644 --- a/src/containers/Header/NavigationMenu/NavigationMenu.scss +++ b/src/containers/Header/NavigationMenu/NavigationMenu.scss @@ -1,6 +1,6 @@ @use 'sass:math'; -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; $menu-toggle-size: 25px; $menu-toggle-line-spacing: math.div($menu-toggle-size, 4); diff --git a/src/containers/Header/NetworkPicker/NetworkPicker.scss b/src/containers/Header/NetworkPicker/NetworkPicker.scss index 9a60d0100..d905c3361 100644 --- a/src/containers/Header/NetworkPicker/NetworkPicker.scss +++ b/src/containers/Header/NetworkPicker/NetworkPicker.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; @mixin dropdown-network-item( $background, diff --git a/src/containers/Header/Search.tsx b/src/containers/Header/Search.tsx index 65f45e3f7..f1c0e8376 100644 --- a/src/containers/Header/Search.tsx +++ b/src/containers/Header/Search.tsx @@ -1,13 +1,20 @@ -import { KeyboardEventHandler, useContext } from 'react' +import { + FC, + KeyboardEventHandler, + useContext, + useEffect, + useState, +} from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' import { XrplClient } from 'xrpl-client' - import { isValidClassicAddress, isValidXAddress, classicAddressToXAddress, } from 'ripple-address-codec' +import CloseIcon from '../shared/images/close.png' + import { useAnalytics } from '../shared/analytics' import SocketContext from '../shared/SocketContext' import { @@ -33,6 +40,7 @@ import { VALIDATOR_ROUTE, MPT_ROUTE, } from '../App/routes' +import TokenSearchResults from '../shared/components/TokenSearchResults/TokenSearchResults' const determineHashType = async (id: string, rippledContext: XrplClient) => { try { @@ -153,6 +161,26 @@ const normalizeAccount = (id: string) => { return id } +const SearchBanner: FC<{ setIsBannerVisible: (visible: boolean) => void }> = ({ + setIsBannerVisible, +}) => { + const { t } = useTranslation() + return ( +
+
+
{t('search_results_banner')}
+ +
+
+ ) +} + export interface SearchProps { callback?: Function } @@ -163,6 +191,8 @@ export const Search = ({ callback = () => {} }: SearchProps) => { const socket = useContext(SocketContext) const navigate = useNavigate() + const [currentSearchInput, setCurrentSearchInput] = useState('') + const handleSearch = async (id: string) => { const strippedId = id.replace(/^["']|["']$/g, '') const route = await getRoute(strippedId, socket) @@ -178,16 +208,40 @@ export const Search = ({ callback = () => {} }: SearchProps) => { const onKeyDown: KeyboardEventHandler = (event) => { if (event.key === 'Enter') { handleSearch(event.currentTarget?.value?.trim()) + setCurrentSearchInput('') } } + const [isBannerVisible, setIsBannerVisible] = useState(true) + + useEffect(() => { + const timeoutId = setTimeout(() => { + setIsBannerVisible(false) + }, 10000) // Disappear after 10 seconds + + return () => clearTimeout(timeoutId) + }, []) + return ( -
- -
+ <> + {process.env.VITE_ENVIRONMENT === 'mainnet' && isBannerVisible && ( + + )} +
+ setCurrentSearchInput(e.target.value)} + /> + {process.env.VITE_ENVIRONMENT === 'mainnet' && ( + + )} +
+ ) } diff --git a/src/containers/Header/header.scss b/src/containers/Header/header.scss index 140c659ba..06704d86e 100644 --- a/src/containers/Header/header.scss +++ b/src/containers/Header/header.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; /* Header */ .header { diff --git a/src/containers/Header/search.scss b/src/containers/Header/search.scss index 56fa80425..b599fd295 100644 --- a/src/containers/Header/search.scss +++ b/src/containers/Header/search.scss @@ -1,4 +1,8 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; + +.search-container { + position: relative; +} .search { display: inline-block; @@ -23,9 +27,81 @@ } } +.search:not(:focus-within) .search-results { + display: none; +} + // Only show search bar when we know which network to query .header-no-network { .search { display: none; } } + +.normal { + opacity: 1; + transition: 0.5s; +} + +.normal.fade-out { + opacity: 0; +} + +.banner-search { + position: absolute; + top: 50px; + left: 10%; + width: 80%; + max-width: 600px; + padding: 12px 16px; + border-radius: 4px; + background-color: $blue-purple-30 !important; + color: $black !important; + + .banner-content { + display: flex; + justify-content: space-between; + font-size: 14px; + font-weight: 500; + } + + .banner-button { + width: 5px; + height: 5px; + padding: 5px; + padding-left: 10px; + border: none; + margin-top: -5px; + background: none; + color: inherit; + } + + @media (width >= 1350px) { + top: -60px; + } + @media (1000px <= width < 1350px) { + top: -70px; + } + @media (900px <= width < 1000px) { + top: -80px; + } + @media (750px <= width < 900px) { + top: 60px; + } + + @media (max-width: $tablet-portrait-upper-boundary){ + left: 5%; + width: 90%; + } +} + +.banner-search::after { + position: absolute; + top: 100%; + left: 30%; + border-width: 8px; + border-style: solid; + border-color: $blue-purple-30 transparent transparent transparent; + content: ""; + transform: translateX(-50%); +} diff --git a/src/containers/Header/test/Header.test.tsx b/src/containers/Header/test/Header.test.tsx index c1c715fab..635ed837d 100644 --- a/src/containers/Header/test/Header.test.tsx +++ b/src/containers/Header/test/Header.test.tsx @@ -4,11 +4,13 @@ import { BrowserRouter as Router } from 'react-router-dom' import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Provider } from 'react-redux' +import { QueryClientProvider } from 'react-query' import { initialState } from '../../../rootReducer' import i18n from '../../../i18n/testConfigEnglish' import SocketContext from '../../shared/SocketContext' import MockWsClient from '../../test/mockWsClient' import { Header } from '../index' +import { queryClient } from '../../shared/QueryClient' describe('Header component', () => { let client @@ -22,7 +24,9 @@ describe('Header component', () => { -
+ +
+ diff --git a/src/containers/Header/test/Search.test.js b/src/containers/Header/test/Search.test.js index 027075dfc..d5fed3e7c 100644 --- a/src/containers/Header/test/Search.test.js +++ b/src/containers/Header/test/Search.test.js @@ -2,12 +2,14 @@ import { mount } from 'enzyme' import { I18nextProvider } from 'react-i18next' import { BrowserRouter as Router } from 'react-router-dom' import moxios from 'moxios' +import { QueryClientProvider } from 'react-query' import i18n from '../../../i18n/testConfig' import { Search } from '../Search' import * as rippled from '../../../rippled/lib/rippled' import SocketContext from '../../shared/SocketContext' import MockWsClient from '../../test/mockWsClient' import { flushPromises } from '../../test/utils' +import { queryClient } from '../../shared/QueryClient' describe('Search component', () => { const createWrapper = () => { @@ -16,7 +18,9 @@ describe('Search component', () => { - + + + , diff --git a/src/containers/Ledger/ledger.scss b/src/containers/Ledger/ledger.scss index f086ff41d..990f1e4e3 100644 --- a/src/containers/Ledger/ledger.scss +++ b/src/containers/Ledger/ledger.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .ledger-page { margin: 0 auto; diff --git a/src/containers/Ledgers/css/ledgerMetrics.scss b/src/containers/Ledgers/css/ledgerMetrics.scss index 1e12bb03b..23938904b 100644 --- a/src/containers/Ledgers/css/ledgerMetrics.scss +++ b/src/containers/Ledgers/css/ledgerMetrics.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .metrics { overflow: hidden; diff --git a/src/containers/Ledgers/css/ledgers.scss b/src/containers/Ledgers/css/ledgers.scss index 1af05ce8f..0ae1812cc 100644 --- a/src/containers/Ledgers/css/ledgers.scss +++ b/src/containers/Ledgers/css/ledgers.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; $ledgers-margin-large: 32px; $ledgers-border: 1px solid $black-70; diff --git a/src/containers/Ledgers/css/legend.scss b/src/containers/Ledgers/css/legend.scss index c59f5d21f..20db2f293 100644 --- a/src/containers/Ledgers/css/legend.scss +++ b/src/containers/Ledgers/css/legend.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .legend { display: flex; diff --git a/src/containers/MPT/MPTHeader/styles.scss b/src/containers/MPT/MPTHeader/styles.scss index 682ea36e4..8a7715c88 100644 --- a/src/containers/MPT/MPTHeader/styles.scss +++ b/src/containers/MPT/MPTHeader/styles.scss @@ -1,5 +1,5 @@ -@import '../../shared/css/variables'; -@import '../../shared/css/table'; +@use '../../shared/css/variables' as *; +@use '../../shared/css/table'; .mpt-header-container { .mpt-bottom-container { diff --git a/src/containers/MPT/styles.scss b/src/containers/MPT/styles.scss index 51c0c4fc3..f4b0d3245 100644 --- a/src/containers/MPT/styles.scss +++ b/src/containers/MPT/styles.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables'; .mpt-page { width: 100%; diff --git a/src/containers/NFT/NFTHeader/styles.scss b/src/containers/NFT/NFTHeader/styles.scss index 656931f8f..30c3519ee 100644 --- a/src/containers/NFT/NFTHeader/styles.scss +++ b/src/containers/NFT/NFTHeader/styles.scss @@ -1,5 +1,5 @@ -@import '../../shared/css/variables'; -@import '../../shared/css/table'; +@use '../../shared/css/variables' as *; +@use '../../shared/css/table'; .nft-header-container { .nft-bottom-container { diff --git a/src/containers/NFT/NFTTabs/styles.scss b/src/containers/NFT/NFTTabs/styles.scss index b831aa028..6c68075a4 100644 --- a/src/containers/NFT/NFTTabs/styles.scss +++ b/src/containers/NFT/NFTTabs/styles.scss @@ -1,5 +1,5 @@ -@import '../../shared/css/variables'; -@import '../../shared/css/table'; +@use '../../shared/css/variables' as *; +@use '../../shared/css/table'; .nft-tabs { margin-top: 60px; diff --git a/src/containers/NFT/styles.scss b/src/containers/NFT/styles.scss index 59c9e6f90..d03f6e9f0 100644 --- a/src/containers/NFT/styles.scss +++ b/src/containers/NFT/styles.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .nft-page { width: 100%; diff --git a/src/containers/Network/css/barchart.scss b/src/containers/Network/css/barchart.scss index dacce66c6..48a3fddb2 100644 --- a/src/containers/Network/css/barchart.scss +++ b/src/containers/Network/css/barchart.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .y-label { fill: $black-40; diff --git a/src/containers/Network/css/hexagons.scss b/src/containers/Network/css/hexagons.scss index 5bd70e421..08413b393 100644 --- a/src/containers/Network/css/hexagons.scss +++ b/src/containers/Network/css/hexagons.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .validators-container { .validators { diff --git a/src/containers/Network/css/map.scss b/src/containers/Network/css/map.scss index f5aefe624..ff58dd19e 100644 --- a/src/containers/Network/css/map.scss +++ b/src/containers/Network/css/map.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .nodes-map { position: relative; diff --git a/src/containers/Network/css/nodesTable.scss b/src/containers/Network/css/nodesTable.scss index 7d8757b25..147fbc66b 100644 --- a/src/containers/Network/css/nodesTable.scss +++ b/src/containers/Network/css/nodesTable.scss @@ -1,5 +1,5 @@ -@import '../../shared/css/variables'; -@import '../../shared/css/table'; +@use '../../shared/css/variables' as *; +@use '../../shared/css/table'; .nodes-table { position: relative; diff --git a/src/containers/Network/css/style.scss b/src/containers/Network/css/style.scss index 091fa0268..c2f1f30e0 100644 --- a/src/containers/Network/css/style.scss +++ b/src/containers/Network/css/style.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .network-page { // Needs additional bottom margin to break up table from the horizontal diff --git a/src/containers/Network/css/validatorsTable.scss b/src/containers/Network/css/validatorsTable.scss index ed5958bce..0b2aab55d 100644 --- a/src/containers/Network/css/validatorsTable.scss +++ b/src/containers/Network/css/validatorsTable.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .validators-table { position: relative; diff --git a/src/containers/NoMatch/nomatch.scss b/src/containers/NoMatch/nomatch.scss index 899425ef9..7459af506 100644 --- a/src/containers/NoMatch/nomatch.scss +++ b/src/containers/NoMatch/nomatch.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .no-match { margin: 10% auto; diff --git a/src/containers/PayStrings/PayStringHeader/styles.scss b/src/containers/PayStrings/PayStringHeader/styles.scss index 73d59bdc2..8eb0170ae 100644 --- a/src/containers/PayStrings/PayStringHeader/styles.scss +++ b/src/containers/PayStrings/PayStringHeader/styles.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .paystring-header { margin-bottom: 16px; diff --git a/src/containers/PayStrings/PayStringMappingsTable/styles.scss b/src/containers/PayStrings/PayStringMappingsTable/styles.scss index 867362f17..b40727079 100644 --- a/src/containers/PayStrings/PayStringMappingsTable/styles.scss +++ b/src/containers/PayStrings/PayStringMappingsTable/styles.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .paystring-table { .col-network, diff --git a/src/containers/PayStrings/styles.scss b/src/containers/PayStrings/styles.scss index 7c7201294..8912ed2c1 100644 --- a/src/containers/PayStrings/styles.scss +++ b/src/containers/PayStrings/styles.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .paystring-page { width: 100%; diff --git a/src/containers/Token/DEXPairs/styles.scss b/src/containers/Token/DEXPairs/styles.scss index 11b19add9..20c52eab4 100644 --- a/src/containers/Token/DEXPairs/styles.scss +++ b/src/containers/Token/DEXPairs/styles.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .dex-pairs-container { padding: 64px 0 100px; diff --git a/src/containers/Token/TokenHeader/styles.scss b/src/containers/Token/TokenHeader/styles.scss index 6ea3030d5..050a892e7 100644 --- a/src/containers/Token/TokenHeader/styles.scss +++ b/src/containers/Token/TokenHeader/styles.scss @@ -1,5 +1,5 @@ -@import '../../shared/css/variables'; -@import '../../shared/css/table'; +@use '../../shared/css/variables' as *; +@use '../../shared/css/table'; .header-container { .bottom-container { diff --git a/src/containers/Token/styles.scss b/src/containers/Token/styles.scss index 823aeba8d..bf32172e2 100644 --- a/src/containers/Token/styles.scss +++ b/src/containers/Token/styles.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables'; .token-page { width: 100%; diff --git a/src/containers/Transactions/DetailTab/detailTab.scss b/src/containers/Transactions/DetailTab/detailTab.scss index 569b8f1a2..6cca63254 100644 --- a/src/containers/Transactions/DetailTab/detailTab.scss +++ b/src/containers/Transactions/DetailTab/detailTab.scss @@ -1,4 +1,4 @@ -@import '../../shared/css/variables'; +@use '../../shared/css/variables' as *; .detail-body { margin-top: 20px; diff --git a/src/containers/Transactions/simpleTab.scss b/src/containers/Transactions/simpleTab.scss index ca374a592..978ef28f3 100644 --- a/src/containers/Transactions/simpleTab.scss +++ b/src/containers/Transactions/simpleTab.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; $subdued-color: $black-40; diff --git a/src/containers/Transactions/transaction.scss b/src/containers/Transactions/transaction.scss index 7c80014fc..45997a886 100644 --- a/src/containers/Transactions/transaction.scss +++ b/src/containers/Transactions/transaction.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .transaction { position: relative; diff --git a/src/containers/Validators/historyTab.scss b/src/containers/Validators/historyTab.scss index 4112abe75..69127e96f 100644 --- a/src/containers/Validators/historyTab.scss +++ b/src/containers/Validators/historyTab.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .history-table { // Col overall styling diff --git a/src/containers/Validators/simpleTab.scss b/src/containers/Validators/simpleTab.scss index 224a55cc7..f95bbde77 100644 --- a/src/containers/Validators/simpleTab.scss +++ b/src/containers/Validators/simpleTab.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .simple-body-validator { .row { diff --git a/src/containers/Validators/validator.scss b/src/containers/Validators/validator.scss index 58a5bfe75..7cda86f67 100644 --- a/src/containers/Validators/validator.scss +++ b/src/containers/Validators/validator.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .validator { position: relative; diff --git a/src/containers/Validators/votingTab.scss b/src/containers/Validators/votingTab.scss index 71b6cd95b..eb6c7a489 100644 --- a/src/containers/Validators/votingTab.scss +++ b/src/containers/Validators/votingTab.scss @@ -1,4 +1,4 @@ -@import '../shared/css/variables'; +@use '../shared/css/variables' as *; .voting { margin-left: 24px; diff --git a/src/containers/shared/SocketContext.tsx b/src/containers/shared/SocketContext.tsx index a8ed6f217..17ef964e4 100644 --- a/src/containers/shared/SocketContext.tsx +++ b/src/containers/shared/SocketContext.tsx @@ -29,11 +29,13 @@ function getSocket(rippledUrl?: string): ExplorerXrplClient { if (host?.includes(':')) { wsUrls.push(`${prefix}://${host}`) + } else if (process.env.VITE_RIPPLED_WS_PORT) { + wsUrls.push(`${prefix}://${host}:${process.env.VITE_RIPPLED_WS_PORT}`) + if (process.env.VITE_ENVIRONMENT === 'custom') { + wsUrls.push(`${prefix}://${host}`) + } } else { - wsUrls.push.apply(wsUrls, [ - `${prefix}://${host}:${process.env.VITE_RIPPLED_WS_PORT}`, - `${prefix}://${host}:443`, - ]) + wsUrls.push(`${prefix}://${host}`) } }) diff --git a/src/containers/shared/analytics.ts b/src/containers/shared/analytics.ts index b11d5c349..305c43521 100644 --- a/src/containers/shared/analytics.ts +++ b/src/containers/shared/analytics.ts @@ -10,6 +10,7 @@ export type AnalyticsEventNames = | 'network_switch' | 'load_more' | 'not_found' + | 'token_search_click' export interface AnalyticsFields { network?: string diff --git a/src/containers/shared/components/Amount.tsx b/src/containers/shared/components/Amount.tsx index e7fe6ecd5..1a767a796 100644 --- a/src/containers/shared/components/Amount.tsx +++ b/src/containers/shared/components/Amount.tsx @@ -29,7 +29,7 @@ export const Amount = ({ const currency = typeof value === 'string' ? 'XRP' : value.currency const amount = typeof value === 'string' ? parseInt(value, 10) / XRP_BASE : value.amount - const isMPT = typeof value === 'string' ? false : value.isMPT + const isMPT = typeof value === 'string' ? false : value.isMPT ?? false const options = { ...CURRENCY_OPTIONS, currency } diff --git a/src/containers/shared/components/DomainLink.tsx b/src/containers/shared/components/DomainLink.tsx index 512482d35..4afe8e831 100644 --- a/src/containers/shared/components/DomainLink.tsx +++ b/src/containers/shared/components/DomainLink.tsx @@ -5,13 +5,15 @@ export interface Props { className?: string decode?: boolean domain: string + keepProtocol?: boolean } // Matches a protocol (e.g. 'http://' or 'https://') at the start of a string. const PROTOCOL_REGEX = /^([a-z][a-z0-9+\-.]*):\/\// +const PROTOCOL_REMOVAL_REGEX = /^(https?:\/\/)?(.*?)(\/)?$/ const DomainLink = (props: Props) => { - const { className, decode = false, domain } = props + const { className, decode = false, domain, keepProtocol = true } = props // If decode is true, decode the domain const decodedDomain = decode ? decodeHex(domain) : domain @@ -26,14 +28,19 @@ const DomainLink = (props: Props) => { href = href.replace('ipfs://', 'https://ipfs.io/ipfs/') } + const domainText = keepProtocol + ? decodedDomain + : decodedDomain.replace(PROTOCOL_REMOVAL_REGEX, '$2') + return ( event.stopPropagation()} > - {decode ? decodeHex(domain) : domain} + {domainText} ) } diff --git a/src/containers/shared/components/Dropdown/dropdown.scss b/src/containers/shared/components/Dropdown/dropdown.scss index 5d23922e5..b49e23cb4 100644 --- a/src/containers/shared/components/Dropdown/dropdown.scss +++ b/src/containers/shared/components/Dropdown/dropdown.scss @@ -1,4 +1,4 @@ -@import '../../css/variables'; +@use '../../css/variables' as *; .dropdown { position: relative; diff --git a/src/containers/shared/components/JsonView/json-view.scss b/src/containers/shared/components/JsonView/json-view.scss index ff09d9907..0ca1c7de1 100644 --- a/src/containers/shared/components/JsonView/json-view.scss +++ b/src/containers/shared/components/JsonView/json-view.scss @@ -1,4 +1,4 @@ -@import '../../css/variables'; +@use '../../css/variables' as *; @import 'react18-json-view/src/style.css'; .json-view { diff --git a/src/containers/shared/components/Notification/styles.scss b/src/containers/shared/components/Notification/styles.scss index 9844d2327..a2dd6dad3 100644 --- a/src/containers/shared/components/Notification/styles.scss +++ b/src/containers/shared/components/Notification/styles.scss @@ -1,4 +1,4 @@ -@import '../../css/variables'; +@use '../../css/variables' as *; .notification { border: 1px solid; diff --git a/src/containers/shared/components/TokenSearchResults/TokenSearchResults.tsx b/src/containers/shared/components/TokenSearchResults/TokenSearchResults.tsx new file mode 100644 index 000000000..d2418319d --- /dev/null +++ b/src/containers/shared/components/TokenSearchResults/TokenSearchResults.tsx @@ -0,0 +1,95 @@ +import { useContext } from 'react' +import './styles.scss' + +import { useTranslation } from 'react-i18next' +import axios from 'axios' +import { useQuery } from 'react-query' +import { useAnalytics } from '../../analytics' +import { TokenSearchRow } from './TokenSearchRow' +import SocketContext from '../../SocketContext' +import Log from '../../log' +import { getAccountLines } from '../../../../rippled/lib/rippled' +import { FETCH_INTERVAL_XRP_USD_ORACLE_MILLIS } from '../../utils' + +const ORACLE_ACCOUNT = 'rXUMMaPpZqPutoRszR29jtC8amWq3APkx' + +interface SearchResultsProps { + currentSearchValue: string + setCurrentSearchInput: (string) => void +} + +const SearchResults = ({ + currentSearchValue, + setCurrentSearchInput, +}: SearchResultsProps): JSX.Element | null => { + const analytics = useAnalytics() + const { t } = useTranslation() + const rippledSocket = useContext(SocketContext) + + const { data: XRPUSDPrice = 0.0 } = useQuery( + ['fetchXRPToUSDRate'], + () => fetchXRPToUSDRate(), + { + refetchInterval: FETCH_INTERVAL_XRP_USD_ORACLE_MILLIS, + onError: (error) => { + Log.error(error) + return 0.0 + }, + }, + ) + + const { data: tokens = [] } = useQuery( + ['fetchTokens', currentSearchValue], + () => fetchTokens(), + { + enabled: !!currentSearchValue, + staleTime: 0, + keepPreviousData: false, + onError: (error) => Log.error(error), + }, + ) + + const fetchXRPToUSDRate = () => + getAccountLines(rippledSocket, ORACLE_ACCOUNT, 1).then( + (accountLines) => accountLines.lines[0]?.limit ?? 0.0, + ) + + const fetchTokens = () => { + if (currentSearchValue === '') { + return [] // Return an empty list if search is cleared + } + + return axios + .get(`/api/v1/tokens/search/${currentSearchValue}`) + .then((response) => response.data.tokens) + } + + const onLinkClick = () => { + analytics.track('token_search_click', { + search_category: 'token', + search_term: currentSearchValue, + }) + + // clear current search on navigation + setCurrentSearchInput('') + } + + return tokens.length > 0 ? ( +
+
+ {t('tokens')} ({tokens.length}) +
+ + {tokens.map((token) => ( + + ))} +
+ ) : null +} + +export default SearchResults diff --git a/src/containers/shared/components/TokenSearchResults/TokenSearchRow.tsx b/src/containers/shared/components/TokenSearchResults/TokenSearchRow.tsx new file mode 100644 index 000000000..0b1e4066a --- /dev/null +++ b/src/containers/shared/components/TokenSearchResults/TokenSearchRow.tsx @@ -0,0 +1,122 @@ +import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { FC } from 'react' +import { Amount } from '../Amount' +import { localizeNumber } from '../../utils' +import Currency from '../Currency' +import DomainLink from '../DomainLink' + +const parsePrice = (dollarPrice: string, xrpPrice: number): number => { + const parsedDollar = Number(dollarPrice) + return Number((parsedDollar * xrpPrice).toFixed(6)) +} + +const TokenLogo: FC<{ token: any }> = ({ token }) => + token && token.meta?.token.icon ? ( + +
+
+ ) : ( +
+ ) + +const TokenName: FC<{ token: any }> = ({ token }) => + token && token.meta?.token.name ? ( +
+ ( + {token.meta.token.name + .trim() + .toUpperCase() + .replace('(', '') + .replace(')', '')} + ) +
+ ) : null + +const IssuerAddress: FC<{ token: any; onClick: any }> = ({ token, onClick }) => + token && (token.issuer || token.meta?.issuer) ? ( + +
+ {token.meta.issuer.name ? `${token.meta.issuer.name} (` : token.issuer} +
+
{token.issuer}
+
)
+ + ) : null + +interface SearchResultRowProps { + token: any + onClick: () => void + xrpPrice: number +} + +export const TokenSearchRow = ({ + token, + onClick, + xrpPrice, +}: SearchResultRowProps): JSX.Element => { + const { t } = useTranslation() + + return ( + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ {t('holders', { + holders: localizeNumber(token.metrics.holders), + })} +
+
+ {t('trustlines', { + trustlines: localizeNumber(token.metrics.trustlines), + })} +
+
+
+
{t('issuer')}:
+ +
+
+ {token.meta.issuer.domain && ( + <> +
{t('website')}:
+
+ +
+ + )} +
+ + ) +} diff --git a/src/containers/shared/components/TokenSearchResults/styles.scss b/src/containers/shared/components/TokenSearchResults/styles.scss new file mode 100644 index 000000000..beec5b9de --- /dev/null +++ b/src/containers/shared/components/TokenSearchResults/styles.scss @@ -0,0 +1,142 @@ +@import '../../css/variables'; + +.search-results-menu { + position: absolute; + z-index: 1; + width: 100%; + max-height: 400px; + border: 1px solid $black-80; + border-radius: 8px; + border-top: 0px; + margin-top: 6px; + background-color: $black-100; + box-shadow: 2px 2px 15px 0px rgb(131 131 134 / 30%); + overflow-wrap: anywhere; + overflow-y: scroll; + scrollbar-color: $black-50 transparent; + scrollbar-width: thin; + + @media (width < 900px) { + max-width: calc(100% - 32px); + } +} + +.search-results-header { + padding: 0.5rem 0rem 0.5rem 1rem; + background-color: $black-80; + font-size: 14px; + font-weight: 500; +} + +.result-row-icon { + width: 1.5rem; + height: 1.5rem; + border-radius: 16px; +} + +.no-logo { + border-radius: 50%; + background-color: $black-50; +} + +.result-currency { + flex-shrink: 0; + padding-top: 2px; + padding-right: 0px; + padding-bottom: 2px; + white-space: nowrap; +} + +.result-token-name { + padding-top: 2px; + padding-bottom: 2px; + margin-left: 3px; +} + +.result-logo { + padding-right: 14px; + padding-left: 1rem; +} + +.search-result-row { + position: relative; + display: flex; + width: 100%; + box-sizing: border-box; + padding-top: 13px; + padding-bottom: 13px; + color: $black-0; + font-size: 14px; + font-weight: 500; + + &:hover { + background-color: $black-70; + color: $black-0; + cursor: pointer; + } +} + +.result-name-line { + display: flex; + flex-wrap: wrap; + align-items: center; + margin-bottom: 0.5rem; + white-space: nowrap; +} + +.result-issuer-line { + display: flex; + overflow: hidden; + padding: 0 1rem; + margin-bottom: 0.5rem; + color: $black-50; +} + +.result-website-line { + display: flex; + flex-direction: row; + padding: 0 1rem; + color: $black-50; +} + +.metric-chip { + padding: 2px 12px; + border: 1px solid $black-70; + border-radius: 100px; + margin-right: 8px; + margin-left: 8px; + background-color: $black-100; + font-size: 12px; + font-weight: 600; + + &:hover { + background-color: $black-100; + cursor: pointer; + } +} + +.issuer-link { + display: inline-flex !important; + overflow: hidden; + margin-left: 0.25rem; + color: $black-50; + + &:hover { + background: transparent; + color: $blue-purple-30; + } + +} + +.issuer-address { + @extend %truncate; +} + +.issuer-name, +.issuer-title { + flex-shrink: 0; +} + +.result-domain-link { + margin-left: 0.25rem; +} diff --git a/src/containers/shared/components/TokenSearchResults/test/TokenSearchResults.test.js b/src/containers/shared/components/TokenSearchResults/test/TokenSearchResults.test.js new file mode 100644 index 000000000..a3392b28a --- /dev/null +++ b/src/containers/shared/components/TokenSearchResults/test/TokenSearchResults.test.js @@ -0,0 +1,75 @@ +import { mount } from 'enzyme' +import moxios from 'moxios' +import i18n from '../../../../../i18n/testConfig' +import testTokens from './mock_data/tokens.json' +import SocketContext from '../../../SocketContext' +import SearchResults from '../TokenSearchResults' +import MockWsClient from '../../../../test/mockWsClient' +import { QuickHarness } from '../../../../test/utils' + +const testQuery = 'test' + +describe('Testing tokens search', () => { + let client + let wrapper + + const createWrapper = () => { + const searchURL = `/api/v1/tokens/search/${testQuery}` + moxios.stubRequest(searchURL, { + status: 200, + response: testTokens, + }) + return mount( + + + + + , + ) + } + + beforeEach(() => { + moxios.install() + client = new MockWsClient() + wrapper = createWrapper() + }) + + afterEach(() => { + client.close() + moxios.uninstall() + wrapper.unmount() + }) + + it('renders without crashing', () => { + wrapper.update() + const searchMenu = wrapper.find('.search-results-menu') + expect(searchMenu.length).toEqual(1) + }) + + it('renders all tokens ', () => { + wrapper.update() + const searchMenu = wrapper.find('.search-results-menu') + + expect(searchMenu.find('.search-results-header').at(0).html()).toEqual( + `
tokens (1)
`, + ) + expect(searchMenu.find('.currency').at(0).html()).toEqual( + `SOLO`, + ) + expect(searchMenu.find('.issuer-name').at(0).html()).toEqual( + `
Sologenic (
`, + ) + expect(searchMenu.find('.issuer-address').at(0).html()).toEqual( + `
rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz
`, + ) + expect( + searchMenu.find('.search-result-row').at(0).find('.metric-chip').length, + ).toEqual(3) + expect(searchMenu.find('.domain').at(0).html()).toEqual( + `sologenic.com`, + ) + }) +}) diff --git a/src/containers/shared/components/TokenSearchResults/test/mock_data/tokens.json b/src/containers/shared/components/TokenSearchResults/test/mock_data/tokens.json new file mode 100644 index 000000000..eae88720e --- /dev/null +++ b/src/containers/shared/components/TokenSearchResults/test/mock_data/tokens.json @@ -0,0 +1,45 @@ +{ + "tokens": [ + { + "currency": "534F4C4F00000000000000000000000000000000", + "issuer": "rsoLo2S1kiGeCcn6hCUXVrCpGMWLrRrLZz", + "meta": { + "token": { + "description": "SOLO is the utility token for the Sologenic ecosystem. It can be used for covering fees when minting NFTs on their platform.", + "icon": "https://s1.xrplmeta.org/icon/C40439709A.png", + "name": "SOLO", + "trust_level": 3 + }, + "issuer": { + "domain": "sologenic.com", + "icon": "https://s1.xrplmeta.org/icon/C40439709A.png", + "kyc": true, + "name": "Sologenic", + "trust_level": 3, + "weblinks": [ + { + "url": "https://sologenic.com" + }, + { + "url": "https://twitter.com/realSologenic", + "type": "socialmedia" + } + ] + } + }, + "metrics": { + "trustlines": 283920, + "holders": 227379, + "supply": "399057268.13655", + "marketcap": "73737104.7448899", + "price": "0.184476049183255", + "volume_24h": "97065.7035689995", + "volume_7d": "1387820.55867489", + "exchanges_24h": "3446", + "exchanges_7d": "38976", + "takers_24h": "125", + "takers_7d": "627" + } + } + ] +} diff --git a/src/containers/shared/components/Transaction/EnableAmendment/Simple.tsx b/src/containers/shared/components/Transaction/EnableAmendment/Simple.tsx index 35f885c53..cd53f50b8 100644 --- a/src/containers/shared/components/Transaction/EnableAmendment/Simple.tsx +++ b/src/containers/shared/components/Transaction/EnableAmendment/Simple.tsx @@ -1,4 +1,5 @@ import { useContext, useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' import { useLanguage } from '../../../hooks' import { SimpleRow } from '../SimpleRow' import { TransactionSimpleProps } from '../types' @@ -16,6 +17,7 @@ const states = { export const Simple = ({ data }: TransactionSimpleProps) => { const language = useLanguage() + const { t } = useTranslation() const [amendmentDetails, setAmendmentDetails] = useState({ name: states.loading, minRippledVersion: states.loading, @@ -56,7 +58,7 @@ export const Simple = ({ data }: TransactionSimpleProps) => { return ( <> - + ) => { {amendmentDetails.name} - + {amendmentStatus} - + {amendmentDetails.minRippledVersion} {amendmentStatus === 'Got Majority' && ( - + {expectedDate} )} diff --git a/src/containers/shared/components/TransactionTable/styles.scss b/src/containers/shared/components/TransactionTable/styles.scss index ad29c981f..fa235d2d4 100644 --- a/src/containers/shared/components/TransactionTable/styles.scss +++ b/src/containers/shared/components/TransactionTable/styles.scss @@ -1,5 +1,5 @@ @use 'sass:color'; -@import '../../../shared/css/variables'; +@use '../../../shared/css/variables' as *; .transaction-table { width: 100%; diff --git a/src/containers/shared/components/test/DomainLink.test.tsx b/src/containers/shared/components/test/DomainLink.test.tsx index 71e589e8a..57e83ad77 100644 --- a/src/containers/shared/components/test/DomainLink.test.tsx +++ b/src/containers/shared/components/test/DomainLink.test.tsx @@ -61,4 +61,12 @@ describe('DomainLink', () => { ) wrapper.unmount() }) + + it('handles domain link with protocol removal', () => { + const url = 'https://example.com/' + const wrapper = mount() + expect(wrapper.find('a').props().className).toEqual('domain') + expect(wrapper.find('a').text()).toEqual('example.com') + expect(wrapper.find('a').props().href).toEqual('https://example.com/') + }) }) diff --git a/src/containers/shared/css/box.scss b/src/containers/shared/css/box.scss index bbd7ea06b..839b8e264 100644 --- a/src/containers/shared/css/box.scss +++ b/src/containers/shared/css/box.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; .box { border-radius: 4px; diff --git a/src/containers/shared/css/form.scss b/src/containers/shared/css/form.scss index ee462e9a2..e4fefc169 100644 --- a/src/containers/shared/css/form.scss +++ b/src/containers/shared/css/form.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; label { color: $black-40; diff --git a/src/containers/shared/css/global.scss b/src/containers/shared/css/global.scss index 8d7bf9b0d..e10fd11d1 100644 --- a/src/containers/shared/css/global.scss +++ b/src/containers/shared/css/global.scss @@ -1,7 +1,7 @@ // ONLY GLOBAL CSS, KEEP IT TO MINIMAL // CSS SHOULD BE SCOPPED TO COMPONENT -@import './variables'; -@import './form'; +@use 'variables' as *; +@use 'form'; /** * `current_symbols` is used for currency codes missing from other fonts, currently `u+e900` (XRP) and `u+e901` (BTC). diff --git a/src/containers/shared/css/simpleTab.scss b/src/containers/shared/css/simpleTab.scss index 574f4adda..087d3005c 100644 --- a/src/containers/shared/css/simpleTab.scss +++ b/src/containers/shared/css/simpleTab.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; $index-width: 324px; diff --git a/src/containers/shared/css/table.scss b/src/containers/shared/css/table.scss index 53dc1e2df..4f53bac7c 100644 --- a/src/containers/shared/css/table.scss +++ b/src/containers/shared/css/table.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; table.basic { overflow: hidden; diff --git a/src/containers/shared/css/tabs.scss b/src/containers/shared/css/tabs.scss index 379fd220c..3ffa955ec 100644 --- a/src/containers/shared/css/tabs.scss +++ b/src/containers/shared/css/tabs.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; .tabs { border-top: 1px solid $black-70; diff --git a/src/containers/shared/css/tooltip.scss b/src/containers/shared/css/tooltip.scss index a1c6d5e4b..eaada0ce5 100644 --- a/src/containers/shared/css/tooltip.scss +++ b/src/containers/shared/css/tooltip.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; .tooltip { position: absolute; diff --git a/src/containers/shared/css/txlabel.scss b/src/containers/shared/css/txlabel.scss index 093f8300c..dd2b520a6 100644 --- a/src/containers/shared/css/txlabel.scss +++ b/src/containers/shared/css/txlabel.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; .tx-label { display: inline-flex; diff --git a/src/containers/shared/css/txstatus.scss b/src/containers/shared/css/txstatus.scss index 0a447f246..66933e784 100644 --- a/src/containers/shared/css/txstatus.scss +++ b/src/containers/shared/css/txstatus.scss @@ -1,4 +1,4 @@ -@import './variables'; +@use 'variables' as *; .tx-status { display: inline-flex; diff --git a/src/containers/shared/test/SocketContext.test.ts b/src/containers/shared/test/SocketContext.test.ts index 2c268ed56..2a67fae26 100644 --- a/src/containers/shared/test/SocketContext.test.ts +++ b/src/containers/shared/test/SocketContext.test.ts @@ -28,7 +28,7 @@ describe('getSocket', () => { const client = getSocket() expect(XrplClient).toHaveBeenNthCalledWith( 1, - ['wss://somewhere.com:51233', 'wss://somewhere.com:443'], + ['wss://somewhere.com:51233'], { tryAllNodes: true, }, @@ -47,12 +47,7 @@ describe('getSocket', () => { const client = getSocket() expect(XrplClient).toHaveBeenNthCalledWith( 1, - [ - 'wss://somewhere.com:51233', - 'wss://somewhere.com:443', - 'wss://elsewhere.com:51233', - 'wss://elsewhere.com:443', - ], + ['wss://somewhere.com:51233', 'wss://elsewhere.com:51233'], { tryAllNodes: true, }, @@ -71,7 +66,7 @@ describe('getSocket', () => { const client = getSocket() expect(XrplClient).toHaveBeenNthCalledWith( 1, - ['ws://somewhere.com:51233', 'ws://somewhere.com:443'], + ['ws://somewhere.com:51233'], { tryAllNodes: true, }, @@ -120,13 +115,9 @@ describe('getSocket', () => { it('should use ws when supplied entry is for a localhost', () => { const client = getSocket('localhost') - expect(XrplClient).toHaveBeenNthCalledWith( - 1, - ['ws://localhost:51233', 'ws://localhost:443'], - { - tryAllNodes: true, - }, - ) + expect(XrplClient).toHaveBeenNthCalledWith(1, ['ws://localhost:51233'], { + tryAllNodes: true, + }) expect((client as any).p2pSocket).not.toBeDefined() }) diff --git a/src/containers/shared/utils.js b/src/containers/shared/utils.js index 9046410e0..9580b4754 100644 --- a/src/containers/shared/utils.js +++ b/src/containers/shared/utils.js @@ -21,6 +21,7 @@ export const FETCH_INTERVAL_MILLIS = 5000 export const FETCH_INTERVAL_VHS_MILLIS = 60 * 1000 // 1 minute export const FETCH_INTERVAL_NODES_MILLIS = 60000 export const FETCH_INTERVAL_ERROR_MILLIS = 300 +export const FETCH_INTERVAL_XRP_USD_ORACLE_MILLIS = 60 * 1000 export const DECIMAL_REGEX = /^\d+$/ export const HASH256_REGEX = /[0-9A-Fa-f]{64}/i diff --git a/src/rippled/lib/rippled.js b/src/rippled/lib/rippled.js index dc7737309..89f47ad6e 100644 --- a/src/rippled/lib/rippled.js +++ b/src/rippled/lib/rippled.js @@ -573,10 +573,11 @@ const getFeature = (rippledSocket, amendmentId) => { } const getMPTIssuance = (rippledSocket, tokenId) => - query(rippledSocket, { + queryP2P(rippledSocket, { command: 'ledger_entry', mpt_issuance: tokenId, ledger_index: 'validated', + include_deleted: true, }).then((resp) => { if ( resp.error === 'entryNotFound' || @@ -620,6 +621,26 @@ const getAccountMPTs = ( return resp }) +const getAccountLines = (rippledSocket, account, limit) => + query(rippledSocket, { + command: 'account_lines', + account, + limit, + }).then((resp) => { + if (resp.error === 'actNotFound') { + throw new Error('account not found', 404) + } + if (resp.error === 'invalidParams') { + return undefined + } + + if (resp.error_message) { + throw new Error(resp.error_message, 500) + } + + return resp + }) + export { getLedger, getLedgerEntry, @@ -642,4 +663,5 @@ export { getFeature, getMPTIssuance, getAccountMPTs, + getAccountLines, }