Compare commits
171 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d674f8500f | ||
|
|
686f24b98e | ||
|
|
40d8da42a5 | ||
|
|
2151ffbac7 | ||
|
|
6ec9bb7362 | ||
|
|
31d0c3c9b3 | ||
|
|
88b7acf3ae | ||
|
|
877e000da6 | ||
|
|
3f05a88409 | ||
|
|
03ab5c80a8 | ||
|
|
4faaa60aea | ||
|
|
3a94a99293 | ||
|
|
e893bc2685 | ||
|
|
cccf6ac680 | ||
|
|
ea5af12b1d | ||
|
|
bf1f613a4f | ||
|
|
49f1acb52a | ||
|
|
a97005e2e1 | ||
|
|
966b02b2de | ||
|
|
024bbce9a4 | ||
|
|
c960c14170 | ||
|
|
39295e9a33 | ||
|
|
0feddebcc3 | ||
|
|
5e4108fbdc | ||
|
|
38cce46c7b | ||
|
|
69ae42f285 | ||
|
|
ef9619b1bd | ||
|
|
041f3d5ba2 | ||
|
|
666bb79833 | ||
|
|
2736d94432 | ||
|
|
57b098f309 | ||
|
|
c802132bd5 | ||
|
|
485764fe38 | ||
|
|
51dc10b467 | ||
|
|
7cf768b8dd | ||
|
|
59b5e81d16 | ||
|
|
8fc98abb1a | ||
|
|
def4ab3bc0 | ||
|
|
bb6de9056b | ||
|
|
cc52da9fc4 | ||
|
|
b98e62cac8 | ||
|
|
6cd1f04584 | ||
|
|
8ffe4e991b | ||
|
|
2b85852a48 | ||
|
|
94015b279c | ||
|
|
1291887049 | ||
|
|
d4a26a5610 | ||
|
|
1249371397 | ||
|
|
2ce7b08244 | ||
|
|
156254afa9 | ||
|
|
e31f4e549b | ||
|
|
f47e1f16d7 | ||
|
|
9954f9502d | ||
|
|
9fbdc3cab1 | ||
|
|
e04be0711f | ||
|
|
5c3caa7143 | ||
|
|
1ea724609f | ||
|
|
2b6948db94 | ||
|
|
0b8026e6fe | ||
|
|
b14831be12 | ||
|
|
b12a61e2fb | ||
|
|
6f6c9d7174 | ||
|
|
89c3ec7526 | ||
|
|
06c81d9304 | ||
|
|
15c59e8149 | ||
|
|
f6b66c759a | ||
|
|
b1c99d4b37 | ||
|
|
add6df46e6 | ||
|
|
b2dfb29f51 | ||
|
|
097ef6a3df | ||
|
|
984cf81911 | ||
|
|
08501234a8 | ||
|
|
3afe7fe9f2 | ||
|
|
bd573724b9 | ||
|
|
684258dc17 | ||
|
|
a53e773e5d | ||
|
|
5307d113a8 | ||
|
|
9e7d59f1fe | ||
|
|
2ccd228f23 | ||
|
|
a726f67453 | ||
|
|
c9b4016b78 | ||
|
|
a7135c9ab1 | ||
|
|
a1b3776686 | ||
|
|
dc2225a2bc | ||
|
|
41571169c6 | ||
|
|
7506dbb29c | ||
|
|
4f25c4a2bb | ||
|
|
fa4e75b777 | ||
|
|
f845695f6e | ||
|
|
dbb2f7f6a2 | ||
|
|
264f145708 | ||
|
|
bb2677ab1b | ||
|
|
29e46455c1 | ||
|
|
cfc9748036 | ||
|
|
715555f340 | ||
|
|
6a1f17ab5a | ||
|
|
cd43e0beaa | ||
|
|
24d00f7c39 | ||
|
|
b8573930b9 | ||
|
|
2c1e608f84 | ||
|
|
f90066263e | ||
|
|
1daf15df9f | ||
|
|
708a813f2a | ||
|
|
18b50283d3 | ||
|
|
d9ff7b15a1 | ||
|
|
cc5309b18f | ||
|
|
a060bf1079 | ||
|
|
1ffb9421b2 | ||
|
|
7978ed97a9 | ||
|
|
b4b6c347e3 | ||
|
|
83cb750b2f | ||
|
|
fc0bf229a7 | ||
|
|
b55680dbce | ||
|
|
136e68201f | ||
|
|
b4bb3d72d5 | ||
|
|
3f812f4aee | ||
|
|
02883aca13 | ||
|
|
ace81ecc84 | ||
|
|
0aafcdf885 | ||
|
|
f55062f9a9 | ||
|
|
1fde2504b4 | ||
|
|
6a3abbfb56 | ||
|
|
9ced714718 | ||
|
|
8ed6481f16 | ||
|
|
d160d1a929 | ||
|
|
1092bc2c58 | ||
|
|
a2e56aaabd | ||
|
|
7ec6a965d3 | ||
|
|
b58d4b72ab | ||
|
|
a2d98607ea | ||
|
|
27d9152949 | ||
|
|
3cb069283c | ||
|
|
4d084d0055 | ||
|
|
4b39dfbd69 | ||
|
|
6c5c1c0032 | ||
|
|
22112c763c | ||
|
|
b230cb62f4 | ||
|
|
9f71e384b2 | ||
|
|
8592a4a54d | ||
|
|
ccc94fdfc2 | ||
|
|
4537a4dc94 | ||
|
|
db0cb41b61 | ||
|
|
4ced70f737 | ||
|
|
9262fec093 | ||
|
|
ff3bcc4693 | ||
|
|
34a22ef9f0 | ||
|
|
5e86cf7b29 | ||
|
|
83172dc5ea | ||
|
|
0ca68bb140 | ||
|
|
430356da9a | ||
|
|
1c50460160 | ||
|
|
2ef9e9b61c | ||
|
|
b906cddc6a | ||
|
|
65fe8d4168 | ||
|
|
8956fba629 | ||
|
|
04884fe1b0 | ||
|
|
ef28667d13 | ||
|
|
0ced5f2402 | ||
|
|
252bf32d2f | ||
|
|
01e87657c6 | ||
|
|
55bf30c0e0 | ||
|
|
95f61487e8 | ||
|
|
3ed3ed4994 | ||
|
|
3a0c4ad4db | ||
|
|
803eb46e5f | ||
|
|
a3f0c54f66 | ||
|
|
52796fcc55 | ||
|
|
ca02a6b56a | ||
|
|
b722a20d96 | ||
|
|
9b5261aaeb | ||
|
|
0efb7f51a4 |
2
.env
2
.env
@@ -4,6 +4,8 @@ REACT_APP_AMPLITUDE_PROXY_URL="https://api.uniswap.org/v1/amplitude-proxy"
|
||||
REACT_APP_AWS_API_REGION="us-east-2"
|
||||
REACT_APP_AWS_API_ENDPOINT="https://beta.api.uniswap.org/v1/graphql"
|
||||
REACT_APP_BNB_RPC_URL="https://rough-sleek-hill.bsc.quiknode.pro/413cc98cbc776cda8fdf1d0f47003583ff73d9bf"
|
||||
REACT_APP_BASE_GOERLI_RPC_URL="https://wiser-compatible-mansion.base-goerli.quiknode.pro/5874f36248e17020a1006149e7f68c63967e1f45/"
|
||||
REACT_APP_BASE_MAINNET_RPC_URL="https://cool-white-diagram.base-mainnet.quiknode.pro/d8f036f35dfab2c68f32dfa822cd971e7a25a117/"
|
||||
REACT_APP_INFURA_KEY="4bf032f2d38a4ed6bb975b80d6340847"
|
||||
REACT_APP_MOONPAY_API="https://api.moonpay.com"
|
||||
REACT_APP_MOONPAY_LINK="https://us-central1-uniswap-mobile.cloudfunctions.net/signMoonpayLinkV2?platform=web&env=staging"
|
||||
|
||||
19
.eslintrc.js
19
.eslintrc.js
@@ -1,5 +1,6 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const { node: restrictedImports } = require('@uniswap/eslint-config/restrictedImports')
|
||||
require('@uniswap/eslint-config/load')
|
||||
|
||||
const rulesDirPlugin = require('eslint-plugin-rulesdir')
|
||||
@@ -27,6 +28,7 @@ module.exports = {
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-restricted-imports': ['error', restrictedImports],
|
||||
'import/no-restricted-paths': [
|
||||
'error',
|
||||
{
|
||||
@@ -57,5 +59,22 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
excludedFiles: ['src/analytics/*'],
|
||||
rules: {
|
||||
'no-restricted-imports': [
|
||||
'error',
|
||||
{
|
||||
paths: [
|
||||
{
|
||||
name: '@uniswap/analytics',
|
||||
message: `Do not import from '@uniswap/analytics' directly. Use 'analytics' instead.`,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
32
.github/actions/cache-on-main/action.yml
vendored
32
.github/actions/cache-on-main/action.yml
vendored
@@ -1,32 +0,0 @@
|
||||
name: Cache on main
|
||||
description: caches node_modules/.cache, but only saves from main
|
||||
inputs:
|
||||
path:
|
||||
description: 'A list of files, directories, and wildcard patterns to cache and store'
|
||||
required: true
|
||||
key:
|
||||
description: 'An explicit key for restoring and saving the cache'
|
||||
required: true
|
||||
restore-keys:
|
||||
description: 'An ordered list of keys to use for restoring stale cache if no cache hit occured for key. Note `cache-hit` returns false in this case.'
|
||||
required: false
|
||||
|
||||
# Many build steps have their own caches to improve subsequent build times.
|
||||
# Build tools are configured to cache to node_modules/.cache, so they are cached independently of node_modules.
|
||||
# Caches are saved every run *on main* (by keying on github.run_id), and the most recent available cache is loaded.
|
||||
# Caches are not saved on feature branches because they have limited utility, and extend the runtime of the workflow.
|
||||
# See https://jongleberry.medium.com/speed-up-your-ci-and-dx-with-node-modules-cache-ac8df82b7bb0.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ inputs.key }}
|
||||
restore-keys: ${{ inputs.restore-keys }}
|
||||
- if: github.ref_name == 'main'
|
||||
uses: actions/cache/save@v3
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ inputs.key }}
|
||||
43
.github/actions/setup/action.yml
vendored
43
.github/actions/setup/action.yml
vendored
@@ -23,9 +23,9 @@ runs:
|
||||
- if: steps.install-cache.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
shell: bash
|
||||
|
||||
# Validators compile quickly, so caching can be omitted.
|
||||
- run: yarn ajv
|
||||
|
||||
# Run patch-package to apply patches to dependencies.
|
||||
- run: yarn patch-package
|
||||
shell: bash
|
||||
|
||||
# Contracts are compiled from source. If source hasn't changed, the contracts do not need to be re-compiled.
|
||||
@@ -40,35 +40,10 @@ runs:
|
||||
run: yarn contracts
|
||||
shell: bash
|
||||
|
||||
# GraphQL is generated from schema. The schema is always fetched, but if unchanged, graphql does not need to be re-generated.
|
||||
- run: yarn graphql:fetch
|
||||
shell: bash
|
||||
- uses: actions/cache@v3
|
||||
id: graphql-cache
|
||||
with:
|
||||
path: src/graphql/**/__generated__
|
||||
key: ${{ runner.os }}-graphql-${{ hashFiles('src/graphql/**/schema.graphql') }}
|
||||
- if: steps.graphql-cache.outputs.cache-hit != 'true'
|
||||
run: yarn graphql:generate
|
||||
shell: bash
|
||||
|
||||
# Messages are extracted from source.
|
||||
# A record of source file content hashes and catalogs is maintained in node_modules/.cache/lingui.
|
||||
# Messages are always extracted, but extraction may short-circuit from the custom extractor's cache.
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-i18n-extract-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-i18n-extract-
|
||||
- run: yarn i18n:extract
|
||||
shell: bash
|
||||
|
||||
# Translations are compiled from messages. If messages haven't changed, the translations do not need to be re-compiled.
|
||||
- uses: actions/cache@v3
|
||||
id: i18n-compile-cache
|
||||
with:
|
||||
path: src/locales/*.js
|
||||
key: ${{ runner.os }}-i18n-compile-${{ hashFiles('src/locales/*.po') }}
|
||||
- if: steps.i18n-compile-cache.outputs.cache-hit !='true'
|
||||
run: yarn i18n:compile
|
||||
# These operations cannot be cached, so they are run concurrently
|
||||
# - ajv: Validators compile quickly, so caching can be omitted.
|
||||
# - graphql: GraphQL is generated from schema and client-side graphql queries. The schema is always fetched and
|
||||
# changes to client-side queries are hard to detect, so it is always re-generated.
|
||||
# - i18n: Messages are extracted from source and compiled. No caching extractor is available (out-of-the-box).
|
||||
- run: yarn concurrently --max-processes=100% npm:ajv npm:graphql npm:i18n
|
||||
shell: bash
|
||||
|
||||
15
.github/workflows/3-staging-to-prod.yml
vendored
15
.github/workflows/3-staging-to-prod.yml
vendored
@@ -14,6 +14,21 @@ jobs:
|
||||
environment:
|
||||
name: push/prod
|
||||
steps:
|
||||
- name: Check test status
|
||||
uses: actions/github-script@v6.4.1
|
||||
with:
|
||||
script: |
|
||||
const statuses = await github.rest.repos.listCommitStatusesForRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: context.sha
|
||||
})
|
||||
const status = statuses.data.find(status => status.context === 'Test / promotion')?.state || 'missing'
|
||||
core.info('Status: ' + status)
|
||||
if (status !== 'success') {
|
||||
core.setFailed('"Test / promotion" must be successful before pushing')
|
||||
}
|
||||
|
||||
- uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_SERVICE_ACCESS_TOKEN }}
|
||||
|
||||
105
.github/workflows/test.yml
vendored
105
.github/workflows/test.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-eslint-${{ github.run_id }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-tsc-${{ github.run_id }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-jest-${{ github.run_id }}
|
||||
@@ -81,25 +81,40 @@ jobs:
|
||||
name: Unit tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
build-e2e:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-build-e2e-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-build-e2e-
|
||||
- run: yarn build:e2e
|
||||
env:
|
||||
NODE_OPTIONS: "--max_old_space_size=4096"
|
||||
path: node_modules/.swc
|
||||
key: ${{ runner.os }}-swc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-swc-
|
||||
- run: yarn build
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
name: build
|
||||
path: build
|
||||
if-no-files-found: error
|
||||
|
||||
cypress-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cypress-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cypress-tsc-
|
||||
- run: yarn typecheck:cypress
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
# Allows for parallel re-runs of cypress tests without re-building.
|
||||
cypress-rerun:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -107,7 +122,7 @@ jobs:
|
||||
- run: exit 0
|
||||
|
||||
cypress-test-matrix:
|
||||
needs: [build-e2e, cypress-rerun]
|
||||
needs: [build, cypress-rerun]
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -116,7 +131,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: /root/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress-${{ hashFiles('**/node_modules/cypress/package.json') }}
|
||||
@@ -126,10 +141,10 @@ jobs:
|
||||
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-e2e
|
||||
name: build
|
||||
path: build
|
||||
|
||||
- uses: ./.github/actions/cache-on-main
|
||||
- uses: actions/cache/restore@v3
|
||||
with:
|
||||
path: cache
|
||||
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
|
||||
@@ -158,17 +173,65 @@ jobs:
|
||||
COMMIT_INFO_TIMESTAMP: ${{ github.event.pull_request.updated_at || github.event.head_commit.timestamp }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ github.event.pull_request.number }}
|
||||
CYPRESS_PULL_REQUEST_URL: ${{ github.event.pull_request.html_url }}
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
flags: e2e-tests
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cypress tests
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: hardhat-cache
|
||||
path: cache
|
||||
|
||||
hardhat-cache:
|
||||
needs: [cypress-test-matrix]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: hardhat-cache
|
||||
path: cache
|
||||
- uses: actions/cache/save@v3
|
||||
with:
|
||||
path: cache
|
||||
key: ${{ runner.os }}-hardhat-${{ hashFiles('hardhat.config.js') }}-${{ github.run_id }}
|
||||
|
||||
cloud-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-tsc-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-tsc-
|
||||
- run: yarn typecheck:cloud
|
||||
- if: failure() && github.ref_name == 'main'
|
||||
uses: ./.github/actions/report
|
||||
with:
|
||||
name: Cloud typecheck
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TEST_REPORTER_WEBHOOK }}
|
||||
|
||||
cloud-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup
|
||||
- uses: actions/cache@v3
|
||||
with:
|
||||
path: node_modules/.cache
|
||||
key: ${{ runner.os }}-cloud-jest-${{ github.run_id }}
|
||||
restore-keys: ${{ runner.os }}-cloud-jest-
|
||||
# Only use 1 worker, so the other can be used for the proxy server under test.
|
||||
- run: yarn test:cloud --coverage --maxWorkers=1
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
flags: cloud-tests
|
||||
|
||||
pre:
|
||||
if: ${{ github.ref_name == 'main' || github.ref_name == 'releases/staging' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -19,6 +19,8 @@ schema.graphql
|
||||
# testing
|
||||
/coverage
|
||||
/cache
|
||||
/functions/coverage
|
||||
/.swc
|
||||
|
||||
# builds
|
||||
/build
|
||||
@@ -46,7 +48,10 @@ notes.txt
|
||||
|
||||
package-lock.json
|
||||
|
||||
cypress/downloads
|
||||
cypress/videos
|
||||
cypress/screenshots
|
||||
|
||||
.vercel
|
||||
|
||||
.wrangler
|
||||
|
||||
4
.husky/pre-commit
Normal file
4
.husky/pre-commit
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
36
.swcrc
Normal file
36
.swcrc
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
// has to duplicate from package.json, see swc issue: https://swc.rs/docs/configuration/compilation#env
|
||||
// this breaks jest because jest is setting target for some reason
|
||||
// "env": {
|
||||
// "targets": "> 0.5%, not dead"
|
||||
// },
|
||||
"jsc": {
|
||||
// without this swc breaks WalletConnect class super() call
|
||||
"target": "es2020",
|
||||
"keepClassNames": true,
|
||||
"experimental": {
|
||||
"plugins": [
|
||||
[
|
||||
"@lingui/swc-plugin",
|
||||
{}
|
||||
],
|
||||
[
|
||||
"@swc/plugin-styled-components",
|
||||
{
|
||||
"displayName": true
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"tsx": true
|
||||
},
|
||||
"transform": {
|
||||
"react": {
|
||||
"runtime": "automatic"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
CODEOWNERS
Normal file
1
CODEOWNERS
Normal file
@@ -0,0 +1 @@
|
||||
@uniswap/web-admins
|
||||
@@ -1,10 +0,0 @@
|
||||
/* eslint-env node */
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
module.exports = {
|
||||
styledComponents: {
|
||||
fileName: isDev,
|
||||
displayName: isDev,
|
||||
},
|
||||
}
|
||||
14
codecov.yml
14
codecov.yml
@@ -6,12 +6,12 @@ ignore:
|
||||
- "**/instrumented/**/*"
|
||||
- "**/styles/**/*"
|
||||
- "styles/**/*"
|
||||
- "**/styled.tsx"
|
||||
- "**/constants/**/*"
|
||||
- "constants/**/*"
|
||||
|
||||
coverage:
|
||||
status:
|
||||
# Omit merging unit/e2e reports into the defaults, as it is nonsensical.
|
||||
project: off
|
||||
patch: off
|
||||
|
||||
@@ -25,20 +25,16 @@ flag_management:
|
||||
removed_code_behavior: adjust_base
|
||||
if_ci_failed: error
|
||||
- type: patch
|
||||
target: 80%
|
||||
target: 50%
|
||||
individual_flags:
|
||||
- name: unit-tests
|
||||
- name: e2e-tests
|
||||
# Wait until all machines have reported coverage - e2e tests run across 4 machines.
|
||||
after_n_builds: 4
|
||||
- name: cloud-tests
|
||||
statuses:
|
||||
- type: patch
|
||||
target: 0%
|
||||
- type: project
|
||||
target: 80%
|
||||
|
||||
comment:
|
||||
layout: flags
|
||||
# Wait until all machines have reported coverage - e2e tests run across 4 machines + unit tests across 1.
|
||||
after_n_builds: 5
|
||||
hide_comment_details: false
|
||||
|
||||
github_checks:
|
||||
|
||||
@@ -5,12 +5,15 @@ const { execSync } = require('child_process')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const path = require('path')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
const { DefinePlugin, IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const TerserPlugin = require('terser-webpack-plugin')
|
||||
const { IgnorePlugin, ProvidePlugin } = require('webpack')
|
||||
const { RetryChunkLoadPlugin } = require('webpack-retry-chunk-load-plugin')
|
||||
|
||||
const commitHash = execSync('git rev-parse HEAD').toString().trim()
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
process.env.REACT_APP_GIT_COMMIT_HASH = commitHash
|
||||
|
||||
// Linting and type checking are only necessary as part of development and testing.
|
||||
// Omit them from production builds, as they slow down the feedback loop.
|
||||
const shouldLintOrTypeCheck = !isProduction
|
||||
@@ -21,32 +24,6 @@ function getCacheDirectory(cacheName) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
babel: {
|
||||
plugins: [
|
||||
'@vanilla-extract/babel-plugin',
|
||||
...(process.env.REACT_APP_ADD_COVERAGE_INSTRUMENTATION
|
||||
? [
|
||||
[
|
||||
'istanbul',
|
||||
{
|
||||
all: true,
|
||||
include: ['src/**/*.tsx', 'src/**/*.ts'],
|
||||
exclude: [
|
||||
'src/**/*.css',
|
||||
'src/**/*.css.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/**/*.test.tsx',
|
||||
'src/**/*.spec.ts',
|
||||
'src/**/*.spec.tsx',
|
||||
'src/**/graphql/**/*',
|
||||
'src/**/*.d.ts',
|
||||
],
|
||||
},
|
||||
],
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
eslint: {
|
||||
enable: shouldLintOrTypeCheck,
|
||||
pluginOptions(eslintConfig) {
|
||||
@@ -69,16 +46,20 @@ module.exports = {
|
||||
configure(jestConfig) {
|
||||
return Object.assign(jestConfig, {
|
||||
cacheDirectory: getCacheDirectory('jest'),
|
||||
transform: Object.assign(jestConfig.transform, {
|
||||
transform: {
|
||||
...Object.entries(jestConfig.transform).reduce((transform, [key, value]) => {
|
||||
if (value.match(/babel/)) return transform
|
||||
return { ...transform, [key]: value }
|
||||
}, {}),
|
||||
// Transform vanilla-extract using its own transformer.
|
||||
// See https://sandroroth.com/blog/vanilla-extract-cra#jest-transform.
|
||||
'\\.css\\.ts$': '@vanilla-extract/jest-transform',
|
||||
}),
|
||||
// Use @uniswap/conedison's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['@uniswap/conedison/format', '@uniswap/conedison/provider'],
|
||||
'\\.(t|j)sx?$': '@swc/jest',
|
||||
},
|
||||
// Use d3-arrays's build directly, as jest does not support its exports.
|
||||
transformIgnorePatterns: ['d3-array'],
|
||||
moduleNameMapper: {
|
||||
'@uniswap/conedison/format': '@uniswap/conedison/dist/format',
|
||||
'@uniswap/conedison/provider': '@uniswap/conedison/dist/provider',
|
||||
'd3-array': 'd3-array/dist/d3-array.min.js',
|
||||
},
|
||||
})
|
||||
},
|
||||
@@ -88,12 +69,9 @@ module.exports = {
|
||||
// Webpack 5 does not polyfill node globals, so we do so for those necessary:
|
||||
new ProvidePlugin({
|
||||
// - react-markdown requires process.cwd
|
||||
process: 'process/browser',
|
||||
process: 'process/browser.js',
|
||||
}),
|
||||
// vanilla-extract has poor performance on M1 machines with 'debug' identifiers, so we use 'short' instead.
|
||||
// See https://vanilla-extract.style/documentation/integrations/webpack/#identifiers for docs.
|
||||
// See https://github.com/vanilla-extract-css/vanilla-extract/issues/771#issuecomment-1249524366.
|
||||
new VanillaExtractPlugin({ identifiers: 'short' }),
|
||||
new VanillaExtractPlugin(),
|
||||
new RetryChunkLoadPlugin({
|
||||
cacheBust: `function() {
|
||||
return 'cache-bust=' + Date.now();
|
||||
@@ -109,14 +87,6 @@ module.exports = {
|
||||
// Configure webpack plugins:
|
||||
webpackConfig.plugins = webpackConfig.plugins
|
||||
.map((plugin) => {
|
||||
// Extend process.env with dynamic values (eg commit hash).
|
||||
// This will make dynamic values available to JavaScript only, not to interpolated HTML (ie index.html).
|
||||
if (plugin instanceof DefinePlugin) {
|
||||
Object.assign(plugin.definitions['process.env'], {
|
||||
REACT_APP_GIT_COMMIT_HASH: JSON.stringify(commitHash),
|
||||
})
|
||||
}
|
||||
|
||||
// CSS ordering is mitigated through scoping / naming conventions, so we can ignore order warnings.
|
||||
// See https://webpack.js.org/plugins/mini-css-extract-plugin/#remove-order-warnings.
|
||||
if (plugin instanceof MiniCssExtractPlugin) {
|
||||
@@ -163,13 +133,9 @@ module.exports = {
|
||||
|
||||
// Configure webpack transpilation (create-react-app specifies transpilation rules in a oneOf):
|
||||
webpackConfig.module.rules[1].oneOf = webpackConfig.module.rules[1].oneOf.map((rule) => {
|
||||
// The fallback rule (eg for dependencies).
|
||||
if (rule.loader && rule.loader.match(/babel-loader/) && !rule.include) {
|
||||
// Allow not-fully-specified modules so that legacy packages are still able to build.
|
||||
rule.resolve = { fullySpecified: false }
|
||||
|
||||
// The class properties transform is required for @uniswap/analytics to build.
|
||||
rule.options.plugins.push('@babel/plugin-proposal-class-properties')
|
||||
if (rule.loader && rule.loader.match(/babel-loader/)) {
|
||||
rule.loader = 'swc-loader'
|
||||
delete rule.options
|
||||
}
|
||||
return rule
|
||||
})
|
||||
@@ -177,6 +143,15 @@ module.exports = {
|
||||
// Configure webpack optimization:
|
||||
webpackConfig.optimization = Object.assign(
|
||||
webpackConfig.optimization,
|
||||
{
|
||||
minimize: isProduction,
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
minify: TerserPlugin.swcMinify,
|
||||
parallel: require('os').cpus().length,
|
||||
}),
|
||||
],
|
||||
},
|
||||
isProduction
|
||||
? {
|
||||
splitChunks: {
|
||||
@@ -192,16 +167,15 @@ module.exports = {
|
||||
: {}
|
||||
)
|
||||
|
||||
// Configure webpack caching:
|
||||
webpackConfig.cache = Object.assign(webpackConfig.cache, {
|
||||
cacheDirectory: getCacheDirectory('webpack'),
|
||||
})
|
||||
// Configure webpack resolution. webpackConfig.cache is unused with swc-loader, but the resolver can still cache:
|
||||
webpackConfig.resolve = Object.assign(webpackConfig.resolve, { unsafeCache: true })
|
||||
|
||||
// Ignore failed source mappings to avoid spamming the console.
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
webpackConfig.ignoreWarnings = [/Failed to parse source map/]
|
||||
webpackConfig.ignoreWarnings = [
|
||||
// Source mappings for a package will fail if the package does not provide them, but the build will still succeed,
|
||||
// so it is unnecessary (and bothersome) to log it. This should be turned off when debugging missing sourcemaps.
|
||||
// See https://webpack.js.org/loaders/source-map-loader#ignoring-warnings.
|
||||
/Failed to parse source map/,
|
||||
]
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import codeCoverageTask from '@cypress/code-coverage/task'
|
||||
import { defineConfig } from 'cypress'
|
||||
import { setupHardhatEvents } from 'cypress-hardhat'
|
||||
import { unlinkSync } from 'fs'
|
||||
|
||||
export default defineConfig({
|
||||
projectId: 'yp82ef',
|
||||
@@ -9,22 +7,10 @@ export default defineConfig({
|
||||
chromeWebSecurity: false,
|
||||
experimentalMemoryManagement: true, // better memory management, see https://github.com/cypress-io/cypress/pull/25462
|
||||
retries: { runMode: 2 },
|
||||
videoCompression: false,
|
||||
video: false, // GH provides 2 CPUs, and cypress video eats one up, see https://github.com/cypress-io/cypress/issues/20468#issuecomment-1307608025
|
||||
e2e: {
|
||||
async setupNodeEvents(on, config) {
|
||||
await setupHardhatEvents(on, config)
|
||||
codeCoverageTask(on, config)
|
||||
|
||||
// Delete recorded videos for specs that passed without flakes.
|
||||
on('after:spec', async (spec, results) => {
|
||||
if (results && results.video) {
|
||||
// If there were no failures (including flakes), delete the recorded video.
|
||||
if (!results.tests?.some((test) => test.attempts.some((attempt) => attempt?.state === 'failed'))) {
|
||||
unlinkSync(results.video)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return config
|
||||
},
|
||||
baseUrl: 'http://localhost:3000',
|
||||
|
||||
@@ -52,7 +52,7 @@ This becomes more relevant as you work with data on the blockchain, as you'll ne
|
||||
|
||||
```
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
@@ -68,7 +68,7 @@ cy.hardhat().then(async (hardhat) => {
|
||||
```
|
||||
|
||||
```
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
@@ -87,7 +87,6 @@ cy.hardhat().then(async (hardhat) => {
|
||||
### Working with the blockchain (ie hardhat)
|
||||
|
||||
Our tests use a local hardhat node to simulate blockchain transactions. This can be accessed with `cy.hardhat().then((hardhat) => ...)`.
|
||||
Currently, tests using hardhat must opt-in in when they load the page: `cy.visit('/swap', { ethereum: 'hardhat' })`. This will not be necessary once we've totally migrated to hardhat.
|
||||
|
||||
By default, automining is turned on, so that any transaction that you send to the blockchain is mined immediately. If you want to assert on intermediate states (between sending a transaction and mining it), you can turn off automining: `cy.hardhat({ automine: false })`.
|
||||
|
||||
|
||||
@@ -9,36 +9,29 @@ describe('Add Liquidity', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/500')
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH/500')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.contains('0.05% fee tier')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/add/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'ETH')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'ETH')
|
||||
it('does not crash if token is duplicated', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('not.contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('token not in storage is loaded', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
cy.get('#add-liquidity-input-tokenb .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('single token can be selected', () => {
|
||||
cy.visit('/add/0x07865c6e87b9f70255377e024ace6630c1eaa37f')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'USDC')
|
||||
it('single token can be selected', () => {
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#add-liquidity-input-tokena .token-symbol-container').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it.skip('loads fee tier distribution', () => {
|
||||
it('loads fee tier distribution', () => {
|
||||
cy.fixture('feeTierDistribution.json').then((feeTierDistribution) => {
|
||||
cy.intercept('POST', '/subgraphs/name/uniswap/uniswap-v3', (req: CyHttpMessages.IncomingHttpRequest) => {
|
||||
if (hasQuery(req, 'FeeTierDistributionQuery')) {
|
||||
req.alias = 'FeeTierDistributionQuery'
|
||||
if (hasQuery(req, 'FeeTierDistribution')) {
|
||||
req.alias = 'FeeTierDistribution'
|
||||
|
||||
req.reply({
|
||||
body: {
|
||||
@@ -53,12 +46,57 @@ describe('Add Liquidity', () => {
|
||||
}
|
||||
})
|
||||
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
|
||||
cy.wait('@FeeTierDistributionQuery')
|
||||
|
||||
cy.visit('/add/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.wait('@FeeTierDistribution')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-label').should('contain.text', '0.3% fee tier')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40%')
|
||||
cy.get('#add-liquidity-selected-fee .selected-fee-percentage').should('contain.text', '40% select')
|
||||
})
|
||||
})
|
||||
|
||||
it('disables increment and decrement until initial prices are inputted', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
|
||||
// Min Price increment / decrement buttons should be disabled
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
// Enter min price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(0).type('900').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('not.be.disabled')
|
||||
|
||||
// Repeat for Max Price step counter
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Enter max price, which should enable the buttons
|
||||
cy.get('.rate-input-0').eq(1).type('1100').blur()
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('not.be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('not.be.disabled')
|
||||
})
|
||||
|
||||
it('allows full range selection on new pool creation', () => {
|
||||
// ETH / BITCOIN pool (0.05% tier not created)
|
||||
cy.visit('/add/ETH/0x72e4f9F808C49A2a61dE9C5896298920Dc4EEEa9/500')
|
||||
// Set starting price in order to enable price range step counters
|
||||
cy.get('.start-price-input').type('1000')
|
||||
cy.get('[data-testid="set-full-range"]').click()
|
||||
// Check that the min price is 0 and the max price is infinity
|
||||
cy.get('.rate-input-0').eq(0).should('have.value', '0')
|
||||
cy.get('.rate-input-0').eq(1).should('have.value', '∞')
|
||||
// Increment and decrement buttons are disabled when full range is selected
|
||||
cy.get('[data-testid="increment-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(0).should('be.disabled')
|
||||
cy.get('[data-testid="increment-price-range"]').eq(1).should('be.disabled')
|
||||
cy.get('[data-testid="decrement-price-range"]').eq(1).should('be.disabled')
|
||||
// Check that url params were added
|
||||
cy.url().then((url) => {
|
||||
const params = new URLSearchParams(url)
|
||||
const minPrice = params.get('minPrice')
|
||||
const maxPrice = params.get('maxPrice')
|
||||
// Note: although 0 and ∞ displayed, actual values in query are ticks at limit
|
||||
return minPrice && maxPrice && parseFloat(minPrice) < parseFloat(maxPrice)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('Buy Crypto Modal', () => {
|
||||
|
||||
it('should open and close, mobile viewport', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/')
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.fiatOnRampButtonOnSwap] })
|
||||
|
||||
// Open the fiat onramp modal
|
||||
cy.get(getTestSelector('buy-fiat-button')).click()
|
||||
|
||||
@@ -3,7 +3,7 @@ import { getTestSelector } from '../../utils'
|
||||
describe('Mini Portfolio account drawer', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(/api.uniswap.org\/v1\/graphql/, cy.spy().as('gqlSpy'))
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
})
|
||||
|
||||
it('fetches balances when account button is first hovered', () => {
|
||||
@@ -41,10 +41,11 @@ describe('Mini Portfolio account drawer', () => {
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('I Got Plenty')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/pools.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Pools').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('No pools yet')
|
||||
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/activity.json' })
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/full_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
cy.get(getTestSelector('mini-portfolio-page')).contains('Contract Interaction')
|
||||
})
|
||||
@@ -76,4 +77,36 @@ describe('Mini Portfolio account drawer', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('fetches ENS name', () => {
|
||||
cy.hardhat().then(() => {
|
||||
const haydenAccount = '0x50EC05ADe8280758E2077fcBC08D878D4aef79C3'
|
||||
const haydenENS = 'hayden.eth'
|
||||
|
||||
// Opens the account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
|
||||
// Close account drawer
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// Switch chain to Polygon
|
||||
cy.get(getTestSelector('chain-selector')).eq(1).click()
|
||||
cy.contains('Polygon').click()
|
||||
|
||||
//Reopen account drawer
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
// Simulate wallet changing to Hayden's account
|
||||
cy.window().then((win) => win.ethereum.emit('accountsChanged', [haydenAccount]))
|
||||
|
||||
// Hayden's ENS name should be shown
|
||||
cy.contains(haydenENS).should('exist')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -94,9 +94,7 @@ describe('mini-portfolio activity history', () => {
|
||||
})
|
||||
|
||||
it('should deduplicate activity history by nonce', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`).hardhat({ automine: false })
|
||||
|
||||
// Input swap info.
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1').should('have.value', '1')
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const PUDGY_COLLECTION_ADDRESS = '0xbd3531da5cf5857e7cfaa92426877b022e612cf8'
|
||||
const BONSAI_COLLECTION_ADDRESS = '0xec9c519d49856fd2f8133a0741b4dbe002ce211b'
|
||||
|
||||
describe('Testing nfts', () => {
|
||||
it('should load nft leaderboard', () => {
|
||||
@@ -38,7 +37,10 @@ describe('Testing nfts', () => {
|
||||
})
|
||||
|
||||
it('should toggle buy now on details page', () => {
|
||||
cy.visit(`#/nfts/asset/${BONSAI_COLLECTION_ADDRESS}/7580`)
|
||||
cy.visit(`/#/nfts/collection/${PUDGY_COLLECTION_ADDRESS}`)
|
||||
cy.get(getTestSelector('nft-filter')).first().click()
|
||||
cy.get(getTestSelector('nft-collection-filter-buy-now')).click()
|
||||
cy.get(getTestSelector('nft-collection-asset')).first().click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('exist')
|
||||
cy.get(getTestSelector('nft-details-description')).click()
|
||||
cy.get(getTestSelector('nft-details-description-text')).should('not.exist')
|
||||
@@ -50,7 +52,7 @@ describe('Testing nfts', () => {
|
||||
cy.visit('/')
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('NFTs').click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).click()
|
||||
cy.get(getTestSelector('mini-portfolio-nft')).first().click()
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -17,9 +17,7 @@ function initiateSwap() {
|
||||
describe('Permit2', () => {
|
||||
function setupInputs(inputToken: Token, outputToken: Token) {
|
||||
// Sets up a swap between inputToken and outputToken.
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
})
|
||||
cy.visit(`/swap/?inputCurrency=${inputToken.address}&outputCurrency=${outputToken.address}`)
|
||||
cy.get('#swap-currency-input .token-amount-input').type('0.01')
|
||||
}
|
||||
|
||||
@@ -29,7 +27,7 @@ describe('Permit2', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getTokenAllowanceForPermit2({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Token allowace: ${allowance.toString()}` })
|
||||
Cypress.log({ name: `Token allowance: ${allowance.toString()}` })
|
||||
cy.wrap(allowance).should('deep.equal', MaxUint256)
|
||||
})
|
||||
}
|
||||
@@ -39,7 +37,7 @@ describe('Permit2', () => {
|
||||
cy.hardhat()
|
||||
.then(({ approval, wallet }) => approval.getPermit2Allowance({ owner: wallet, token: inputToken }))
|
||||
.then((allowance) => {
|
||||
Cypress.log({ name: `Permit2 allowace: ${allowance.amount.toString()}` })
|
||||
Cypress.log({ name: `Permit2 allowance: ${allowance.amount.toString()}` })
|
||||
cy.wrap(allowance.amount).should('deep.equal', MaxUint160)
|
||||
// Asserts that the on-chain expiration is in 30 days, within a tolerance of 40 seconds.
|
||||
const THIRTY_DAYS_SECONDS = 2_592_000
|
||||
@@ -77,8 +75,9 @@ describe('Permit2', () => {
|
||||
cy.contains('Allow DAI to be used for swapping')
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Swap submitted')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -101,7 +100,7 @@ describe('Permit2', () => {
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
@@ -144,7 +143,7 @@ describe('Permit2', () => {
|
||||
// Verify transaction
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
})
|
||||
@@ -160,7 +159,7 @@ describe('Permit2', () => {
|
||||
initiateSwap()
|
||||
|
||||
// Verify transaction
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
})
|
||||
|
||||
@@ -199,7 +198,7 @@ describe('Permit2', () => {
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -233,7 +232,7 @@ describe('Permit2', () => {
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
@@ -251,7 +250,7 @@ describe('Permit2', () => {
|
||||
|
||||
// Verify permit2 approval
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Success')
|
||||
cy.contains('Swap success!')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
expectPermit2AllowanceForUniversalRouterToBeMax(DAI)
|
||||
})
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
describe('Remove Liquidity', () => {
|
||||
it('eth remove', () => {
|
||||
it('loads the token pair', () => {
|
||||
cy.visit('/remove/v2/ETH/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'ETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('eth remove swap order', () => {
|
||||
cy.visit('/remove/v2/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/ETH')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'UNI')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'ETH')
|
||||
})
|
||||
|
||||
it('loads the two correct tokens', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'UNI')
|
||||
})
|
||||
|
||||
it('does not crash if ETH is duplicated', () => {
|
||||
cy.visit('/remove/v2/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6/0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6')
|
||||
cy.get('#remove-liquidity-tokena-symbol').should('contain.text', 'WETH')
|
||||
cy.get('#remove-liquidity-tokenb-symbol').should('contain.text', 'WETH')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { BigNumber } from '@ethersproject/bignumber'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { CurrencyAmount } from '@uniswap/sdk-core'
|
||||
|
||||
import { DEFAULT_DEADLINE_FROM_NOW } from '../../../src/constants/misc'
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { DAI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getBalance, getTestSelector } from '../../utils'
|
||||
|
||||
const UNI_MAINNET = UNI[ChainId.MAINNET]
|
||||
|
||||
describe('Swap errors', () => {
|
||||
it('wallet rejection', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat().then((hardhat) => {
|
||||
// Stub the wallet to reject any transaction.
|
||||
cy.stub(hardhat.wallet, 'sendTransaction').log(false).rejects(new Error('user cancelled'))
|
||||
@@ -30,7 +28,7 @@ describe('Swap errors', () => {
|
||||
})
|
||||
|
||||
it('transaction past deadline', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Enter amount to swap
|
||||
@@ -64,10 +62,19 @@ describe('Swap errors', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it.skip('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${UNI_MAINNET.address}`, { ethereum: 'hardhat' })
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
it('slippage failure', () => {
|
||||
cy.visit(`/swap?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 500e6))
|
||||
await hardhat.mine()
|
||||
await Promise.all([
|
||||
hardhat.approval.setTokenAllowanceForPermit2({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
hardhat.approval.setPermit2Allowance({ owner: hardhat.wallet, token: USDC_MAINNET }),
|
||||
])
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
getBalance(DAI).then((initialBalance) => {
|
||||
// Gas estimation fails for this transaction (that would normally fail), so we stub it.
|
||||
cy.hardhat().then((hardhat) => {
|
||||
const send = cy.stub(hardhat.provider, 'send').log(false)
|
||||
@@ -90,7 +97,9 @@ describe('Swap errors', () => {
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction').wait('@eth_getTransactionReceipt')
|
||||
cy.contains('Swap submitted')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
if (i === 0) {
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
}
|
||||
}
|
||||
cy.get(getTestSelector('web3-status-connected')).should('contain', '2 Pending')
|
||||
|
||||
@@ -98,10 +107,13 @@ describe('Swap errors', () => {
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.wait('@eth_getTransactionReceipt')
|
||||
|
||||
// Verify transaction did not occur
|
||||
cy.contains('Swap failed')
|
||||
|
||||
// Verify only 1 transaction occurred
|
||||
cy.get(getTestSelector('web3-status-connected')).should('not.contain', 'Pending')
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.get(getTestSelector('popups')).contains('Swap failed')
|
||||
getBalance(UNI_MAINNET).should('eq', initialBalance)
|
||||
getBalance(DAI).should('be.closeTo', initialBalance + 200, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,9 +3,10 @@ import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap settings', () => {
|
||||
it('Opens and closes the settings menu', () => {
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled], ethereum: 'hardhat' })
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled] })
|
||||
cy.contains('Settings').should('not.exist')
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('mobile-settings-menu')).should('not.exist')
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
@@ -13,4 +14,16 @@ describe('Swap settings', () => {
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Settings').should('not.exist')
|
||||
})
|
||||
|
||||
it('should open the mobile settings menu', () => {
|
||||
cy.viewport('iphone-6')
|
||||
cy.visit('/swap', { featureFlags: [FeatureFlag.uniswapXEnabled] })
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('mobile-settings-menu')).should('exist')
|
||||
cy.contains('Max slippage').should('exist')
|
||||
cy.contains('Transaction deadline').should('exist')
|
||||
cy.contains('UniswapX').should('exist')
|
||||
cy.contains('Local routing').should('exist')
|
||||
cy.get(getTestSelector('mobile-settings-scrim')).click()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
import { UNI, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
@@ -52,18 +53,25 @@ describe('Swap', () => {
|
||||
})
|
||||
|
||||
it('swaps ETH for USDC', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
cy.hardhat({ automine: false })
|
||||
getBalance(USDC_MAINNET).then((initialBalance) => {
|
||||
// Select USDC
|
||||
cy.get('#swap-currency-output .open-currency-select-button').click()
|
||||
cy.get(getTestSelector('token-search-input')).type(USDC_MAINNET.address)
|
||||
cy.contains('USDC').click()
|
||||
cy.get(getTestSelector('common-base-USDC')).click()
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Review swap')
|
||||
|
||||
76
cypress/e2e/swap/swapFlowLogging.test.ts
Normal file
76
cypress/e2e/swap/swapFlowLogging.test.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
|
||||
import { USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('swap flow logging', () => {
|
||||
it('completes two swaps and verifies the TTS logging for the first, plus all intermediate steps along the way', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
cy.hardhat()
|
||||
|
||||
// First swap in the session:
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify first swap action
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_FIRST_ACTION).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_first_swap_action')
|
||||
cy.wrap(event.event_properties.time_to_first_swap_action).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_swap_action).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Verify Swap Quote
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH).then((event: any) => {
|
||||
// Price quotes don't include these values, so we only verify the types if they exist
|
||||
if (event.event_properties.time_to_first_quote_request) {
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.gte', 0)
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.gte', 0)
|
||||
}
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_swap')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_swap).should('be.gte', 0)
|
||||
cy.wrap(event.event_properties).should('have.property', 'time_to_swap_since_first_input')
|
||||
cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.time_to_swap_since_first_input).should('be.gte', 0)
|
||||
})
|
||||
|
||||
// Second swap in the session:
|
||||
// Enter amount to swap (different from first trade, to trigger a new quote request)
|
||||
cy.get('#swap-currency-output .token-amount-input').clear().type('10').should('have.value', '10')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
|
||||
// Verify second Swap Quote
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_FETCH).then((event: any) => {
|
||||
// Price quotes don't include these values, so we only verify the types if they exist
|
||||
if (event.event_properties.time_to_first_quote_request) {
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request).should('be.undefined')
|
||||
cy.wrap(event.event_properties.time_to_first_quote_request_since_first_input).should('be.undefined')
|
||||
}
|
||||
})
|
||||
|
||||
// Submit transaction
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
cy.get(getTestSelector('popups')).contains('Swapped')
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_TRANSACTION_COMPLETED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap')
|
||||
cy.wrap(event.event_properties).should('not.have.property', 'time_to_swap_since_first_input')
|
||||
})
|
||||
})
|
||||
})
|
||||
27
cypress/e2e/swap/unconnected.test.ts
Normal file
27
cypress/e2e/swap/unconnected.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { SwapEventName } from '@uniswap/analytics-events'
|
||||
import { USDC_MAINNET } from 'constants/tokens'
|
||||
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
describe('Swap inputs with no wallet connected', () => {
|
||||
it('can input and load a quote with no wallet connected', () => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${USDC_MAINNET.address}`)
|
||||
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
// click twice, first time to show confirmation, second to confirm
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('wallet-disconnect')).should('contain', 'Disconnect')
|
||||
cy.get(getTestSelector('wallet-disconnect')).click()
|
||||
cy.get(getTestSelector('close-account-drawer')).click()
|
||||
|
||||
// Enter amount to swap
|
||||
cy.get('#swap-currency-output .token-amount-input').type('1').should('have.value', '1')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('not.have.value', '')
|
||||
// Verify logging
|
||||
cy.waitForAmplitudeEvent(SwapEventName.SWAP_QUOTE_RECEIVED).then((event: any) => {
|
||||
cy.wrap(event.event_properties).should('have.property', 'quote_latency_milliseconds')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.a', 'number')
|
||||
cy.wrap(event.event_properties.quote_latency_milliseconds).should('be.gte', 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
355
cypress/e2e/swap/uniswapx.test.ts
Normal file
355
cypress/e2e/swap/uniswapx.test.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import { ChainId, CurrencyAmount } from '@uniswap/sdk-core'
|
||||
|
||||
import { DAI, nativeOnChain, USDC_MAINNET } from '../../../src/constants/tokens'
|
||||
import { getTestSelector } from '../../utils'
|
||||
|
||||
const QuoteEndpoint = 'https://api.uniswap.org/v2/quote'
|
||||
const QuoteWhereUniswapXIsBetter = 'uniswapx/quote1.json'
|
||||
const QuoteWithEthInput = 'uniswapx/quote2.json'
|
||||
|
||||
const OrderSubmissionEndpoint = 'https://api.uniswap.org/v2/order'
|
||||
|
||||
const OrderStatusEndpoint =
|
||||
'https://api.uniswap.org/v2/orders?swapper=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266&orderHashes=0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19'
|
||||
|
||||
/** Stubs the provider to return a tx receipt corresponding to the mock filled uniswapx order's txHash */
|
||||
function stubSwapTxReceipt() {
|
||||
cy.hardhat().then((hardhat) => {
|
||||
cy.fixture('uniswapx/fillTransactionReceipt.json').then((mockTxReceipt) => {
|
||||
const getTransactionReceiptStub = cy.stub(hardhat.provider, 'getTransactionReceipt').log(false)
|
||||
getTransactionReceiptStub.withArgs(mockTxReceipt.transactionHash).resolves(mockTxReceipt)
|
||||
getTransactionReceiptStub.callThrough()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
describe('UniswapX Toggle', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
})
|
||||
|
||||
it('only displays uniswapx ui when setting is on', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
|
||||
// UniswapX UI should not be visible
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
|
||||
|
||||
// Opt-in to UniswapX
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// UniswapX UI should be visible
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
|
||||
})
|
||||
|
||||
it('prompts opt-in if UniswapX is better', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
|
||||
// UniswapX should not display in gas estimate row before opt-in
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('not.exist')
|
||||
|
||||
// UniswapX mustache should be visible
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Opt-in dialog should now be hidden
|
||||
cy.contains('Try it now').should('not.be.visible')
|
||||
|
||||
// UniswapX should display in gas estimate row
|
||||
cy.get(getTestSelector('gas-estimate-uniswapx-icon')).should('exist')
|
||||
|
||||
// Opt-in dialog should not reappear if user manually toggles UniswapX off
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.get(getTestSelector('toggle-uniswap-x-button')).click()
|
||||
cy.get(getTestSelector('open-settings-dialog-button')).click()
|
||||
cy.contains('Try it now').should('not.be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
describe('UniswapX Orders', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.hardhat().then((hardhat) => hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8)))
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
})
|
||||
|
||||
it('can swap using uniswapX', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
|
||||
it('renders proper view if uniswapx order expires', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Return expired order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' })
|
||||
|
||||
// Verify swap failure message
|
||||
cy.contains('Swap expired')
|
||||
})
|
||||
|
||||
it('renders proper view if uniswapx order has insufficient funds', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Return insufficient_funds order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/insufficientFundsStatusResponse.json' })
|
||||
|
||||
// Verify swap failure message
|
||||
cy.contains('Insufficient funds')
|
||||
})
|
||||
})
|
||||
|
||||
describe('UniswapX Eth Input', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWithEthInput })
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
// Turn off automine so that intermediate screens are available to assert on.
|
||||
cy.hardhat({ automine: false }).then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(nativeOnChain(ChainId.MAINNET), 2e18))
|
||||
await hardhat.mine()
|
||||
})
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.visit(`/swap/?inputCurrency=ETH&outputCurrency=${DAI.address}`)
|
||||
})
|
||||
|
||||
it('can swap using uniswapX with ETH as input', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('1')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Prompt ETH wrap to use for order
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.contains('Wrap ETH')
|
||||
|
||||
// Wrap ETH
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.contains('Pending...')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
cy.contains('Wrapped')
|
||||
|
||||
// Approve WETH spend
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Verify signed order submission
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
|
||||
it('switches swap input to WETH after wrap', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('1')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Prompt ETH wrap and confirm
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
|
||||
// Close review modal before wrap is confirmed on chain
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Confirm wrap is successful and WETH is now input token
|
||||
cy.contains('Wrapped')
|
||||
cy.contains('WETH')
|
||||
|
||||
// Reopen review modal and continue swap
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Approve WETH spend
|
||||
cy.wait('@eth_sendRawTransaction')
|
||||
cy.hardhat().then((hardhat) => hardhat.mine())
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.contains('Swap submitted')
|
||||
cy.contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Verify swap success
|
||||
cy.contains('Swapped')
|
||||
})
|
||||
})
|
||||
|
||||
describe('UniswapX activity history', () => {
|
||||
beforeEach(() => {
|
||||
cy.intercept(QuoteEndpoint, { fixture: QuoteWhereUniswapXIsBetter })
|
||||
cy.intercept(OrderSubmissionEndpoint, { fixture: 'uniswapx/orderResponse.json' })
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/openStatusResponse.json' })
|
||||
|
||||
stubSwapTxReceipt()
|
||||
|
||||
cy.hardhat().then(async (hardhat) => {
|
||||
await hardhat.fund(hardhat.wallet, CurrencyAmount.fromRawAmount(USDC_MAINNET, 3e8))
|
||||
})
|
||||
cy.visit(`/swap/?inputCurrency=${USDC_MAINNET.address}&outputCurrency=${DAI.address}`)
|
||||
})
|
||||
|
||||
it('can view UniswapX order status progress in activity', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Open mini portfolio and navigate to activity history
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Open pending order modal
|
||||
cy.contains('Swapping').click()
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapping')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Learn more about swapping with UniswapX')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapped')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('View on Explorer')
|
||||
})
|
||||
|
||||
it('can view UniswapX order status progress in activity upon expiry', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Open mini portfolio and navigate to activity history
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.intercept(/graphql/, { fixture: 'mini-portfolio/empty_activity.json' })
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Open pending order modal
|
||||
cy.contains('Swapping').click()
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swapping')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/expiredStatusResponse.json' })
|
||||
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('Swap expired')
|
||||
cy.get(getTestSelector('offchain-activity-modal')).contains('learn more')
|
||||
})
|
||||
|
||||
it('deduplicates remote vs local uniswapx orders', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
cy.wait('@eth_signTypedData_v4')
|
||||
cy.get(getTestSelector('confirmation-close-icon')).click()
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
cy.contains('Swapped')
|
||||
|
||||
// Open mini portfolio
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
|
||||
cy.fixture('mini-portfolio/uniswapx_activity.json').then((uniswapXActivity) => {
|
||||
// Replace fixture's timestamp with current time
|
||||
uniswapXActivity.data.portfolios[0].assetActivities[0].timestamp = Date.now() / 1000
|
||||
cy.intercept(/graphql/, uniswapXActivity)
|
||||
})
|
||||
|
||||
// Open activity history
|
||||
cy.get(getTestSelector('mini-portfolio-navbar')).contains('Activity').click()
|
||||
|
||||
// Ensure gql and local order have been deduped, such that there is only one swap activity listed
|
||||
cy.get(getTestSelector('activity-content')).contains('Swapped').should('have.length', 1)
|
||||
})
|
||||
|
||||
it('balances should refetch after uniswapx swap', () => {
|
||||
// Setup a swap
|
||||
cy.get('#swap-currency-input .token-amount-input').type('300')
|
||||
cy.contains('Try it now').click()
|
||||
|
||||
const gqlSpy = cy.spy().as('gqlSpy')
|
||||
cy.intercept(/graphql/, (req) => {
|
||||
// Spy on request frequency
|
||||
req.on('response', gqlSpy)
|
||||
// Reply with a fixture to speed up test
|
||||
req.reply({
|
||||
fixture: 'mini-portfolio/tokens.json',
|
||||
})
|
||||
})
|
||||
|
||||
// Expect balances to fetch upon opening mini portfolio
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get('@gqlSpy').should('have.been.calledOnce')
|
||||
|
||||
// Submit uniswapx order signature
|
||||
cy.get('#swap-button').click()
|
||||
cy.contains('Confirm swap').click()
|
||||
|
||||
// Expect balances to refetch after approval
|
||||
cy.get('@gqlSpy').should('have.been.calledTwice')
|
||||
|
||||
// Return filled order status from uniswapx api
|
||||
cy.intercept(OrderStatusEndpoint, { fixture: 'uniswapx/filledStatusResponse.json' })
|
||||
|
||||
// Expect balances to refetch after swap
|
||||
cy.get('@gqlSpy').should('have.been.calledThrice')
|
||||
})
|
||||
})
|
||||
@@ -6,9 +6,7 @@ const WETH = WETH9[ChainId.MAINNET]
|
||||
|
||||
describe('Swap wrap', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`, { ethereum: 'hardhat' }).hardhat({
|
||||
automine: false,
|
||||
})
|
||||
cy.visit(`/swap?inputCurrency=ETH&outputCurrency=${WETH.address}`).hardhat({ automine: false })
|
||||
})
|
||||
|
||||
it('ETH to wETH is same value (wrapped swaps have no price impact)', () => {
|
||||
|
||||
@@ -93,9 +93,7 @@ describe('Token details', () => {
|
||||
beforeEach(() => {
|
||||
// On mobile widths, we just link back to /swap instead of rendering the swap component.
|
||||
cy.viewport(1200, 800)
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`, {
|
||||
ethereum: 'hardhat',
|
||||
}).then(() => {
|
||||
cy.visit(`/tokens/ethereum/${UNI_MAINNET.address}`).then(() => {
|
||||
cy.wait('@eth_blockNumber')
|
||||
cy.scrollTo('top')
|
||||
})
|
||||
@@ -145,7 +143,7 @@ describe('Token details', () => {
|
||||
})
|
||||
|
||||
it('should show a L2 token even if the user is connected to a different network', () => {
|
||||
cy.visit('/tokens', { ethereum: 'hardhat' })
|
||||
cy.visit('/tokens')
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-option-arbitrum')).click()
|
||||
cy.get(getTestSelector('tokens-network-filter-selected')).should('contain', 'Arbitrum')
|
||||
|
||||
@@ -18,7 +18,9 @@ describe('Token explore filter', () => {
|
||||
searchFor('dao')
|
||||
|
||||
cy.get('@filteredTokens').then((filteredTokens) => {
|
||||
cy.get('[data-cy="token-name"]').should('deep.equal', filteredTokens)
|
||||
cy.get('[data-cy="token-name"]').then((tokens) => {
|
||||
cy.wrap(Array.from(tokens)).should('deep.equal', Array.from(filteredTokens))
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('Token explore', () => {
|
||||
.then(function ($elem) {
|
||||
cy.wrap($elem.text()).as('yearlyEthVol')
|
||||
})
|
||||
expect(cy.get('@dailyEthVol')).to.not.equal(cy.get('@yearlyEthVol'))
|
||||
cy.get('@dailyEthVol').should('not.equal', cy.get('@yearlyEthVol'))
|
||||
})
|
||||
|
||||
it('should navigate to token detail page when row clicked', () => {
|
||||
|
||||
@@ -1,45 +1,72 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { UNI } from 'constants/tokens'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
const UNI_ADDRESS = UNI[ChainId.MAINNET].address.toLowerCase()
|
||||
|
||||
describe('Universal search bar', () => {
|
||||
function openSearch() {
|
||||
// can't just type "/" because on mobile it doesn't respond to that
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/')
|
||||
cy.get('[data-cy="magnifying-icon"]').parent().eq(1).click()
|
||||
})
|
||||
|
||||
it('should yield clickable result for regular token or nft collection search term', () => {
|
||||
// Search for uni token by name.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('uni')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]')
|
||||
function getSearchBar() {
|
||||
return cy.get('[data-cy="search-bar-input"]').last()
|
||||
}
|
||||
|
||||
it('should yield clickable result that is then added to recent searches', () => {
|
||||
// Search for UNI token by name.
|
||||
openSearch()
|
||||
getSearchBar().clear().type('uni')
|
||||
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('contain.text', 'Uniswap')
|
||||
.and('contain.text', 'UNI')
|
||||
.and('contain.text', '$')
|
||||
.and('contain.text', '%')
|
||||
cy.get('[data-cy="searchbar-token-row-UNI"]').first().click()
|
||||
.click()
|
||||
cy.location('hash').should('equal', '#/tokens/ethereum/0x1f9840a85d5af5bf1d1762f925bdaddc4201f984')
|
||||
})
|
||||
|
||||
it.skip('should show recent tokens and popular tokens with empty search term', () => {
|
||||
cy.get('[data-cy="magnifying-icon"]')
|
||||
.parent()
|
||||
.then(($navIcon) => {
|
||||
$navIcon.click()
|
||||
})
|
||||
// Recently searched UNI token should exist.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear()
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Recent searches')
|
||||
.find('[data-cy="searchbar-token-row-UNI"]')
|
||||
openSearch()
|
||||
cy.get(getTestSelector('searchbar-dropdown'))
|
||||
.contains(getTestSelector('searchbar-dropdown'), 'Recent searches')
|
||||
.find(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
.should('exist')
|
||||
|
||||
// Most popular 3 tokens should be shown.
|
||||
cy.get('[data-cy="searchbar-dropdown"]')
|
||||
.contains('[data-cy="searchbar-dropdown"]', 'Popular tokens')
|
||||
.find('[data-cy^="searchbar-token-row"]')
|
||||
.its('length')
|
||||
.should('be.eq', 3)
|
||||
})
|
||||
|
||||
it.skip('should show blocked badge when blocked token is searched for', () => {
|
||||
// Search for mTSLA, which is a blocked token.
|
||||
cy.get('[data-cy="search-bar-input"]').last().clear().type('mtsla')
|
||||
cy.get('[data-cy="searchbar-token-row-mTSLA"]').find('[data-cy="blocked-icon"]').should('exist')
|
||||
})
|
||||
it(
|
||||
'should go to the selected result when recent results are shown',
|
||||
// this test is experiencing flake despite being correct, i can see the right value in DOM
|
||||
// but for some reason cypress doesn't find it, so adding retries for now :/
|
||||
{
|
||||
// @ts-ignore see https://uniswapteam.slack.com/archives/C047U65H422/p1691455547556309
|
||||
// basically cypress has bad types due to overlap with jest and you just have to deal with it
|
||||
// i tried removing jest types but still happens
|
||||
retries: {
|
||||
runMode: 3,
|
||||
openMode: 3,
|
||||
},
|
||||
},
|
||||
() => {
|
||||
// Seed recent results with UNI.
|
||||
openSearch()
|
||||
getSearchBar().type('uni')
|
||||
cy.get(getTestSelector(`searchbar-token-row-ETHEREUM-${UNI_ADDRESS}`))
|
||||
getSearchBar().clear().type('{esc}')
|
||||
|
||||
// Search a different token by name.
|
||||
openSearch()
|
||||
getSearchBar().type('eth')
|
||||
cy.get(getTestSelector('searchbar-token-row-ETHEREUM-NATIVE'))
|
||||
|
||||
// Validate that we go to the searched/selected result.
|
||||
getSearchBar().type('{enter}')
|
||||
cy.url().should('contain', 'tokens/ethereum/NATIVE')
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DISCONNECTED_WALLET_USER_STATE } from '../../utils/user-state'
|
||||
|
||||
describe('disconnect wallet', () => {
|
||||
it('should clear state', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
cy.get('#swap-currency-input .token-amount-input').clear().type('1')
|
||||
|
||||
// Verify wallet is connected
|
||||
@@ -22,13 +22,13 @@ describe('disconnect wallet', () => {
|
||||
cy.contains('Connect Wallet')
|
||||
|
||||
// Verify swap input is cleared
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('connect wallet', () => {
|
||||
it('should load state', () => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat', userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
cy.visit('/swap', { userState: DISCONNECTED_WALLET_USER_STATE })
|
||||
|
||||
// Connect the wallet
|
||||
cy.get(getTestSelector('navbar-connect-wallet')).contains('Connect').click()
|
||||
|
||||
@@ -12,7 +12,7 @@ function switchChain(chain: string) {
|
||||
|
||||
describe('network switching', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
})
|
||||
|
||||
@@ -111,6 +111,7 @@ describe('network switching', () => {
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.url().should('contain', 'chain=polygon')
|
||||
|
||||
// Verify that the input/output fields were reset
|
||||
cy.get('#swap-currency-input .token-amount-input').should('have.value', '')
|
||||
@@ -122,9 +123,22 @@ describe('network switching', () => {
|
||||
|
||||
describe('network switching from URL param', () => {
|
||||
it('should switch network from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon', { ethereum: 'hardhat' })
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
})
|
||||
|
||||
it('should be able to switch network after loading from URL param', () => {
|
||||
cy.visit('/swap?chain=polygon')
|
||||
cy.get(getTestSelector('web3-status-connected'))
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Polygon')
|
||||
|
||||
// switching to another chain clears query param
|
||||
switchChain('Ethereum')
|
||||
cy.wait('@wallet_switchEthereumChain')
|
||||
waitsForActiveChain('Ethereum')
|
||||
cy.url().should('not.contain', 'chain=polygon')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FeatureFlag } from 'featureFlags'
|
||||
|
||||
import { getTestSelector } from '../utils'
|
||||
|
||||
describe('Wallet Dropdown', () => {
|
||||
@@ -21,10 +23,14 @@ describe('Wallet Dropdown', () => {
|
||||
})
|
||||
}
|
||||
|
||||
function itChangesLocale() {
|
||||
function itChangesLocale({ featureFlag = false }: { featureFlag?: boolean } = {}) {
|
||||
it('should change locale', () => {
|
||||
cy.contains('Uniswap available in: English').should('not.exist')
|
||||
|
||||
if (featureFlag) {
|
||||
cy.get(getTestSelector('language-settings-button')).click()
|
||||
}
|
||||
|
||||
cy.get(getTestSelector('wallet-language-item')).contains('Afrikaans').click({ force: true })
|
||||
cy.location('hash').should('match', /\?lng=af-ZA$/)
|
||||
cy.contains('Uniswap available in: English')
|
||||
@@ -45,6 +51,15 @@ describe('Wallet Dropdown', () => {
|
||||
itChangesLocale()
|
||||
})
|
||||
|
||||
describe('should change locale with feature flag', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/', { featureFlags: [FeatureFlag.currencyConversion] })
|
||||
cy.get(getTestSelector('web3-status-connected')).click()
|
||||
cy.get(getTestSelector('wallet-settings')).click()
|
||||
})
|
||||
itChangesLocale({ featureFlag: true })
|
||||
})
|
||||
|
||||
describe('testnet toggle', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/swap')
|
||||
|
||||
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
12
cypress/fixtures/mini-portfolio/empty_activity.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
1
cypress/fixtures/mini-portfolio/pools.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
File diff suppressed because one or more lines are too long
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
102
cypress/fixtures/mini-portfolio/uniswapx_activity.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"data": {
|
||||
"portfolios": [
|
||||
{
|
||||
"id": "UG9ydGZvbGlvOjB4ZjM5RmQ2ZTUxYWFkODhGNkY0Y2U2YUI4ODI3Mjc5Y2ZmRmI5MjI2Ng==",
|
||||
"assetActivities": [
|
||||
{
|
||||
"id": "QXNzZXRBY3Rpdml0eTpWSEpoYm5OaFkzUnBiMjQ2TUhnNE9EZGpOemN5TlRRNU1qWTVNVEkwWVRkbVpUTXlNams1TjJJNU0yUTJabUV3TjJObE1UQXhOamxrTjJJd1pXUXhObUV6TldabU16SmtOMk13TWpBeVh6QjRaREkzTXpnek1EUTRaalF4WldZMlpXRXhaV1EzWWpBeFltVTVOemRqTjJVME1HSXdaRGswTmw4d2VEUTNZVFF5TVdKalpXTTJORE5oWWpSallURmpZamc0TmpOaU4yWm1PV0ppWm1SaU5HVmlNVE09",
|
||||
"timestamp": 1691001923,
|
||||
"type": "SWAP_ORDER",
|
||||
"chain": "ETHEREUM",
|
||||
"details": {
|
||||
"__typename": "TransactionDetails",
|
||||
"id": "VHJhbnNhY3Rpb246MHg4ODdjNzcyNTQ5MjY5MTI0YTdmZTMyMjk5N2I5M2Q2ZmEwN2NlMTAxNjlkN2IwZWQxNmEzNWZmMzJkN2MwMjAyXzB4ZDI3MzgzMDQ4ZjQxZWY2ZWExZWQ3YjAxYmU5NzdjN2U0MGIwZDk0Nl8weDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTM=",
|
||||
"type": "SWAP_ORDER",
|
||||
"from": "0xd27383048f41ef6ea1ed7b01be977c7e40b0d946",
|
||||
"to": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"hash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"nonce": 439,
|
||||
"status": "CONFIRMED"
|
||||
},
|
||||
"assetChanges": [
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDgwYmVjYjgwOGJmYWRlNDE0MzE4M2U1OGQxOGYyMDgwZTg0ZTU3YTFfMHg0N2E0MjFiY2VjNjQzYWI0Y2ExY2I4ODYzYjdmZjliYmZkYjRlYjEzXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHhhMGI4Njk5MWM2MjE4YjM2YzFkMTlkNGEyZTllYjBjZTM2MDZlYjQ4",
|
||||
"name": "USD Coin",
|
||||
"symbol": "USDC",
|
||||
"address": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
|
||||
"decimals": 6,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4YTBiODY5OTFjNjIxOGIzNmMxZDE5ZDRhMmU5ZWIwY2UzNjA2ZWI0OA==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHhBMGI4Njk5MWM2MjE4YjM2YzFkMTlENGEyZTlFYjBjRTM2MDZlQjQ4L2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "300.0",
|
||||
"sender": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"recipient": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"direction": "OUT",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjMwMC4xNDkxNTIwOTE5NDE2M19VU0Q=",
|
||||
"currency": "USD",
|
||||
"value": 300.14915209194163,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
},
|
||||
{
|
||||
"__typename": "TokenTransfer",
|
||||
"id": "VG9rZW5UcmFuc2ZlcjoweDQ3YTQyMWJjZWM2NDNhYjRjYTFjYjg4NjNiN2ZmOWJiZmRiNGViMTNfMHg4MGJlY2I4MDhiZmFkZTQxNDMxODNlNThkMThmMjA4MGU4NGU1N2ExXzB4ODg3Yzc3MjU0OTI2OTEyNGE3ZmUzMjI5OTdiOTNkNmZhMDdjZTEwMTY5ZDdiMGVkMTZhMzVmZjMyZDdjMDIwMg==",
|
||||
"asset": {
|
||||
"id": "VG9rZW46RVRIRVJFVU1fMHg2YjE3NTQ3NGU4OTA5NGM0NGRhOThiOTU0ZWVkZWFjNDk1MjcxZDBm",
|
||||
"name": "Dai Stablecoin",
|
||||
"symbol": "DAI",
|
||||
"address": "0x6b175474e89094c44da98b954eedeac495271d0f",
|
||||
"decimals": 18,
|
||||
"chain": "ETHEREUM",
|
||||
"standard": null,
|
||||
"project": {
|
||||
"id": "VG9rZW5Qcm9qZWN0OkVUSEVSRVVNXzB4NmIxNzU0NzRlODkwOTRjNDRkYTk4Yjk1NGVlZGVhYzQ5NTI3MWQwZg==",
|
||||
"isSpam": false,
|
||||
"logo": {
|
||||
"id": "SW1hZ2U6aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL1VuaXN3YXAvYXNzZXRzL21hc3Rlci9ibG9ja2NoYWlucy9ldGhlcmV1bS9hc3NldHMvMHg2QjE3NTQ3NEU4OTA5NEM0NERhOThiOTU0RWVkZUFDNDk1MjcxZDBGL2xvZ28ucG5n",
|
||||
"url": "https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png",
|
||||
"__typename": "Image"
|
||||
},
|
||||
"__typename": "TokenProject"
|
||||
},
|
||||
"__typename": "Token"
|
||||
},
|
||||
"tokenStandard": "ERC20",
|
||||
"quantity": "280.573117586837733376",
|
||||
"sender": "0x47a421bcec643ab4ca1cb8863b7ff9bbfdb4eb13",
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"direction": "IN",
|
||||
"transactedValue": {
|
||||
"id": "QW1vdW50OjI4MC42ODc3OTU0NTg2ODE4X1VTRA==",
|
||||
"currency": "USD",
|
||||
"value": 280.6877954586818,
|
||||
"__typename": "Amount"
|
||||
}
|
||||
}
|
||||
],
|
||||
"__typename": "AssetActivity"
|
||||
}
|
||||
],
|
||||
"__typename": "Portfolio"
|
||||
}
|
||||
]
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/expiredStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "expired",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
114
cypress/fixtures/uniswapx/fillTransactionReceipt.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"to": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"from": "0xa17Fbb0b5a251A7ACA3BD7377e7eCC4F700A2C09",
|
||||
"contractAddress": null,
|
||||
"transactionIndex": 61,
|
||||
"gasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x03e0c8"
|
||||
},
|
||||
"logsBloom":
|
||||
"0x00000000000000000000008000200100000020000000000000000000000000000000000000000000000000010000000000000000000020000000000001000000000280000000000808000008000000000000000000000000000000000000200010000000100000000008000000000004402000080000000000000010000800000000000000000800000800000000000000000000010000000000000000000000000000000000200000000000005000000000000000000000000000000000000000000002000000000000000000000000040002000000000000000100000000090000000400000000000400000020080000000000000000000000000000000000",
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c",
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"logs": [
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 103,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xbD7F9D0239f81C94b728d827a87b9864972661eC",
|
||||
"topics": [
|
||||
"0x78ad7ec0e9f89e74012afa58738b6b661c024cb0fd185ee2f616c0a28924bd66",
|
||||
"0xd10e1d90145460003d98ba4b788564e9549cc93c65a12c9b297720a9d6a586de",
|
||||
"0x000000000000000000000000a17fbb0b5a251a7aca3bd7377e7ecc4f700a2c09",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x8e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de00",
|
||||
"logIndex": 104,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000056b9a675be430b502",
|
||||
"logIndex": 105,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x0000000000000000000000005777d92f208679db4b9778590fa3cab3ac9e2168"
|
||||
],
|
||||
"data": "0x0000000000000000000000000000000000000000000000000000000005f5e100",
|
||||
"logIndex": 106,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"topics": [
|
||||
"0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
|
||||
"0x00000000000000000000000068b3465833fb72a70ecdf485e0e4c7bd8665fc45",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf"
|
||||
],
|
||||
"data": "0xfffffffffffffffffffffffffffffffffffffffffffffffa946598a41bcf4afe0000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000010c7063b90a5e90d13830000000000000000000000000000000000000000000071b57cb2bb0b5b28224ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffbc89c",
|
||||
"logIndex": 107,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
},
|
||||
{
|
||||
"transactionIndex": 61,
|
||||
"blockNumber": 17444757,
|
||||
"transactionHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"topics": [
|
||||
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
|
||||
"0x000000000000000000000000c59938e2d9ff9a0ecccbedf39031b1600d008eaf",
|
||||
"0x00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1"
|
||||
],
|
||||
"data": "0x000000000000000000000000000000000000000000000004f409bcc7a52b6358",
|
||||
"logIndex": 108,
|
||||
"blockHash": "0x79cf0785f317f984eeaf737c592afff806cabf4fe0c46a84f62a4a0212cfab5c"
|
||||
}
|
||||
],
|
||||
"blockNumber": 17444757,
|
||||
"confirmations": 392238,
|
||||
"cumulativeGasUsed": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x4065ac"
|
||||
},
|
||||
"effectiveGasPrice": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x04aa792df0"
|
||||
},
|
||||
"status": 1,
|
||||
"type": 2,
|
||||
"byzantium": true
|
||||
}
|
||||
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
33
cypress/fixtures/uniswapx/filledStatusResponse.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"settledAmounts": [
|
||||
{
|
||||
"tokenOut": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"amountOut": "91371770080538616664"
|
||||
}
|
||||
],
|
||||
"orderStatus": "filled",
|
||||
"txHash": "0x9f8382a94ee80ca119bc690908ab5f69c4c72f7497ee10f37e9ede0ded83cca6",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "insufficient-funds",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
26
cypress/fixtures/uniswapx/openStatusResponse.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"orders": [
|
||||
{
|
||||
"outputs": [
|
||||
{
|
||||
"recipient": "0x80becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"startAmount": "91371770080538616664",
|
||||
"endAmount": "90914911230135923580",
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F"
|
||||
}
|
||||
],
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064837e2a0000000000000000000000000000000000000000000000000000000064837e6600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000005f5e1000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000bd7f9d0239f81c94b728d827a87b9864972661ec00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a18e32c6335b6f657322448399bd12ff5c22b7b1aa770850ff4eed36c750e2de000000000000000000000000000000000000000000000000000000000064837e66000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f000000000000000000000000000000000000000000000004f409bcc7a52b6358000000000000000000000000000000000000000000000004edb2a613726c737c00000000000000000000000080becb808bfade4143183e58d18f2080e84e57a1",
|
||||
"signature": "0x973882a290778b5c8aae691ef777385259928cde0513d224ea1131538379258d2db7a69804110320b08558380394879a31ab8dea61152c2dba7623acbfa11d0e1b",
|
||||
"input": {
|
||||
"endAmount": "100000000",
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "100000000"
|
||||
},
|
||||
"orderStatus": "open",
|
||||
"createdAt": 1686339087,
|
||||
"chainId": 1,
|
||||
"orderHash": "0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19",
|
||||
"type": "Dutch"
|
||||
}
|
||||
]
|
||||
}
|
||||
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
1
cypress/fixtures/uniswapx/orderResponse.json
Normal file
@@ -0,0 +1 @@
|
||||
{"hash":"0xa9dd6f05ad6d6c79bee654c31ede4d0d2392862711be0f3bc4a9124af24a6a19"}
|
||||
491
cypress/fixtures/uniswapx/quote1.json
Normal file
491
cypress/fixtures/uniswapx/quote1.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": "57335948072881703373319552024074512292695687510330025934414357004397546394368",
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": "0",
|
||||
"input": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"startAmount": "300000000",
|
||||
"endAmount": "300000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "289951120815684452958",
|
||||
"endAmount": "267060007981523637666",
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064c91e6e0000000000000000000000000000000000000000000000000000000064c91eaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb480000000000000000000000000000000000000000000000000000000011e1a3000000000000000000000000000000000000000000000000000000000011e1a30000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e37953000000000000000000000000000000000000000000000000000000000064c91eb6000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000006b175474e89094c44da98b954eedeac495271d0f00000000000000000000000000000000000000000000000fb7e15027ad3e025e00000000000000000000000000000000000000000000000e7a33be508bb395a200000000000000000000000067d615d6bccaa1562b1cca9786384b4840597ecd",
|
||||
"quoteId": "f9f47cd7-a62c-4622-9ac7-51d0e662245a",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x67d615D6bccAA1562B1cca9786384b4840597ecD",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x7ec2ff20796a08922e11fd828e3871a6aa9a80e6495e30cd41be24b7e3795300"
|
||||
},
|
||||
"deadline": 1690902198,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1690902126,
|
||||
"decayEndTime": 1690902186,
|
||||
"exclusiveFiller": "0x0000000000000000000000000000000000000000",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x00"
|
||||
},
|
||||
"inputToken": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x11e1a300"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0fb7e15027ad3e025e"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0e7a33be508bb395a2"
|
||||
},
|
||||
"recipient": "0x67d615D6bccAA1562B1cca9786384b4840597ecD"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17820918",
|
||||
"amount": "300000000",
|
||||
"amountDecimals": "300",
|
||||
"quote": "299952256425393549464",
|
||||
"quoteDecimals": "299.952256425393549464",
|
||||
"quoteGasAdjusted": "289922128602824170541",
|
||||
"quoteGasAdjustedDecimals": "289.922128602824170541",
|
||||
"gasUseEstimateQuote": "10030127822569378922",
|
||||
"gasUseEstimateQuoteDecimals": "10.030127822569378922",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "10.031724",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "42803167855",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "6",
|
||||
"address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"symbol": "USDC"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "100",
|
||||
"liquidity": "534676532046235168447130",
|
||||
"sqrtRatioX96": "79230505815006815109584",
|
||||
"tickCurrent": "-276324",
|
||||
"amountIn": "300000000",
|
||||
"amountOut": "299952256425393549464"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = USDC -- 0.01% [0x5777d92f208679DB4b9778590Fa3CAB3aC9e2168] --> DAI",
|
||||
"quoteId": "1dd3bd14-780e-41c6-88e1-30a763f97482",
|
||||
"requestId": "2d16f993-6429-4755-ba50-1383789459dc",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
491
cypress/fixtures/uniswapx/quote2.json
Normal file
491
cypress/fixtures/uniswapx/quote2.json
Normal file
@@ -0,0 +1,491 @@
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"allQuotes": [
|
||||
{
|
||||
"routing": "DUTCH_LIMIT",
|
||||
"quote": {
|
||||
"orderInfo": {
|
||||
"chainId": 1,
|
||||
"permit2Address": "0x000000000022d473030f116ddee9f6b43ac78ba3",
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": "1993350209834725680308575292465150260730647098062962750049345504775310970881",
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x",
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": "100",
|
||||
"input": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"startAmount": "1000000000000000000",
|
||||
"endAmount": "1000000000000000000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": "929502510517534478575",
|
||||
"endAmount": "919795986077127665276",
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
},
|
||||
"encodedOrder": "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000064cd4f240000000000000000000000000000000000000000000000000000000064cd4f60000000000000000000000000165d98de005d2818176b99b1a93b9325dbe581810000000000000000000000000000000000000000000000000000000000000064000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000006000da47483062a0d734ba3dc7576ce6a0b645c400000000000000000000000000000000000000000000000000000000000000000468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d8090010000000000000000000000000000000000000000000000000000000064cd4f6c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000092d6c1e31e14520e676a687f0a93788b716beff500000000000000000000000000000000000000000000003263704899af6e50ef000000000000000000000000000000000000000000000031dcbbc80c9555e67c0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"quoteId": "09ce28b7-1ddf-4317-a28d-d21092be9f84",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"auctionPeriodSecs": 60,
|
||||
"deadlineBufferSecs": 12,
|
||||
"slippageTolerance": "0.5",
|
||||
"permitData": {
|
||||
"domain": {
|
||||
"name": "Permit2",
|
||||
"chainId": 1,
|
||||
"verifyingContract": "0x000000000022d473030f116ddee9f6b43ac78ba3"
|
||||
},
|
||||
"types": {
|
||||
"PermitWitnessTransferFrom": [
|
||||
{
|
||||
"name": "permitted",
|
||||
"type": "TokenPermissions"
|
||||
},
|
||||
{
|
||||
"name": "spender",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "witness",
|
||||
"type": "ExclusiveDutchOrder"
|
||||
}
|
||||
],
|
||||
"TokenPermissions": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "amount",
|
||||
"type": "uint256"
|
||||
}
|
||||
],
|
||||
"ExclusiveDutchOrder": [
|
||||
{
|
||||
"name": "info",
|
||||
"type": "OrderInfo"
|
||||
},
|
||||
{
|
||||
"name": "decayStartTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "decayEndTime",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "exclusiveFiller",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "exclusivityOverrideBps",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputToken",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "inputStartAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "inputEndAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "outputs",
|
||||
"type": "DutchOutput[]"
|
||||
}
|
||||
],
|
||||
"OrderInfo": [
|
||||
{
|
||||
"name": "reactor",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "swapper",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "nonce",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "deadline",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationContract",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "additionalValidationData",
|
||||
"type": "bytes"
|
||||
}
|
||||
],
|
||||
"DutchOutput": [
|
||||
{
|
||||
"name": "token",
|
||||
"type": "address"
|
||||
},
|
||||
{
|
||||
"name": "startAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "endAmount",
|
||||
"type": "uint256"
|
||||
},
|
||||
{
|
||||
"name": "recipient",
|
||||
"type": "address"
|
||||
}
|
||||
]
|
||||
},
|
||||
"values": {
|
||||
"permitted": {
|
||||
"token": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"amount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
}
|
||||
},
|
||||
"spender": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"witness": {
|
||||
"info": {
|
||||
"reactor": "0x6000da47483062A0D734Ba3dc7576Ce6A0B645C4",
|
||||
"swapper": "0x0000000000000000000000000000000000000000",
|
||||
"nonce": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0468323c9682990e3dc0646f899b437e62fbfb52a63cc8de721280222d809001"
|
||||
},
|
||||
"deadline": 1691176812,
|
||||
"additionalValidationContract": "0x0000000000000000000000000000000000000000",
|
||||
"additionalValidationData": "0x"
|
||||
},
|
||||
"decayStartTime": 1691176740,
|
||||
"decayEndTime": 1691176800,
|
||||
"exclusiveFiller": "0x165D98de005d2818176B99B1A93b9325dBE58181",
|
||||
"exclusivityOverrideBps": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x64"
|
||||
},
|
||||
"inputToken": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"inputStartAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"inputEndAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x0de0b6b3a7640000"
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"token": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"startAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x3263704899af6e50ef"
|
||||
},
|
||||
"endAmount": {
|
||||
"type": "BigNumber",
|
||||
"hex": "0x31dcbbc80c9555e67c"
|
||||
},
|
||||
"recipient": "0x0000000000000000000000000000000000000000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"routing": "CLASSIC",
|
||||
"quote": {
|
||||
"blockNumber": "17843654",
|
||||
"amount": "1000000000000000000",
|
||||
"amountDecimals": "1",
|
||||
"quote": "931181529570145926787",
|
||||
"quoteDecimals": "931.181529570145926787",
|
||||
"quoteGasAdjusted": "929033336026294051828",
|
||||
"quoteGasAdjustedDecimals": "929.033336026294051828",
|
||||
"gasUseEstimateQuote": "2148193543851874958",
|
||||
"gasUseEstimateQuoteDecimals": "2.148193543851874958",
|
||||
"gasUseEstimate": "128000",
|
||||
"gasUseEstimateUSD": "4.174934",
|
||||
"simulationStatus": "UNATTEMPTED",
|
||||
"simulationError": false,
|
||||
"gasPriceWei": "17811260539",
|
||||
"route": [
|
||||
[
|
||||
{
|
||||
"type": "v3-pool",
|
||||
"address": "0xD8de6af55F618a7Bc69835D55DDC6582220c36c0",
|
||||
"tokenIn": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
||||
"symbol": "WETH"
|
||||
},
|
||||
"tokenOut": {
|
||||
"chainId": 1,
|
||||
"decimals": "18",
|
||||
"address": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
|
||||
"symbol": "DAI"
|
||||
},
|
||||
"fee": "3000",
|
||||
"liquidity": "62287359628325896425115",
|
||||
"sqrtRatioX96": "2591813593283507889384697884",
|
||||
"tickCurrent": "-68403",
|
||||
"amountIn": "1000000000000000000",
|
||||
"amountOut": "931181529570145926787"
|
||||
}
|
||||
]
|
||||
],
|
||||
"routeString": "[V3] 100.00% = WETH -- 0.3% [0xD8de6af55F618a7Bc69835D55DDC6582220c36c0] --> DAI",
|
||||
"quoteId": "414e5f1c-120a-4e35-9760-c54d4b09e91d",
|
||||
"requestId": "f00535d4-461a-4363-afbe-7a5ab7061cd1",
|
||||
"tradeType": "EXACT_INPUT",
|
||||
"slippage": 0.5
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import 'cypress-hardhat/lib/browser'
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
|
||||
import { FeatureFlag } from '../../src/featureFlags'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE } from '../utils/user-state'
|
||||
import { injected } from './ethereum'
|
||||
import { initialState, UserState } from '../../src/state/user/reducer'
|
||||
import { CONNECTED_WALLET_USER_STATE, setInitialUserState } from '../utils/user-state'
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
@@ -13,15 +12,19 @@ declare global {
|
||||
interface ApplicationWindow {
|
||||
ethereum: Eip1193Bridge
|
||||
}
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Wait for a specific event to be sent to amplitude. If the event is found, the subject will be the event.
|
||||
*
|
||||
* @param {string} eventName - The type of the event to search for e.g. SwapEventName.SWAP_TRANSACTION_COMPLETED
|
||||
* @param {number} timeout - The maximum amount of time (in ms) to wait for the event.
|
||||
* @returns {Chainable<Subject>}
|
||||
*/
|
||||
waitForAmplitudeEvent(eventName: string, timeout?: number): Chainable<Subject>
|
||||
}
|
||||
interface VisitOptions {
|
||||
serviceWorker?: true
|
||||
featureFlags?: Array<FeatureFlag>
|
||||
/**
|
||||
* The mock ethereum provider to inject into the page.
|
||||
* @default 'goerli'
|
||||
*/
|
||||
// TODO(INFRA-175): Migrate all usage of 'goerli' to 'hardhat'.
|
||||
ethereum?: 'goerli' | 'hardhat'
|
||||
/**
|
||||
* Initial user state.
|
||||
* @default {@type import('../utils/user-state').CONNECTED_WALLET_USER_STATE}
|
||||
@@ -39,8 +42,7 @@ Cypress.Commands.overwrite(
|
||||
if (typeof url !== 'string') throw new Error('Invalid arguments. The first argument to cy.visit must be the path.')
|
||||
|
||||
// Add a hash in the URL if it is not present (to use hash-based routing correctly with queryParams).
|
||||
let hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
if (options?.ethereum === 'goerli') hashUrl += `${url.includes('?') ? '&' : '?'}chain=goerli`
|
||||
const hashUrl = url.startsWith('/') && url.length > 2 && !url.startsWith('/#') ? `/#${url}` : url
|
||||
|
||||
return cy
|
||||
.intercept('/service-worker.js', options?.serviceWorker ? undefined : { statusCode: 404 })
|
||||
@@ -52,14 +54,12 @@ Cypress.Commands.overwrite(
|
||||
onBeforeLoad(win) {
|
||||
options?.onBeforeLoad?.(win)
|
||||
|
||||
// We want to test from a clean state, so we clear the local storage (which clears redux).
|
||||
win.localStorage.clear()
|
||||
|
||||
// Set initial user state.
|
||||
win.localStorage.setItem(
|
||||
'redux_localstorage_simple_user', // storage key for the user reducer using 'redux-localstorage-simple'
|
||||
JSON.stringify({ ...CONNECTED_WALLET_USER_STATE, ...(options?.userState ?? {}) })
|
||||
)
|
||||
setInitialUserState(win, {
|
||||
...initialState,
|
||||
hideUniswapWalletBanner: true,
|
||||
...CONNECTED_WALLET_USER_STATE,
|
||||
...(options?.userState ?? {}),
|
||||
})
|
||||
|
||||
// Set feature flags, if configured.
|
||||
if (options?.featureFlags) {
|
||||
@@ -68,13 +68,29 @@ Cypress.Commands.overwrite(
|
||||
}
|
||||
|
||||
// Inject the mock ethereum provider.
|
||||
if (options?.ethereum === 'hardhat') {
|
||||
win.ethereum = provider
|
||||
} else {
|
||||
win.ethereum = injected
|
||||
}
|
||||
win.ethereum = provider
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
Cypress.Commands.add('waitForAmplitudeEvent', (eventName, timeout = 5000 /* 5s */) => {
|
||||
const startTime = new Date().getTime()
|
||||
|
||||
function checkRequest() {
|
||||
return cy.wait('@amplitude', { timeout }).then((interception) => {
|
||||
const events = interception.request.body.events
|
||||
const event = events.find((event: any) => event.event_type === eventName)
|
||||
|
||||
if (event) {
|
||||
return cy.wrap(event)
|
||||
} else if (new Date().getTime() - startTime > timeout) {
|
||||
throw new Error(`Event ${eventName} not found within the specified timeout`)
|
||||
} else {
|
||||
return checkRequest()
|
||||
}
|
||||
})
|
||||
}
|
||||
return checkRequest()
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
import '@cypress/code-coverage/support'
|
||||
import './commands'
|
||||
import './setupTests'
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/**
|
||||
* Updates cy.visit() to include an injected window.ethereum provider.
|
||||
*/
|
||||
|
||||
import { Eip1193Bridge } from '@ethersproject/experimental/lib/eip1193-bridge'
|
||||
import { JsonRpcProvider } from '@ethersproject/providers'
|
||||
import { Wallet } from '@ethersproject/wallet'
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
|
||||
// todo: figure out how env vars actually work in CI
|
||||
// const TEST_PRIVATE_KEY = Cypress.env('INTEGRATION_TEST_PRIVATE_KEY')
|
||||
const TEST_PRIVATE_KEY = '0xe580410d7c37d26c6ad1a837bbae46bc27f9066a466fb3a66e770523b4666d19'
|
||||
|
||||
// address of the above key
|
||||
const TEST_ADDRESS_NEVER_USE = new Wallet(TEST_PRIVATE_KEY).address
|
||||
const CHAIN_ID = ChainId.GOERLI
|
||||
const HEXLIFIED_CHAIN_ID = `0x${CHAIN_ID.toString(16)}`
|
||||
|
||||
const provider = new JsonRpcProvider('https://goerli.infura.io/v3/4bf032f2d38a4ed6bb975b80d6340847', 5)
|
||||
const signer = new Wallet(TEST_PRIVATE_KEY, provider)
|
||||
export const injected = new (class extends Eip1193Bridge {
|
||||
chainId = CHAIN_ID
|
||||
|
||||
async sendAsync(...args: any[]) {
|
||||
console.debug('sendAsync called', ...args)
|
||||
return this.send(...args)
|
||||
}
|
||||
async send(...args: any[]) {
|
||||
console.debug('send called', ...args)
|
||||
const isCallbackForm = typeof args[0] === 'object' && typeof args[1] === 'function'
|
||||
let callback
|
||||
let method
|
||||
let params
|
||||
if (isCallbackForm) {
|
||||
callback = args[1]
|
||||
method = args[0].method
|
||||
params = args[0].params
|
||||
} else {
|
||||
method = args[0]
|
||||
params = args[1]
|
||||
}
|
||||
if (method === 'eth_requestAccounts' || method === 'eth_accounts') {
|
||||
if (isCallbackForm) {
|
||||
callback({ result: [TEST_ADDRESS_NEVER_USE] })
|
||||
} else {
|
||||
return Promise.resolve([TEST_ADDRESS_NEVER_USE])
|
||||
}
|
||||
}
|
||||
if (method === 'eth_chainId') {
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result: HEXLIFIED_CHAIN_ID })
|
||||
} else {
|
||||
return Promise.resolve(HEXLIFIED_CHAIN_ID)
|
||||
}
|
||||
}
|
||||
try {
|
||||
const result = await super.send(method, params)
|
||||
console.debug('result received', method, params, result)
|
||||
if (isCallbackForm) {
|
||||
callback(null, { result })
|
||||
} else {
|
||||
return result
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCallbackForm) {
|
||||
callback(error, null)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
})(signer, provider)
|
||||
@@ -9,13 +9,8 @@ beforeEach(() => {
|
||||
req.headers['origin'] = 'https://app.uniswap.org'
|
||||
})
|
||||
|
||||
// Infura uses a test endpoint, which allow-lists http://localhost:3000 instead.
|
||||
cy.intercept(/infura.io/, (req) => {
|
||||
req.headers['referer'] = 'http://localhost:3000'
|
||||
req.headers['origin'] = 'http://localhost:3000'
|
||||
req.alias = req.body.method
|
||||
req.continue()
|
||||
})
|
||||
// Infura is disabled for cypress tests - calls should be routed through the connected wallet instead.
|
||||
cy.intercept(/infura.io/, { statusCode: 404 })
|
||||
|
||||
// Log requests to hardhat.
|
||||
cy.intercept(/:8545/, logJsonRpc)
|
||||
@@ -24,6 +19,7 @@ beforeEach(() => {
|
||||
cy.intercept('https://api.uniswap.org/v1/amplitude-proxy', (req) => {
|
||||
const requestBody = JSON.stringify(req.body)
|
||||
const byteSize = new Blob([requestBody]).size
|
||||
req.alias = 'amplitude'
|
||||
req.reply(
|
||||
JSON.stringify({
|
||||
code: 200,
|
||||
@@ -49,7 +45,7 @@ function logJsonRpc(req: CyHttpMessages.IncomingHttpRequest) {
|
||||
const log = Cypress.log({
|
||||
autoEnd: false,
|
||||
name: req.body.method,
|
||||
message: req.body.params?.map((param: unknown) =>
|
||||
message: req.body.params?.map((param: any) =>
|
||||
typeof param === 'object' ? '{...}' : param?.toString().substring(0, 10)
|
||||
),
|
||||
})
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"isolatedModules": false,
|
||||
"noImplicitAny": false,
|
||||
"target": "ES5",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/cypress", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["cypress", "node"],
|
||||
"jsx": "react"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
import { ConnectionType } from '../../src/connection/types'
|
||||
import { UserState } from '../../src/state/user/reducer'
|
||||
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: 'INJECTED' }
|
||||
export const CONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: ConnectionType.INJECTED }
|
||||
|
||||
export const DISCONNECTED_WALLET_USER_STATE: Partial<UserState> = { selectedWallet: undefined }
|
||||
|
||||
/**
|
||||
* This sets the initial value of the "user" slice in IndexedDB.
|
||||
* Other persisted slices are not set, so they will be filled with their respective initial values
|
||||
* when the app runs.
|
||||
*/
|
||||
export function setInitialUserState(win: Cypress.AUTWindow, initialUserState: any) {
|
||||
win.indexedDB.deleteDatabase('redux')
|
||||
|
||||
const dbRequest = win.indexedDB.open('redux')
|
||||
|
||||
dbRequest.onsuccess = function () {
|
||||
const db = dbRequest.result
|
||||
const transaction = db.transaction('keyvaluepairs', 'readwrite')
|
||||
const store = transaction.objectStore('keyvaluepairs')
|
||||
store.put(
|
||||
{
|
||||
user: initialUserState,
|
||||
},
|
||||
'persist:interface'
|
||||
)
|
||||
}
|
||||
|
||||
dbRequest.onupgradeneeded = function () {
|
||||
const db = dbRequest.result
|
||||
db.createObjectStore('keyvaluepairs')
|
||||
}
|
||||
}
|
||||
|
||||
50
functions/README.md
Normal file
50
functions/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Cloudflare Cloud Functions
|
||||
|
||||
## Purpose
|
||||
|
||||
These functions utilize Cloudflare Functions to dynamically inject meta tags server side for richer link sharing capabilities.
|
||||
|
||||
## Functions
|
||||
|
||||
Currently, there are 2 types of cloudflare functions developed
|
||||
|
||||
- Meta Data Injectors - Workers that inject [Open Graph](https://ogp.me/) standardized meta tags into the `header` of specific webpages.
|
||||
- Currently we support this functionaltiy for three separate webpages: NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL and then formats them into HTML `meta` tags to be injected
|
||||
- Dynamically Generated Images - Utilizes Vercel's [Open Graph Image Generation Library](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation) to create custom thumbnails for specific webpages
|
||||
- Currently supports NFT Assets, NFT Collections, and Token Detail Pages
|
||||
- These functions query data from GraphQL, and utilize `Satori` to convert HTML into a png image response which is then returned when the api is called.
|
||||
- Can be found in the `api/image` folder.
|
||||
|
||||
## Testing
|
||||
|
||||
Testing is done utilizing a custom jest environment as well as Cloudflare's local tester: `wrangler`. Wrangler enables testing locally by running a proxy to wrap `localhost`. Testing can be done the following ways.
|
||||
- Manually by running `yarn start:cloud` to setup wrangler on `localhost:3000`
|
||||
- Automated tests by running `yarn test:cloud` to setup both a jest and wrangler environment and automatically test features
|
||||
|
||||
## Deployment
|
||||
|
||||
Functions will be deployed to Cloudlfare where they will be ran automatically when the appropriate route is hit.
|
||||
|
||||
## Miscellaneous
|
||||
- Caching: In order to speed up webpage requests, repeated GraphQL queries will be saved and pulled using Cloudflare's Cache API.
|
||||
|
||||
## Scripts
|
||||
|
||||
- `yarn start:cloud` (NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --node-compat --proxy=3001 --port=3000 -- yarn start), script to start local wrangler environment
|
||||
- `npx wrangler pages dev`: this basis of this command which starts a local instance of wrangler to test cloud functions
|
||||
- `--node-compat`: wrangler option that enables compatibility with Node.js modules
|
||||
- `--proxy:3001`: telling the proxy to listen on port 3001
|
||||
- `--port=3000`: telling wrangler to run our proxy on port 3000
|
||||
- `NODE_OPTIONS=--dns-result-order=ipv4first`: wrangler still serves to IPv4 which isn't compatible with Node 18 which default resolves to IPv6 so we need to specify to serve to IPv4
|
||||
- `PORT-3001 --yarn start`: runs default yarn start on port 3001
|
||||
- `yarn test:cloud` (NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json), script to test cloud functions with jest
|
||||
- `NODE_OPTIONS=--experimental-vm-modules`: support for ES Modules and Web Assembly
|
||||
- `--config=functions/jest.config.json`: specifying which config file to use
|
||||
|
||||
## Additional Documents
|
||||
- [Open Graph Protocol](https://ogp.me/)
|
||||
- [Open Graph Image Generation](https://vercel.com/docs/concepts/functions/edge-functions/og-image-generation)
|
||||
- [Cloudflare Workers](https://developers.cloudflare.com/workers/)
|
||||
- [HTML Rewriter](https://developers.cloudflare.com/workers/runtime-apis/html-rewriter/)
|
||||
- [Cache API](https://developers.cloudflare.com/workers/runtime-apis/cache/)
|
||||
18
functions/[[index]].ts
Normal file
18
functions/[[index]].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { MetaTagInjector } from './components/metaTagInjector'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ request, next }) => {
|
||||
const imageUri = new URL(request.url).origin + '/images/1200x630_Rich_Link_Preview_Image.png'
|
||||
const data = {
|
||||
title: 'Uniswap Interface',
|
||||
image: imageUri,
|
||||
url: request.url,
|
||||
description: 'Swap or provide liquidity on the Uniswap Protocol',
|
||||
}
|
||||
const res = next()
|
||||
try {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(data)).transform(await res)
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
71
functions/api/image/nfts/asset/[[index]].tsx
Normal file
71
functions/api/image/nfts/asset/[[index]].tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { WATERMARK_URL } from '../../../../constants'
|
||||
import getAsset from '../../../../utils/getAsset'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
const cacheUrl = origin + '/nfts/asset/' + collectionAddress + '/' + tokenId
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getAsset(collectionAddress, tokenId, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getAsset>>> => Boolean(data.ogImage)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Asset not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const fontData = await getFont(origin)
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<img src={data.ogImage} alt={data.title} width="1200px" />
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '72px',
|
||||
right: '72px',
|
||||
display: 'flex',
|
||||
gap: '24px',
|
||||
}}
|
||||
>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
20
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
20
functions/api/image/nfts/asset/nftImage.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
const assetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/804',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
]
|
||||
|
||||
test.each(assetImageUrl)('assetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidAssetImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/10001',
|
||||
'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/44700',
|
||||
]
|
||||
|
||||
test.each(invalidAssetImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
117
functions/api/image/nfts/collection/[index].tsx
Normal file
117
functions/api/image/nfts/collection/[index].tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { CHECK_URL, WATERMARK_URL } from '../../../../constants'
|
||||
import getCollection from '../../../../utils/getCollection'
|
||||
import getColor from '../../../../utils/getColor'
|
||||
import getFont from '../../../../utils/getFont'
|
||||
import { getRequest } from '../../../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
const cacheUrl = origin + '/nfts/collection/' + collectionAddress
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getCollection(collectionAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getCollection>>> =>
|
||||
Boolean(data.ogImage && data.name && data.isVerified)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Collection not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage)])
|
||||
|
||||
// Split name into words to wrap them since satori does not support inline text wrapping
|
||||
const words = data.name.split(' ')
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]}, 0.75)`,
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
gap: '48px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={data.ogImage}
|
||||
alt={data.name}
|
||||
width="500px"
|
||||
height="500px"
|
||||
style={{
|
||||
borderRadius: '60px',
|
||||
objectFit: 'cover',
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '32px',
|
||||
width: '45%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
gap: '12px',
|
||||
fontSize: '72px',
|
||||
fontFamily: 'Inter',
|
||||
color: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
{words.map((word: string) => (
|
||||
<text key={word + index}>{word}</text>
|
||||
))}
|
||||
{data.isVerified && <img src={CHECK_URL} height="54px" />}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
20
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
20
functions/api/image/nfts/collection/collectionImage.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
const collectionImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
]
|
||||
|
||||
test.each(collectionImageUrl)('collectionImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidCollectionImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
]
|
||||
|
||||
test.each(invalidCollectionImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
177
functions/api/image/tokens/[[index]].tsx
Normal file
177
functions/api/image/tokens/[[index]].tsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import React from 'react'
|
||||
|
||||
import { WATERMARK_URL } from '../../../constants'
|
||||
import getColor from '../../../utils/getColor'
|
||||
import getFont from '../../../utils/getFont'
|
||||
import getNetworkLogoUrl from '../../../utils/getNetworkLogoURL'
|
||||
import { getRequest } from '../../../utils/getRequest'
|
||||
import getToken from '../../../utils/getToken'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request }) => {
|
||||
try {
|
||||
const origin = new URL(request.url).origin
|
||||
const { index } = params
|
||||
const networkName = String(index[0])
|
||||
const tokenAddress = String(index[1])
|
||||
|
||||
const cacheUrl = origin + '/tokens/' + networkName + '/' + tokenAddress
|
||||
|
||||
const data = await getRequest(
|
||||
cacheUrl,
|
||||
() => getToken(networkName, tokenAddress, cacheUrl),
|
||||
(data): data is NonNullable<Awaited<ReturnType<typeof getToken>>> => Boolean(data.symbol && data.name)
|
||||
)
|
||||
|
||||
if (!data) {
|
||||
return new Response('Token not found.', { status: 404 })
|
||||
}
|
||||
|
||||
const [fontData, palette] = await Promise.all([getFont(origin), getColor(data.ogImage, true)])
|
||||
|
||||
const networkLogo = getNetworkLogoUrl(networkName.toUpperCase(), origin)
|
||||
|
||||
// Capitalize name such that each word starts with a capital letter
|
||||
let words = data.name.split(' ')
|
||||
words = words.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
let name = words.join(' ')
|
||||
name = name.trim()
|
||||
|
||||
return new ImageResponse(
|
||||
(
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'black',
|
||||
display: 'flex',
|
||||
width: '1200px',
|
||||
height: '630px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
backgroundColor: `rgba(${palette[0]}, ${palette[1]}, ${palette[2]})`,
|
||||
alignItems: 'center',
|
||||
height: '100%',
|
||||
padding: '72px',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.ogImage ? (
|
||||
<img src={data.ogImage} width="144px" style={{ borderRadius: '100%' }}>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</img>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
width: '144px',
|
||||
height: '144px',
|
||||
borderRadius: '100%',
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.12)',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '48px',
|
||||
lineHeight: '58px',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{data.name.slice(0, 3).toUpperCase()}
|
||||
</div>
|
||||
{networkLogo != '' && (
|
||||
<img
|
||||
src={networkLogo}
|
||||
width="48px"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '2px',
|
||||
bottom: '0px',
|
||||
borderRadius: '100%',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '72px',
|
||||
lineHeight: '72px',
|
||||
marginLeft: '-5px',
|
||||
marginTop: '24px',
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-end',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'Inter',
|
||||
fontSize: '168px',
|
||||
lineHeight: '133px',
|
||||
marginLeft: '-13px',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{data.symbol}
|
||||
</div>
|
||||
<img src={WATERMARK_URL} alt="Uniswap" height="72px" width="324px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
fonts: [
|
||||
{
|
||||
name: 'Inter',
|
||||
data: fontData,
|
||||
style: 'normal',
|
||||
},
|
||||
],
|
||||
}
|
||||
) as Response
|
||||
} catch (error: any) {
|
||||
return new Response(error.message || error.toString(), { status: 500 })
|
||||
}
|
||||
}
|
||||
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
22
functions/api/image/tokens/tokenImage.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const tokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE',
|
||||
]
|
||||
|
||||
test.each(tokenImageUrl)('tokenImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.headers.get('content-type')).toBe('image/png')
|
||||
})
|
||||
|
||||
const invalidTokenImageUrl = [
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/api/image/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/api/image/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokenImageUrl)('invalidAssetImageUrl', async (url) => {
|
||||
const response = await fetch(new Request(url))
|
||||
expect(response.status).toBe(404)
|
||||
})
|
||||
20
functions/client.ts
Normal file
20
functions/client.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ApolloClient, InMemoryCache } from '@apollo/client'
|
||||
const GRAPHQL_ENDPOINT = 'https://api.uniswap.org/v1/graphql'
|
||||
|
||||
//TODO: Figure out how to make ApolloClient global variable
|
||||
export default new ApolloClient({
|
||||
connectToDevTools: true,
|
||||
uri: GRAPHQL_ENDPOINT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Origin: 'https://app.uniswap.org',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.5735.110 Safari/537.36',
|
||||
},
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: {
|
||||
fetchPolicy: 'cache-first',
|
||||
},
|
||||
},
|
||||
})
|
||||
38
functions/components/metaTagInjector.test.ts
Normal file
38
functions/components/metaTagInjector.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { MetaTagInjector } from './metaTagInjector'
|
||||
|
||||
test('should append meta tag to element', () => {
|
||||
const element = {
|
||||
append: jest.fn(),
|
||||
} as unknown as Element
|
||||
const property = 'property'
|
||||
const content = 'content'
|
||||
const injector = new MetaTagInjector({
|
||||
title: 'test',
|
||||
url: 'testUrl',
|
||||
image: 'testImage',
|
||||
description: 'testDescription',
|
||||
})
|
||||
injector.append(element, property, content)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
|
||||
injector.element(element)
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:description" content="testDescription"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:width" content="1200"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:height" content="630"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:image:alt" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:type" content="website"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="og:url" content="testUrl"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:card" content="summary_large_image"/>`, {
|
||||
html: true,
|
||||
})
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:title" content="test"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image" content="testImage"/>`, { html: true })
|
||||
expect(element.append).toHaveBeenCalledWith(`<meta property="twitter:image:alt" content="test"/>`, { html: true })
|
||||
|
||||
expect(element.append).toHaveBeenCalledTimes(13)
|
||||
})
|
||||
42
functions/components/metaTagInjector.ts
Normal file
42
functions/components/metaTagInjector.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
type MetaTagInjectorInput = {
|
||||
title: string
|
||||
image?: string
|
||||
url: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener class for Cloudflare's HTMLRewriter {@link https://developers.cloudflare.com/workers/runtime-apis/html-rewriter}
|
||||
* to inject meta tags into the <head> of an HTML document.
|
||||
*/
|
||||
export class MetaTagInjector implements HTMLRewriterElementContentHandlers {
|
||||
constructor(private input: MetaTagInjectorInput) {}
|
||||
|
||||
append(element: Element, property: string, content: string) {
|
||||
element.append(`<meta property="${property}" content="${content}"/>`, { html: true })
|
||||
}
|
||||
|
||||
element(element: Element) {
|
||||
//Open Graph Tags
|
||||
this.append(element, 'og:title', this.input.title)
|
||||
if (this.input.description) {
|
||||
this.append(element, 'og:description', this.input.description)
|
||||
}
|
||||
if (this.input.image) {
|
||||
this.append(element, 'og:image', this.input.image)
|
||||
this.append(element, 'og:image:width', '1200')
|
||||
this.append(element, 'og:image:height', '630')
|
||||
this.append(element, 'og:image:alt', this.input.title)
|
||||
}
|
||||
this.append(element, 'og:type', 'website')
|
||||
this.append(element, 'og:url', this.input.url)
|
||||
|
||||
//Twitter Tags
|
||||
this.append(element, 'twitter:card', 'summary_large_image')
|
||||
this.append(element, 'twitter:title', this.input.title)
|
||||
if (this.input.image) {
|
||||
this.append(element, 'twitter:image', this.input.image)
|
||||
this.append(element, 'twitter:image:alt', this.input.title)
|
||||
}
|
||||
}
|
||||
}
|
||||
76
functions/constants.ts
Normal file
76
functions/constants.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
export const WATERMARK_URL = 'https://app.uniswap.org/images/324x74_App_Watermark.png'
|
||||
export const CHECK_URL = 'https://app.uniswap.org/images/54x54_Verified_Check.svg'
|
||||
|
||||
export const DEFAULT_COLOR = [35, 43, 43]
|
||||
|
||||
export const predefinedTokenColors: { [key: string]: number[] } = {
|
||||
// old WBTC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599/logo.png':
|
||||
[240, 146, 65],
|
||||
// new WBTC
|
||||
'https://assets.coingecko.com/coins/images/7598/large/wrapped_bitcoin_wbtc.png?1548822744': [240, 146, 65],
|
||||
// DAI
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6B175474E89094C44Da98b954EedeAC495271d0F/logo.png':
|
||||
[250, 176, 27],
|
||||
// UNI
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1f9840a85d5aF5bf1D1762F925BDADdC4201F984/logo.png':
|
||||
[230, 53, 140],
|
||||
// BUSD
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4Fabb145d64652a948d72533023f6E7A623C7C53/logo.png':
|
||||
[239, 186, 9],
|
||||
// AI-X
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/26984.png': [41, 161, 241],
|
||||
// ETH
|
||||
'https://token-icons.s3.amazonaws.com/eth.png': [73, 112, 213],
|
||||
// HARRYPOTTERSHIBAINUBITCOIN
|
||||
'https://assets.coingecko.com/coins/images/30323/large/hpos10i_logo_casino_night-dexview.png?1684117567': [
|
||||
222, 49, 16,
|
||||
],
|
||||
// PEPE
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x6982508145454Ce325dDbE47a25d4ec3d2311933/logo.png':
|
||||
[62, 174, 20],
|
||||
// Unibot V2
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/25436.png': [74, 10, 79],
|
||||
// UNIBOT v1
|
||||
'https://assets.coingecko.com/coins/images/30462/small/logonoline_%281%29.png?1687510315': [74, 10, 79],
|
||||
// USDC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/logo.png':
|
||||
[0, 102, 217],
|
||||
// HEX
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39/logo.png':
|
||||
[249, 63, 140],
|
||||
// MONG
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x1ce270557C1f68Cfb577b856766310Bf8B47FD9C/logo.png':
|
||||
[169, 109, 255],
|
||||
// ARB
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1/logo.png':
|
||||
[41, 161, 241],
|
||||
// PSYOP
|
||||
'https://s2.coinmarketcap.com/static/img/coins/64x64/25422.png': [232, 143, 0],
|
||||
// MATIC
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0/logo.png':
|
||||
[169, 109, 255],
|
||||
// TURBO
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xA35923162C49cF95e6BF26623385eb431ad920D3/logo.png':
|
||||
[189, 110, 41],
|
||||
// AIDOGE
|
||||
'https://assets.coingecko.com/coins/images/29852/large/photo_2023-04-18_14-25-28.jpg?1681799160': [41, 161, 241],
|
||||
// SIMPSON
|
||||
'https://assets.coingecko.com/coins/images/30243/large/1111.png?1683692033': [232, 143, 0],
|
||||
// OX
|
||||
'https://assets.coingecko.com/coins/images/30604/large/Logo2.png?1685522119': [41, 89, 217],
|
||||
// ANGLE
|
||||
'https://assets.coingecko.com/coins/images/19060/large/ANGLE_Token-light.png?1666774221': [255, 85, 85],
|
||||
// APE
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x4d224452801ACEd8B2F0aebE155379bb5D594381/logo.png':
|
||||
[5, 74, 169],
|
||||
// GUSD
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd/logo.png':
|
||||
[0, 164, 189],
|
||||
// OGN
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x8207c1FfC5B6804F6024322CcF34F29c3541Ae26/logo.png':
|
||||
[5, 74, 169],
|
||||
// RPL
|
||||
'https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0xD33526068D116cE69F19A9ee46F0bd304F21A51f/logo.png':
|
||||
[255, 123, 79],
|
||||
}
|
||||
22
functions/default.test.ts
Normal file
22
functions/default.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const defaultUrls = ['http://127.0.0.1:3000/', 'http://127.0.0.1:3000/swap', 'http://127.0.0.1:3000/pools']
|
||||
|
||||
test.each(defaultUrls)('should inject metadata for valid collections', async (defaultUrl) => {
|
||||
const body = await fetch(new Request(defaultUrl)).then((res) => res.text())
|
||||
expect(body).toContain(`<meta property="og:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:description" content="Swap or provide liquidity on the Uniswap Protocol"/>`
|
||||
)
|
||||
expect(body).toContain(
|
||||
`<meta property="og:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Uniswap Interface"/>`)
|
||||
expect(body).toContain(
|
||||
`<meta property="twitter:image" content="http://127.0.0.1:3000/images/1200x630_Rich_Link_Preview_Image.png"/>`
|
||||
)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Uniswap Interface"/>`)
|
||||
})
|
||||
@@ -4,6 +4,16 @@ module.exports = async function globalSetup() {
|
||||
globalThis.servers = await setup({
|
||||
command: `yarn start:cloud`,
|
||||
port: 3000,
|
||||
launchTimeout: 50000,
|
||||
launchTimeout: 120000, // takes ~2m on CI
|
||||
})
|
||||
// Wait for wrangler to return a request before running tests
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const res = await fetch(new Request('http://127.0.0.1:3000/tokens/ethereum/NATIVE'))
|
||||
if (res.ok) {
|
||||
return
|
||||
}
|
||||
// Set timeout to make sure the server isn't flooded with requests if wrangler is not running
|
||||
await new Promise((resolve) => setTimeout(resolve, 500 * (i + 1)))
|
||||
}
|
||||
throw new Error('Failed to start server')
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{
|
||||
"globalSetup": "<rootDir>/global-setup.ts",
|
||||
"globalTeardown": "<rootDir>/global-teardown.ts",
|
||||
"setupFilesAfterEnv": ["<rootDir>/setupAfterEnv.ts"],
|
||||
"preset": "ts-jest",
|
||||
"transform": {
|
||||
"'^.+\\.(ts|tsx)?$'": "ts-jest",
|
||||
"^.+\\.(js|jsx)$": "babel-jest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"testTimeout": 360000,
|
||||
"cacheDirectory": "../node_modules/.cache/cloud-jest"
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
test('example', async () => {
|
||||
expect(true).toBe(true)
|
||||
})
|
||||
15
functions/nfts/asset/[[index]].ts
Normal file
15
functions/nfts/asset/[[index]].ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getAsset from '../../utils/getAsset'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index[0]?.toString()
|
||||
const tokenId = index[1]?.toString()
|
||||
return getMetadataRequest(res, request.url, () => getAsset(collectionAddress, tokenId, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
394
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
394
functions/nfts/asset/__snapshots__/nft.test.ts.snap
Normal file
@@ -0,0 +1,394 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid assets 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki #2550"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki #2550"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki #2550"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550"/><meta property="twitter:image:alt" content="Azuki #2550"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club #3735"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club #3735"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club #3735"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club #3735"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid assets 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CryptoPunk #3947"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CryptoPunk #3947"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CryptoPunk #3947"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947"/><meta property="twitter:image:alt" content="CryptoPunk #3947"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
62
functions/nfts/asset/nft.test.ts
Normal file
62
functions/nfts/asset/nft.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
const assets = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
assetId: '2550',
|
||||
collectionName: 'Azuki',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/2550',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
assetId: '3735',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/3735',
|
||||
},
|
||||
{
|
||||
address: '0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb',
|
||||
assetId: '3947',
|
||||
collectionName: 'CryptoPunk',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/asset/0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb/3947',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(assets)('should inject metadata for valid assets', async (nft) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/asset/' + nft.address + '/' + nft.assetId
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${nft.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${nft.collectionName} #${nft.assetId}"/>`)
|
||||
})
|
||||
|
||||
const invalidAssets = [
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/100000',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544/-1',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//',
|
||||
'http://127.0.0.1:3000/nfts/asset/0xed5af388653567af2f388e6224dc7c4b3241c544//2550',
|
||||
]
|
||||
|
||||
test.each(invalidAssets)('should not inject metadata for invalid asset calls', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
14
functions/nfts/collection/[index].ts
Normal file
14
functions/nfts/collection/[index].ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import getCollection from '../../utils/getCollection'
|
||||
import { getMetadataRequest } from '../../utils/getRequest'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const collectionAddress = index?.toString()
|
||||
return getMetadataRequest(res, request.url, () => getCollection(collectionAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
394
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
394
functions/nfts/collection/__snapshots__/collection.test.ts.snap
Normal file
@@ -0,0 +1,394 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid collections 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Azuki on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Azuki on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Azuki on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544"/><meta property="twitter:image:alt" content="Azuki on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid collections 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Bored Ape Yacht Club on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Bored Ape Yacht Club on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d"/><meta property="twitter:image:alt" content="Bored Ape Yacht Club on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid collections 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b"/><meta property="twitter:image:alt" content="CLONE X - X TAKASHI MURAKAMI on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
59
functions/nfts/collection/collection.test.ts
Normal file
59
functions/nfts/collection/collection.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
const collections = [
|
||||
{
|
||||
address: '0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
collectionName: 'Azuki',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c544',
|
||||
},
|
||||
{
|
||||
address: '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
collectionName: 'Bored Ape Yacht Club',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d',
|
||||
},
|
||||
{
|
||||
address: '0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
collectionName: 'CLONE X - X TAKASHI MURAKAMI',
|
||||
image: 'http://127.0.0.1:3000/api/image/nfts/collection/0x49cf6f5d44e70224e2e23fdcdd2c053f30ada28b',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(collections)('should inject metadata for valid collections', async (collection) => {
|
||||
const url = 'http://127.0.0.1:3000/nfts/collection/' + collection.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="${collection.collectionName} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${collection.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="${collection.collectionName} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const invalidCollections = [
|
||||
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545',
|
||||
'http://127.0.0.1:3000/nfts/collection/0xed5af388653567af2f388e6224dc7c4b3241c545//',
|
||||
]
|
||||
|
||||
test.each(invalidCollections)(
|
||||
'should not inject metadata for invalid collection urls',
|
||||
async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
},
|
||||
50000
|
||||
)
|
||||
1
functions/setupAfterEnv.ts
Normal file
1
functions/setupAfterEnv.ts
Normal file
@@ -0,0 +1 @@
|
||||
jest.retryTimes(3)
|
||||
18
functions/tokens/[[index]].ts
Normal file
18
functions/tokens/[[index]].ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable import/no-unused-modules */
|
||||
import { getMetadataRequest } from '../utils/getRequest'
|
||||
import getToken from '../utils/getToken'
|
||||
|
||||
export const onRequest: PagesFunction = async ({ params, request, next }) => {
|
||||
const res = next()
|
||||
try {
|
||||
const { index } = params
|
||||
const networkName = index[0]?.toString()
|
||||
const tokenAddress = index[1]?.toString()
|
||||
if (!tokenAddress) {
|
||||
return res
|
||||
}
|
||||
return getMetadataRequest(res, request.url, () => getToken(networkName, tokenAddress, request.url))
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
525
functions/tokens/__snapshots__/token.test.ts.snap
Normal file
525
functions/tokens/__snapshots__/token.test.ts.snap
Normal file
@@ -0,0 +1,525 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`should inject metadata for valid tokens 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get USDC on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get USDC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get USDC on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"/><meta property="twitter:image:alt" content="Get USDC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get ETH on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get ETH on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get ETH on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE"/><meta property="twitter:image:alt" content="Get ETH on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 3`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get MATIC on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get MATIC on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/polygon/NATIVE"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get MATIC on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE"/><meta property="twitter:image:alt" content="Get MATIC on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`should inject metadata for valid tokens 4`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html translate="no">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
<title>Uniswap Interface</title>
|
||||
|
||||
<!--
|
||||
will be replaced with the URL of the \`public\` folder during build.
|
||||
Only files inside the \`public\` folder can be referenced from the HTML.
|
||||
-->
|
||||
<link rel="shortcut icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/images/192x192_App_Icon.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/images/512x512_App_Icon.png" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta name="theme-color" content="#FC72FF" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
|
||||
content="script-src 'self' 'unsafe-inline'"
|
||||
|
||||
/>
|
||||
|
||||
<!--
|
||||
Apple Smart App Banner for Safari on iOS
|
||||
https://developer.apple.com/documentation/webkit/promoting_apps_with_smart_app_banners
|
||||
-->
|
||||
<meta name="apple-itunes-app" content="app-id=6443944476">
|
||||
|
||||
<!--
|
||||
manifest.json provides metadata used when the app is installed as a PWA.
|
||||
See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<link rel="preload" href="/fonts/Inter-roman.var.woff2" as="font" type="font/woff2" crossorigin />
|
||||
|
||||
<style>
|
||||
* {
|
||||
font-family: 'Inter', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/**
|
||||
Explicitly load Inter var from public/ so it does not block LCP's critical path.
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter custom';
|
||||
font-weight: 100 900;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
font-named-instance: 'Regular';
|
||||
src: url(/fonts/Inter-roman.var.woff2) format('woff2 supports variations(gvar)'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2-variations'),
|
||||
url(/fonts/Inter-roman.var.woff2) format('woff2');
|
||||
}
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
* {
|
||||
font-family: 'Inter custom', sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
font-variant: none;
|
||||
font-smooth: always;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
/* Use this to apply network-specific gradient backgrounds, in RadialGradientByChainUpdater.ts */
|
||||
#background-radial-gradient {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
width: 200vw;
|
||||
height: 200vh;
|
||||
transform: translate(-50vw, -100vh);
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background: linear-gradient(180deg, #202738 0%, #070816 100%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background: radial-gradient(100% 100% at 50% 0%, rgba(255, 184, 226, 0.51) 0%, rgba(255, 255, 255, 0) 100%), #FFFFFF
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script defer src="/static/js/bundle.js"></script><meta property="og:title" content="Get PEPE on Uniswap"/><meta property="og:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="og:image:width" content="1200"/><meta property="og:image:height" content="630"/><meta property="og:image:alt" content="Get PEPE on Uniswap"/><meta property="og:type" content="website"/><meta property="og:url" content="http://127.0.0.1:3000/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:card" content="summary_large_image"/><meta property="twitter:title" content="Get PEPE on Uniswap"/><meta property="twitter:image" content="http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933"/><meta property="twitter:image:alt" content="Get PEPE on Uniswap"/></head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
<div id="root">
|
||||
<!-- Triggers the font to load immediately and then is replaced by the app -->
|
||||
<div> </div>
|
||||
</div>
|
||||
|
||||
<div id="background-radial-gradient"></div>
|
||||
</body>
|
||||
</html>
|
||||
"
|
||||
`;
|
||||
68
functions/tokens/token.test.ts
Normal file
68
functions/tokens/token.test.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
const tokens = [
|
||||
{
|
||||
address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
network: 'ethereum',
|
||||
symbol: 'USDC',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'ethereum',
|
||||
symbol: 'ETH',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/NATIVE',
|
||||
},
|
||||
{
|
||||
address: 'NATIVE',
|
||||
network: 'polygon',
|
||||
symbol: 'MATIC',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/polygon/NATIVE',
|
||||
},
|
||||
{
|
||||
address: '0x6982508145454ce325ddbe47a25d4ec3d2311933',
|
||||
network: 'ethereum',
|
||||
symbol: 'PEPE',
|
||||
image: 'http://127.0.0.1:3000/api/image/tokens/ethereum/0x6982508145454ce325ddbe47a25d4ec3d2311933',
|
||||
},
|
||||
]
|
||||
|
||||
test.each(tokens)('should inject metadata for valid tokens', async (token) => {
|
||||
const url = 'http://127.0.0.1:3000/tokens/' + token.network + '/' + token.address
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).toMatchSnapshot()
|
||||
expect(body).toContain(`<meta property="og:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).not.toContain(`<meta property="og:description"`)
|
||||
expect(body).toContain(`<meta property="og:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:width" content="1200"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:height" content="630"/>`)
|
||||
expect(body).toContain(`<meta property="og:type" content="website"/>`)
|
||||
expect(body).toContain(`<meta property="og:url" content="${url}"/>`)
|
||||
expect(body).toContain(`<meta property="og:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:card" content="summary_large_image"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:title" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image" content="${token.image}"/>`)
|
||||
expect(body).toContain(`<meta property="twitter:image:alt" content="Get ${token.symbol} on Uniswap"/>`)
|
||||
})
|
||||
|
||||
const invalidTokens = [
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb49',
|
||||
'http://127.0.0.1:3000/tokens/ethereum',
|
||||
'http://127.0.0.1:3000/tokens/ethereun',
|
||||
'http://127.0.0.1:3000/tokens/ethereum/0x0',
|
||||
'http://127.0.0.1:3000/tokens/ethereum//',
|
||||
'http://127.0.0.1:3000/tokens/potato/?potato=1',
|
||||
]
|
||||
|
||||
test.each(invalidTokens)('should not inject metadata for invalid tokens', async (url) => {
|
||||
const body = await fetch(new Request(url)).then((res) => res.text())
|
||||
expect(body).not.toContain('og:title')
|
||||
expect(body).not.toContain('og:image')
|
||||
expect(body).not.toContain('og:image:width')
|
||||
expect(body).not.toContain('og:image:height')
|
||||
expect(body).not.toContain('og:type')
|
||||
expect(body).not.toContain('og:url')
|
||||
expect(body).not.toContain('og:image:alt')
|
||||
expect(body).not.toContain('twitter:card')
|
||||
expect(body).not.toContain('twitter:title')
|
||||
expect(body).not.toContain('twitter:image')
|
||||
expect(body).not.toContain('twitter:image:alt')
|
||||
})
|
||||
@@ -1,19 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": "functions",
|
||||
"composite": false,
|
||||
"incremental": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"target": "ES6",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["jest", "node"],
|
||||
"isolatedModules": false,
|
||||
"jsx": "react",
|
||||
"moduleResolution": "NodeNext",
|
||||
"tsBuildInfoFile": "../node_modules/.cache/tsbuildinfo/functions", // avoid clobbering the build tsbuildinfo
|
||||
"types": ["jest", "node", "@cloudflare/workers-types"],
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["**/*.ts"],
|
||||
"watchOptions": {
|
||||
"excludeDirectories": ["node_modules"]
|
||||
}
|
||||
"include": ["**/*.ts", ".ts", "**/*.tsx"],
|
||||
}
|
||||
1
functions/types.d.ts
vendored
Normal file
1
functions/types.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'colorthief/src/color-thief-node'
|
||||
43
functions/utils/cache.test.ts
Normal file
43
functions/utils/cache.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import CacheMock from 'browser-cache-mock'
|
||||
|
||||
import { mocked } from '../../src/test-utils/mocked'
|
||||
import Cache from './cache'
|
||||
|
||||
const cacheMock = new CacheMock()
|
||||
|
||||
const data = {
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
const globalAny: any = global
|
||||
globalAny.caches = {
|
||||
open: async () => cacheMock,
|
||||
...cacheMock,
|
||||
}
|
||||
})
|
||||
|
||||
test('Should put cache properly', async () => {
|
||||
jest.spyOn(cacheMock, 'put')
|
||||
await Cache.put(data, 'https://example.com')
|
||||
expect(cacheMock.put).toHaveBeenCalledWith('https://example.com', expect.anything())
|
||||
const call = mocked(cacheMock.put).mock.calls[0]
|
||||
const response = JSON.parse(await (call[1] as Response).clone().text())
|
||||
expect(response).toStrictEqual(data)
|
||||
|
||||
await expect(Cache.match('https://example.com')).resolves.toStrictEqual(data)
|
||||
})
|
||||
|
||||
test('Should match cache properly', async () => {
|
||||
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify(data)))
|
||||
const response = await Cache.match('https://example.com')
|
||||
expect(response).toStrictEqual(data)
|
||||
})
|
||||
|
||||
test('Should return undefined if not all data is present', async () => {
|
||||
jest.spyOn(cacheMock, 'match').mockResolvedValueOnce(new Response(JSON.stringify({ ...data, title: undefined })))
|
||||
const response = await Cache.match('https://example.com')
|
||||
expect(response).toBeUndefined()
|
||||
})
|
||||
32
functions/utils/cache.ts
Normal file
32
functions/utils/cache.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export interface Data {
|
||||
title: string
|
||||
image: string
|
||||
url: string
|
||||
name?: string
|
||||
ogImage?: string
|
||||
isVerified?: boolean
|
||||
symbol?: string
|
||||
}
|
||||
|
||||
const CACHE_NAME = 'functions-cache' as const
|
||||
|
||||
class Cache {
|
||||
async match(request: string): Promise<Data | undefined> {
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
const response = await cache.match(request)
|
||||
if (!response) return undefined
|
||||
const data: Data = JSON.parse(await response.text())
|
||||
if (!data.title || !data.image || !data.url) return undefined
|
||||
return data
|
||||
}
|
||||
|
||||
async put(data: Data, request: string) {
|
||||
// Set max age to 1 week
|
||||
const response = new Response(JSON.stringify(data))
|
||||
response.headers.set('Cache-Control', 'max-age=604800')
|
||||
const cache = await caches.open(CACHE_NAME)
|
||||
await cache.put(request, response)
|
||||
}
|
||||
}
|
||||
|
||||
export default new Cache()
|
||||
41
functions/utils/getAsset.ts
Normal file
41
functions/utils/getAsset.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { AssetDocument, AssetQuery } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
function formatTitleName(name: string | undefined, collectionName: string | undefined, tokenId: string) {
|
||||
if (name) {
|
||||
return name
|
||||
}
|
||||
if (collectionName && tokenId) {
|
||||
return collectionName + ' #' + tokenId
|
||||
}
|
||||
if (tokenId) {
|
||||
return 'Asset #' + tokenId
|
||||
}
|
||||
return 'View NFT on Uniswap'
|
||||
}
|
||||
|
||||
export default async function getAsset(collectionAddress: string, tokenId: string, url: string) {
|
||||
const origin = new URL(url).origin
|
||||
const image = origin + '/api/image/nfts/asset/' + collectionAddress + '/' + tokenId
|
||||
const { data } = await client.query<AssetQuery>({
|
||||
query: AssetDocument,
|
||||
variables: {
|
||||
address: collectionAddress,
|
||||
filter: {
|
||||
tokenIds: [tokenId],
|
||||
},
|
||||
},
|
||||
})
|
||||
const asset = data?.nftAssets?.edges[0]?.node
|
||||
if (!asset) {
|
||||
return undefined
|
||||
}
|
||||
const title = formatTitleName(asset.name, asset.collection?.name, asset.tokenId)
|
||||
const formattedAsset = {
|
||||
title,
|
||||
image,
|
||||
url,
|
||||
ogImage: asset.image?.url ?? origin + '/images/192x192_App_Icon.png',
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
26
functions/utils/getCollection.ts
Normal file
26
functions/utils/getCollection.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { CollectionDocument, CollectionQuery } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
export default async function getCollection(collectionAddress: string, url: string) {
|
||||
const origin = new URL(url).origin
|
||||
const image = origin + '/api/image/nfts/collection/' + collectionAddress
|
||||
const { data } = await client.query<CollectionQuery>({
|
||||
query: CollectionDocument,
|
||||
variables: {
|
||||
addresses: collectionAddress,
|
||||
},
|
||||
})
|
||||
const collection = data?.nftCollections?.edges[0]?.node
|
||||
if (!collection || !collection.name) {
|
||||
return undefined
|
||||
}
|
||||
const formattedAsset = {
|
||||
title: collection.name + ' on Uniswap',
|
||||
image,
|
||||
url,
|
||||
name: collection.name ?? 'Collection',
|
||||
ogImage: collection.image?.url ?? origin + '/images/192x192_App_Icon.png',
|
||||
isVerified: collection.isVerified ?? false,
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
39
functions/utils/getColor.test.ts
Normal file
39
functions/utils/getColor.test.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { DEFAULT_COLOR } from '../constants'
|
||||
import getColor from './getColor'
|
||||
|
||||
test('should return the average color of a black PNG image', async () => {
|
||||
const image = 'https://static.vecteezy.com/system/resources/previews/001/209/957/original/square-png.png'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([0, 0, 0])
|
||||
})
|
||||
|
||||
test('should return the average color of a blue PNG image', async () => {
|
||||
const image = 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTB2Ztcim-RKbOu57kfjYpXnnS1MO5YMUaUH9Lk5Eg&s'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([2, 6, 251])
|
||||
})
|
||||
|
||||
test('should return the average color of a white PNG image', async () => {
|
||||
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([255, 255, 255])
|
||||
})
|
||||
|
||||
test('should return the average color of a white PNG image with whiteness dimmed', async () => {
|
||||
const image = 'https://www.cac.cornell.edu/wiki/images/4/44/White_square.png'
|
||||
const color = await getColor(image, true)
|
||||
expect(color).toEqual(DEFAULT_COLOR)
|
||||
})
|
||||
|
||||
test('should return the average color of a black JPG image', async () => {
|
||||
const image =
|
||||
'https://imageio.forbes.com/specials-images/imageserve/5ed6636cdd5d320006caf841/0x0.jpg?format=jpg&width=1200'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual([0, 0, 0])
|
||||
})
|
||||
|
||||
test('should return default color for a gif image', async () => {
|
||||
const image = 'https://thumbs.gfycat.com/AgitatedLiveAgouti-size_restricted.gif'
|
||||
const color = await getColor(image)
|
||||
expect(color).toEqual(DEFAULT_COLOR)
|
||||
})
|
||||
75
functions/utils/getColor.ts
Normal file
75
functions/utils/getColor.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Buffer } from 'buffer'
|
||||
import JPEG from 'jpeg-js'
|
||||
import PNG from 'png-ts'
|
||||
|
||||
import { DEFAULT_COLOR, predefinedTokenColors } from '../constants'
|
||||
|
||||
export default async function getColor(image: string | undefined, checkDistance = false) {
|
||||
if (!image) {
|
||||
return DEFAULT_COLOR
|
||||
}
|
||||
if (image in predefinedTokenColors) {
|
||||
return predefinedTokenColors[image]
|
||||
}
|
||||
try {
|
||||
const data = await fetch(image)
|
||||
const buffer = await data.arrayBuffer()
|
||||
const arrayBuffer = Buffer.from(buffer)
|
||||
|
||||
const type = data.headers.get('content-type') ?? ''
|
||||
return getAverageColor(arrayBuffer, type, checkDistance)
|
||||
} catch (e) {
|
||||
return DEFAULT_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
function getAverageColor(arrayBuffer: Uint8Array, type: string, checkDistance: boolean) {
|
||||
let pixels
|
||||
switch (type) {
|
||||
case 'image/png': {
|
||||
const image = PNG.load(arrayBuffer)
|
||||
pixels = image.decode()
|
||||
break
|
||||
}
|
||||
case 'image/jpeg' || 'image/jpg': {
|
||||
const jpeg = JPEG.decode(arrayBuffer, { useTArray: true })
|
||||
pixels = jpeg.data
|
||||
break
|
||||
}
|
||||
default: {
|
||||
return DEFAULT_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
const pixelCount = pixels.length / 4
|
||||
|
||||
let transparentPixels = 0
|
||||
|
||||
let r = 0
|
||||
let g = 0
|
||||
let b = 0
|
||||
|
||||
for (let i = 0; i < pixelCount; i++) {
|
||||
if (pixels[i * 4 + 3] === 0) {
|
||||
transparentPixels++
|
||||
continue
|
||||
}
|
||||
r += pixels[i * 4]
|
||||
g += pixels[i * 4 + 1]
|
||||
b += pixels[i * 4 + 2]
|
||||
}
|
||||
|
||||
r = Math.floor(r / (pixelCount - transparentPixels))
|
||||
g = Math.floor(g / (pixelCount - transparentPixels))
|
||||
b = Math.floor(b / (pixelCount - transparentPixels))
|
||||
|
||||
if (checkDistance) {
|
||||
const distance = Math.sqrt(Math.pow(r - 255, 2) + Math.pow(g - 255, 2) + Math.pow(b - 255, 2))
|
||||
|
||||
if (distance < 50) {
|
||||
return DEFAULT_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
return [r, g, b]
|
||||
}
|
||||
5
functions/utils/getFont.ts
Normal file
5
functions/utils/getFont.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default async function getFont(origin: string) {
|
||||
const url = origin + '/fonts/Inter-normal.var.ttf'
|
||||
const font = await fetch(url)
|
||||
return font.arrayBuffer()
|
||||
}
|
||||
16
functions/utils/getNetworkLogoURL.ts
Normal file
16
functions/utils/getNetworkLogoURL.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
|
||||
export default function getNetworkLogoUrl(network: string, origin: string) {
|
||||
switch (network) {
|
||||
case Chain.Polygon:
|
||||
return origin + '/images/logos/Polygon_Logo.png'
|
||||
case Chain.Arbitrum:
|
||||
return origin + '/images/logos/Arbitrum_Logo.png'
|
||||
case Chain.Optimism:
|
||||
return origin + '/images/logos/Optimism_Logo.png'
|
||||
case Chain.Celo:
|
||||
return origin + '/images/logos/Celo_Logo.png'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
38
functions/utils/getRequest.test.ts
Normal file
38
functions/utils/getRequest.test.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import * as matchers from 'jest-extended'
|
||||
expect.extend(matchers)
|
||||
|
||||
import { mocked } from '../../src/test-utils/mocked'
|
||||
import Cache, { Data } from './cache'
|
||||
import { getRequest } from './getRequest'
|
||||
|
||||
jest.mock('./cache', () => ({
|
||||
match: jest.fn(),
|
||||
put: jest.fn(),
|
||||
}))
|
||||
|
||||
test('should call Cache.match before calling getData when request is not cached', async () => {
|
||||
const url = 'https://example.com'
|
||||
const getData = jest.fn().mockResolvedValueOnce({
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
})
|
||||
await getRequest(url, getData, (data): data is Data => true)
|
||||
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||
expect(getData).toHaveBeenCalled()
|
||||
expect(Cache.match).toHaveBeenCalledBefore(getData)
|
||||
expect(Cache.put).toHaveBeenCalledAfter(getData)
|
||||
})
|
||||
|
||||
test('getData should not be called when request is cached', async () => {
|
||||
const url = 'https://example.com'
|
||||
mocked(Cache.match).mockResolvedValueOnce({
|
||||
title: 'test',
|
||||
image: 'testImage',
|
||||
url: 'testUrl',
|
||||
})
|
||||
const getData = jest.fn()
|
||||
await getRequest(url, getData, (data): data is Data => true)
|
||||
expect(Cache.match).toHaveBeenCalledWith(url)
|
||||
expect(getData).not.toHaveBeenCalled()
|
||||
})
|
||||
42
functions/utils/getRequest.ts
Normal file
42
functions/utils/getRequest.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { MetaTagInjector } from '../components/metaTagInjector'
|
||||
import Cache from './cache'
|
||||
import { Data } from './cache'
|
||||
|
||||
export async function getMetadataRequest(
|
||||
res: Promise<Response>,
|
||||
url: string,
|
||||
getData: () => Promise<Data | undefined>
|
||||
) {
|
||||
try {
|
||||
const cachedData = await getRequest(url, getData, (data): data is Data => true)
|
||||
if (cachedData) {
|
||||
return new HTMLRewriter().on('head', new MetaTagInjector(cachedData)).transform(await res)
|
||||
} else {
|
||||
return res
|
||||
}
|
||||
} catch (e) {
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRequest<T extends Data>(
|
||||
url: string,
|
||||
getData: () => Promise<T | undefined>,
|
||||
validateData: (data: Data) => data is T
|
||||
): Promise<T | undefined> {
|
||||
try {
|
||||
const cachedData = await Cache.match(url)
|
||||
if (cachedData && validateData(cachedData)) {
|
||||
return cachedData
|
||||
} else {
|
||||
const data = await getData()
|
||||
if (!data) {
|
||||
return undefined
|
||||
}
|
||||
await Cache.put(data, url)
|
||||
return data
|
||||
}
|
||||
} catch (e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
57
functions/utils/getToken.ts
Normal file
57
functions/utils/getToken.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { TokenDocument, TokenQuery } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import { Chain } from '../../src/graphql/data/__generated__/types-and-hooks'
|
||||
import client from '../client'
|
||||
|
||||
function formatTitleName(symbol: string | undefined, name: string | undefined) {
|
||||
if (symbol) {
|
||||
return 'Get ' + symbol + ' on Uniswap'
|
||||
}
|
||||
if (name) {
|
||||
return 'Get ' + name + ' on Uniswap'
|
||||
}
|
||||
return 'View Token on Uniswap'
|
||||
}
|
||||
|
||||
const convertTokenAddress = (networkName: string, tokenAddress: string) => {
|
||||
if (tokenAddress === 'NATIVE') {
|
||||
switch (networkName) {
|
||||
case Chain.Celo:
|
||||
return '0x471EcE3750Da237f93B8E339c536989b8978a438'
|
||||
case Chain.Polygon:
|
||||
return '0x0000000000000000000000000000000000001010'
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
return tokenAddress
|
||||
}
|
||||
|
||||
export default async function getToken(networkName: string, tokenAddress: string, url: string) {
|
||||
const origin = new URL(url).origin
|
||||
const image = origin + '/api/image/tokens/' + networkName + '/' + tokenAddress
|
||||
const uppercaseNetworkName = networkName.toUpperCase()
|
||||
const convertedTokenAddress = convertTokenAddress(uppercaseNetworkName, tokenAddress)
|
||||
const { data } = await client.query<TokenQuery>({
|
||||
query: TokenDocument,
|
||||
variables: {
|
||||
chain: uppercaseNetworkName,
|
||||
address: convertedTokenAddress,
|
||||
},
|
||||
})
|
||||
const asset = data?.token
|
||||
if (!asset) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const title = formatTitleName(asset.symbol, asset.name)
|
||||
|
||||
const formattedAsset = {
|
||||
title,
|
||||
image,
|
||||
url,
|
||||
symbol: asset.symbol ?? 'UNK',
|
||||
ogImage: asset.project?.logoUrl,
|
||||
name: asset.name ?? 'Token',
|
||||
}
|
||||
return formattedAsset
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
import { ChainId } from '@uniswap/sdk-core'
|
||||
import { UNIVERSAL_ROUTER_CREATION_BLOCK } from '@uniswap/universal-router-sdk'
|
||||
|
||||
/* eslint-env node */
|
||||
require('dotenv').config()
|
||||
|
||||
// Block selection is arbitrary, as e2e tests will build up their own state.
|
||||
// The only requirement is that all infrastructure under test (eg Permit2 contracts) are already deployed.
|
||||
// TODO(WEB-2187): Make more dynamic to avoid manually updating
|
||||
const BLOCK_NUMBER = 17693163
|
||||
const POLYGON_BLOCK_NUMBER = 43600000
|
||||
|
||||
const forkingConfig = {
|
||||
httpHeaders: {
|
||||
Origin: 'localhost:3000', // infura allowlists requests by origin
|
||||
@@ -18,12 +13,12 @@ const forkingConfig = {
|
||||
const forks = {
|
||||
[ChainId.MAINNET]: {
|
||||
url: `https://mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: BLOCK_NUMBER,
|
||||
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.MAINNET),
|
||||
...forkingConfig,
|
||||
},
|
||||
[ChainId.POLYGON]: {
|
||||
url: `https://polygon-mainnet.infura.io/v3/${process.env.REACT_APP_INFURA_KEY}`,
|
||||
blockNumber: POLYGON_BLOCK_NUMBER,
|
||||
blockNumber: UNIVERSAL_ROUTER_CREATION_BLOCK(ChainId.POLYGON),
|
||||
...forkingConfig,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,64 +1,4 @@
|
||||
/* eslint-env node */
|
||||
import { default as babelExtractor } from '@lingui/cli/api/extractors/babel'
|
||||
import { createHash } from 'crypto'
|
||||
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
|
||||
import { existsSync } from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
/** A custom caching extractor built on top of babelExtractor. */
|
||||
const cachingExtractor: typeof babelExtractor = {
|
||||
/** Delegates to babelExtractor.match. */
|
||||
match(filename: string) {
|
||||
return babelExtractor.match(filename)
|
||||
},
|
||||
/**
|
||||
* Checks a cache before extraction, only delegating to babelExtractor.extract if the file has changed.
|
||||
*
|
||||
* The lingui extractor works by extracting JSON (the catalog) from `filename` to `buildDir/filename.json`.
|
||||
* Caching works by man-in-the-middling this:
|
||||
* - File freshness is computed as a hash of `filename` contents.
|
||||
* - Before extracting, we check the cache to see if we already have a fresh catalog for the file.
|
||||
* If we do, we copy it to `localeDir/filename.json`. Copying is significantly faster than extracting.
|
||||
* - After extracting, we copy the catalog to the cache.
|
||||
*/
|
||||
extract(filename: string, localeDir: string, ...options: unknown[]) {
|
||||
// This runs from node_modules/@lingui/conf, so we need to back out to the root.
|
||||
const root = __dirname.split('/node_modules')[0]
|
||||
|
||||
// This logic mimics catalogFilename in @lingui/babel-plugin-extract-messages.
|
||||
const buildDir = path.join(localeDir, '_build')
|
||||
const localePath = path.join(buildDir, filename + '.json')
|
||||
|
||||
const filePath = path.join(root, filename)
|
||||
const fileHash = createHash('sha256').update(readFileSync(filePath)).digest('hex')
|
||||
|
||||
const cacheRoot = path.join(root, 'node_modules/.cache/lingui')
|
||||
const cachePath = path.join(cacheRoot, filename + '.json')
|
||||
|
||||
// If we have a matching cached copy of the catalog, we can copy it to localePath and return early.
|
||||
if (existsSync(cachePath)) {
|
||||
const { hash, catalog } = JSON.parse(readFileSync(cachePath, 'utf8'))
|
||||
if (hash === fileHash) {
|
||||
if (catalog) {
|
||||
mkdirSync(path.dirname(localePath), { recursive: true })
|
||||
writeFileSync(localePath, JSON.stringify(catalog, null, 2))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
babelExtractor.extract(filename, localeDir, ...options)
|
||||
|
||||
// Cache the extracted catalog.
|
||||
mkdirSync(path.dirname(cachePath), { recursive: true })
|
||||
if (existsSync(localePath)) {
|
||||
const catalog = JSON.parse(readFileSync(localePath, 'utf8'))
|
||||
writeFileSync(cachePath, JSON.stringify({ hash: fileHash, catalog }))
|
||||
} else {
|
||||
writeFileSync(cachePath, JSON.stringify({ hash: fileHash }))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
const linguiConfig = {
|
||||
catalogs: [
|
||||
@@ -120,7 +60,6 @@ const linguiConfig = {
|
||||
rootDir: '.',
|
||||
runtimeConfigModule: ['@lingui/core', 'i18n'],
|
||||
sourceLocale: 'en-US',
|
||||
extractors: [cachingExtractor],
|
||||
}
|
||||
|
||||
export default linguiConfig
|
||||
|
||||
142
package.json
142
package.json
@@ -2,7 +2,6 @@
|
||||
"name": "@uniswap/interface",
|
||||
"version": "1.1.0",
|
||||
"description": "Uniswap Interface",
|
||||
"homepage": ".",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"ajv": "node scripts/compile-ajv-validators.js",
|
||||
@@ -19,18 +18,30 @@
|
||||
"i18n": "yarn i18n:extract --clean && yarn i18n:compile",
|
||||
"prepare": "concurrently \"npm:ajv\" \"npm:contracts\" \"npm:graphql\" \"npm:i18n\"",
|
||||
"start": "craco start",
|
||||
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --proxy=3001 --port=3000 -- yarn start",
|
||||
"start:cloud": "NODE_OPTIONS=--dns-result-order=ipv4first PORT=3001 npx wrangler pages dev --compatibility-flags=nodejs_compat --compatibility-date=2023-08-01 --proxy=3001 --port=3000 -- yarn start",
|
||||
"build": "craco build",
|
||||
"build:e2e": "REACT_APP_CSP_ALLOW_UNSAFE_EVAL=true REACT_APP_ADD_COVERAGE_INSTRUMENTATION=true craco build",
|
||||
"analyze": "source-map-explorer 'build/static/js/*.js' --only-mapped",
|
||||
"serve": "serve build -l 3000",
|
||||
"lint": "yarn eslint --ignore-path .gitignore --cache --cache-location node_modules/.cache/eslint/ .",
|
||||
"typecheck": "tsc",
|
||||
"typecheck:cloud": "tsc -p functions/tsconfig.json",
|
||||
"typecheck:cypress": "tsc -p cypress/tsconfig.json",
|
||||
"test": "craco test",
|
||||
"test:cloud": "NODE_OPTIONS=--experimental-vm-modules yarn jest functions --watch --config=functions/jest.config.json",
|
||||
"test:cloud": "NODE_OPTIONS=--experimental-vm-modules yarn jest functions --config=functions/jest.config.json",
|
||||
"cypress:open": "cypress open --browser chrome --e2e",
|
||||
"cypress:run": "cypress run --browser chrome --e2e",
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest"
|
||||
"deduplicate": "yarn-deduplicate --strategy=highest",
|
||||
"postinstall": "yarn patch-package"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"yarn.lock": [
|
||||
"yarn deduplicate"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"collectCoverageFrom": [
|
||||
@@ -57,9 +68,8 @@
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
">0.5%",
|
||||
"not dead"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
@@ -69,10 +79,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.22.7",
|
||||
"@cloudflare/workers-types": "^4.20230518.0",
|
||||
"@cloudflare/workers-types": "^4.20230710.1",
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@ethersproject/experimental": "^5.4.0",
|
||||
"@lingui/cli": "^3.9.0",
|
||||
"@lingui/cli": "^4.3.0",
|
||||
"@lingui/swc-plugin": "^4.0.4",
|
||||
"@swc/core": "^1.3.72",
|
||||
"@swc/jest": "^0.2.27",
|
||||
"@swc/plugin-styled-components": "^1.5.70",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
@@ -80,11 +94,11 @@
|
||||
"@types/array.prototype.flat": "^1.2.1",
|
||||
"@types/array.prototype.flatmap": "^1.2.2",
|
||||
"@types/d3": "^6.7.1",
|
||||
"@types/jest": "^25.2.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/lingui__core": "^2.7.1",
|
||||
"@types/lingui__macro": "^2.7.4",
|
||||
"@types/lingui__react": "^2.8.3",
|
||||
"@types/ms.macro": "^2.0.0",
|
||||
"@types/ms": "^0.7.31",
|
||||
"@types/multicodec": "^1.0.0",
|
||||
"@types/node": "^13.13.5",
|
||||
"@types/qs": "^6.9.2",
|
||||
@@ -103,93 +117,102 @@
|
||||
"@types/wcag-contrast": "^3.0.0",
|
||||
"@uniswap/default-token-list": "^11.2.0",
|
||||
"@uniswap/eslint-config": "^1.2.0",
|
||||
"@vanilla-extract/babel-plugin": "^1.1.7",
|
||||
"@vanilla-extract/jest-transform": "^1.1.1",
|
||||
"@vanilla-extract/webpack-plugin": "^2.1.11",
|
||||
"@vanilla-extract/webpack-plugin": "^2.2.0",
|
||||
"@vercel/og": "0.5.8",
|
||||
"@walletconnect/types": "^2.8.6",
|
||||
"babel-jest": "^29.6.1",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"browser-cache-mock": "^0.1.7",
|
||||
"buffer": "^6.0.3",
|
||||
"concurrently": "^8.0.1",
|
||||
"cypress": "12.12.0",
|
||||
"cypress-hardhat": "^2.4.2",
|
||||
"cypress-hardhat": "^2.5.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^7.11.0",
|
||||
"eslint-plugin-import": "^2.27",
|
||||
"eslint-plugin-rulesdir": "^0.2.2",
|
||||
"hardhat": "^2.14.0",
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.6.1",
|
||||
"jest-dev-server": "^9.0.0",
|
||||
"jest-extended": "^4.0.1",
|
||||
"jest-fail-on-console": "^3.1.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"jest-styled-components": "^7.0.8",
|
||||
"ms.macro": "^2.0.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"lint-staged": "^14.0.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"patch-package": "^7.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"png-ts": "^0.0.3",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.8.8",
|
||||
"process": "^0.11.10",
|
||||
"react-scripts": "^5.0.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"serve": "^11.3.2",
|
||||
"source-map-explorer": "^2.5.3",
|
||||
"swc-loader": "^0.2.3",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-transform-graphql-tag": "^0.2.1",
|
||||
"tsafe": "^1.6.4",
|
||||
"typechain": "^5.0.0",
|
||||
"typescript": "^4.4.3",
|
||||
"typescript": "^4.9.4",
|
||||
"webpack": "^5.88.2",
|
||||
"webpack-retry-chunk-load-plugin": "^3.1.1",
|
||||
"wrangler": "https://prerelease-registry.devprod.cloudflare.dev/workers-sdk/runs/4925945367/npm-package-wrangler-3048",
|
||||
"wrangler": "^3.5.0",
|
||||
"yarn-deduplicate": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.2",
|
||||
"@coinbase/wallet-sdk": "^3.6.4",
|
||||
"@cypress/code-coverage": "^3.10.0",
|
||||
"@fontsource/ibm-plex-mono": "^4.5.1",
|
||||
"@fontsource/inter": "^4.5.1",
|
||||
"@graphql-codegen/cli": "^2.15.0",
|
||||
"@graphql-codegen/client-preset": "^1.2.1",
|
||||
"@graphql-codegen/typescript": "^2.8.3",
|
||||
"@graphql-codegen/typescript-operations": "^2.5.8",
|
||||
"@graphql-codegen/cli": "^3.3.1",
|
||||
"@graphql-codegen/client-preset": "^3.0.1",
|
||||
"@graphql-codegen/typescript": "^3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.2",
|
||||
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
||||
"@graphql-codegen/typescript-resolvers": "^2.7.8",
|
||||
"@graphql-codegen/typescript-resolvers": "^3.2.1",
|
||||
"@juggle/resize-observer": "^3.4.0",
|
||||
"@lingui/core": "^3.14.0",
|
||||
"@lingui/macro": "^3.14.0",
|
||||
"@lingui/react": "^3.14.0",
|
||||
"@lingui/core": "^4.3.0",
|
||||
"@lingui/macro": "^4.3.0",
|
||||
"@lingui/react": "^4.3.0",
|
||||
"@looksrare/sdk": "^0.10.2",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@opensea/seaport-js": "^1.2.0",
|
||||
"@popperjs/core": "^2.4.4",
|
||||
"@reach/dialog": "^0.10.3",
|
||||
"@reach/portal": "^0.10.3",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@sentry/react": "^7.45.0",
|
||||
"@sentry/tracing": "^7.45.0",
|
||||
"@sentry/types": "^7.45.0",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@uniswap/analytics": "^1.3.1",
|
||||
"@uniswap/analytics-events": "^2.13.0",
|
||||
"@uniswap/conedison": "^1.8.0",
|
||||
"@uniswap/analytics": "^1.4.0",
|
||||
"@uniswap/analytics-events": "^2.17.0",
|
||||
"@uniswap/governance": "^1.0.2",
|
||||
"@uniswap/liquidity-staker": "^1.0.2",
|
||||
"@uniswap/merkle-distributor": "1.0.1",
|
||||
"@uniswap/permit2-sdk": "1.2.0",
|
||||
"@uniswap/merkle-distributor": "^1.0.1",
|
||||
"@uniswap/permit2-sdk": "^1.2.0",
|
||||
"@uniswap/redux-multicall": "^1.1.8",
|
||||
"@uniswap/router-sdk": "^1.3.0",
|
||||
"@uniswap/sdk-core": "^3.2.6",
|
||||
"@uniswap/smart-order-router": "3.13.5",
|
||||
"@uniswap/router-sdk": "^1.6.0",
|
||||
"@uniswap/sdk-core": "^4.0.3",
|
||||
"@uniswap/smart-order-router": "^3.15.0",
|
||||
"@uniswap/token-lists": "^1.0.0-beta.33",
|
||||
"@uniswap/uniswapx-sdk": "^1.0.0",
|
||||
"@uniswap/universal-router-sdk": "^1.5.3",
|
||||
"@uniswap/v2-core": "1.0.0",
|
||||
"@uniswap/uniswapx-sdk": "^1.3.0",
|
||||
"@uniswap/universal-router-sdk": "^1.5.6",
|
||||
"@uniswap/v2-core": "^1.0.1",
|
||||
"@uniswap/v2-periphery": "^1.1.0-beta.0",
|
||||
"@uniswap/v2-sdk": "^3.0.1",
|
||||
"@uniswap/v3-core": "1.0.0",
|
||||
"@uniswap/v2-sdk": "^3.2.0",
|
||||
"@uniswap/v3-core": "^1.0.1",
|
||||
"@uniswap/v3-periphery": "^1.1.1",
|
||||
"@uniswap/v3-sdk": "^3.9.0",
|
||||
"@vanilla-extract/css": "^1.7.2",
|
||||
"@vanilla-extract/css-utils": "^0.1.2",
|
||||
"@vanilla-extract/dynamic": "^2.0.2",
|
||||
"@vanilla-extract/sprinkles": "^1.4.1",
|
||||
"@uniswap/v3-sdk": "^3.10.0",
|
||||
"@vanilla-extract/css": "^1.12.0",
|
||||
"@vanilla-extract/css-utils": "^0.1.3",
|
||||
"@vanilla-extract/dynamic": "^2.0.3",
|
||||
"@vanilla-extract/sprinkles": "^1.6.1",
|
||||
"@visx/axis": "^2.12.2",
|
||||
"@visx/event": "^2.6.0",
|
||||
"@visx/glyph": "^2.10.0",
|
||||
@@ -197,16 +220,16 @@
|
||||
"@visx/react-spring": "^2.12.2",
|
||||
"@visx/responsive": "^2.10.0",
|
||||
"@visx/shape": "^2.11.1",
|
||||
"@web3-react/coinbase-wallet": "^8.2.0",
|
||||
"@web3-react/core": "^8.2.0",
|
||||
"@web3-react/eip1193": "^8.2.0",
|
||||
"@web3-react/empty": "^8.2.0",
|
||||
"@web3-react/gnosis-safe": "^8.2.1",
|
||||
"@web3-react/metamask": "^8.2.0",
|
||||
"@web3-react/network": "^8.2.0",
|
||||
"@web3-react/types": "^8.2.0",
|
||||
"@web3-react/url": "^8.2.0",
|
||||
"@web3-react/walletconnect-v2": "^8.3.7",
|
||||
"@web3-react/coinbase-wallet": "^8.2.2",
|
||||
"@web3-react/core": "^8.2.2",
|
||||
"@web3-react/eip1193": "^8.2.2",
|
||||
"@web3-react/empty": "^8.2.2",
|
||||
"@web3-react/gnosis-safe": "^8.2.3",
|
||||
"@web3-react/metamask": "^8.2.3",
|
||||
"@web3-react/network": "^8.2.2",
|
||||
"@web3-react/types": "^8.2.2",
|
||||
"@web3-react/url": "^8.2.2",
|
||||
"@web3-react/walletconnect-v2": "^8.5.0",
|
||||
"ajv": "^8.11.0",
|
||||
"ajv-formats": "^2.1.1",
|
||||
"array.prototype.flat": "^1.2.4",
|
||||
@@ -216,6 +239,7 @@
|
||||
"copy-to-clipboard": "^3.2.0",
|
||||
"d3": "^7.6.1",
|
||||
"ethers": "^5.7.2",
|
||||
"ext-name": "^5.0.0",
|
||||
"focus-visible": "^5.2.0",
|
||||
"get-graphql-schema": "^2.1.2",
|
||||
"graphql": "^16.5.0",
|
||||
@@ -224,7 +248,9 @@
|
||||
"inter-ui": "^3.13.1",
|
||||
"jotai": "^1.3.7",
|
||||
"jsbi": "^3.1.4",
|
||||
"localforage": "^1.10.0",
|
||||
"make-plural": "^7.0.0",
|
||||
"ms": "^2.1.3",
|
||||
"multicodec": "^3.0.1",
|
||||
"multihashes": "^4.0.2",
|
||||
"node-vibrant": "^3.2.1-alpha.1",
|
||||
@@ -253,7 +279,7 @@
|
||||
"react-window-infinite-loader": "^1.0.8",
|
||||
"rebass": "^4.0.7",
|
||||
"redux": "^4.1.2",
|
||||
"redux-localstorage-simple": "^2.3.1",
|
||||
"redux-persist": "^6.0.0",
|
||||
"statsig-react": "^1.22.0",
|
||||
"styled-components": "^5.3.5",
|
||||
"tiny-invariant": "^1.2.0",
|
||||
|
||||
50
patches/@vercel+og+0.5.8.patch
Normal file
50
patches/@vercel+og+0.5.8.patch
Normal file
@@ -0,0 +1,50 @@
|
||||
diff --git a/node_modules/@vercel/og/dist/index.edge.js b/node_modules/@vercel/og/dist/index.edge.js
|
||||
index 5187f88..c4a1c41 100644
|
||||
--- a/node_modules/@vercel/og/dist/index.edge.js
|
||||
+++ b/node_modules/@vercel/og/dist/index.edge.js
|
||||
@@ -18673,8 +18673,8 @@ var Resvg2 = class extends Resvg {
|
||||
};
|
||||
|
||||
// src/index.edge.ts
|
||||
-import resvg_wasm from "./resvg.wasm?module";
|
||||
-import yoga_wasm from "./yoga.wasm?module";
|
||||
+import resvg_wasm from "./resvg.wasm";
|
||||
+import yoga_wasm from "./yoga.wasm";
|
||||
|
||||
// src/emoji/index.ts
|
||||
var U200D = String.fromCharCode(8205);
|
||||
@@ -18809,18 +18809,18 @@ async function render(satori, resvg, opts, defaultFonts, element) {
|
||||
// src/index.edge.ts
|
||||
var initializedResvg = initWasm(resvg_wasm);
|
||||
var initializedYoga = initYoga(yoga_wasm).then((yoga2) => Ll(yoga2));
|
||||
-var fallbackFont = fetch(new URL("./noto-sans-v27-latin-regular.ttf", import.meta.url)).then((res) => res.arrayBuffer());
|
||||
+// var fallbackFont = fetch(new URL("https://fonts.gstatic.com/s/notosans/v28/o-0IIpQlx3QUlC5A4PNr6zRF.ttf", import.meta.url)).then((res) => res.arrayBuffer());
|
||||
var ImageResponse = class {
|
||||
constructor(element, options = {}) {
|
||||
const result = new ReadableStream({
|
||||
async start(controller) {
|
||||
await initializedYoga;
|
||||
await initializedResvg;
|
||||
- const fontData = await fallbackFont;
|
||||
+ // const fontData = await fallbackFont;
|
||||
const fonts = [
|
||||
{
|
||||
name: "sans serif",
|
||||
- data: fontData,
|
||||
+ // data: fontData,
|
||||
weight: 700,
|
||||
style: "normal"
|
||||
}
|
||||
diff --git a/node_modules/@vercel/og/dist/types.d.ts b/node_modules/@vercel/og/dist/types.d.ts
|
||||
index dde26cc..eb59ff4 100644
|
||||
--- a/node_modules/@vercel/og/dist/types.d.ts
|
||||
+++ b/node_modules/@vercel/og/dist/types.d.ts
|
||||
@@ -30,7 +30,7 @@ declare type ImageOptions = {
|
||||
* @type {{ data: ArrayBuffer; name: string; weight?: 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900; style?: 'normal' | 'italic' }[]}
|
||||
* @default Noto Sans Latin Regular.
|
||||
*/
|
||||
- fonts?: SatoriOptions['fonts'];
|
||||
+ fonts: SatoriOptions['fonts'];
|
||||
/**
|
||||
* Using a specific Emoji style. Defaults to `twemoji`.
|
||||
*
|
||||
29
public/.well-known/assetlinks.json
Normal file
29
public/.well-known/assetlinks.json
Normal file
@@ -0,0 +1,29 @@
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap",
|
||||
"sha256_cert_fingerprints":
|
||||
["97:A5:81:51:DA:AF:8F:6E:65:3A:90:1E:82:12:6C:FB:61:2D:36:C7:CF:20:61:6B:A3:4C:52:CA:BC:58:43:8E", "F9:E9:E3:F0:04:28:66:62:81:44:50:7E:D6:A9:5F:B9:65:39:02:70:1D:13:74:15:D3:E1:A3:1B:D4:38:3A:1F"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.beta",
|
||||
"sha256_cert_fingerprints":
|
||||
["E5:39:87:DC:4D:FD:4C:1B:A6:74:36:7D:3A:3B:6B:ED:9E:B3:66:89:92:8A:1B:B8:FC:1B:22:56:56:B4:46:A3", "54:4B:62:33:17:9B:5F:A8:E6:5D:D3:A6:E5:9D:80:5F:A5:02:7F:E2:14:B8:C1:7A:AC:4B:8D:E0:65:49:87:41"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.uniswap.dev",
|
||||
"sha256_cert_fingerprints":
|
||||
["5A:6D:23:50:2F:1E:0D:01:DC:96:65:F3:3A:18:4C:4C:8C:67:E0:09:99:9B:B1:9B:BF:44:99:D0:D1:D0:FC:5E", "02:E6:1C:76:8C:75:C3:78:C8:8C:FE:7B:2E:8F:4B:E1:FA:47:F2:F6:1A:DB:57:69:4A:41:99:C6:71:2C:AB:E3", "FA:C6:17:45:DC:09:03:78:6F:B9:ED:E6:2A:96:2B:39:9F:73:48:F0:BB:6F:89:9B:83:32:66:75:91:03:3B:9C"]
|
||||
}
|
||||
}
|
||||
]
|
||||
46
public/apple-app-site-association
Normal file
46
public/apple-app-site-association
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"applinks": {
|
||||
"details": [
|
||||
{
|
||||
"appIDs": [
|
||||
"JH3UHGZD75.com.uniswap.mobile",
|
||||
"JH3UHGZD75.com.uniswap.mobile.dev"
|
||||
],
|
||||
"components": [
|
||||
{
|
||||
"#": "/nfts/asset/*",
|
||||
"comment": "NFT Item"
|
||||
},
|
||||
{
|
||||
"#": "/nfts/collection/*",
|
||||
"comment": "NFT Collection"
|
||||
},
|
||||
{
|
||||
"#": "/tokens/*",
|
||||
"comment": "Token address"
|
||||
},
|
||||
{
|
||||
"#": "/address/*",
|
||||
"comment": "Wallet address"
|
||||
},
|
||||
{
|
||||
"/": "/nfts/asset/*",
|
||||
"comment": "NFT Item"
|
||||
},
|
||||
{
|
||||
"/": "/nfts/collection/*",
|
||||
"comment": "NFT Collection"
|
||||
},
|
||||
{
|
||||
"/": "/tokens/*",
|
||||
"comment": "Token address"
|
||||
},
|
||||
{
|
||||
"/": "/address/*",
|
||||
"comment": "Wallet address"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user