Merge branch 'release/v2021.10.03-otterscan'

This commit is contained in:
Willian Mitsuda 2021-10-19 15:16:56 -03:00
commit acd9c993d0
19 changed files with 479 additions and 190 deletions

3
.gitmodules vendored
View File

@ -4,3 +4,6 @@
[submodule "trustwallet"]
path = trustwallet
url = https://github.com/trustwallet/assets.git
[submodule "topic0"]
path = topic0
url = https://github.com/wmitsuda/topic0.git

2
4bytes

@ -1 +1 @@
Subproject commit 1cc7e25c840ae9d985c12768b0cbd0ece3fc5400
Subproject commit 20537524bfb01bee859c9cfa9a8784baacbcc7ae

View File

@ -19,8 +19,13 @@ WORKDIR /signatures
COPY 4bytes/signatures /signatures/
COPY 4bytes/with_parameter_names /signatures/
FROM alpine:3.14.0 AS topic0builder
WORKDIR /topic0
COPY topic0/with_parameter_names /topic0/
FROM nginx:1.21.1-alpine
RUN apk add jq
COPY --from=topic0builder /topic0 /usr/share/nginx/html/topic0/
COPY --from=fourbytesbuilder /signatures /usr/share/nginx/html/signatures/
COPY --from=logobuilder /assets /usr/share/nginx/html/assets/
COPY nginx.conf /etc/nginx/conf.d/default.conf

65
docs/ipfs.md Normal file
View File

@ -0,0 +1,65 @@
# IPFS
This doc describes various topics regarding our use of IPFS (currently only used for Sourcify integration).
## Default public gateway by default
By default every IPFS resource is linked to the default public IPFS gateway at https://ipfs.io.
This is done so even users without a local node can use IPFS integration by default.
However this brings us some privacy concerns, since queries are made against the public gateway.
We recommend the use of [IPFS Companion](https://docs.ipfs.io/install/ipfs-companion/), a popular browser extension that intercepts gateway calls and redirect them to your local gateway.
This also solves the problem of users not having a local gateway at http://localhost:8080 (the default bind address of `ipfs` daemon, but also a very common bind address for other software leading to collisions), but instead having a private gateway inside the local network. The IPFS Companion allows you to customize which gateway you want it to redirect requests to.
This way we can provide an out-of-box usable integration, but still allowing users to address privacy concerns.
## Slow IPNS resolution
By default IPNS resolution _may_ be **extremelly** slow. There is an experimental IPFS feature called IPNS pubsub that speeds it up significantly (https://github.com/ipfs/go-ipfs/blob/master/docs/experimental-features.md#ipns-pubsub), but for it to work, it needs to be enabled in **both** resolver and publisher nodes.
We asked Sourcify to enable it in their publisher node, so you just have to enable it in your local node to have an usable performance.
On IPFS Companion, make sure **both** `Enable PubSub` and `Enable IPNS over PubSub` options are enabled.
![IPFS pubsub](./ipns-pubsub.png)
## Pinning Sourcify locally
If you have an IPFS node, you can speedup things significantly and also help to spread their data among the network by pinning their entire repository.
However, due to their repository characteristics (> 5 GB of thousands of small files and many nested directories), there are a few gotchas.
> Please note this is based on Otterscan author's experience, it would be welcome to have more datapoints.
### Use `badgerds`
By default ipfs uses the `flatfs` datastore, which stores objects in simple files.
I simply couldn't finish pinning the entire repo using the default settings, after 6 hours it was able to get only ~10% of the total repo, the pinning process itself slows down the computer a lot.
`badgerds` is an alternative ipfs node repository format, still labeled as experimental, but marked as a _"to be turned into the default"_ repository format in future. It uses a key-value database internally.
You can create a repository from scratch by using `ipfs init -p badgerds`, or convert an existing repository using [ipfs-ds-convert](https://github.com/ipfs/ipfs-ds-convert).
> Use it at your own risk!
My experiments pinning the entire repo on `badgerds` gave different timings, varying from 1 to 2 hours in a standard MacBook Pro laptop, totally affordable for home users, with no perceived system degradation.
```
$ time ipfs pin add --progress /ipns/k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p
pinned QmVn7fcwo4Eai19hRX6dG9jAV8piHyxcrPTuobyZjEKhMW recursively
ipfs pin add --progress 4.47s user 1.85s system 0% cpu 2:22:41.00 total
```
### IPFS root changes hourly
The Sourcify root IPNS name is `/ipns/k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p`.
Pinning it resolves it to the current IPFS root hash. However, any change to their contents changes the root hash (e.g., new contracts are verified).
Sourcify current runs a cron job the updates the IPNS name hourly. It means that your pin will eventually become stale.
But we think it is still worth pinning the repo because data always gets added to their repo, so by pinning current data you are speeding up your own queries to existing contracts and contributing to spread their data over the network so other people don't have to rely 100% on their node availability.

BIN
docs/ipns-pubsub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

View File

@ -2,32 +2,32 @@
We get the contract source code and metadata from [Sourcify](https://sourcify.dev/).
There are multiple ways to consume their data we support, each one with pros and cons:
## Integration modes
## IPNS/IPFS
There are multiple ways to consume their data that we support, each one with pros and cons:
### IPFS
This is the default integration method, we resolve the public Sourcify IPNS to get the latest known IPFS root hash of their repository.
The downside is that recently verified contracts may not have yet been added to the root hash and republished into IPNS.
It assumes a local IPFS gateway at localhost:8080 to avoid leaking your queries to public gateways.
It uses the public gateway at https://ipfs.io by default.
> This option is actually not working, but it is provided for completeness, follow https://github.com/ethereum/sourcify/issues/495
Please see our [ipfs integration docs](./ipfs.md) for more info about how we handle all IPFS integrations and privacy concerns.
## Direct HTTP connection to Sourcify's repository
### Direct HTTP connection to Sourcify's repository
Standard HTTP connection to their repo at https://repo.sourcify.dev/
Fast access to fresh verified data. On the other hand it is less private and centralized.
## Local snapshot
### Local snapshot **(deprecated; soon to be removed)**
As a midterm solution, we are making available a snapshot docker image of their repository, containing only mainnet full verified contracts.
This would allow you to play with existing contracts up to the snapshot date/time locally, not depending on their service or IPFS connectivity availability.
> It is very likely this run mode will be deprecated in future.
The Sourcify snapshot is provided as a nginx image at: https://hub.docker.com/repository/docker/otterscan/sourcify-snapshot
You can run it with:

View File

@ -43,6 +43,40 @@ server {
}
}
location /topic0 {
root /usr/share/nginx/html;
expires 30d;
# Base on: https://michielkalkman.com/snippets/nginx-cors-open-configuration/
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
#
# Om nom nom cookies
#
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS';
#
# Custom headers and headers various browsers *should* be OK with but aren't
#
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
#
# Tell client that this pre-flight info is valid for 20 days
#
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
if ($request_method = 'GET') {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type' always;
}
}
location /assets {
root /usr/share/nginx/html;
expires 30d;

128
package-lock.json generated
View File

@ -20,18 +20,18 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@fortawesome/react-fontawesome": "^0.1.16",
"@headlessui/react": "^1.4.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24",
"@types/node": "^14.17.5",
"@types/react": "^17.0.26",
"@types/react": "^17.0.30",
"@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9",
"@types/react-highlight": "^0.12.4",
"@types/react-router-dom": "^5.3.0",
"@types/react-highlight": "^0.12.5",
"@types/react-router-dom": "^5.3.1",
"@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.5.1",
"ethers": "^5.4.7",
@ -47,13 +47,13 @@
"react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.4",
"serve": "^12.0.1",
"typescript": "^4.4.3",
"typescript": "^4.4.4",
"use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1"
},
"devDependencies": {
"autoprefixer": "^9.8.7",
"postcss": "^7.0.38",
"autoprefixer": "^9.8.8",
"postcss": "^7.0.39",
"source-map-explorer": "^2.5.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6"
}
@ -2089,14 +2089,14 @@
}
},
"node_modules/@fortawesome/react-fontawesome": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"version": "0.1.16",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.16.tgz",
"integrity": "sha512-aLmzDwC9rEOAJv2UJdMns89VZR5Ry4IHu5dQQh24Z/lWKEm44lfQr1UNalZlkUaQN8d155tNh+CS7ntntj1VMA==",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/fontawesome-svg-core": "~1 || >=1.3.0-beta1",
"react": ">=16.x"
}
},
@ -3090,9 +3090,9 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.26.tgz",
"integrity": "sha512-MXxuXrH2xOcv5cp/su4oz69dNQnSA90JjFw5HBd5wifw6Ihi94j7dRJm7qNsB30tnruXSCPc9qmlhGop4nh9Hw==",
"version": "17.0.30",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.30.tgz",
"integrity": "sha512-3Dt/A8gd3TCXi2aRe84y7cK1K8G+N9CZRDG8kDGguOKa0kf/ZkSwTmVIDPsm/KbQOVMaDJXwhBtuOXxqwdpWVg==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -3116,9 +3116,9 @@
}
},
"node_modules/@types/react-highlight": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.4.tgz",
"integrity": "sha512-eq+1LUJYDTpZMx31KD3e8K0OJcfUlgWaxYGldKfD7uMkmg04x6O2+h6kG7Rm0SY1+XkeBWYhg2lyIFv8WpGrLQ==",
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.5.tgz",
"integrity": "sha512-P8+mTxltxDdQ+99l+pjn40clziSbNrZy5d5zmvG+j3jKzokAhCoCZlIRmmnFgETTYubuqwKjvXSlvesBZcTfvQ==",
"dependencies": {
"@types/react": "*"
}
@ -3133,9 +3133,9 @@
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-svUzpEpKDwK8nmfV2vpZNSsiijFNKY8+gUqGqvGGOVrXvX58k1JIJubZa5igkwacbq/0umphO5SsQn/BQsnKpw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.1.tgz",
"integrity": "sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A==",
"dependencies": {
"@types/history": "*",
"@types/react": "*",
@ -4163,15 +4163,15 @@
}
},
"node_modules/autoprefixer": {
"version": "9.8.7",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.7.tgz",
"integrity": "sha512-7Hg99B1eTH5+LgmUBUSmov1Z3bsggQJS7v3IMGo6wcScnbRuvtMc871J9J+4bSbIqa9LSX/zypFXJ8sXHpMJeQ==",
"version": "9.8.8",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
"integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==",
"dependencies": {
"browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001109",
"nanocolors": "^0.2.8",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
"picocolors": "^0.2.1",
"postcss": "^7.0.32",
"postcss-value-parser": "^4.1.0"
},
@ -11883,11 +11883,6 @@
"license": "MIT",
"optional": true
},
"node_modules/nanocolors": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.10.tgz",
"integrity": "sha512-i+EDWGsJClQwR/bhLIG/CObZZwaYaS5qt+yjxZbfV+77QiNHNzE9nj4d9Ut1TGZ0R0eSwPcQWzReASzXuw/7oA=="
},
"node_modules/nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -12728,6 +12723,11 @@
"version": "2.1.0",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
},
"node_modules/picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@ -12896,11 +12896,11 @@
}
},
"node_modules/postcss": {
"version": "7.0.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.38.tgz",
"integrity": "sha512-wNrSHWjHDQJR/IZL5IKGxRtFgrYNaAA/UrkW2WqbtZO6uxSLMxMN+s2iqUMwnAWm3fMROlDYZB41dr0Mt7vBwQ==",
"version": "7.0.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
"dependencies": {
"nanocolors": "^0.2.2",
"picocolors": "^0.2.1",
"source-map": "^0.6.1"
},
"engines": {
@ -18161,9 +18161,9 @@
}
},
"node_modules/typescript": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==",
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -20969,9 +20969,9 @@
}
},
"@fortawesome/react-fontawesome": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.15.tgz",
"integrity": "sha512-/HFHdcoLESxxMkqZAcZ6RXDJ69pVApwdwRos/B2kiMWxDSAX2dFK8Er2/+rG+RsrzWB/dsAyjefLmemgmfE18g==",
"version": "0.1.16",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.1.16.tgz",
"integrity": "sha512-aLmzDwC9rEOAJv2UJdMns89VZR5Ry4IHu5dQQh24Z/lWKEm44lfQr1UNalZlkUaQN8d155tNh+CS7ntntj1VMA==",
"requires": {
"prop-types": "^15.7.2"
}
@ -21619,9 +21619,9 @@
"version": "1.5.4"
},
"@types/react": {
"version": "17.0.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.26.tgz",
"integrity": "sha512-MXxuXrH2xOcv5cp/su4oz69dNQnSA90JjFw5HBd5wifw6Ihi94j7dRJm7qNsB30tnruXSCPc9qmlhGop4nh9Hw==",
"version": "17.0.30",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.30.tgz",
"integrity": "sha512-3Dt/A8gd3TCXi2aRe84y7cK1K8G+N9CZRDG8kDGguOKa0kf/ZkSwTmVIDPsm/KbQOVMaDJXwhBtuOXxqwdpWVg==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -21645,9 +21645,9 @@
}
},
"@types/react-highlight": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.4.tgz",
"integrity": "sha512-eq+1LUJYDTpZMx31KD3e8K0OJcfUlgWaxYGldKfD7uMkmg04x6O2+h6kG7Rm0SY1+XkeBWYhg2lyIFv8WpGrLQ==",
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/@types/react-highlight/-/react-highlight-0.12.5.tgz",
"integrity": "sha512-P8+mTxltxDdQ+99l+pjn40clziSbNrZy5d5zmvG+j3jKzokAhCoCZlIRmmnFgETTYubuqwKjvXSlvesBZcTfvQ==",
"requires": {
"@types/react": "*"
}
@ -21662,9 +21662,9 @@
}
},
"@types/react-router-dom": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.0.tgz",
"integrity": "sha512-svUzpEpKDwK8nmfV2vpZNSsiijFNKY8+gUqGqvGGOVrXvX58k1JIJubZa5igkwacbq/0umphO5SsQn/BQsnKpw==",
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.1.tgz",
"integrity": "sha512-UvyRy73318QI83haXlaMwmklHHzV9hjl3u71MmM6wYNu0hOVk9NLTa0vGukf8zXUqnwz4O06ig876YSPpeK28A==",
"requires": {
"@types/history": "*",
"@types/react": "*",
@ -22342,15 +22342,15 @@
"version": "2.1.2"
},
"autoprefixer": {
"version": "9.8.7",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.7.tgz",
"integrity": "sha512-7Hg99B1eTH5+LgmUBUSmov1Z3bsggQJS7v3IMGo6wcScnbRuvtMc871J9J+4bSbIqa9LSX/zypFXJ8sXHpMJeQ==",
"version": "9.8.8",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz",
"integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==",
"requires": {
"browserslist": "^4.12.0",
"caniuse-lite": "^1.0.30001109",
"nanocolors": "^0.2.8",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
"picocolors": "^0.2.1",
"postcss": "^7.0.32",
"postcss-value-parser": "^4.1.0"
},
@ -27527,11 +27527,6 @@
"version": "2.14.2",
"optional": true
},
"nanocolors": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.10.tgz",
"integrity": "sha512-i+EDWGsJClQwR/bhLIG/CObZZwaYaS5qt+yjxZbfV+77QiNHNzE9nj4d9Ut1TGZ0R0eSwPcQWzReASzXuw/7oA=="
},
"nanoid": {
"version": "3.1.23",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz",
@ -28076,6 +28071,11 @@
"performance-now": {
"version": "2.1.0"
},
"picocolors": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz",
"integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA=="
},
"picomatch": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz",
@ -28182,11 +28182,11 @@
"version": "0.1.1"
},
"postcss": {
"version": "7.0.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.38.tgz",
"integrity": "sha512-wNrSHWjHDQJR/IZL5IKGxRtFgrYNaAA/UrkW2WqbtZO6uxSLMxMN+s2iqUMwnAWm3fMROlDYZB41dr0Mt7vBwQ==",
"version": "7.0.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz",
"integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==",
"requires": {
"nanocolors": "^0.2.2",
"picocolors": "^0.2.1",
"source-map": "^0.6.1"
}
},
@ -31844,9 +31844,9 @@
}
},
"typescript": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.3.tgz",
"integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA=="
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.4.4.tgz",
"integrity": "sha512-DqGhF5IKoBl8WNf8C1gu8q0xZSInh9j1kJJMqT3a94w1JzVaBU4EXOSMrz9yDqMT0xt3selp83fuFMQ0uzv6qA=="
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4"

View File

@ -15,18 +15,18 @@
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/react-fontawesome": "^0.1.15",
"@fortawesome/react-fontawesome": "^0.1.16",
"@headlessui/react": "^1.4.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24",
"@types/node": "^14.17.5",
"@types/react": "^17.0.26",
"@types/react": "^17.0.30",
"@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.9",
"@types/react-highlight": "^0.12.4",
"@types/react-router-dom": "^5.3.0",
"@types/react-highlight": "^0.12.5",
"@types/react-router-dom": "^5.3.1",
"@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.5.1",
"ethers": "^5.4.7",
@ -42,7 +42,7 @@
"react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.4",
"serve": "^12.0.1",
"typescript": "^4.4.3",
"typescript": "^4.4.4",
"use-keyboard-shortcut": "^1.0.6",
"web-vitals": "^1.0.1"
},
@ -52,8 +52,8 @@
"test": "craco test",
"eject": "react-scripts eject",
"source-map-explorer": "source-map-explorer build/static/js/*.js",
"assets-start": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/signatures:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/signatures:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-start-with-param-names": "docker run --rm -p 3001:80 --name otterscan-assets -d -v$(pwd)/4bytes/with_parameter_names:/usr/share/nginx/html/signatures/ -v$(pwd)/trustwallet/blockchains/ethereum/assets:/usr/share/nginx/html/assets -v$(pwd)/topic0/with_parameter_names:/usr/share/nginx/html/topic0/ -v$(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf nginx:1.21.1-alpine",
"assets-stop": "docker stop otterscan-assets",
"docker-build": "DOCKER_BUILDKIT=1 docker build -t otterscan -f Dockerfile .",
"docker-start": "docker run --rm -p 5000:80 --name otterscan -d otterscan",
@ -78,8 +78,8 @@
]
},
"devDependencies": {
"autoprefixer": "^9.8.7",
"postcss": "^7.0.38",
"autoprefixer": "^9.8.8",
"postcss": "^7.0.39",
"source-map-explorer": "^2.5.2",
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.6"
}

63
public/sourcify.svg Normal file
View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="15.875mm"
height="15.875mm"
viewBox="0 0 15.875 15.875"
version="1.1"
id="svg990"
inkscape:version="1.1.1 (c3084ef, 2021-09-22)"
sodipodi:docname="sourcify.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview992"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.57905443"
inkscape:cx="384.24712"
inkscape:cy="511.1782"
inkscape:window-width="1440"
inkscape:window-height="872"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs987" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-3.0286537,-13.080962)">
<g
id="g972"
transform="matrix(0.26458333,0,0,0.26458333,3.0286537,13.080962)">
<path
d="M 0,30 C 0,46.5685 13.4315,60 30,60 46.5685,60 60,46.5685 60,30 60,13.4315 46.5685,0 30,0 13.4315,0 0,13.4315 0,30 Z"
fill="#2b50aa"
id="path874" />
<path
d="m 30.0587,59.413 c 16.2119,0 29.3542,-13.1423 29.3542,-29.3542 0,-16.2119 -13.1423,-29.35421 -29.3542,-29.35421 -16.2118,0 -29.354171,13.14231 -29.354171,29.35421 0,16.2119 13.142371,29.3542 29.354171,29.3542 z"
fill="#2b50aa"
id="path876" />
<path
d="m 21.9326,42.1567 c 0.1284,0.1176 0.2889,0.1819 0.4387,0.1819 0.1497,0 0.3209,-0.0643 0.4387,-0.1819 l 1.4765,-1.4123 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3424 -0.2032,-0.46 L 14.0366,30.0343 24.2865,20.2446 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3422 -0.2032,-0.46 L 22.81,17.9121 c -0.1285,-0.1176 -0.2782,-0.1819 -0.4387,-0.1819 -0.1605,0 -0.321,0.0643 -0.4387,0.1819 L 9.71403,29.5743 c -0.1284,0.1178 -0.20329,0.2888 -0.20329,0.46 0,0.1712 0.07489,0.3424 0.20329,0.4602 z M 46.0808,30.0343 35.8309,39.8242 c -0.1283,0.1176 -0.2033,0.2888 -0.2033,0.46 0,0.1712 0.075,0.3424 0.2033,0.4602 l 1.4765,1.4123 c 0.1285,0.1176 0.289,0.1819 0.4387,0.1819 0.1498,0 0.321,-0.0643 0.4387,-0.1819 L 50.4034,30.4945 c 0.1283,-0.1178 0.2032,-0.289 0.2032,-0.4602 0,-0.1712 -0.0749,-0.3422 -0.2032,-0.46 L 38.1848,17.9121 c -0.1284,-0.1176 -0.2889,-0.1819 -0.4387,-0.1819 -0.1605,0 -0.3209,0.0643 -0.4387,0.1819 l -1.4765,1.4123 c -0.1283,0.1178 -0.2033,0.2888 -0.2033,0.46 0,0.1712 0.075,0.3424 0.2033,0.4602 z"
fill="#c5d5ea"
id="path878" />
<path
d="m 21.8471,28.8355 c -0.0107,0.4172 0.1498,0.8131 0.4493,1.1126 l 0.0107,0.0107 c 0.2888,0.289 0.6634,0.4495 1.0699,0.4495 0.3746,0 0.7276,-0.1391 1.0058,-0.3853 l 4.1619,-3.7019 V 37.673 c 0,0.8346 0.6848,1.5194 1.5194,1.5194 0.8345,0 1.5193,-0.6848 1.5193,-1.5194 V 26.3104 l 4.1619,3.702 c 0.2783,0.2461 0.6421,0.3851 1.0058,0.3851 0.4066,0 0.7918,-0.1604 1.0806,-0.4494 l 0.0107,-0.0107 c 0.2996,-0.2995 0.4495,-0.6847 0.4495,-1.1127 -0.0108,-0.4172 -0.182,-0.8024 -0.4922,-1.0913 L 31.0805,21.56 c -0.2782,-0.2568 -0.6419,-0.3959 -1.0164,-0.3959 -0.3745,0 -0.7383,0.1391 -1.0165,0.3959 l -6.7191,6.1734 c -0.2996,0.2995 -0.4814,0.6848 -0.4814,1.1021 z"
fill="#c5d5ea"
id="path880" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -13,7 +13,6 @@ import queryString from "query-string";
import Blockies from "react-blockies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
@ -240,7 +239,13 @@ const AddressTransactions: React.FC = () => {
</span>
) : (
<span className="self-center text-green-500">
<FontAwesomeIcon icon={faCheckCircle} />
<img
src="/sourcify.svg"
alt="Sourcify logo"
title="Verified by Sourcify"
width={16}
height={16}
/>
</span>
)}
</span>
@ -300,10 +305,6 @@ const AddressTransactions: React.FC = () => {
disabled={controller === undefined}
/>
</div>
<ResultHeader
feeDisplay={feeDisplay}
feeDisplayToggler={feeDisplayToggler}
/>
</SelectionContext.Provider>
) : (
<PendingResults />

View File

@ -45,8 +45,8 @@ const Block: React.FC = () => {
try {
return block && toUtf8String(block.extraData);
} catch (err) {
console.error("Error while converting block extra data to string");
console.error(err);
console.info("Error while converting block extra data to string");
console.info(err);
}
}, [block]);
const burntFees =
@ -160,7 +160,7 @@ const Block: React.FC = () => {
</InfoRow>
<InfoRow title="Extra Data">
{extraStr} (Hex:{" "}
<span className="font-data">{block.extraData}</span>)
<span className="font-data break-all">{block.extraData}</span>)
</InfoRow>
<InfoRow title="Ether Price">
<USDValue value={blockETHUSDPrice} />

View File

@ -76,7 +76,7 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
<TransactionLink txHash={tx.hash} />
</span>
</div>
<MethodName data={tx.data} />
{tx.to !== null ? <MethodName data={tx.data} /> : <span></span>}
<span>
<BlockLink blockTag={tx.blockNumber} />
</span>

View File

@ -72,10 +72,13 @@ const Details: React.FC<DetailsProps> = ({
}
}, [txData]);
const fourBytes = rawInputTo4Bytes(txData.data);
const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x";
const fourBytesEntry = use4Bytes(fourBytes);
const fourBytesTxDesc = useMemo(() => {
if (!txData || !fourBytesEntry?.signature) {
if (!fourBytesEntry) {
return fourBytesEntry;
}
if (!txData || !fourBytesEntry.signature) {
return undefined;
}
const sig = fourBytesEntry?.signature;
@ -202,9 +205,11 @@ const Details: React.FC<DetailsProps> = ({
</div>
)}
</InfoRow>
<InfoRow title="Transaction Action">
<MethodName data={txData.data} />
</InfoRow>
{txData.to && (
<InfoRow title="Transaction Action">
<MethodName data={txData.data} />
</InfoRow>
)}
{txData.tokenTransfers.length > 0 && (
<InfoRow title={`Tokens Transferred (${txData.tokenTransfers.length})`}>
<div>
@ -353,7 +358,7 @@ const Details: React.FC<DetailsProps> = ({
) : resolvedTxDesc === undefined ? (
<>Waiting for data...</>
) : resolvedTxDesc === null ? (
<>No decoded data</>
<>Can't decode data</>
) : (
<DecodedParamsTable
args={resolvedTxDesc.args}

View File

@ -1,6 +1,6 @@
import React, { Fragment } from "react";
import React, { useMemo } from "react";
import { Log } from "@ethersproject/abstract-provider";
import { LogDescription } from "@ethersproject/abi";
import { Fragment, Interface, LogDescription } from "@ethersproject/abi";
import { Tab } from "@headlessui/react";
import AddressHighlighter from "../components/AddressHighlighter";
import DecoratedAddressLink from "../components/DecoratedAddressLink";
@ -9,6 +9,7 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { TransactionData } from "../types";
import { useTopic0 } from "../useTopic0";
type LogEntryProps = {
txData: TransactionData;
@ -16,101 +17,132 @@ type LogEntryProps = {
logDesc: LogDescription | null | undefined;
};
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => (
<div className="flex space-x-10 py-5">
<div>
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
{log.logIndex}
</span>
</div>
<div className="w-full space-y-2">
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="font-bold text-right">Address</div>
<div className="col-span-11 mr-auto">
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={log.address}>
<DecoratedAddressLink
address={log.address}
miner={log.address === txData.confirmedData?.miner}
txFrom={log.address === txData.from}
txTo={log.address === txData.to}
/>
</AddressHighlighter>
<Copy value={log.address} />
const LogEntry: React.FC<LogEntryProps> = ({ txData, log, logDesc }) => {
const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0);
const topic0LogDesc = useMemo(() => {
if (!topic0) {
return topic0;
}
if (!topic0.signatures) {
return undefined;
}
const sigs = topic0.signatures;
for (const sig of sigs) {
const logFragment = Fragment.fromString(`event ${sig}`);
const intf = new Interface([logFragment]);
try {
return intf.parseLog(log);
} catch (err) {
// Ignore on purpose; try to match other sigs
}
}
return undefined;
}, [topic0, log]);
const resolvedLogDesc = logDesc ?? topic0LogDesc;
return (
<div className="flex space-x-10 py-5">
<div>
<span className="rounded-full w-12 h-12 flex items-center justify-center bg-green-50 text-green-500">
{log.logIndex}
</span>
</div>
<div className="w-full space-y-2">
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="font-bold text-right">Address</div>
<div className="col-span-11 mr-auto">
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
<AddressHighlighter address={log.address}>
<DecoratedAddressLink
address={log.address}
miner={log.address === txData.confirmedData?.miner}
txFrom={log.address === txData.from}
txTo={log.address === txData.to}
/>
</AddressHighlighter>
<Copy value={log.address} />
</div>
</div>
</div>
</div>
<Tab.Group>
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right">Parameters</div>
<div className="col-span-11 flex space-x-1 mb-1">
<ModeTab>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
</div>
</Tab.List>
<Tab.Panels as={Fragment}>
<Tab.Panel className="space-y-2">
{logDesc === undefined ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11">
Waiting for data...
</div>
</div>
) : logDesc === null ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11">
No decoded data
</div>
</div>
) : (
<>
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono">
<DecodedLogSignature event={logDesc.eventFragment} />
</div>
</div>
<Tab.Group>
<Tab.List className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right">Parameters</div>
<div className="col-span-11 flex space-x-1 mb-1">
<ModeTab>Decoded</ModeTab>
<ModeTab>Raw</ModeTab>
</div>
</Tab.List>
<Tab.Panels as={React.Fragment}>
<Tab.Panel className="space-y-2">
{resolvedLogDesc === undefined ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11">
<DecodedParamsTable
args={logDesc.args}
paramTypes={logDesc.eventFragment.inputs}
txData={txData}
/>
Waiting for data...
</div>
</div>
</>
)}
</Tab.Panel>
<Tab.Panel className="space-y-2">
{log.topics.map((t, i) => (
<div
className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"
key={i}
>
<div className="text-right">{i === 0 && "Topics"}</div>
<div className="flex space-x-2 items-center col-span-11 font-mono">
<span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs">
{i}
</span>
<span>{t}</span>
) : resolvedLogDesc === null ? (
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11">
Can't decode data
</div>
</div>
) : (
<>
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11 font-mono">
<DecodedLogSignature
event={resolvedLogDesc.eventFragment}
/>
</div>
</div>
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="col-start-2 flex space-x-2 items-center col-span-11">
<DecodedParamsTable
args={resolvedLogDesc.args}
paramTypes={resolvedLogDesc.eventFragment.inputs}
txData={txData}
hasParamNames={resolvedLogDesc === logDesc}
/>
</div>
</div>
</>
)}
</Tab.Panel>
<Tab.Panel className="space-y-2">
{log.topics.map((t, i) => (
<div
className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm"
key={i}
>
<div className="text-right">{i === 0 && "Topics"}</div>
<div className="flex space-x-2 items-center col-span-11 font-mono">
<span className="rounded bg-gray-100 text-gray-500 px-2 py-1 text-xs">
{i}
</span>
<span>{t}</span>
</div>
</div>
))}
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right pt-2">Data</div>
<div className="col-span-11">
<textarea
className="w-full h-40 bg-gray-50 font-mono focus:outline-none border rounded p-2"
value={log.data}
readOnly
/>
</div>
</div>
))}
<div className="grid grid-cols-12 gap-x-3 gap-y-5 text-sm">
<div className="text-right pt-2">Data</div>
<div className="col-span-11">
<textarea
className="w-full h-40 bg-gray-50 font-mono focus:outline-none border rounded p-2"
value={log.data}
readOnly
/>
</div>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div>
</div>
</div>
);
);
};
export default React.memo(LogEntry);

View File

@ -5,6 +5,9 @@ export const fourBytesURL = (
fourBytes: string
): string => `${assetsURLPrefix}/signatures/${fourBytes}`;
export const topic0URL = (assetsURLPrefix: string, topic0: string): string =>
`${assetsURLPrefix}/topic0/${topic0}`;
export const tokenLogoURL = (
assetsURLPrefix: string,
address: string
@ -27,13 +30,13 @@ export enum SourcifySource {
const sourcifyIPNS =
"k51qzi5uqu5dll0ocge71eudqnrgnogmbr37gsgl12uubsinphjoknl6bbi41p";
const ipfsGatewayPrefix = `http://localhost:8080/ipns/${sourcifyIPNS}`;
const defaultIpfsGatewayPrefix = `https://ipfs.io/ipns/${sourcifyIPNS}`;
const sourcifyHttpRepoPrefix = `https://repo.sourcify.dev`;
const snapshotPrefix = "http://localhost:3006";
const resolveSourcifySource = (source: SourcifySource) => {
if (source === SourcifySource.IPFS_IPNS) {
return ipfsGatewayPrefix;
return defaultIpfsGatewayPrefix;
}
if (source === SourcifySource.CENTRAL_SERVER) {
return sourcifyHttpRepoPrefix;

77
src/useTopic0.ts Normal file
View File

@ -0,0 +1,77 @@
import { useState, useEffect, useContext } from "react";
import { RuntimeContext } from "./useRuntime";
import { topic0URL } from "./url";
export type Topic0Entry = {
signatures: string[] | undefined;
};
const fullCache = new Map<string, Topic0Entry | null>();
/**
* Extract topic0 DB info
*
* @param rawTopic0 an hex string containing the keccak256 of event signature
*/
export const useTopic0 = (
rawTopic0: string
): Topic0Entry | null | undefined => {
if (rawTopic0.length !== 66 || !rawTopic0.startsWith("0x")) {
throw new Error(
`rawTopic0 must contain a 32 bytes hex event signature starting with 0x; received value: "${rawTopic0}"`
);
}
const runtime = useContext(RuntimeContext);
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
const topic0 = rawTopic0.slice(2);
const [entry, setEntry] = useState<Topic0Entry | null | undefined>(
fullCache.get(topic0)
);
useEffect(() => {
if (assetsURLPrefix === undefined) {
return;
}
const signatureURL = topic0URL(assetsURLPrefix, topic0);
fetch(signatureURL)
.then(async (res) => {
if (!res.ok) {
console.error(`Signature does not exist in topic0 DB: ${topic0}`);
fullCache.set(topic0, null);
setEntry(null);
return;
}
// Get only the first occurrence, for now ignore alternative param names
const sig = await res.text();
const sigs = sig.split(";");
const entry: Topic0Entry = {
signatures: sigs,
};
setEntry(entry);
fullCache.set(topic0, entry);
})
.catch((err) => {
console.error(`Couldn't fetch signature URL ${signatureURL}`, err);
setEntry(null);
fullCache.set(topic0, null);
});
}, [topic0, assetsURLPrefix]);
if (assetsURLPrefix === undefined) {
return undefined;
}
// Try to resolve topic0 name
if (entry === null || entry === undefined) {
return entry;
}
// Simulates LRU
// TODO: implement LRU purging
fullCache.delete(topic0);
fullCache.set(topic0, entry);
return entry;
};

1
topic0 Submodule

@ -0,0 +1 @@
Subproject commit 52559d5690d491f8191a2d3fdb3c037516adc68f

@ -1 +1 @@
Subproject commit d612796060acaa8a621b1dd25c1a41a24452953e
Subproject commit e439c36937deb321ad01c23ac23941e8a491efe9