Commit 9cda74f3 authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into address-logs-tx

parents 61b2d54a 330ac0bf
......@@ -51,3 +51,4 @@ NEXT_PUBLIC_API_PORT=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_PORT__
# external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
......@@ -6,6 +6,7 @@ WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock ./
RUN apk add git
RUN yarn --frozen-lockfile
# Rebuild the source code only when needed
......
......@@ -125,6 +125,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_SENTRY_DSN | `string` *(optional)* | Client key for your Sentry.io app | `<secret>` |
| SENTRY_CSP_REPORT_URI | `string` *(optional)* | URL for sending CSP-reports to your Sentry.io app | `<secret>` |
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` *(optional)* | Client id for [Auth0](https://auth0.com/) provider | `<secret>` |
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | `<secret>` |
### Marketplace app configuration properties
......
......@@ -115,6 +115,9 @@ const config = Object.freeze({
showGasTracker: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER) === 'false' ? false : true,
showAvgBlockTime: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME) === 'false' ? false : true,
},
walletConnect: {
projectId: getEnvValue(process.env.NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID),
},
});
export default config;
......@@ -3,11 +3,11 @@ NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Ethereum','url':'https://blockscout.com
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]
# network config
NEXT_PUBLIC_NETWORK_NAME=Ethereum
NEXT_PUBLIC_NETWORK_NAME=Goerli
NEXT_PUBLIC_NETWORK_SHORT_NAME=Goerli
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=ethereum
NEXT_PUBLIC_NETWORK_TYPE=goerli
NEXT_PUBLIC_NETWORK_ID=420
NEXT_PUBLIC_NETWORK_ID=5
NEXT_PUBLIC_NETWORK_CURRENCY_NAME=Ether
NEXT_PUBLIC_NETWORK_CURRENCY_SYMBOL=ETH
NEXT_PUBLIC_NETWORK_CURRENCY_DECIMALS=18
......
......@@ -61,6 +61,10 @@ const oldUrls = [
oldPath: '/address/:id/validations',
newPath: `${ PATHS.address_index }?tab=blocks_validated`,
},
{
oldPath: '/address/:id/tokens/:hash/token-transfers',
newPath: `${ PATHS.address_index }?tab=token_transfers&token=:hash`,
},
];
async function redirects() {
......
......@@ -126,14 +126,16 @@ frontend:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:cRcbMzOW2AFyDz/lqv4T9SpDCabXdBKLFN9dbq/rFg4=,iv:G9afggfvZ+BpuE5u31KDVfIxhlP38IE28Yt9pMQsd6E=,tag:t1kkjXPolc/58fuQVULLfA==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:LhVaS9L3ujRwznCe6D+edYU5XT4GLXm4jJ4lKGsrzyA=,iv:ibEJAfSrm3ZWtVJ5Du9MagbC6/Tv5L3xQQRjeQ1BGDA=,tag:IqtgpmJPhmHnSgMuIfI/0A==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-17T09:16:34Z"
mac: ENC[AES256_GCM,data:kMQ1Hxpvdg90p54JD4SFMdPlH3bSi2L8QOX2d80ZFUpli2FYYrqlr4AA+cLLEoar9Vfs9yy5t8Wo0s4pjEJXnMd6hHdp8zon3Y99EO/6/+8O3nP/uvvRONrHy8gJHL+afbWWkmzTDE1gBgB3x7/06mVv2XWgXZfvr323f3yggzU=,iv:5ux8DilPzqzoRAxowl2EXYteg4Pjd8E5d4kb36LSKBU=,tag:TcX7FtlHIhfHZPWKxfqsgA==,type:str]
lastmodified: "2023-01-18T10:42:25Z"
mac: ENC[AES256_GCM,data:QZixaOd5zjucSuwtyBcgACtNynt2X23B6Dxqxm2ZQtsvwaqz51i7TOe1BlmORT+71KDJhnaDmodc+xcNAta0K0e8uS0qFvDaE3aew77yfpn02kM5/2PwYc2xlh7nKg6dsfddxERx5UzaWLQWnU7ODN7hpsZ3Q5Hurf9fI5APleI=,iv:XufptrfeRp63XuwLHEmUFUEi5kwsYtNNaJ63fyaLqOQ=,tag:oFOiQSlhPiw3I9Pe0lPaGw==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -29,6 +29,7 @@ blockscout:
- 'nginx.ingress.kubernetes.io/cors-allow-methods: PUT, GET, POST, OPTIONS, DELETE, PATCH'
- 'nginx.ingress.kubernetes.io/enable-cors: "true"'
- 'nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,x-csrf-token"'
- 'nginx.ingress.kubernetes.io/cors-expose-headers: "x-bs-account-csrf"'
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
......@@ -140,7 +141,7 @@ blockscout:
ENABLE_RUST_VERIFICATION_SERVICE:
_default: 'true'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
_default: http://sc-verifier-svc:80
INDEXER_MEMORY_LIMIT:
_default: 3
ACCOUNT_ENABLED:
......@@ -170,14 +171,14 @@ postgres:
resources:
limits:
memory:
_default: "6Gi"
_default: "5Gi"
cpu:
_default: "4"
_default: "1.5"
requests:
memory:
_default: "6Gi"
_default: "5Gi"
cpu:
_default: "4"
_default: "1.5"
# node label
nodeSelector:
enabled: true
......@@ -376,14 +377,14 @@ scVerifier:
resources:
limits:
memory:
_default: "0.5Gi"
_default: "0.05Gi"
cpu:
_default: "0.25"
_default: "0.01"
requests:
memory:
_default: "0.5Gi"
_default: "0.05Gi"
cpu:
_default: "0.25"
_default: "0.01"
# node label
nodeSelector:
enabled: true
......@@ -472,17 +473,19 @@ frontend:
# - "/address"
- "/stats"
- "/search-results"
- "/tokens"
- "/accounts"
resources:
limits:
memory:
_default: "0.3Gi"
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.1"
requests:
memory:
_default: "0.3Gi"
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.1"
# node label
nodeSelector:
enabled: true
......
......@@ -72,14 +72,16 @@ frontend:
_default: ENC[AES256_GCM,data:Hf4azYsGh2lysotK7afaHI85IaLBJeQmPlGE/lwokmX2eaQHVZJ/i5RsaoKoOCSiNyfAowr7P+6IBEC16BUTQMpbhYveBd7c/xjIsoomoHbTdoAdZA/QxNVpS4a2qVthruFudyT1BoZUHIGQ,iv:Mid+PfbslOyivrFSXopdeW96YvOcLP+g2RGcw1o7B98=,tag:IuPUsGdbZQodCMyi1DR04Q==,type:str]
NEXT_PUBLIC_AUTH0_CLIENT_ID:
_default: ENC[AES256_GCM,data:4cTeqxQnGcpzPK4bMqxZpLgMeFSSDbajN/fmb1UunH8=,iv:bPTQfahGfWF1OfArvYQeSQItMa0Ymkt6eUfDZFBQSOY=,tag:8xHe5AUkbH/rl6cOfkVAKg==,type:str]
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID:
_default: ENC[AES256_GCM,data:x1CNJWk9wmqxjKWzD62pIb+scdzt5V22SPrXjvmsIR0=,iv:UjcwWfuGk3HDazHT5OcruevkQX/qAXiaHu6uVoJrSmE=,tag:NcCDR3tRULpiGJRpwBK0GQ==,type:str]
sops:
kms: []
gcp_kms: []
azure_kv: []
hc_vault: []
age: []
lastmodified: "2022-11-28T16:58:46Z"
mac: ENC[AES256_GCM,data:QJvVfWWWVDk5mI66T9J8EnEyVwmJoGEsWO9Pr8vK7jyC3rhAYD2WdKYfpkbwwMKrJzcMBe7UeaOeEY6aApuMNdobeEjsJAvstXCOBzMe5H9XtAFiAY+oxf8r4ELNvQP/gIBZSja+ehSbXBcaP4DkLn4FboaBhkoE8A37W2R6/QA=,iv:FnIC6iGLEZNwRSrbF81vF6eQuyq0yQHNPRTPrx3FB+8=,tag:LRnZCwYkCh4o8lDUcG2m9A==,type:str]
lastmodified: "2023-01-18T20:08:13Z"
mac: ENC[AES256_GCM,data:Qk1EyzFy/tYS5bDREC+EROmZ2mhSLNAa6ZJNByBYqMOqYTcrQhYC95I7/2qAHNnjTqNWwPSKb7LxEDSk60I3FZkXZr42gribWSoblf95KRbKsSiBhX1omE6tAK6gEpWuLuhwdLrUOdWM4fCUnwdFn7IV4y8KknhbSZ/yE0kNEnY=,iv:yc4E7yGZ8ftVU3XN2VhwhvjwtSbpXLWFgAG897TbrLk=,tag:SZrlReFY1cN9u54hsgJBKQ==,type:str]
pgp:
- created_at: "2022-09-14T13:42:28Z"
enc: |
......
......@@ -322,18 +322,20 @@ frontend:
- "/stats"
- "/search-results"
- "/token"
- "/tokens"
- "/accounts"
resources:
limits:
memory:
_default: "0.3Gi"
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.1"
requests:
memory:
_default: "0.3Gi"
_default: "0.1Gi"
cpu:
_default: "0.2"
_default: "0.1"
# node label
nodeSelector:
enabled: true
......@@ -401,4 +403,3 @@ frontend:
_default: https://rpc.ankr.com/eth_goerli
NEXT_PUBLIC_NETWORK_EXPLORERS:
_default: "[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/ethereum/goerli/transaction','address':'/ethereum/ethereum/goerli/address'}},{'title':'Etherscan','baseUrl':'https://goerli.etherscan.io/','paths':{'tx':'/tx','address':'/address'}}]"
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.155.156v10.844a.467.467 0 0 1-.934 0V3.09A1.089 1.089 0 0 1 6.401 2h7.474a.467.467 0 0 1 .322.137l4.355 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.933 0V7.29h-3.89a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.763l-2.763-2.763Zm-3.11 14.385c0-.186.064-.305.157-.385.1-.086.275-.16.542-.16.32.001.631.105.888.297a.467.467 0 0 0 .558-.749 2.427 2.427 0 0 0-1.444-.481h-.002c-.433 0-.841.12-1.15.384-.315.27-.483.657-.483 1.094 0 .235.065.447.189.628.12.175.28.297.436.386.28.16.633.253.915.327l.051.014c.327.087.568.157.733.257.13.08.165.145.165.254 0 .24-.084.34-.184.404-.134.086-.36.14-.671.14-.32 0-.632-.104-.888-.296a.467.467 0 0 0-.558.749c.417.31.923.48 1.444.481h.001c.388 0 .823-.062 1.175-.287.386-.247.614-.654.614-1.19 0-.513-.277-.847-.613-1.052-.298-.181-.674-.281-.967-.36l-.011-.002c-.333-.089-.576-.154-.745-.25a.377.377 0 0 1-.128-.103c-.012-.018-.025-.044-.025-.1Zm4.02-1.364c.238-.1.51.013.61.25l1.125 2.7 1.124-2.7a.467.467 0 1 1 .862.36l-1.556 3.733a.467.467 0 0 1-.861 0l-1.556-3.733a.466.466 0 0 1 .252-.61Zm-8.067.897c-.653.001-1.243.585-1.243 1.4s.59 1.399 1.243 1.4c.287-.003.564-.112.776-.306a.467.467 0 1 1 .63.69 2.1 2.1 0 0 1-1.4.55h-.004C5.934 21.244 5 20.163 5 18.91s.934-2.333 2.178-2.333h.004a2.1 2.1 0 0 1 1.4.55.467.467 0 1 1-.63.689 1.167 1.167 0 0 0-.776-.306Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m18.27 5.648-2.916-2.916A2.512 2.512 0 0 0 13.586 2H6.5A2.5 2.5 0 0 0 4 4.5v15A2.5 2.5 0 0 0 6.5 22h10c1.375 0 2.5-1.125 2.5-2.5V7.414c0-.66-.262-1.297-.73-1.766ZM17.125 19.5c0 .345-.28.625-.625.625h-10a.625.625 0 0 1-.624-.625L5.875 4.505c0-.345.28-.625.625-.625h6.25V7c0 .69.56 1.25 1.25 1.25h3.09V19.5h.035Zm-4.715-6.094a.547.547 0 0 0-.455.244l-1.549 2.327-.456-.685a.544.544 0 0 0-.909-.002l-1.823 2.735a.547.547 0 0 0-.027.561.53.53 0 0 0 .446.289h7.656a.547.547 0 0 0 .455-.85L12.83 13.65c-.065-.154-.237-.244-.42-.244ZM9 13.25a1.25 1.25 0 1 0 0-2.5 1.25 1.25 0 0 0 0 2.5Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.4 2.933a.156.156 0 0 0-.156.156v10.844a.467.467 0 0 1-.933 0V3.09A1.089 1.089 0 0 1 6.4 2h7.476a.467.467 0 0 1 .32.137l4.356 4.355a.467.467 0 0 1 .137.33v7.111a.467.467 0 0 1-.934 0V7.29h-3.889a.467.467 0 0 1-.466-.467v-3.89h-7Zm7.933.66v2.763h2.762l-2.762-2.763Z" fill="currentColor" stroke="currentColor" stroke-width=".4" stroke-linecap="round" stroke-linejoin="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.752 20.137c-.016-.04-.03-.08-.043-.12-.068-.223.123-.42.356-.42h.074c.16 0 .29.123.369.263.045.08.103.147.17.2a.787.787 0 0 0 .256.136.99.99 0 0 0 .326.05.944.944 0 0 0 .421-.085.591.591 0 0 0 .26-.232.69.69 0 0 0 .086-.348.538.538 0 0 0-.036-.22.46.46 0 0 0-.119-.173c-.103-.096-.261-.173-.473-.232l-.63-.172a1.612 1.612 0 0 1-.55-.257 1.142 1.142 0 0 1-.36-.442 1.475 1.475 0 0 1-.125-.63c0-.293.065-.55.194-.768.13-.218.31-.388.538-.508.23-.12.495-.179.794-.179.31 0 .575.06.795.183.221.122.392.287.51.493.051.085.092.174.124.267.075.217-.117.414-.346.414-.194 0-.35-.152-.462-.31a.655.655 0 0 0-.25-.218.817.817 0 0 0-.379-.082c-.22 0-.395.06-.522.183a.559.559 0 0 0-.141.204.654.654 0 0 0-.048.258c0 .145.049.264.147.36a.922.922 0 0 0 .412.21l.634.173c.222.06.415.144.578.254.157.101.29.25.383.43.091.178.137.403.137.674a1.6 1.6 0 0 1-.191.789 1.308 1.308 0 0 1-.55.528c-.24.126-.531.19-.876.19-.26 0-.486-.036-.68-.109a1.375 1.375 0 0 1-.487-.303 1.355 1.355 0 0 1-.296-.45Zm-3.168-.04a1.803 1.803 0 0 1-.045-.17c-.044-.212.136-.39.354-.39h.05c.187 0 .332.157.412.325a.56.56 0 0 0 .197.224.448.448 0 0 0 .267.072c.194 0 .337-.066.43-.198.093-.132.14-.318.14-.56v-2.898a.404.404 0 0 1 .808 0v2.874c0 .53-.122.93-.365 1.209-.242.276-.576.415-1.005.415a1.402 1.402 0 0 1-.58-.113 1.17 1.17 0 0 1-.415-.32 1.428 1.428 0 0 1-.248-.47Zm9.279-1.906v.628c0 .308-.04.565-.12.771-.067.19-.182.352-.328.466a.795.795 0 0 1-.48.151.8.8 0 0 1-.48-.151 1.027 1.027 0 0 1-.326-.466 2.159 2.159 0 0 1-.12-.77v-.629c0-.31.04-.566.12-.77.067-.19.181-.352.326-.466a.787.787 0 0 1 .48-.155c.18 0 .34.052.48.155.146.113.26.275.329.465.08.205.119.461.119.771Zm.82.625v-.617c0-.454-.07-.844-.21-1.17a1.674 1.674 0 0 0-.602-.758c-.258-.176-.57-.265-.935-.265a1.65 1.65 0 0 0-.939.265c-.26.173-.47.437-.601.755-.14.326-.21.717-.21 1.173v.617c0 .45.07.84.21 1.17.14.326.34.577.601.753.263.174.576.26.94.26s.676-.086.935-.26c.261-.176.462-.427.601-.753.14-.33.21-.72.21-1.17ZM16 17.69v2.838a.38.38 0 0 1-.762 0v-4.003a.426.426 0 0 1 .797-.208l1.66 2.97a.02.02 0 0 0 .04-.01v-2.796a.383.383 0 1 1 .765 0v4.006a.422.422 0 0 1-.79.206l-1.661-2.974a.056.056 0 0 0-.049-.028Z" fill="currentColor" stroke="currentColor" stroke-width=".3"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m12.18 3.765-1.944-1.944a1.674 1.674 0 0 0-1.179-.488H4.333c-.92 0-1.666.746-1.666 1.667v10c0 .92.746 1.667 1.667 1.667H11c.917 0 1.667-.75 1.667-1.667V4.943c0-.44-.175-.865-.487-1.178ZM11.417 13c0 .23-.187.417-.417.417H4.334A.417.417 0 0 1 3.917 13V3.003c0-.23.186-.416.416-.416H8.5v2.08c0 .46.373.833.833.833h2.06V13h.024ZM8.273 8.938a.365.365 0 0 0-.303.162l-1.032 1.55-.305-.456a.363.363 0 0 0-.606-.001l-1.215 1.823a.364.364 0 0 0-.018.374c.063.12.186.192.297.192h5.104a.365.365 0 0 0 .304-.566L8.554 9.1c-.044-.103-.158-.162-.28-.162ZM6 8.833a.833.833 0 1 0 0-1.666.833.833 0 0 0 0 1.666Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 3c0-.184.15-.333.333-.333h4.334v2c0 .368.298.666.666.666h2v1.334h1.334v-.92c0-.441-.176-.865-.489-1.178L9.431 1.822a1.667 1.667 0 0 0-1.179-.489H4.333c-.92 0-1.666.747-1.666 1.667v3.667H4V3Zm0 10v-1H2.667v1c0 .92.746 1.667 1.666 1.667H11c.92 0 1.667-.746 1.667-1.667v-1h-1.334v1a.333.333 0 0 1-.333.333H4.333A.333.333 0 0 1 4 13Zm1.167-5.833H4.75c-.575 0-1.042.466-1.042 1.041v2.084c0 .575.466 1.041 1.042 1.041h.417a1.04 1.04 0 0 0 1.041-1.041v-.209a.418.418 0 0 0-.416-.416.418.418 0 0 0-.417.416v.209a.209.209 0 0 1-.208.208H4.75a.209.209 0 0 1-.208-.208V8.208c0-.114.093-.208.208-.208h.417a.21.21 0 0 1 .208.208v.209a.417.417 0 0 0 .833 0v-.209a1.04 1.04 0 0 0-1.041-1.041Zm2.666 0a1.21 1.21 0 0 0-.599 2.258l.662.377a.375.375 0 0 1-.188.7h-.666a.418.418 0 0 0-.417.417c0 .23.188.417.417.417h.666a1.21 1.21 0 0 0 .6-2.258l-.662-.377a.375.375 0 0 1 .188-.7h.458a.418.418 0 0 0 0-.833h-.459Zm2.125.416v.823c0 .6.144 1.188.417 1.719a3.737 3.737 0 0 0 .417-1.719v-.823a.417.417 0 0 1 .833 0v.823a4.58 4.58 0 0 1-.77 2.542l-.134.2a.415.415 0 0 1-.692 0l-.133-.2a4.58 4.58 0 0 1-.771-2.542v-.823a.417.417 0 0 1 .833 0Z" fill="currentColor"/>
</svg>
......@@ -2,31 +2,19 @@ import { compile } from 'path-to-regexp';
import appConfig from 'configs/app/config';
import isNeedProxy from './isNeedProxy';
import { RESOURCES } from './resources';
import type { ApiResource, ResourceName } from './resources';
export default function buildUrl(
_resource: ApiResource | ResourceName,
pathParams?: Record<string, string | undefined>,
queryParams?: Record<string, string | number | undefined>,
queryParams?: Record<string, string | Array<string> | number | undefined>,
) {
// FIXME
// 1. I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server
// 2. and there is an issue with API and csrf token
// for some reason API will reply with error "Bad request" to any PUT / POST CORS request
// even though valid csrf-token is passed in header
// we also can pass token in request body but in this case API will replay with "Forbidden" error
// @nikitosing said it will take a lot of time to debug this problem on back-end side, maybe he'll change his mind in future :)
// To sum up, we are using next.js proxy for all instances where app host is not the same as API host (incl. localhost)
// will need to change the condition if there are more micro services that need authentication and DB state changes
const needProxy = appConfig.host !== appConfig.api.host;
const resource: ApiResource = typeof _resource === 'string' ? RESOURCES[_resource] : _resource;
const baseUrl = needProxy ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const baseUrl = isNeedProxy() ? appConfig.baseUrl : (resource.endpoint || appConfig.api.endpoint);
const basePath = resource.basePath !== undefined ? resource.basePath : appConfig.api.basePath;
const path = needProxy ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const path = isNeedProxy() ? '/node-api/proxy' + basePath + resource.path : basePath + resource.path;
const url = new URL(compile(path)(pathParams), baseUrl);
queryParams && Object.entries(queryParams).forEach(([ key, value ]) => {
......
import appConfig from 'configs/app/config';
// FIXME
// I was not able to figure out how to send CORS with credentials from localhost
// unsuccessfully tried different ways, even custom local dev domain
// so for local development we have to use next.js api as proxy server
export default function isNeedProxy() {
return appConfig.host === 'localhost' && appConfig.host !== appConfig.api.host;
}
......@@ -24,8 +24,9 @@ export default function fetchFactory(
});
return nodeFetch(url, {
headers,
...init,
headers,
body: init?.body ? JSON.stringify(init.body) : undefined,
});
};
}
......@@ -14,6 +14,7 @@ import type {
AddressTokensFilter,
AddressTokensResponse,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters } from 'types/api/block';
import type { ChartMarketResponse, ChartTransactionResponse } from 'types/api/charts';
import type { SmartContract, SmartContractReadMethod, SmartContractWriteMethod } from 'types/api/contract';
......@@ -22,8 +23,9 @@ import type { InternalTransactionsResponse } from 'types/api/internalTransaction
import type { LogsResponseTx, LogsResponseAddress } from 'types/api/log';
import type { RawTracesResponse } from 'types/api/rawTrace';
import type { SearchResult, SearchResultFilters } from 'types/api/search';
import type { Stats, Charts, HomeStats } from 'types/api/stats';
import type { Counters, StatsCharts, StatsChart, HomeStats } from 'types/api/stats';
import type { TokenCounters, TokenInfo, TokenHolders } from 'types/api/tokenInfo';
import type { TokensResponse, TokensFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
import type { TransactionsResponseValidated, TransactionsResponsePending, Transaction } from 'types/api/transaction';
import type { TTxsFilters } from 'types/api/txsFilters';
......@@ -70,8 +72,13 @@ export const RESOURCES = {
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_charts: {
path: '/api/v1/charts/line',
stats_lines: {
path: '/api/v1/lines',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
stats_line: {
path: '/api/v1/lines/:id',
endpoint: appConfig.statsApi.endpoint,
basePath: appConfig.statsApi.basePath,
},
......@@ -122,6 +129,13 @@ export const RESOURCES = {
path: '/api/v2/transactions/:id/raw-trace',
},
// ADDRESSES
addresses: {
path: '/api/v2/addresses/',
paginationFields: [ 'fetched_coin_balance' as const, 'hash' as const, 'items_count' as const ],
filterFields: [ ],
},
// ADDRESS
address: {
path: '/api/v2/addresses/:id',
......@@ -145,7 +159,7 @@ export const RESOURCES = {
address_token_transfers: {
path: '/api/v2/addresses/:id/token-transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const, 'transaction_index' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
filterFields: [ 'filter' as const, 'type' as const, 'token' as const ],
},
address_blocks_validated: {
path: '/api/v2/addresses/:id/blocks-validated',
......@@ -203,6 +217,16 @@ export const RESOURCES = {
paginationFields: [ 'items_count' as const, 'value' as const ],
filterFields: [],
},
token_transfers: {
path: '/api/v2/tokens/:hash/transfers',
paginationFields: [ 'block_number' as const, 'items_count' as const, 'index' as const ],
filterFields: [],
},
tokens: {
path: '/api/v2/tokens',
paginationFields: [ 'holder_count' as const, 'items_count' as const, 'name' as const ],
filterFields: [ 'filter' as const, 'type' as const ],
},
// HOMEPAGE
homepage_stats: {
......@@ -270,10 +294,11 @@ export type ResourceErrorAccount<T> = ResourceError<{ errors: T }>
export type PaginatedResources = 'blocks' | 'block_txs' |
'txs_validated' | 'txs_pending' |
'tx_internal_txs' | 'tx_logs' | 'tx_token_transfers' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
'address_logs' | 'address_tokens' |
'token_holders';
'token_transfers' | 'token_holders' | 'tokens';
export type PaginatedResponse<Q extends PaginatedResources> = ResourcePayload<Q>;
......@@ -292,8 +317,9 @@ Q extends 'homepage_chart_market' ? ChartMarketResponse :
Q extends 'homepage_blocks' ? Array<Block> :
Q extends 'homepage_txs' ? Array<Transaction> :
Q extends 'homepage_indexing_status' ? IndexingStatus :
Q extends 'stats_counters' ? Stats :
Q extends 'stats_charts' ? Charts :
Q extends 'stats_counters' ? Counters :
Q extends 'stats_lines' ? StatsCharts :
Q extends 'stats_line' ? StatsChart :
Q extends 'blocks' ? BlocksResponse :
Q extends 'block' ? Block :
Q extends 'block_txs' ? BlockTransactionsResponse :
......@@ -304,6 +330,7 @@ Q extends 'tx_internal_txs' ? InternalTransactionsResponse :
Q extends 'tx_logs' ? LogsResponseTx :
Q extends 'tx_token_transfers' ? TokenTransferResponse :
Q extends 'tx_raw_trace' ? RawTracesResponse :
Q extends 'addresses' ? AddressesResponse :
Q extends 'address' ? Address :
Q extends 'address_counters' ? AddressCounters :
Q extends 'address_token_balances' ? Array<AddressTokenBalance> :
......@@ -317,7 +344,9 @@ Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
Q extends 'token' ? TokenInfo :
Q extends 'token_counters' ? TokenCounters :
Q extends 'token_transfers' ? TokenTransferResponse :
Q extends 'token_holders' ? TokenHolders :
Q extends 'tokens' ? TokensResponse :
Q extends 'search' ? SearchResult :
Q extends 'contract' ? SmartContract :
Q extends 'contract_methods_read' ? Array<SmartContractReadMethod> :
......@@ -332,9 +361,11 @@ export type PaginationFilters<Q extends PaginatedResources> =
Q extends 'blocks' ? BlockFilters :
Q extends 'txs_validated' | 'txs_pending' ? TTxsFilters :
Q extends 'tx_token_transfers' ? TokenTransferFilters :
Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
Q extends 'search' ? SearchResultFilters :
Q extends 'tokens' ? TokensFilters :
never;
/* eslint-enable @typescript-eslint/indent */
......@@ -10,7 +10,7 @@ import type { ApiResource } from './resources';
export interface Params {
pathParams?: Record<string, string | undefined>;
queryParams?: Record<string, string | number | undefined>;
queryParams?: Record<string, string | Array<string> | number | undefined>;
fetchParams?: Pick<FetchParams, 'body' | 'method' | 'signal'>;
}
......
......@@ -7,6 +7,7 @@ export enum NAMES {
API_TOKEN='_explorer_key',
TXS_SORT='txs_sort',
COLOR_MODE='chakra-ui-color-mode',
INDEXING_ALERT='indexing_alert',
}
export function get(name?: NAMES | undefined | null, serverCookie?: string) {
......
......@@ -68,8 +68,16 @@ function makePolicyMap() {
appConfig.api.socket,
appConfig.statsApi.endpoint,
// chain RPC server
appConfig.network.rpcUrl,
// ad
'request-global.czilladx.com',
// walletconnect
'*.walletconnect.com',
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
],
'script-src': [
......@@ -130,6 +138,9 @@ function makePolicyMap() {
// ad
'servedbyadbutler.com',
'cdn.coinzilla.io',
// walletconnect
'*.walletconnect.com',
],
'font-src': [
......
......@@ -28,7 +28,6 @@ export default function useFetch() {
headers: {
...(hasBody ? { 'Content-type': 'application/json' } : undefined),
...params?.headers,
// ...(token ? { 'x-csrf-token': token } : {}),
},
};
......
import { useQuery } from '@tanstack/react-query';
import buildUrl from 'lib/api/buildUrl';
import isNeedProxy from 'lib/api/isNeedProxy';
import { getResourceKey } from 'lib/api/useApiQuery';
import * as cookies from 'lib/cookies';
import useFetch from 'lib/hooks/useFetch';
export default function useGetCsrfToken() {
const nodeApiFetch = useFetch();
useQuery(getResourceKey('csrf'), async() => {
if (!isNeedProxy()) {
const url = buildUrl('csrf');
const apiResponse = await fetch(url, { credentials: 'include' });
const csrfFromHeader = apiResponse.headers.get('x-bs-account-csrf');
// eslint-disable-next-line no-console
console.log('>>> RESPONSE HEADERS <<<');
// eslint-disable-next-line no-console
console.table([ {
'content-length': apiResponse.headers.get('content-length'),
'x-bs-account-csrf': csrfFromHeader,
} ]);
return csrfFromHeader ? { token: csrfFromHeader } : undefined;
}
return nodeApiFetch('/node-api/csrf');
}, {
enabled: Boolean(cookies.get(cookies.NAMES.API_TOKEN)),
});
}
......@@ -12,6 +12,7 @@ import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import watchlistIcon from 'icons/watchlist.svg';
import link from 'lib/link/link';
import useCurrentRoute from 'lib/link/useCurrentRoute';
......@@ -27,6 +28,7 @@ export default function useNavItems() {
{ text: 'Blocks', url: link('blocks'), icon: blocksIcon, isActive: currentRoute.startsWith('block'), isNewUi: false },
{ text: 'Transactions', url: link('txs'), icon: transactionsIcon, isActive: currentRoute.startsWith('tx'), isNewUi: false },
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
{ text: 'Accounts', url: link('accounts'), icon: walletIcon, isActive: currentRoute === 'accounts', isNewUi: false },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: false },
......
......@@ -12,7 +12,8 @@ function getSocketParams(router: NextRouter) {
if (
router.pathname === ROUTES.txs.pattern &&
(router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
......@@ -21,7 +22,12 @@ function getSocketParams(router: NextRouter) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
if (router.pathname === ROUTES.txs.pattern && router.query.tab === 'pending' && !router.query.block_number) {
if (
router.pathname === ROUTES.txs.pattern &&
router.query.tab === 'pending' &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
}
......
......@@ -15,10 +15,12 @@
"token_instance_item": "/token/:hash/instance/:id",
"address_index": "/address/:id",
"address_contract_verification": "/address/:id/contract_verifications/new",
"accounts": "/accounts",
"apps": "/apps",
"app_index": "/apps/:id",
"search_results": "/search-results",
"auth": "/auth/auth0",
"stats": "/stats",
"visualize_sol2uml": "/visualize/sol2uml"
"visualize_sol2uml": "/visualize/sol2uml",
"csv_export": "/csv-export"
}
......@@ -77,6 +77,12 @@ export const ROUTES = {
crossNetworkNavigation: true,
},
// ACCOUNTS
accounts: {
pattern: PATHS.accounts,
crossNetworkNavigation: true,
},
// APPS
apps: {
pattern: PATHS.apps,
......@@ -99,6 +105,10 @@ export const ROUTES = {
pattern: PATHS.visualize_sol2uml,
},
csv_export: {
pattern: PATHS.csv_export,
},
// AUTH
auth: {
pattern: PATHS.auth,
......
......@@ -2,6 +2,8 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import type { Transaction } from 'types/api/transaction';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
......@@ -13,6 +15,10 @@ SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance |
SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.AddressTokenTransfer |
SocketMessage.TokenTransfers |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -34,5 +40,9 @@ export namespace SocketMessage {
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type AddressTokenTransfer = SocketMessageParamsGeneric<'token_transfer', { token_transfer: TokenTransfer }>;
export type TokenTransfers = SocketMessageParamsGeneric<'token_transfer', {token_transfer: number }>
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
import type { TokenType } from 'types/api/tokenInfo';
const TOKEN_TYPE: Array<{ title: string; id: TokenType }> = [
{ title: 'ERC-20', id: 'ERC-20' },
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
export default TOKEN_TYPE;
import type { Address } from 'types/api/address';
import type { AddressParam } from 'types/api/addressParams';
import { publicTag, privateTag, watchlistName } from 'mocks/address/tag';
import { tokenInfo } from 'mocks/tokens/tokenInfo';
export const hash = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859';
export const withName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: hash,
implementation_name: null,
is_contract: true,
is_verified: null,
......@@ -15,7 +18,7 @@ export const withName: AddressParam = {
};
export const withoutName: AddressParam = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
hash: hash,
implementation_name: null,
is_contract: true,
is_verified: null,
......@@ -25,8 +28,8 @@ export const withoutName: AddressParam = {
public_tags: [],
};
export const withToken: Address = {
hash: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859',
export const token: Address = {
hash: hash,
implementation_name: null,
is_contract: true,
is_verified: false,
......@@ -37,8 +40,8 @@ export const withToken: Address = {
token: tokenInfo,
block_number_balance_updated_at: 8201413,
coin_balance: '1',
creation_tx_hash: null,
creator_address_hash: null,
creation_tx_hash: '0xc38cf7377bf72d6436f63c37b01b24d032101f20ec1849286dc703c712f10c98',
creator_address_hash: '0x34A9c688512ebdB575e82C50c9803F6ba2916E72',
exchange_rate: null,
implementation_address: null,
has_custom_methods_read: false,
......@@ -49,7 +52,65 @@ export const withToken: Address = {
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_token_transfers: true,
has_tokens: true,
has_validated_blocks: false,
};
export const contract: Address = {
block_number_balance_updated_at: 30811263,
coin_balance: '27826501896887194214322205',
creation_tx_hash: '0xf2aff6501b632604c39978b47d309813d8a1bcca721864bbe86abf59704f195e',
creator_address_hash: '0x803ad3F50b9e1fF68615e8B053A186e1be288943',
exchange_rate: '0.04311',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: true,
has_methods_read: true,
has_methods_read_proxy: true,
has_methods_write: true,
has_methods_write_proxy: true,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: false,
hash: hash,
implementation_address: '0x2F4F4A52295940C576417d29F22EEb92B440eC89',
implementation_name: 'HomeBridge',
is_contract: true,
is_verified: true,
name: 'EternalStorageProxy',
private_tags: [ publicTag ],
public_tags: [ privateTag ],
token: null,
watchlist_names: [ watchlistName ],
};
export const validator: Address = {
block_number_balance_updated_at: 30811932,
coin_balance: '22910462800601256910890',
creation_tx_hash: null,
creator_address_hash: null,
exchange_rate: '0.00432018',
has_custom_methods_read: false,
has_custom_methods_write: false,
has_decompiled_code: false,
has_logs: false,
has_methods_read: false,
has_methods_read_proxy: false,
has_methods_write: false,
has_methods_write_proxy: false,
has_token_transfers: false,
has_tokens: false,
has_validated_blocks: true,
hash: hash,
implementation_address: null,
implementation_name: null,
is_contract: false,
is_verified: false,
name: 'Kiryl Ihnatsyeu',
private_tags: [],
public_tags: [],
token: null,
watchlist_names: [],
};
import type { AddressCounters } from 'types/api/address';
export const forContract: AddressCounters = {
gas_usage_count: '319340525',
token_transfers_count: '0',
transactions_count: '5462',
validations_count: '0',
};
export const forToken: AddressCounters = {
gas_usage_count: '247479698',
token_transfers_count: '1',
transactions_count: '8474',
validations_count: '0',
};
export const forValidator: AddressCounters = {
gas_usage_count: '91675762951',
token_transfers_count: '0',
transactions_count: '820802',
validations_count: '1726416',
};
import type { AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo';
export const erc20a: AddressTokenBalance = {
token: {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20a,
token_id: null,
value: '1169320000000000000000000',
};
export const erc20b: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20b,
token_id: null,
value: '872500000000',
};
export const erc20c: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20c,
token_id: null,
value: '9852000000000000000000',
};
export const erc20d: AddressTokenBalance = {
token: {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token: tokens.tokenInfoERC20d,
token_id: null,
value: '39000000000000000000',
};
export const erc721a: AddressTokenBalance = {
token: {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token: tokens.tokenInfoERC721a,
token_id: null,
value: '51',
};
export const erc721b: AddressTokenBalance = {
token: {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token: tokens.tokenInfoERC721b,
token_id: null,
value: '1',
};
export const erc721c: AddressTokenBalance = {
token: {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token: tokens.tokenInfoERC721c,
token_id: null,
value: '5',
};
export const erc1155a: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155a,
token_id: '42',
value: '24',
};
export const erc1155b: AddressTokenBalance = {
token: {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155b,
token_id: '100010000000001',
value: '11',
};
export const erc1155withoutName: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token: tokens.tokenInfoERC1155WithoutName,
token_id: '64532245',
value: '42',
};
......
/* eslint-disable max-len */
import type { SmartContract } from 'types/api/contract';
export const verified: Partial<SmartContract> = {
abi: [ { anonymous: false, inputs: [ { indexed: true, internalType: 'address', name: 'src', type: 'address' }, { indexed: true, internalType: 'address', name: 'guy', type: 'address' }, { indexed: false, internalType: 'uint256', name: 'wad', type: 'uint256' } ], name: 'Approval', type: 'event' } ],
can_be_visualized_via_sol2uml: true,
compiler_version: 'v0.5.16+commit.9c3226ce',
constructor_args: 'constructor_args',
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
compiler_settings: 'compiler_settings',
evm_version: 'default',
is_verified: true,
name: 'WPOA',
optimization_enabled: true,
optimization_runs: 1500,
source_code: 'source_code',
verified_at: '2021-08-03T10:40:41.679421Z',
decoded_constructor_args: [
[ '0xc59615da2da226613b1c78f0c6676cac497910bc', { internalType: 'address', name: '_token', type: 'address' } ],
[ '1800', { internalType: 'uint256', name: '_duration', type: 'uint256' } ],
[ '900000000', { internalType: 'uint256', name: '_totalSupply', type: 'uint256' } ],
],
external_libraries: [
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'Sol' },
{ address_hash: '0xa62744BeE8646e237441CDbfdedD3458861748A8', name: 'math' },
],
};
export const withMultiplePaths: Partial<SmartContract> = {
...verified,
file_path: './simple_storage.sol',
additional_sources: [
{
file_path: 'contracts/1_Storage.sol',
source_code: '// SPDX-License-Identifier: GPL-3.0 \n pragma solidity >=0.7.0 <0.9.0; \n contract Storage {\n //2112313123; \nuint256 number; \n function store(uint256 num) public {\nnumber = num;\n}\n function retrieve() public view returns (uint256)\n {\nreturn number;\n}\n}',
},
],
};
export const verifiedViaSourcify: Partial<SmartContract> = {
...verified,
is_verified_via_sourcify: true,
is_fully_verified: false,
is_partially_verified: true,
sourcify_repo_url: 'https://repo.sourcify.dev/contracts//full_match/99/0x51891596E158b2857e5356DC847e2D15dFbCF2d0/',
};
export const withTwinAddress: Partial<SmartContract> = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const withProxyAddress: Partial<SmartContract> = {
...verified,
is_verified: false,
verified_twin_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
minimal_proxy_address_hash: '0xa62744bee8646e237441cdbfdedd3458861748a8',
};
export const selfDestructed: Partial<SmartContract> = {
...verified,
is_self_destructed: true,
};
export const withChangedByteCode: Partial<SmartContract> = {
...verified,
is_changed_bytecode: true,
};
export const nonVerified: Partial<SmartContract> = {
is_verified: false,
creation_bytecode: 'creation_bytecode',
deployed_bytecode: 'deployed_bytecode',
};
import type {
SmartContractQueryMethodReadError,
SmartContractQueryMethodReadSuccess,
SmartContractReadMethod,
SmartContractWriteMethod,
} from 'types/api/contract';
export const read: Array<SmartContractReadMethod> = [
{
constant: true,
inputs: [
{ internalType: 'address', name: '', type: 'address' },
],
method_id: '70a08231',
name: 'balanceOf',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256', value: '' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '06fdde03',
name: 'name',
outputs: [
{ internalType: 'string', name: '', type: 'string', value: 'Wrapped POA' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '18160ddd',
name: 'totalSupply',
outputs: [
{ internalType: 'uint256', name: '', type: 'uint256', value: '139905710421584994690047413' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
error: '(-32015) VM execution error. (revert)',
inputs: [],
method_id: 'df0ad3de',
name: 'upgradeabilityAdmin',
outputs: [
{ name: '', type: 'address', value: '' },
],
payable: false,
stateMutability: 'view',
type: 'function',
},
{
constant: true,
inputs: [],
method_id: '165ec2e2',
name: 'arianeeWhitelist',
outputs: [
{
name: '',
type: 'address',
value: '0xd3eee7f8e8021db24825c3457d5479f2b57f40ef',
},
],
payable: false,
stateMutability: 'view',
type: 'function',
},
];
export const readResultSuccess: SmartContractQueryMethodReadSuccess = {
is_error: false,
result: {
names: [ 'uint256' ],
output: [
{ type: 'uint256', value: '42' },
],
},
};
export const readResultError: SmartContractQueryMethodReadError = {
is_error: true,
result: {
message: 'Some shit happened',
code: -32017,
},
};
export const write: Array<SmartContractWriteMethod> = [
{
payable: true,
stateMutability: 'payable',
type: 'fallback',
},
{
constant: false,
inputs: [
{ internalType: 'address', name: 'guy', type: 'address' },
{ internalType: 'uint256', name: 'wad', type: 'uint256' },
],
name: 'approve',
outputs: [
{ internalType: 'bool', name: '', type: 'bool' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ internalType: 'address', name: 'src', type: 'address' },
{ internalType: 'address', name: 'dst', type: 'address' },
],
name: 'transferFrom',
outputs: [
{ internalType: 'bool', name: '', type: 'bool' },
],
payable: true,
stateMutability: 'payable',
type: 'function',
},
{
stateMutability: 'payable',
type: 'receive',
},
{
constant: false,
inputs: [],
name: 'pause',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_from', type: 'address' },
{ name: '_to', type: 'address' },
{ name: '_tokenId', type: 'uint256' },
{ name: '_data', type: 'bytes' },
],
name: 'safeTransferFrom',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_tokenId', type: 'uint256' },
{ name: '_hash', type: 'bytes32' },
{ name: '_keepRequestToken', type: 'bool' },
{ name: '_newOwner', type: 'address' },
{ name: '_signature', type: 'bytes' },
],
name: 'requestToken',
outputs: [
{ name: 'reward', type: 'uint256' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
{
constant: false,
inputs: [
{ name: '_tokenId', type: 'uint256' },
{ name: '_imprint', type: 'bytes32' },
{ name: '_uri', type: 'string' },
{ name: '_initialKey', type: 'address' },
{ name: '_tokenRecoveryTimestamp', type: 'uint256' },
{ name: '_initialKeyIsRequestKey', type: 'bool' },
],
name: 'hydrateToken',
outputs: [
{ name: '', type: 'uint256' },
],
payable: false,
stateMutability: 'nonpayable',
type: 'function',
},
];
......@@ -15,3 +15,113 @@ export const tokenCounters: TokenCounters = {
token_holders_count: '8838883',
transfers_count: '88282281',
};
export const tokenInfoERC20a: TokenInfo = {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20b: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20c: TokenInfo = {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC20d: TokenInfo = {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
};
export const tokenInfoERC721a: TokenInfo = {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
};
export const tokenInfoERC721b: TokenInfo = {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC721c: TokenInfo = {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
};
export const tokenInfoERC1155a: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
};
export const tokenInfoERC1155b: TokenInfo = {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
};
export const tokenInfoERC1155WithoutName: TokenInfo = {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
};
......@@ -38,6 +38,9 @@ export const erc20: TokenTransfer = {
tx_hash: '0x62d597ebcf3e8d60096dd0363bc2f0f5e2df27ba1dacd696c51aa7c9409f3193',
type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
};
export const erc721: TokenTransfer = {
......@@ -77,6 +80,9 @@ export const erc721: TokenTransfer = {
tx_hash: '0xf13bc7afe5e02b494dd2f22078381d36a4800ef94a0ccc147431db56c301e6cc',
type: 'token_transfer',
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
method: 'updateSmartAsset',
};
export const erc1155: TokenTransfer = {
......@@ -118,6 +124,8 @@ export const erc1155: TokenTransfer = {
tx_hash: '0x05d6589367633c032d757a69c5fb16c0e33e3994b0d9d1483f82aeee1f05d746',
type: 'token_minting',
timestamp: '2022-10-10T14:34:30.000000Z',
block_hash: '1',
log_index: '1',
};
export const erc1155multiple: TokenTransfer = {
......
......@@ -21,7 +21,7 @@
"lint:tsc": "./node_modules/.bin/tsc -p ./tsconfig.json",
"prepare": "husky install",
"format-svg": "./node_modules/.bin/svgo -r ./icons",
"test:pw": "./playwright/make-envs-script.sh && playwright test -c playwright-ct.config.ts",
"test:pw": "./playwright/make-envs-script.sh && NODE_OPTIONS=\"--max-old-space-size=4096\" playwright test -c playwright-ct.config.ts",
"test:pw:local": "export NODE_PATH=$(pwd)/node_modules && yarn test:pw",
"test:pw:docker": "docker run --rm --network host -v $(pwd):/work/ -w /work/ -it mcr.microsoft.com/playwright:v1.28.0-focal ./playwright/run-tests.sh",
"test:jest": "jest",
......@@ -40,6 +40,8 @@
"@tanstack/react-query-devtools": "^4.0.10",
"@types/papaparse": "^5.3.5",
"@types/react-scroll": "^1.8.4",
"@web3modal/ethereum": "^2.0.0-rc.2",
"@web3modal/react": "^2.0.0-rc.2",
"ace-builds": "^1.14.0",
"bignumber.js": "^9.1.0",
"d3": "^7.6.1",
......@@ -64,7 +66,8 @@
"react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4",
"react-scroll": "^1.8.7",
"use-font-face-observer": "^1.2.1"
"use-font-face-observer": "^1.2.1",
"wagmi": "^0.10.6"
},
"devDependencies": {
"@playwright/experimental-ct-react": "^1.28.0",
......
......@@ -50,7 +50,7 @@ const CustomErrorComponent = (props: Props) => {
);
}
const colorModeCookie = cookies.getFromCookieString(props.cookies, cookies.NAMES.COLOR_MODE);
const colorModeCookie = cookies.getFromCookieString(props.cookies || '', cookies.NAMES.COLOR_MODE);
return <NextErrorComponent statusCode={ props.statusCode } withDarkMode={ colorModeCookie === 'dark' }/>;
};
......
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Accounts from 'ui/pages/Accounts';
const AccountsPage: NextPage = () => {
const title = `Top Accounts - ${ getNetworkTitle() }`;
return (
<>
<Head><title>{ title }</title></Head>
<Accounts/>
</>
);
};
export default AccountsPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import getNetworkTitle from 'lib/networks/getNetworkTitle';
import Tokens from 'ui/pages/Tokens';
const TokensPage: NextPage = () => {
const title = getNetworkTitle();
return (
<>
<Head>
<title>{ title }</title>
</Head>
<Tokens/>
</>
);
};
export default TokensPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -51,6 +51,12 @@ const config: PlaywrightTestConfig = {
exportAsDefault: true,
}),
],
build: {
// it actually frees some memory that vite needs a lot
// https://github.com/storybookjs/builder-vite/issues/409#issuecomment-1152848986
sourcemap: false,
minify: false,
},
},
},
......
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { providers } from 'ethers';
import React from 'react';
import { createClient, WagmiConfig } from 'wagmi';
import { mainnet } from 'wagmi/chains';
import { MockConnector } from 'wagmi/connectors/mock';
import { AppContextProvider } from 'lib/appContext';
import type { Props as PageProps } from 'lib/next/getServerSideProps';
......@@ -23,6 +27,29 @@ const defaultAppContext = {
},
};
// >>> Web3 stuff
const provider = new providers.JsonRpcProvider(
'http://localhost:8545',
{
name: 'POA',
chainId: 99,
},
);
const connector = new MockConnector({
chains: [ mainnet ],
options: {
signer: provider.getSigner(),
},
});
const wagmiClient = createClient({
autoConnect: true,
connectors: [ connector ],
provider,
});
// <<<<
const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props) => {
const [ queryClient ] = React.useState(() => new QueryClient({
defaultOptions: {
......@@ -38,7 +65,9 @@ const TestApp = ({ children, withSocket, appContext = defaultAppContext }: Props
<QueryClientProvider client={ queryClient }>
<SocketProvider url={ withSocket ? `ws://localhost:${ PORT }` : undefined }>
<AppContextProvider { ...appContext }>
{ children }
<WagmiConfig client={ wagmiClient }>
{ children }
</WagmiConfig>
</AppContextProvider>
</SocketProvider>
</QueryClientProvider>
......
......@@ -47,7 +47,7 @@ const variantOutline = defineStyle((props) => {
const isGrayTheme = c === 'gray' || c === 'gray-dark';
const color = isGrayTheme ? mode('blackAlpha.800', 'whiteAlpha.800')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const borderColor = isGrayTheme ? mode('gray.300', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const borderColor = isGrayTheme ? mode('gray.200', 'gray.600')(props) : mode(`${ c }.600`, `${ c }.300`)(props);
const activeBg = isGrayTheme ? mode('blue.50', 'gray.600')(props) : mode(`${ c }.50`, 'gray.600')(props);
const activeColor = (() => {
if (c === 'gray') {
......@@ -70,15 +70,15 @@ const variantOutline = defineStyle((props) => {
borderColor,
bg: 'transparent',
_hover: {
color: 'blue.400',
borderColor: 'blue.400',
color: 'link_hovered',
borderColor: 'link_hovered',
bg: 'transparent',
_active: {
bg: props.isActive ? activeBg : 'transparent',
borderColor: props.isActive ? activeBg : 'blue.400',
color: props.isActive ? activeColor : 'blue.400',
borderColor: props.isActive ? activeBg : 'link_hovered',
color: props.isActive ? activeColor : 'link_hovered',
p: {
color: 'blue.400',
color: 'link_hovered',
},
},
_disabled: {
......@@ -86,7 +86,7 @@ const variantOutline = defineStyle((props) => {
borderColor,
},
p: {
color: 'blue.400',
color: 'link_hovered',
},
},
_disabled: {
......@@ -147,7 +147,7 @@ const variantSubtle = defineStyle((props) => {
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
_hover: {
color: 'blue.400',
color: 'link_hovered',
_disabled: {
color: mode('blackAlpha.800', 'whiteAlpha.800')(props),
bg: mode('blackAlpha.200', 'whiteAlpha.200')(props),
......@@ -160,7 +160,7 @@ const variantSubtle = defineStyle((props) => {
bg: `${ c }.100`,
color: `${ c }.600`,
_hover: {
color: 'blue.400',
color: 'link_hovered',
},
};
});
......
......@@ -8,9 +8,9 @@ const baseStyle = defineStyle(getDefaultTransitionProps());
const variantPrimary = defineStyle((props) => {
return {
color: mode('blue.600', 'blue.300')(props),
color: 'link',
_hover: {
color: mode('blue.400', 'blue.200')(props),
color: 'link_hovered',
textDecorationStyle: props.textDecorationStyle || 'solid',
},
};
......
import { menuAnatomy as parts } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
cssVar,
defineStyle,
} from '@chakra-ui/styled-system';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const $bg = cssVar('menu-bg');
const $shadow = cssVar('menu-shadow');
const baseStyleList = defineStyle({
[$bg.variable]: '#fff',
[$shadow.variable]: 'shadows.2xl',
_dark: {
[$bg.variable]: 'colors.gray.900',
[$shadow.variable]: 'shadows.dark-lg',
},
borderWidth: '0',
bg: $bg.reference,
boxShadow: $shadow.reference,
});
const baseStyleItem = defineStyle({
_focus: {
[$bg.variable]: 'colors.blue.50',
_dark: {
[$bg.variable]: 'colors.gray.800',
},
},
_hover: {
[$bg.variable]: 'colors.blue.50',
_dark: {
[$bg.variable]: 'colors.gray.800',
},
},
bg: $bg.reference,
});
const baseStyle = definePartsStyle({
list: baseStyleList,
item: baseStyleItem,
});
const Menu = defineMultiStyleConfig({
baseStyle,
});
export default Menu;
......@@ -51,7 +51,7 @@ const baseStyleCloseButton = defineStyle((props) => {
height: 10,
width: 10,
color: mode('gray.700', 'gray.500')(props),
_hover: { color: 'blue.400' },
_hover: { color: 'link_hovered' },
_active: { bg: 'none' },
};
});
......
......@@ -29,6 +29,7 @@ const baseStyleContent = defineStyle((props) => {
_dark: {
[$popperBg.variable]: `colors.gray.900`,
[$arrowShadowColor.variable]: `colors.whiteAlpha.300`,
boxShadow: 'dark-lg',
},
width: 'xs',
border: 'none',
......
import { selectAnatomy as parts } from '@chakra-ui/anatomy';
import {
createMultiStyleConfigHelpers,
} from '@chakra-ui/styled-system';
import { mode } from '@chakra-ui/theme-tools';
import Input from './Input';
const { defineMultiStyleConfig, definePartsStyle } =
createMultiStyleConfigHelpers(parts.keys);
const variantOutline = definePartsStyle((props) => {
return {
field: {
...Input.variants?.outline(props).field,
borderColor: mode('gray.200', 'gray.600')(props),
_hover: {
borderColor: mode('gray.300', 'gray.500')(props),
},
_focusVisible: {
borderColor: mode('gray.200', 'gray.600')(props),
boxShadow: 'none',
},
cursor: 'pointer',
},
};
});
const Select = defineMultiStyleConfig({
variants: {
...Input.variants,
outline: variantOutline,
},
});
export default Select;
......@@ -23,7 +23,7 @@ const variantSimple = definePartsStyle((props) => {
...transitionProps,
},
td: {
borderColor: mode('blackAlpha.200', 'whiteAlpha.200')(props),
borderColor: 'divider',
...transitionProps,
},
};
......
......@@ -22,7 +22,7 @@ const variantSoftRounded = definePartsStyle((props) => {
},
},
_hover: {
color: 'blue.400',
color: 'link_hovered',
},
},
};
......
......@@ -10,10 +10,6 @@ const variantSecondary = defineStyle((props) => ({
color: mode('gray.500', 'gray.400')(props),
}));
const variantAlternate = defineStyle((props) => ({
color: mode('blue.600', 'blue.300')(props),
}));
const variantInherit = {
color: 'inherit',
};
......@@ -21,7 +17,6 @@ const variantInherit = {
const variants: Record<string, SystemStyleInterpolation> = {
primary: variantPrimary,
secondary: variantSecondary,
alternate: variantAlternate,
inherit: variantInherit,
};
......
......@@ -8,9 +8,11 @@ import FormLabel from './FormLabel';
import Heading from './Heading';
import Input from './Input';
import Link from './Link';
import Menu from './Menu';
import Modal from './Modal';
import Popover from './Popover';
import Radio from './Radio';
import Select from './Select';
import Skeleton from './Skeleton';
import Spinner from './Spinner';
import Switch from './Switch';
......@@ -32,9 +34,11 @@ const components = {
Form,
FormLabel,
Link,
Menu,
Modal,
Popover,
Radio,
Select,
Skeleton,
Spinner,
Switch,
......
const semanticTokens = {
colors: {
divider: {
'default': 'blackAlpha.200',
_dark: 'whiteAlpha.200',
},
link: {
'default': 'blue.600',
_dark: 'blue.300',
},
link_hovered: {
'default': 'blue.400',
},
},
};
export default semanticTokens;
......@@ -5,6 +5,7 @@ import config from './config';
import borders from './foundations/borders';
import breakpoints from './foundations/breakpoints';
import colors from './foundations/colors';
import semanticTokens from './foundations/semanticTokens';
import transition from './foundations/transition';
import typography from './foundations/typography';
import zIndices from './foundations/zIndices';
......@@ -22,6 +23,7 @@ const overrides = {
breakpoints,
transition,
zIndices,
semanticTokens,
};
export default extendTheme(overrides);
......@@ -30,6 +30,10 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
_hover: {
borderColor: 'transparent',
},
':-webkit-autofill': {
// background color for disabled input which value was selected from browser autocomplete popup
'-webkit-box-shadow': `0 0 0px 1000px ${ mode('rgba(16, 17, 18, 0.08)', 'rgba(255, 255, 255, 0.08)')(props) } inset`,
},
},
_invalid: {
borderColor: getColor(theme, errorColor),
......
......@@ -81,8 +81,9 @@ export interface AddressTokenTransferResponse {
}
export type AddressTokenTransferFilters = {
filter: AddressFromToFilter;
type: Array<TokenType>;
filter?: AddressFromToFilter;
type?: Array<TokenType>;
token?: string;
}
export type AddressTokensFilter = {
......
import type { AddressParam } from './addressParams';
export type AddressesItem = AddressParam &{ tx_count: string; coin_balance: string }
export type AddressesResponse = {
items: Array<AddressesItem>;
next_page_params: {
fetched_coin_balance: string;
hash: string;
items_count: number;
};
total_supply: string;
}
import type { Abi } from 'abitype';
export type SmartContractMethodArgType = 'address' | 'uint256' | 'bool' | 'string' | 'bytes' | 'bytes32';
export type SmartContractMethodStateMutability = 'view' | 'nonpayable' | 'payable';
export interface SmartContract {
deployed_bytecode: string | null;
creation_bytecode: string | null;
is_self_destructed: boolean;
abi: Array<Record<string, unknown>> | null;
abi: Abi | null;
compiler_version: string | null;
evm_version: string | null;
optimization_enabled: boolean | null;
......@@ -10,8 +15,38 @@ export interface SmartContract {
name: string | null;
verified_at: string | null;
is_verified: boolean | null;
is_changed_bytecode: boolean | null;
// sourcify info >>>
is_verified_via_sourcify: boolean | null;
is_fully_verified: boolean | null;
is_partially_verified: boolean | null;
sourcify_repo_url: string | null;
// <<<<
source_code: string | null;
constructor_args: string | null;
decoded_constructor_args: Array<SmartContractDecodedConstructorArg> | null;
can_be_visualized_via_sol2uml: boolean | null;
is_vyper_contract: boolean | null;
file_path: string;
additional_sources: Array<{ file_path: string; source_code: string }>;
external_libraries: Array<SmartContractExternalLibrary> | null;
compiler_settings: unknown;
verified_twin_address_hash: string | null;
minimal_proxy_address_hash: string | null;
}
export type SmartContractDecodedConstructorArg = [
string,
{
internalType: SmartContractMethodArgType;
name: string;
type: SmartContractMethodArgType;
}
]
export interface SmartContractExternalLibrary {
address_hash: string;
name: string;
}
export interface SmartContractMethodBase {
......@@ -19,9 +54,10 @@ export interface SmartContractMethodBase {
outputs: Array<SmartContractMethodOutput>;
constant: boolean;
name: string;
stateMutability: string;
stateMutability: SmartContractMethodStateMutability;
type: 'function';
payable: boolean;
error?: string;
}
export interface SmartContractReadMethod extends SmartContractMethodBase {
......@@ -29,21 +65,51 @@ export interface SmartContractReadMethod extends SmartContractMethodBase {
}
export interface SmartContractWriteFallback {
payable: true;
payable?: true;
stateMutability: 'payable';
type: 'fallback';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback;
export interface SmartContractWriteReceive {
payable?: true;
stateMutability: 'payable';
type: 'receive';
}
export type SmartContractWriteMethod = SmartContractMethodBase | SmartContractWriteFallback | SmartContractWriteReceive;
export type SmartContractMethod = SmartContractReadMethod | SmartContractWriteMethod;
export interface SmartContractMethodInput {
internalType: string;
internalType?: SmartContractMethodArgType;
name: string;
type: string;
type: SmartContractMethodArgType;
}
export interface SmartContractMethodOutput extends SmartContractMethodInput {
value?: string;
}
export interface SmartContractQueryMethodReadSuccess {
is_error: false;
result: {
names: Array<string>;
output: Array<{
type: string;
value: string;
}>;
};
}
export interface SmartContractQueryMethodReadError {
is_error: true;
result: {
code: number;
message: string;
raw?: string;
} | {
error: string;
};
}
export type SmartContractQueryMethodRead = SmartContractQueryMethodReadSuccess | SmartContractQueryMethodReadError;
......@@ -19,28 +19,36 @@ export type GasPrices = {
slow: number;
}
export type Stats = {
counters: {
totalBlocks: string;
averageBlockTime: string;
totalTransactions: string;
completedTransactions: string;
export type Counters = {
counters: Array<Counter>;
}
totalAccounts: string;
type Counter = {
id: string;
value: string;
title: string;
units: string;
}
totalTokens: string;
export type StatsCharts = {
sections: Array<StatsChartsSection>;
}
totalNativeCoinHolders: string;
totalNativeCoinTransfers: string;
};
export type StatsChartsSection = {
id: string;
title: string;
charts: Array<StatsChartInfo>;
}
export type Charts = {
chart: Array<ChartsItem>;
export type StatsChartInfo = {
id: string;
title: string;
description: string;
}
export type ChartsItem ={
export type StatsChart = { chart: Array<StatsChartItem> };
export type StatsChartItem = {
date: string;
value: string;
}
......@@ -37,6 +37,9 @@ interface TokenTransferBase {
from: AddressParam;
to: AddressParam;
timestamp: string;
block_hash: string;
log_index: string;
method?: string;
}
export type TokenTransferPagination = {
......
import type { TokenInfo, TokenType } from './tokenInfo';
export type TokensResponse = {
items: Array<TokenInfo>;
next_page_params: {
holder_count: number;
items_count: number;
name: string;
};
}
export type TokensFilters = { filter: string; type: Array<TokenType> | undefined };
export type StatsSection = { id: StatsSectionIds; title: string; charts: Array<StatsChart> }
export type StatsSectionIds = keyof typeof StatsSectionId;
export enum StatsSectionId {
'all',
'accounts',
'blocks',
'tokens',
'transactions',
'gas',
}
export type StatsInterval = { id: StatsIntervalIds; title: string }
export type StatsIntervalIds = keyof typeof StatsIntervalId;
export enum StatsIntervalId {
......@@ -18,9 +7,3 @@ export enum StatsIntervalId {
'sixMonths',
'oneYear',
}
export type StatsChart = {
apiId: string;
title: string;
description: string;
}
import { useColorModeValue, useToken } from '@chakra-ui/react';
import {
EthereumClient,
modalConnectors,
walletConnectProvider,
} from '@web3modal/ethereum';
import { Web3Modal } from '@web3modal/react';
import React from 'react';
import type { Chain } from 'wagmi';
import { configureChains, createClient, WagmiConfig } from 'wagmi';
import type { RoutedSubTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import { ContractContextProvider } from 'ui/address/contract/context';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
interface Props {
tabs: Array<RoutedSubTab>;
}
export const currentChain: Chain = {
id: Number(appConfig.network.id),
name: appConfig.network.name || '',
network: appConfig.network.name || '',
nativeCurrency: {
decimals: appConfig.network.currency.decimals,
name: appConfig.network.currency.name || '',
symbol: appConfig.network.currency.symbol || '',
},
rpcUrls: {
'default': {
http: [ appConfig.network.rpcUrl || '' ],
},
},
blockExplorers: {
'default': {
name: 'Blockscout',
url: appConfig.baseUrl,
},
},
};
const chains = [ currentChain ];
const { provider } = configureChains(chains, [
walletConnectProvider({ projectId: appConfig.walletConnect.projectId || '' }),
]);
const wagmiClient = createClient({
autoConnect: true,
connectors: modalConnectors({ appName: 'web3Modal', chains }),
provider,
});
const ethereumClient = new EthereumClient(wagmiClient, chains);
const TAB_LIST_PROPS = {
columnGap: 3,
};
const AddressContract = ({ tabs }: Props) => {
return <RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={{ columnGap: 3 }}/>;
const modalZIndex = useToken<string>('zIndices', 'modal');
return (
<WagmiConfig client={ wagmiClient }>
<ContractContextProvider>
<RoutedTabs tabs={ tabs } variant="outline" colorScheme="gray" size="sm" tabListProps={ TAB_LIST_PROPS }/>
</ContractContextProvider>
<Web3Modal
projectId={ appConfig.walletConnect.projectId }
ethereumClient={ ethereumClient }
themeZIndex={ Number(modalZIndex) }
themeMode={ useColorModeValue('light', 'dark') }
themeBackground="themeColor"
/>
</WagmiConfig>
);
};
export default React.memo(AddressContract);
import { chakra, Icon, Link, Tooltip } from '@chakra-ui/react';
import React from 'react';
import svgFileIcon from 'icons/files/csv.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link';
interface Props {
address: string;
type: 'transactions' | 'internal-transactions' | 'token-transfers';
className?: string;
}
const AddressCsvExportLink = ({ className, address, type }: Props) => {
const isMobile = useIsMobile();
return (
<Tooltip isDisabled={ !isMobile } label="Download CSV">
<Link
className={ className }
display="inline-flex"
alignItems="center"
href={ link('csv_export', undefined, { type, address }) }
>
<Icon as={ svgFileIcon } boxSize={{ base: '30px', lg: 6 }}/>
{ !isMobile && <chakra.span ml={ 1 }>Download CSV</chakra.span> }
</Link>
</Tooltip>
);
};
export default React.memo(chakra(AddressCsvExportLink));
import type { MetaMaskInpageProvider } from '@metamask/providers';
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { Address } from 'types/api/address';
import * as addressMock from 'mocks/address/address';
import * as countersMock from 'mocks/address/counters';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressDetails from './AddressDetails';
import MockAddressPage from './testUtils/MockAddressPage';
const ADDRESS_HASH = addressMock.hash;
const API_URL_ADDRESS = buildApiUrl('address', { id: ADDRESS_HASH });
const API_URL_COUNTERS = buildApiUrl('address_counters', { id: ADDRESS_HASH });
const API_URL_TOKEN_BALANCES = buildApiUrl('address_token_balances', { id: ADDRESS_HASH });
const hooksConfig = {
router: {
query: { id: ADDRESS_HASH },
},
};
test('contract +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.contract),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forContract),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.contract } as UseQueryResult<Address, unknown>}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('token', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.token),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forToken),
}));
await page.route(API_URL_TOKEN_BALANCES, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenBalanceMock.baseList),
}));
await page.evaluate(() => {
window.ethereum = { } as MetaMaskInpageProvider;
});
const component = await mount(
<TestApp>
<MockAddressPage>
<AddressDetails addressQuery={{ data: addressMock.token } as UseQueryResult<Address, unknown>}/>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('validator +@mobile', async({ mount, page }) => {
await page.route(API_URL_ADDRESS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(addressMock.validator),
}));
await page.route(API_URL_COUNTERS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(countersMock.forValidator),
}));
const component = await mount(
<TestApp>
<AddressDetails addressQuery={{ data: addressMock.validator } as UseQueryResult<Address, unknown>}/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
......@@ -18,6 +18,7 @@ import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import HashStringShortenDynamic from 'ui/shared/HashStringShortenDynamic';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance';
......@@ -99,11 +100,23 @@ const AddressDetails = ({ addressQuery, scrollRef }: Props) => {
title="Creator"
hint="Transaction and address of creation."
>
<AddressLink hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at </Text>
<AddressLink type="address" hash={ addressQuery.data.creator_address_hash } truncation="constant"/>
<Text whiteSpace="pre"> at txn </Text>
<AddressLink hash={ addressQuery.data.creation_tx_hash } type="transaction" truncation="constant"/>
</DetailsInfoItem>
) }
{ addressQuery.data.is_contract && addressQuery.data.implementation_address && (
<DetailsInfoItem
title="Implementation"
hint="Implementation address of the proxy contract."
columnGap={ 1 }
>
<Link href={ link('address_index', { id: addressQuery.data.implementation_address }) }>{ addressQuery.data.implementation_name }</Link>
<Text variant="secondary" overflow="hidden">
<HashStringShortenDynamic hash={ `(${ addressQuery.data.implementation_address })` }/>
</Text>
</DetailsInfoItem>
) }
<AddressBalance data={ addressQuery.data }/>
{ addressQuery.data.has_tokens && (
<DetailsInfoItem
......
......@@ -17,6 +17,7 @@ import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
import AddressIntTxsList from './internals/AddressIntTxsList';
......@@ -80,13 +81,14 @@ const AddressInternalTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivE
return (
<>
<ActionBar mt={ -6 }>
<ActionBar mt={ -6 } justifyContent="left">
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
{ isPaginationVisible && <Pagination ml="auto" { ...pagination }/> }
<AddressCsvExportLink address={ queryIdStr } type="internal-transactions" ml={{ base: 2, lg: 'auto' }}/>
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar>
{ content }
</>
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { erc1155 } from 'mocks/tokens/tokenTransfer';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import AddressTokenTransfers from './AddressTokenTransfers';
const API_URL = buildApiUrl('address_token_transfers', { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' }) +
'?token=0x1189a607CEac2f0E14867de4EB15b15C9FFB5859';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859', token: '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('with token filter and pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ], next_page_params: { block_number: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('with token filter and no pagination +@mobile', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ erc1155 ] }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTokenTransfers/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import TokenTransfer from 'ui/shared/TokenTransfer/TokenTransfer';
import type { SocketMessage } from 'lib/socket/types';
import { AddressFromToFilterValues } from 'types/api/address';
import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
import type { TokenTransfer } from 'types/api/tokenTransfer';
import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import { apos } from 'lib/html-entities';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TOKEN_TYPE from 'lib/token/tokenTypes';
import EmptySearchResult from 'ui/apps/EmptySearchResult';
import ActionBar from 'ui/shared/ActionBar';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import Pagination from 'ui/shared/Pagination';
import SkeletonList from 'ui/shared/skeletons/SkeletonList';
import SkeletonTable from 'ui/shared/skeletons/SkeletonTable';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenLogo from 'ui/shared/TokenLogo';
import { flattenTotal } from 'ui/shared/TokenTransfer/helpers';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable';
import AddressCsvExportLink from './AddressCsvExportLink';
type Filters = {
type: Array<TokenType>;
filter: AddressFromToFilter | undefined;
}
const TOKEN_TYPES = TOKEN_TYPE.map(i => i.id);
const getTokenFilterValue = (getFilterValuesFromQuery<TokenType>).bind(null, TOKEN_TYPES);
const getAddressFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const OVERLOAD_COUNT = 75;
const matchFilters = (filters: Filters, tokenTransfer: TokenTransfer, address?: string) => {
if (filters.filter) {
if (filters.filter === 'from' && tokenTransfer.from.hash !== address) {
return false;
}
if (filters.filter === 'to' && tokenTransfer.to.hash !== address) {
return false;
}
}
if (filters.type && filters.type.length) {
if (!filters.type.includes(tokenTransfer.token.type)) {
return false;
}
}
return true;
};
const AddressTokenTransfers = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const tokenFilter = router.query.token ? router.query.token.toString() : undefined;
const [ filters, setFilters ] = React.useState<Filters>(
{
type: getTokenFilterValue(router.query.type) || [],
filter: getAddressFilterValue(router.query.filter),
},
);
const { isError, isLoading, data, pagination, onFilterChange, isPaginationVisible } = useQueryWithPages({
resourceName: 'address_token_transfers',
pathParams: { id: currentAddress },
filters: tokenFilter ? { token: tokenFilter } : filters,
scrollRef,
});
const handleTypeFilterChange = React.useCallback((nextValue: Array<TokenType>) => {
onFilterChange({ ...filters, type: nextValue });
setFilters((prevState) => ({ ...prevState, type: nextValue }));
}, [ filters, onFilterChange ]);
const handleAddressFilterChange = React.useCallback((nextValue: string) => {
const filterVal = getAddressFilterValue(nextValue);
onFilterChange({ ...filters, filter: filterVal });
setFilters((prevState) => ({ ...prevState, filter: filterVal }));
}, [ filters, onFilterChange ]);
const resetTokenFilter = React.useCallback(() => {
onFilterChange({});
}, [ onFilterChange ]);
const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert('');
if (data?.items && data.items.length >= OVERLOAD_COUNT) {
if (matchFilters(filters, payload.token_transfer, currentAddress)) {
setNewItemsCount(prev => prev + 1);
}
} else {
queryClient.setQueryData(
getResourceKey('address_token_transfers', { pathParams: { id: router.query.id?.toString() }, queryParams: { ...filters } }),
(prevData: AddressTokenTransferResponse | undefined) => {
if (!prevData) {
return;
}
if (!matchFilters(filters, payload.token_transfer, currentAddress)) {
return prevData;
}
return {
...prevData,
items: [
payload.token_transfer,
...prevData.items,
],
};
});
}
};
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new token transfers.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new token transfers. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: pagination.page !== 1 || Boolean(tokenFilter),
});
useSocketMessage({
channel,
event: 'token_transfer',
handler: handleNewSocketMessage,
});
const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0);
const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length;
const content = (() => {
if (isLoading) {
return (
<>
<Hide below="lg">
<SkeletonTable columns={ [ '44px', '185px', '160px', '25%', '25%', '25%', '25%' ] }/>
</Hide>
<Show below="lg">
<SkeletonList/>
</Show>
</>
);
}
if (isError) {
return <DataFetchAlert/>;
}
if (!data.items?.length && !numActiveFilters) {
return <Text as="span">There are no token transfers</Text>;
}
if (!data.items?.length) {
return <EmptySearchResult text={ `Couldn${ apos }t find any token transfer that matches your query.` }/>;
}
const items = data.items.reduce(flattenTotal, []);
return (
<>
<Hide below="lg">
<TokenTransferTable
data={ items }
baseAddress={ currentAddress }
showTxInfo
top={ 80 }
enableTimeIncrement
showSocketInfo={ pagination.page === 1 && !tokenFilter }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</Hide>
<Show below="lg">
{ pagination.page === 1 && !tokenFilter && (
<SocketNewItemsNotice
url={ window.location.href }
num={ newItemsCount }
alert={ socketAlert }
type="token_transfer"
borderBottomRadius={ 0 }
/>
) }
<TokenTransferList
data={ items }
baseAddress={ currentAddress }
showTxInfo
enableTimeIncrement
/>
</Show>
</>
);
})();
const tokenFilterComponent = tokenFilter && (
<Flex alignItems="center" py={ 1 } flexWrap="wrap" mb={{ base: isPaginationVisible ? 6 : 3, lg: 0 }}>
Filtered by token
<TokenLogo hash={ tokenFilter } boxSize={ 6 } mx={ 2 }/>
{ isMobile ? tokenFilter.slice(0, 4) + '...' + tokenFilter.slice(-4) : tokenFilter }
<Tooltip label="Reset filter">
<Flex>
<Icon
as={ crossIcon }
boxSize={ 6 }
ml={ 1 }
color={ resetTokenIconColor }
cursor="pointer"
_hover={{ color: resetTokenIconHoverColor }}
onClick={ resetTokenFilter }
/>
</Flex>
</Tooltip>
</Flex>
);
const hash = router.query.id;
return (
<TokenTransfer
resourceName="address_token_transfers"
pathParams={{ id: hash?.toString() }}
baseAddress={ typeof hash === 'string' ? hash : undefined }
enableTimeIncrement
scrollRef={ scrollRef }
/>
<>
{ isMobile && tokenFilterComponent }
{ !isActionBarHidden && (
<ActionBar mt={ -6 }>
{ !isMobile && tokenFilterComponent }
{ !tokenFilter && (
<TokenTransferFilter
defaultTypeFilters={ filters.type }
onTypeFilterChange={ handleTypeFilterChange }
appliedFiltersNum={ numActiveFilters }
withAddressFilter
onAddressFilterChange={ handleAddressFilterChange }
defaultAddressFilter={ filters.filter }
/>
) }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="token-transfers" ml={{ base: 2, lg: 'auto' }}/> }
{ isPaginationVisible && <Pagination ml={{ base: 'auto', lg: 8 }} { ...pagination }/> }
</ActionBar>
) }
{ content }
</>
);
};
......
import castArray from 'lodash/castArray';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const isMobile = useIsMobile();
const currentAddress = router.query.id?.toString();
const [ filterValue, setFilterValue ] = React.useState<AddressFromToFilter>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({
resourceName: 'address_txs',
pathParams: { id: castArray(router.query.id)[0] },
pathParams: { id: currentAddress },
filters: { filter: filterValue },
scrollRef,
});
......@@ -37,6 +49,83 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
setSocketAlert('');
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
if (
!filterValue ||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
) {
setNewItemsCount(prev => prev + 1);
}
}
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { id: router.query.id?.toString() }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash);
if (currIndex > -1) {
prevData.items[currIndex] = payload.transaction;
return prevData;
}
if (prevData.items.length >= OVERLOAD_COUNT) {
return prevData;
}
if (filterValue) {
if (
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress)
) {
return prevData;
}
}
return {
...prevData,
items: [
payload.transaction,
...prevData.items,
],
};
});
};
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
const filter = (
<AddressTxsFilter
defaultFilter={ filterValue }
......@@ -50,15 +139,18 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination }/> }
{ currentAddress && <AddressCsvExportLink address={ currentAddress } type="transactions" ml="auto"/> }
{ addressTxsQuery.isPaginationVisible && <Pagination { ...addressTxsQuery.pagination } ml={ 8 }/> }
</ActionBar>
) }
<TxsContent
filter={ filter }
query={ addressTxsQuery }
showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</>
);
......
......@@ -10,7 +10,7 @@ import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import FilterButton from 'ui/shared/FilterButton';
import FilterButton from 'ui/shared/filters/FilterButton';
interface Props {
isActive: boolean;
......
......@@ -49,11 +49,11 @@ const AddressCoinBalanceHistory = ({ query }: Props) => {
<Table variant="simple" size="sm">
<Thead top={ 80 }>
<Tr>
<Th width="25%">Block</Th>
<Th width="25%">Txn</Th>
<Th width="25%">Age</Th>
<Th width="25%" isNumeric pr={ 1 }/>
<Th width="120px" isNumeric>Balance { appConfig.network.currency.symbol }</Th>
<Th width="20%">Block</Th>
<Th width="20%">Txn</Th>
<Th width="20%">Age</Th>
<Th width="20%" isNumeric pr={ 1 }>Balance { appConfig.network.currency.symbol }</Th>
<Th width="20%" isNumeric>Delta</Th>
</Tr>
</Thead>
<Tbody>
......
......@@ -30,7 +30,7 @@ const AddressCoinBalanceListItem = (props: Props) => {
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) }
{ deltaBn.toFormat() }
</Text>
</StatHelpText>
</Stat>
......
......@@ -46,7 +46,7 @@ const AddressCoinBalanceTableItem = (props: Props) => {
<StatHelpText display="flex" mb={ 0 } alignItems="center">
<StatArrow type={ isPositiveDelta ? 'increase' : 'decrease' }/>
<Text as="span" color={ isPositiveDelta ? 'green.500' : 'red.500' } fontWeight={ 600 }>
{ deltaBn.toFixed(8) }
{ deltaBn.toFormat() }
</Text>
</StatHelpText>
</Stat>
......
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import * as contractMock from 'mocks/contract/info';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import ContractCode from './ContractCode';
const addressHash = 'hash';
const CONTRACT_API_URL = buildApiUrl('contract', { id: addressHash });
const hooksConfig = {
router: {
query: { id: addressHash },
},
};
test('verified with changed byte code +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withChangedByteCode),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
test('verified with multiple sources', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withMultiplePaths),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
const section = page.locator('section', { hasText: 'Contract source code' });
await expect(section).toHaveScreenshot();
});
test('verified via sourcify', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.verifiedViaSourcify),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(page).toHaveScreenshot({ clip: { x: 0, y: 0, width: 1200, height: 110 } });
});
test('self destructed', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.selfDestructed),
}));
await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
const section = page.locator('section', { hasText: 'Contract creation code' });
await expect(section).toHaveScreenshot();
});
test('with twin address alert +@mobile', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withTwinAddress),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('with proxy address alert +@mobile', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.withProxyAddress),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component.getByRole('alert')).toHaveScreenshot();
});
test('non verified', async({ mount, page }) => {
await page.route(CONTRACT_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(contractMock.nonVerified),
}));
const component = await mount(
<TestApp>
<ContractCode/>
</TestApp>,
{ hooksConfig },
);
await expect(component).toHaveScreenshot();
});
import { Flex, Skeleton, Button, Grid, GridItem, Text } from '@chakra-ui/react';
import dynamic from 'next/dynamic';
import { Flex, Skeleton, Button, Grid, GridItem, Text, Alert, Link, chakra, Box } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import link from 'lib/link/link';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import ExternalLink from 'ui/shared/ExternalLink';
import RawDataSnippet from 'ui/shared/RawDataSnippet';
const DynamicContractSourceCode = dynamic(
() => import('./ContractSourceCode'),
{ ssr: false },
);
import ContractSourceCode from './ContractSourceCode';
const InfoItem = ({ label, value }: { label: string; value: string }) => (
<GridItem display="flex" columnGap={ 6 }>
......@@ -23,10 +23,12 @@ const InfoItem = ({ label, value }: { label: string; value: string }) => (
const ContractCode = () => {
const router = useRouter();
const addressHash = router.query.id?.toString();
const { data, isLoading, isError } = useApiQuery('contract', {
pathParams: { id: router.query.id?.toString() },
pathParams: { id: addressHash },
queryOptions: {
enabled: Boolean(router.query.id),
enabled: Boolean(addressHash),
refetchOnMount: false,
},
});
......@@ -57,14 +59,92 @@ const ContractCode = () => {
ml="auto"
mr={ 3 }
as="a"
href={ link('address_contract_verification', { id: router.query.id?.toString() }) }
href={ link('address_contract_verification', { id: addressHash }) }
>
Verify & publish
</Button>
);
const constructorArgs = (() => {
if (!data.decoded_constructor_args) {
return data.constructor_args;
}
const decoded = data.decoded_constructor_args
.map(([ value, { name, type } ], index) => {
const valueEl = type === 'address' ? <Link href={ link('address_index', { id: value }) }>{ value }</Link> : <span>{ value }</span>;
return (
<Box key={ index }>
<span>Arg [{ index }] { name || '' } ({ type }): </span>
{ valueEl }
</Box>
);
});
return (
<>
<span>{ data.constructor_args }</span>
<br/><br/>
{ decoded }
</>
);
})();
const externalLibraries = (() => {
if (!data.external_libraries || data.external_libraries.length === 0) {
return null;
}
return data.external_libraries.map((item) => (
<Box key={ item.address_hash }>
<chakra.span fontWeight={ 500 }>{ item.name }: </chakra.span>
<Link href={ link('address_index', { id: item.address_hash }, { tab: 'contract' }) }>{ item.address_hash }</Link>
</Box>
));
})();
return (
<>
<Flex flexDir="column" rowGap={ 2 } mb={ 6 } _empty={{ display: 'none' }}>
{ data.is_verified && <Alert status="success">Contract Source Code Verified (Exact Match)</Alert> }
{ data.is_verified_via_sourcify && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>This contract has been { data.is_partially_verified ? 'partially ' : '' }verified via Sourcify. </span>
{ data.sourcify_repo_url && <ExternalLink href={ data.sourcify_repo_url } title="View contract in Sourcify repository" fontSize="md"/> }
</Alert>
) }
{ data.is_changed_bytecode && (
<Alert status="warning">
Warning! Contract bytecode has been changed and does not match the verified one. Therefore, interaction with this smart contract may be risky.
</Alert>
) }
{ !data.is_verified && data.verified_twin_address_hash && !data.minimal_proxy_address_hash && (
<Alert status="warning" whiteSpace="pre-wrap" flexWrap="wrap">
<span>Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB </span>
<Address>
<AddressIcon address={{ hash: data.verified_twin_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink type="address" hash={ data.verified_twin_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<chakra.span mt={ 1 }>All functions displayed below are from ABI of that contract. In order to verify current contract, proceed with </chakra.span>
<Link href={ link('address_contract_verification', { id: data.verified_twin_address_hash }) }>Verify & Publish</Link>
<span> page</span>
</Alert>
) }
{ data.minimal_proxy_address_hash && (
<Alert status="warning" flexWrap="wrap" whiteSpace="pre-wrap">
<span>Minimal Proxy Contract for </span>
<Address>
<AddressIcon address={{ hash: data.minimal_proxy_address_hash, is_contract: true, implementation_name: null }}/>
<AddressLink type="address" hash={ data.minimal_proxy_address_hash } truncation="constant" ml={ 2 }/>
</Address>
<span>. </span>
<Box>
<Link href="https://eips.ethereum.org/EIPS/eip-1167">EIP-1167</Link>
<span> - minimal bytecode implementation that delegates all calls to a known address</span>
</Box>
</Alert>
) }
</Flex>
{ data.is_verified && (
<Grid templateColumns={{ base: '1fr', lg: '1fr 1fr' }} rowGap={ 4 } columnGap={ 6 } mb={ 8 }>
{ data.name && <InfoItem label="Contract name" value={ data.name }/> }
......@@ -76,18 +156,35 @@ const ContractCode = () => {
</Grid>
) }
<Flex flexDir="column" rowGap={ 6 }>
{ constructorArgs && (
<RawDataSnippet
data={ constructorArgs }
title="Constructor Arguments"
textareaMaxHeight="200px"
/>
) }
{ data.source_code && (
<DynamicContractSourceCode
<ContractSourceCode
data={ data.source_code }
hasSol2Yml={ Boolean(data.can_be_visualized_via_sol2uml) }
address={ router.query.id?.toString() }
address={ addressHash }
isViper={ Boolean(data.is_vyper_contract) }
filePath={ data.file_path }
additionalSource={ data.additional_sources }
/>
) }
{ Boolean(data.compiler_settings) && (
<RawDataSnippet
data={ JSON.stringify(data.compiler_settings) }
title="Compiler Settings"
textareaMaxHeight="200px"
/>
) }
{ data.abi && (
<RawDataSnippet
data={ JSON.stringify(data.abi) }
title="Contract ABI"
textareaMinHeight="200px"
textareaMaxHeight="200px"
/>
) }
{ data.creation_bytecode && (
......@@ -95,12 +192,27 @@ const ContractCode = () => {
data={ data.creation_bytecode }
title="Contract creation code"
rightSlot={ data.is_verified ? null : verificationButton }
beforeSlot={ data.is_self_destructed ? (
<Alert status="info" whiteSpace="pre-wrap" mb={ 3 }>
Contracts that self destruct in their constructors have no contract code published and cannot be verified.
Displaying the init data provided of the creating transaction.
</Alert>
) : null }
textareaMaxHeight="200px"
/>
) }
{ data.deployed_bytecode && (
<RawDataSnippet
data={ data.deployed_bytecode }
title="Deployed ByteCode"
textareaMaxHeight="200px"
/>
) }
{ externalLibraries && (
<RawDataSnippet
data={ externalLibraries }
title="External Libraries"
textareaMaxHeight="200px"
/>
) }
</Flex>
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment