Merge branch 'release/v2022.01.03-otterscan'

This commit is contained in:
Willian Mitsuda 2022-02-03 16:09:00 -03:00
commit 35fb50df92
51 changed files with 1534 additions and 977 deletions

3
.gitmodules vendored
View File

@ -1,9 +1,12 @@
[submodule "4bytes"] [submodule "4bytes"]
path = 4bytes path = 4bytes
url = https://github.com/ethereum-lists/4bytes.git url = https://github.com/ethereum-lists/4bytes.git
ignore = dirty
[submodule "trustwallet"] [submodule "trustwallet"]
path = trustwallet path = trustwallet
url = https://github.com/trustwallet/assets.git url = https://github.com/trustwallet/assets.git
ignore = dirty
[submodule "topic0"] [submodule "topic0"]
path = topic0 path = topic0
url = https://github.com/wmitsuda/topic0.git url = https://github.com/wmitsuda/topic0.git
ignore = dirty

2
4bytes

@ -1 +1 @@
Subproject commit 20537524bfb01bee859c9cfa9a8784baacbcc7ae Subproject commit 0ee722e516c91dc6a3de01c26ea06955123eeddb

View File

@ -56,93 +56,15 @@ However, you will see that we made many UI improvements.
## Install instructions ## Install instructions
This software is currently distributed as a docker image. [Here](./docs/install.md).
It depends heavily on a working Erigon installation with Otterscan patches applied, so let's begin with it first. ## Contract verification
### Install Erigon We make use of [Sourcify](https://sourcify.dev/) for displaying contract verification info. More info [here](docs/sourcify.md).
You will need an Erigon executing node (`erigon`). Also you will need Erigon RPC daemon (`rpcdaemon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing. ## Otterscan JSON-RPC API extensions
My personal experience: at the moment of this writing (~block 12,700,000), setting up an archive node takes over 5-6 days and ~1.3 TB of SSD. We implemented new JSON-RPC APIs to expose information not available in a standard ETH node. They can be useful for non-Otterscan users and their specification is available [here](./docs/custom-jsonrpc.md).
They have weekly stable releases, make sure you are running on of them, not development ones.
### Install Otterscan patches on top of Erigon
Add our forked Erigon git tree as an additional remote and checkout the corresponding branch.
The repository with Otterscan patches is [here](https://github.com/wmitsuda/erigon).
```
git remote add otterscan https://github.com/wmitsuda/erigon.git
```
Checkout the tag corresponding to the stable version you are running. For each supported Erigon version, there should be a corresponding tag containing Otterscan patches.
For example, if you are running Erigon from `v2021.07.01` tag, checkout the tag `v2021.07.01-otterscan` and rebuild `rpcdaemon`.
We intend to release a compatible rebased version containing our changes every week just after Erigon's weekly release, as time permits.
```
git fetch --all
git fetch otterscan --tags
git checkout <version-tag-otterscan>
```
Build the patched `rpcdaemon` binary.
```
make rpcdaemon
```
Run it paying attention to enable the `erigon`, `ots`, `eth` apis to whatever cli options you are using to start `rpcdaemon`.
`ots` stands for Otterscan and it is the namespace we use for our own custom APIs.
```
./build/bin/rpcdaemon --http.api "eth,erigon,ots,<your-other-apis>" --private.api.addr 127.0.0.1:9090 --datadir <erigon-datadir> --http.corsdomain "*"
```
Be sure to include both `--private.api.addr` and `--datadir` parameter so you run it in dual mode, otherwise the performance will be much worse.
Also pay attention to the `--http.corsdomain` parameter, CORS is required for the browser to call the node directly.
Now you should have an Erigon node with Otterscan jsonrpc APIs enabled, running in dual mode with CORS enabled.
### Run Otterscan docker image from Docker Hub
The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories).
```
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag>
```
This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5000 (see the `-p` docker run parameter).
To stop Otterscan service, run:
```
docker stop otterscan
```
By default it assumes your Erigon node is at `http://127.0.0.1:8545`. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`:
```
docker run --rm -p 5000:80 --name otterscan -d --env ERIGON_URL="<your-erigon-node-url>" otterscan/otterscan:<versiontag>
```
This is the preferred way to run Otterscan. You can read about other ways [here](docs/other-ways-to-run-otterscan.md).
## Validating the installation (all methods)
You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button.
## Source verification
We make use of [Sourcify](https://sourcify.dev/) for displaying contract verification info.
More info [here](docs/sourcify.md).
## Kudos (in no particular order) ## Kudos (in no particular order)

337
docs/custom-jsonrpc.md Normal file
View File

@ -0,0 +1,337 @@
# Otterscan JSON-RPC API extensions
The [standard Ethereum JSON-RPC APIs](https://ethereum.org/en/developers/docs/apis/json-rpc/) are very limited and in some cases non-performant for what you can do with an archive node.
There is plenty of useful data that can be extracted and we implemented some extra RPC methods for them.
They are all used by Otterscan, but we are documenting them here so others can try it, give feedback and eventually get it merged upstream if they are generalized enough.
We take an incremental approach when design the APIs, so there may be some methods very specific to Otterscan use cases, others that look more generic.
Please see the [install instructions](./install.md) if you want to run a patched Erigon with those customizations enabled.
## Quick FAQ
### Why don't you use _Some Product XXX_ for Otterscan? And why shouldn't I?
If you are happy using _Some Product XXX_, go ahead.
Otterscan pursues a minimalistic approach and at the same time it is very easy to modify Erigon for your own needs.
Most of the features we implemented are quite basic and it is unfortunate they are not part of the standard API.
> We believe most people end up using _Some Product XXX_ not because of its own unique features, but because the standard JSON-RPC API is quite limited even for basic features.
Implementing everything in-node allows you to plug a dapp directly to your node itself. No need to install any additional indexer middleware or SQL database, each of it own consuming extra disk space and CPU.
> Take Otterscan as an example, **ALL** you need is Otterscan itself (a SPA, can be served by any static provider) and our modified Erigon's rpcdaemon.
### But your API doesn't scale and it is slower than _Some Product XXX_!!!
Not everyone needs to serve thousands of requests per second. Go ahead and use _Some Product XXX_.
Some people just want to run standalone development tools and calculating some data on-the-fly works fine for single user local apps.
Even so, we may introduce custom indexes to speed up some operations in future if there is such demand, so you may opt-in for a better performance by spending more disk space.
### Wen PR upstream?
API design is hard and once it goes public you have to support it forever. For this reason we are primarily keeping it in our own fork and under a vendor specific namespace (`ots_`).
Also, the quality level of the current APIs differs, some are very generic, some are very Otterscan specific. Our API design has been driven mainly by Otterscan feature needs, which is a good thing (tm), so no useless features.
Having said that, we want to have people experimenting with our APIs, bringing other use cases, and driving the API evolution. If there are enough users vouching for a certain feature, we would gladly submit a PR to Erigon upstream repo.
The first step to achieving that is having this own page properly documenting our APIs so people don't have to look at our source code 😅.
Your feedback is important, please get in touch using our communication channels.
## How to use it?
They are all JSON-RPC methods, so your favorite web3 library _should_ have some way to custom call them.
For example, ethers.js wraps standard calls in nice, user-friendly classes and parses results into easy-to-use objects, but also allows you to do custom calls and get raw results while still taking advantage of their capabilities like automatic batching, network timeout handling, etc.
I'll use ethers.js as an example here because it is what I use in Otterscan, please check your web3 library docs for custom call support.
Let's call the `ots_getTransactionError` method to obtain the revert reason of a failed transaction. It accepts one string parameter containing the transaction hash and returns a byte blob that can be ABI-decoded:
```
const provider = ...; // Obtain a JsonRpcProvider object
const txHash = "..."; // Set the transaction hash
const result = (await provider.send("ots_getTransactionError", [txHash])) as string;
```
## Method summary
All methods are prefixed with the `ots_` namespace in order to make it clear it is vendor-specific and there is no name clash with other same-name implementations.
| Name | Description | Reasoning |
|-------------------|------------------|-----------|
| `ots_getApiLevel` | Totally Otterscan internal API, absolutely no reason for anything outside Otterscan to use it. | Used by Otterscan to check if it's connecting to a compatible patched Erigon node and display a friendly message if it is not. |
| `ots_getInternalOperations` | Return the internal ETH transfers inside a transaction. | For complex contract interactions, there may be internal calls that forward ETH between addresses. A very common example is someone swapping some token for ETH, in this case there is an ETH send to the sender address which is only unveiled by examining the internal calls. |
| `ots_hasCode` | Check if a certain address contains a deployed code. | A common way to check if an address is a contract or an EOA is calling `eth_getCode` to see if it has some code deployed. However this call is expensive regarding this purpose, as it returns the entire contract code over the network just for the client to check its presence. This call just returns a boolean. |
| `ots_getTransactionError` | Extract the transaction raw error output. | In order to get the error message or custom error from a failed transaction, you need to get its error output and decoded it. This info is not exposed through standard APIs. |
| `ots_traceTransaction` | Extract all variations of calls, contract creation and self-destructs and returns a call tree. | This is an optimized version of tracing; regular tracing returns lots of data, and custom tracing using a JS tracer could be slow. |
| `ots_getBlockDetails` | Tailor-made and expanded version of `eth_getBlock*` for block details page in Otterscan. | The standard `eth_getBlock*` is quite verbose and it doesn't bring all info we need. We explicitly remove the transaction list (unnecessary for that page and also this call doesn't scale well), log blooms and other unnecessary fields. We add issuance and block fees info and return all of this in just one call. |
| `ots_getBlockTransactions` | Get paginated transactions for a certain block. Also remove some verbose fields like logs. | As block size increases, getting all transactions from a block at once doesn't scale, so the first point here is to add pagination support. The second point is that receipts may have big, unnecessary information, like logs. So we cap all of them to save network bandwidth. |
| `ots_searchTransactionsBefore` and `ots_searchTransactionsAfter` | Gets paginated inbound/outbound transaction calls for a certain address. | There is no native support for any kind of transaction search in the standard JSON-RPC API. We don't want to introduce an additional indexer middleware in Otterscan, so we implemented in-node search. |
| `ots_getTransactionBySenderAndNonce` | Gets the transaction hash for a certain sender address, given its nonce. | There is no native support for this search in the standard JSON-RPC API. Otterscan needs it to allow user navigation between nonces from the same sender address. |
## Method details
> Some methods include a sample call so you call try it from cli. The examples use `curl` and assume you are running `rpcdaemon` at `http://127.0.0.1:8545`.
### `ots_getApiLevel`
Very simple API versioning scheme. Every time we add a new capability, the number is incremented. This allows for Otterscan to check if the Erigon node contains all API it needs.
Parameters:
`<none>`
Returns:
- `number` containing the API version.
### `ots_getInternalOperations`
Trace internal ETH transfers, contracts creation (CREATE/CREATE2) and self-destructs for a certain transaction.
Parameters:
1. `txhash` - The transaction hash.
Returns:
- `array` of operations, sorted by their occurrence inside the transaction.
The operation is an object with the following fields:
- `type` - transfer (`0`), self-destruct (`1`), create (`2`) or create2 (`3`).
- `from` - the ETH sender, contract creator or contract address being self-destructed.
- `to` - the ETH receiver, newly created contract address or the target ETH receiver resulting of the self-destruction.
- `value` - the amount of ETH transferred.
### `ots_hasCode`
Check if an ETH address contains a deployed code.
Parameters:
1. `address` - The ETH address to be checked.
2. `block` - The block number at which the code presence will be checked or "latest" to check the latest state.
Returns:
- `boolean` indicating if the address contains a bytecode or not.
Example 1: does Uniswap V1 Router address have a code deployed? (yes, it is a contract)
Request:
```
$ curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "id": 1, "method":"ots_hasCode","params":["0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95", "latest"]}' http://127.0.0.1:8545
```
Response:
```
{
"jsonrpc": "2.0",
"id": 1,
"result": true
}
```
Example 2: does Vitalik's public address have a code deployed? (no, it is an EOA)
Request:
```
$ curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "id": 1, "method":"ots_hasCode","params":["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "latest"]}' http://127.0.0.1:8545
```
Response:
```
{
"jsonrpc": "2.0",
"id": 1,
"result": false
}
```
### `ots_traceTransaction`
Trace a transaction and generate a trace call tree.
Parameters:
1. `txhash` - The transaction hash.
Returns:
- `object` containing the trace tree.
### `ots_getTransactionError`
Given a transaction hash, returns its raw revert reason.
The returned byte blob should be ABI decoded in order to be presented to the user.
For instance, the most common error format is a `string` revert message; in this case, it should be decoded using the `Error(string)` method selector, which will allow you to extract the string message.
If it is not the case, it should probably be a solidity custom error, so you must have the custom error ABI in order to decoded it.
Parameters:
1. `txhash` - The transaction hash.
Returns:
- `string` containing the hexadecimal-formatted error blob or simply a "0x" if the transaction was sucessfully executed. It is returns "0x" if it failed with no revert reason or out of gas, make sure to analyze this return value together with the transaction success/fail result.
Example: get the revert reason of a random transaction spotted in the wild to Uniswap V3.
Request:
```
$ curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "id": 1, "method":"ots_getTransactionError","params":["0xcdb0e53c4f1b5f37ea7f0d2a8428b13a5bff47fb457d11ef9bc85ccdc489635b"]}' http://127.0.0.1:8545
```
Response:
```
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000135472616e73616374696f6e20746f6f206f6c6400000000000000000000000000"
}
```
> ABI-decoding this byte string against `Error(string)` should result in the "Transaction too old" error message.
### `ots_getBlockDetails`
Given a block number, return its data. Similar to the standard `eth_getBlockByNumber/Hash` method, but optimized.
Parameters:
1. `number` representing the desired block number.
Returns:
- `object` in a format _similar_ to the one returned by `eth_getBlockByNumber/Hash` (please refer to their docs), with some small differences:
- the block data comes nested inside a `block` attribute.
- the `transactions` attribute is not returned. The reason is that it doesn't scale, the standard methods return either the transaction hash list or the transaction list with their bodies. So we cap the transaction list entirely to avoid unnecessary network traffic.
- the transaction count is returned in a `transactionCount` attribute.
- the `logsBloom` attribute comes with `null`. It is a byte blob thas is rarely used, so we cap it to avoid unnecessary network traffic.
- an extra `issuance` attribute returns an `object` with the fields:
- `blockReward` - the miner reward.
- `uncleReward` - the total reward issued to uncle blocks.
- `issuance` - the total ETH issued in this block (miner + uncle rewards).
- an extra `totalFees` attribute containing the sum of fees paid by senders in this block. Note that due to EIP-1559 this is **NOT** the same amount earned by the miner as block fees since it contains the amount paid as base fee.
### `ots_getBlockTransactions`
Gets paginated transaction data for a certain block. Think of an optimized `eth_getBlockBy*` + `eth_getTransactionReceipt`.
The `transactions` field contains the transaction list with their bodies in a similar format of `eth_getBlockBy*` with transaction bodies, with a few differences:
- the `input` field returns only the 4 bytes method selector instead of the entire calldata byte blob.
The `receipts` attribute contains the transactions receipt list, in the same sort order as the block transactions. Returning it here avoid the caller of making N+1 calls (`eth_getBlockBy*` and `eth_getTransactionReceipt`).
For receipts, it contains some differences from the `eth_getTransactionReceipt` object format:
- `logs` attribute returns `null`.
- `logsBloom` attribute returns `null`.
### `ots_searchTransactionsBefore` and `ots_searchTransactionsAfter`
These are address history navigation methods. They are similar, the difference is `ots_searchTransactionsBefore` searches the history backwards and `ots_searchTransactionsAfter` searches forward a certain point in time.
They are paginated, you **MUST** inform the page size. Some addresses like exchange addresses or very popular DeFi contracts like Uniswap Router will return millions of results.
They return inbound (`to`), outbound (`from`) and "internal" transactions. By internal it means that if a transaction calls a contract and somewhere in the call stack it sends ETH to the address you are searching for or the address is a contract and it calls a method on it, the transaction is matched and returned in the search results.
Parameters:
1. `address` - The ETH address to be searched.
2. `blockNumber` - It searches for occurrences of `address` before/after `blockNumber`. A value of `0` means you want to search from the most recent block (`ots_searchTransactionsBefore`) or from the genesis (`ots_searchTransactionsAfter`).
3. `pageSize` - How many transactions it may return. See the detailed explanation about this parameter bellow.
Returns:
- `object` containing the following attributes:
- `txs` - An array of objects representing the transaction results. The results are returned sorted from the most recent to the older one (descending order).
- `receipts` - An array of objects containing the transaction receipts for the transactions returned in the `txs` attribute.
- `firstPage` - Boolean indicating this is the first page. It should be `true` when calling `ots_searchTransactionsBefore` with `blockNumber` == 0 (search from `latest`); because the results are in descending order, the search from the most recent block is the "first" one. It should also return `true` when calling `ots_searchTransactionsAfter` with a `blockNumber` which results in no more transactions after the returned ones because it searched forward up to the tip of the chain.
- `lastPage` - Boolean indicating this is the last page. It should be `true` when calling `ots_searchTransactionsAfter` with `blockNumber` == 0 (search from genesis); because the results are in descending order, the genesis page is the "last" one. It should also return `true` when calling `ots_searchTransactionsBefore` with a `blockNumber` which results in no more transactions before the returned ones because it searched backwards up to the genesis block.
There is a small gotcha regarding `pageSize`. If there are less results than `pageSize`, they are just returned as is.
But if there are more than `pageSize` results, they are capped by the last found block. For example, let's say you are searching for Uniswap Router address and it already found 24 matches; it then looks at the next block containing this addresses occurrences and there are 5 matches inside the block. They are all returned, so it returns 30 transaction results. The caller code should be aware of this.
Example: get the first 5 transactions that touched Uniswap V1 router (including the contract creation).
Request:
```
$ curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "id": 1, "method":"ots_searchTransactionsAfter","params":["0xc0a47dFe034B400B47bDaD5FecDa2621de6c4d95", 0, 5]}' http://127.0.0.1:8545
```
Response:
```
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"txs": [
{
"blockHash": "0x06a77abe52c486f58696665eaebd707f17fbe97eb54480c6533db725769ce3b7",
"blockNumber": "0x652284",
"from": "0xd1c24f50d05946b3fabefbae3cd0a7e9938c63f2",
"gas": "0xf4240",
"gasPrice": "0x2cb417800",
"hash": "0x14455f1af43a52112d4ccf6043cb081fea4ea3a07d90dd57f2a9e1278114be94",
"input": "0x1648f38e000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498",
"nonce": "0x6",
"to": "0xc0a47dfe034b400b47bdad5fecda2621de6c4d95",
"transactionIndex": "0x71",
...
}
```
### `ots_getTransactionBySenderAndNonce`
Given a sender address and a nonce, returns the tx hash or `null` if not found. It returns only the tx hash on success, you can use the standard `eth_getTransactionByHash` after that to get the full transaction data.
Parameters:
1. `sender` - The sender ETH address.
2. `nonce` - The sender nonce.
Returns:
- `string` containing the corresponding transaction hash or `null` if it doesn't exist.
Example: get the 4th transaction sent by Vitalik's public address (nonce == 3).
Request:
```
$ curl -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0", "id": 1, "method":"ots_getTransactionBySenderAndNonce","params":["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", 3]}' http://127.0.0.1:8545
```
Response:
```
{
"jsonrpc": "2.0",
"id": 1,
"result": "0x021304206b2517c3f8f2df07014a55b79aac2ae097488fa807cc88eccd851a50"
}
```

63
docs/install.md Normal file
View File

@ -0,0 +1,63 @@
# Install instructions
This software is currently distributed as a docker image.
It depends heavily on a working Erigon installation with Otterscan patches applied, so let's begin with it first.
## Install Erigon
You will need an Erigon executing node (`erigon`). Also you will need Erigon RPC daemon (`rpcdaemon`) with Otterscan patches. Since setting up an Erigon environment itself can take some work, make sure to follow their instructions and have a working archive node before continuing.
My personal experience: at the moment of this writing (~block 14,000,000), setting up an archive node takes over 5-6 days and ~1.7 TB of SSD.
They have weekly stable releases, make sure you are running on of them, not development ones.
## Install Otterscan-patched rpcdaemon
We rely on custom JSON-RPC APIs which are not available in a standard ETH node. We keep a separated repository containing an Erigon fork here: https://github.com/wmitsuda/erigon.
Please follow the instructions in the repository `README` and replace the original Erigon `rpcdaemon` with our patched one.
## Enable Otterscan namespace on rpcdaemon
When running `rpcdaemon`, make sure to enable the `erigon`, `ots`, `eth` APIs in addition to whatever cli options you are using to start `rpcdaemon`.
`ots` stands for Otterscan and it is the namespace we use for our own custom APIs.
```
<path-to-rpcdaemon-binary>/rpcdaemon --http.api "eth,erigon,ots,<your-other-apis>" --private.api.addr 127.0.0.1:9090 --datadir <erigon-datadir> --http.corsdomain "*"
```
Be sure to include both `--private.api.addr` and `--datadir` parameter so you run it in dual mode, otherwise the performance will be much worse.
Also pay attention to the `--http.corsdomain` parameter, CORS is **required** for the browser to call the node directly.
Now you should have an Erigon node with Otterscan JSON-RPC APIs enabled, running in dual mode with CORS enabled.
## Run Otterscan docker image from Docker Hub
The Otterscan official repo on Docker Hub is [here](https://hub.docker.com/orgs/otterscan/repositories).
```
docker run --rm -p 5000:80 --name otterscan -d otterscan/otterscan:<versiontag>
```
This will download the Otterscan image from Docker Hub, run it locally using the default parameters, binding it to port 5000 (see the `-p` docker run parameter).
To stop Otterscan service, run:
```
docker stop otterscan
```
By default it assumes your Erigon node is at `http://127.0.0.1:8545`. You can override the URL by setting the `ERIGON_URL` env variable on `docker run`:
```
docker run --rm -p 5000:80 --name otterscan -d --env ERIGON_URL="<your-erigon-node-url>" otterscan/otterscan:<versiontag>
```
This is the preferred way to run Otterscan. You can read about other ways [here](./other-ways-to-run-otterscan.md).
## Validating the installation (all methods)
You can make sure it is working correctly if the homepage is able to show the latest block/timestamp your Erigon node is at just bellow the search button.

390
package-lock.json generated
View File

@ -11,7 +11,7 @@
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0", "@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.2", "@chainlink/contracts": "^0.2.2",
"@craco/craco": "^6.4.2", "@craco/craco": "^6.4.3",
"@fontsource/fira-code": "^4.5.2", "@fontsource/fira-code": "^4.5.2",
"@fontsource/roboto": "^4.5.1", "@fontsource/roboto": "^4.5.1",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
@ -26,15 +26,15 @@
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^16.11.14",
"@types/react": "^17.0.37", "@types/react": "^17.0.38",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-highlight": "^0.12.5", "@types/react-highlight": "^0.12.5",
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.6.2", "chart.js": "^3.7.0",
"ethers": "^5.5.2", "ethers": "^5.5.2",
"highlightjs-solidity": "^2.0.2", "highlightjs-solidity": "^2.0.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",
"react-chartjs-2": "^4.0.0", "react-chartjs-2": "^4.0.0",
@ -42,12 +42,13 @@
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.1.2", "react-helmet-async": "^1.1.2",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^6.0.2", "react-router-dom": "^6.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.5", "react-syntax-highlighter": "^15.4.5",
"serve": "^13.0.2", "serve": "^13.0.2",
"typescript": "^4.5.2", "swr": "^1.1.2",
"use-keyboard-shortcut": "^1.0.6", "typescript": "^4.5.4",
"use-keyboard-shortcut": "^1.1.2",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"devDependencies": { "devDependencies": {
@ -1247,11 +1248,12 @@
} }
}, },
"node_modules/@craco/craco": { "node_modules/@craco/craco": {
"version": "6.4.2", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz", "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.3.tgz",
"integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==", "integrity": "sha512-RzkXYmNzRCGUyG7mM+IUMM+nvrpSfA34352sPSGQN76UivAmCAht3sI4v5JKgzO05oUK9Zwi6abCKD7iKXI8hQ==",
"dependencies": { "dependencies": {
"cosmiconfig": "^7.0.1", "cosmiconfig": "^7.0.1",
"cosmiconfig-typescript-loader": "^1.0.0",
"cross-spawn": "^7.0.0", "cross-spawn": "^7.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"semver": "^7.3.2", "semver": "^7.3.2",
@ -1267,6 +1269,30 @@
"react-scripts": "^4.0.0" "react-scripts": "^4.0.0"
} }
}, },
"node_modules/@craco/craco/node_modules/acorn": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@craco/craco/node_modules/acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/@craco/craco/node_modules/arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"node_modules/@craco/craco/node_modules/cosmiconfig": { "node_modules/@craco/craco/node_modules/cosmiconfig": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
@ -1282,6 +1308,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@craco/craco/node_modules/cosmiconfig-typescript-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.0.tgz",
"integrity": "sha512-Ky5EjOcer3sKf+lWRPC1pM8pca6OtxFi07Xaf5rS0G4NP4pf873W32lq/M0Idm2+DSx0NCZv6h0X9yWguyCE8Q==",
"dependencies": {
"cosmiconfig": "^7",
"ts-node": "^10.4.0"
},
"engines": {
"node": ">=12",
"npm": ">=7"
},
"peerDependencies": {
"@types/node": "*",
"cosmiconfig": ">=7",
"typescript": ">=3"
}
},
"node_modules/@craco/craco/node_modules/semver": { "node_modules/@craco/craco/node_modules/semver": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -1296,6 +1340,65 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@craco/craco/node_modules/ts-node": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
"integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==",
"dependencies": {
"@cspotcode/source-map-support": "0.7.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"yn": "3.1.1"
},
"bin": {
"ts-node": "dist/bin.js",
"ts-node-cwd": "dist/bin-cwd.js",
"ts-node-script": "dist/bin-script.js",
"ts-node-transpile-only": "dist/bin-transpile.js",
"ts-script": "dist/bin-script-deprecated.js"
},
"peerDependencies": {
"@swc/core": ">=1.2.50",
"@swc/wasm": ">=1.2.50",
"@types/node": "*",
"typescript": ">=2.7"
},
"peerDependenciesMeta": {
"@swc/core": {
"optional": true
},
"@swc/wasm": {
"optional": true
}
}
},
"node_modules/@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg==",
"engines": {
"node": ">= 12"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"dependencies": {
"@cspotcode/source-map-consumer": "0.8.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@csstools/convert-colors": { "node_modules/@csstools/convert-colors": {
"version": "1.4.0", "version": "1.4.0",
"license": "CC0-1.0", "license": "CC0-1.0",
@ -2965,6 +3068,26 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
},
"node_modules/@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
},
"node_modules/@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
},
"node_modules/@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
},
"node_modules/@types/anymatch": { "node_modules/@types/anymatch": {
"version": "1.3.1", "version": "1.3.1",
"license": "MIT" "license": "MIT"
@ -3085,9 +3208,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "14.17.5", "version": "16.11.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.14.tgz",
"integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" "integrity": "sha512-mK6BKLpL0bG6v2CxHbm0ed6RcZrAtTHBTd/ZpnlVPVa3HkumsqLE4BC4u6TQ8D7pnrRbOU0am6epuALs+Ncnzw=="
}, },
"node_modules/@types/normalize-package-data": { "node_modules/@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
@ -3110,9 +3233,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "17.0.37", "version": "17.0.38",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -5566,9 +5689,9 @@
} }
}, },
"node_modules/chart.js": { "node_modules/chart.js": {
"version": "3.6.2", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg==" "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
}, },
"node_modules/check-types": { "node_modules/check-types": {
"version": "11.1.2", "version": "11.1.2",
@ -6171,9 +6294,7 @@
"node_modules/create-require": { "node_modules/create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
"optional": true,
"peer": true
}, },
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
@ -6911,8 +7032,6 @@
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=0.3.1" "node": ">=0.3.1"
} }
@ -9243,14 +9362,14 @@
} }
}, },
"node_modules/highlightjs-solidity": { "node_modules/highlightjs-solidity": {
"version": "2.0.2", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.2.tgz", "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.3.tgz",
"integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ==" "integrity": "sha512-tjFm5dtIE61VQBzjlZmkCtY5fLs3CaEABbVuUNyXeW+UuOCsxMg3MsPFy0kCelHP74hPpkoqDejLrbnV1axAIw=="
}, },
"node_modules/history": { "node_modules/history": {
"version": "5.1.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==", "integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.7.6" "@babel/runtime": "^7.7.6"
} }
@ -11430,9 +11549,7 @@
"node_modules/make-error": { "node_modules/make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
"optional": true,
"peer": true
}, },
"node_modules/makeerror": { "node_modules/makeerror": {
"version": "1.0.11", "version": "1.0.11",
@ -14503,23 +14620,23 @@
} }
}, },
"node_modules/react-router": { "node_modules/react-router": {
"version": "6.0.2", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==", "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"dependencies": { "dependencies": {
"history": "^5.1.0" "history": "^5.2.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8" "react": ">=16.8"
} }
}, },
"node_modules/react-router-dom": { "node_modules/react-router-dom": {
"version": "6.0.2", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==", "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"dependencies": { "dependencies": {
"history": "^5.1.0", "history": "^5.2.0",
"react-router": "6.0.2" "react-router": "6.2.1"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">=16.8", "react": ">=16.8",
@ -17089,6 +17206,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/swr": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/swr/-/swr-1.1.2.tgz",
"integrity": "sha512-UsM0eo5T+kRPyWFZtWRx2XR5qzohs/LS4lDC0GCyLpCYFmsfTk28UCVDbOE9+KtoXY4FnwHYiF+ZYEU3hnJ1lQ==",
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/symbol-tree": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"license": "MIT" "license": "MIT"
@ -18005,9 +18130,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.5.2", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@ -18248,9 +18373,9 @@
} }
}, },
"node_modules/use-keyboard-shortcut": { "node_modules/use-keyboard-shortcut": {
"version": "1.0.6", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.0.6.tgz", "resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.1.2.tgz",
"integrity": "sha512-xdH5+XZ6fpey4Cvuh/T0q0tBYu9RGlhg1Xf2W6THtGi5CnI/M90kVaCpfy/Ew8ZDyJnUEwSh4gYolVI2QtgciQ==", "integrity": "sha512-VrAu1avPLuFHShGo1RiPtCZ6htwsnLRlZc/w4+jmK99HCvIGG5WThz1KsoGgP8KCxP5c8a+pDjtnCgBg+3bXzA==",
"peerDependencies": { "peerDependencies": {
"react": "^16.8.0", "react": "^16.8.0",
"react-dom": "^16.8.0" "react-dom": "^16.8.0"
@ -19449,8 +19574,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -20296,17 +20419,33 @@
} }
}, },
"@craco/craco": { "@craco/craco": {
"version": "6.4.2", "version": "6.4.3",
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz", "resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.3.tgz",
"integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==", "integrity": "sha512-RzkXYmNzRCGUyG7mM+IUMM+nvrpSfA34352sPSGQN76UivAmCAht3sI4v5JKgzO05oUK9Zwi6abCKD7iKXI8hQ==",
"requires": { "requires": {
"cosmiconfig": "^7.0.1", "cosmiconfig": "^7.0.1",
"cosmiconfig-typescript-loader": "^1.0.0",
"cross-spawn": "^7.0.0", "cross-spawn": "^7.0.0",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"semver": "^7.3.2", "semver": "^7.3.2",
"webpack-merge": "^4.2.2" "webpack-merge": "^4.2.2"
}, },
"dependencies": { "dependencies": {
"acorn": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw=="
},
"acorn-walk": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz",
"integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA=="
},
"arg": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA=="
},
"cosmiconfig": { "cosmiconfig": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
@ -20319,6 +20458,15 @@
"yaml": "^1.10.0" "yaml": "^1.10.0"
} }
}, },
"cosmiconfig-typescript-loader": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-1.0.0.tgz",
"integrity": "sha512-Ky5EjOcer3sKf+lWRPC1pM8pca6OtxFi07Xaf5rS0G4NP4pf873W32lq/M0Idm2+DSx0NCZv6h0X9yWguyCE8Q==",
"requires": {
"cosmiconfig": "^7",
"ts-node": "^10.4.0"
}
},
"semver": { "semver": {
"version": "7.3.5", "version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -20326,9 +20474,41 @@
"requires": { "requires": {
"lru-cache": "^6.0.0" "lru-cache": "^6.0.0"
} }
},
"ts-node": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
"integrity": "sha512-g0FlPvvCXSIO1JDF6S232P5jPYqBkRL9qly81ZgAOSU7rwI0stphCgd2kLiCrU9DjQCrJMWEqcNSjQL02s6d8A==",
"requires": {
"@cspotcode/source-map-support": "0.7.0",
"@tsconfig/node10": "^1.0.7",
"@tsconfig/node12": "^1.0.7",
"@tsconfig/node14": "^1.0.0",
"@tsconfig/node16": "^1.0.2",
"acorn": "^8.4.1",
"acorn-walk": "^8.1.1",
"arg": "^4.1.0",
"create-require": "^1.1.0",
"diff": "^4.0.1",
"make-error": "^1.1.1",
"yn": "3.1.1"
}
} }
} }
}, },
"@cspotcode/source-map-consumer": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz",
"integrity": "sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg=="
},
"@cspotcode/source-map-support": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz",
"integrity": "sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA==",
"requires": {
"@cspotcode/source-map-consumer": "0.8.0"
}
},
"@csstools/convert-colors": { "@csstools/convert-colors": {
"version": "1.4.0" "version": "1.4.0"
}, },
@ -21320,6 +21500,26 @@
} }
} }
}, },
"@tsconfig/node10": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz",
"integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg=="
},
"@tsconfig/node12": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz",
"integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw=="
},
"@tsconfig/node14": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz",
"integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg=="
},
"@tsconfig/node16": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz",
"integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA=="
},
"@types/anymatch": { "@types/anymatch": {
"version": "1.3.1" "version": "1.3.1"
}, },
@ -21423,9 +21623,9 @@
"version": "3.0.3" "version": "3.0.3"
}, },
"@types/node": { "@types/node": {
"version": "14.17.5", "version": "16.11.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.14.tgz",
"integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA==" "integrity": "sha512-mK6BKLpL0bG6v2CxHbm0ed6RcZrAtTHBTd/ZpnlVPVa3HkumsqLE4BC4u6TQ8D7pnrRbOU0am6epuALs+Ncnzw=="
}, },
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0" "version": "2.4.0"
@ -21443,9 +21643,9 @@
"version": "1.5.4" "version": "1.5.4"
}, },
"@types/react": { "@types/react": {
"version": "17.0.37", "version": "17.0.38",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==", "integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"@types/scheduler": "*", "@types/scheduler": "*",
@ -23148,9 +23348,9 @@
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==" "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
}, },
"chart.js": { "chart.js": {
"version": "3.6.2", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg==" "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
}, },
"check-types": { "check-types": {
"version": "11.1.2" "version": "11.1.2"
@ -23580,9 +23780,7 @@
"create-require": { "create-require": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
"optional": true,
"peer": true
}, },
"cross-spawn": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
@ -24066,9 +24264,7 @@
"diff": { "diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
"optional": true,
"peer": true
}, },
"diff-sequences": { "diff-sequences": {
"version": "26.6.2" "version": "26.6.2"
@ -25601,14 +25797,14 @@
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==" "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
}, },
"highlightjs-solidity": { "highlightjs-solidity": {
"version": "2.0.2", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.2.tgz", "resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.3.tgz",
"integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ==" "integrity": "sha512-tjFm5dtIE61VQBzjlZmkCtY5fLs3CaEABbVuUNyXeW+UuOCsxMg3MsPFy0kCelHP74hPpkoqDejLrbnV1axAIw=="
}, },
"history": { "history": {
"version": "5.1.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz", "resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
"integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==", "integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
"requires": { "requires": {
"@babel/runtime": "^7.7.6" "@babel/runtime": "^7.7.6"
} }
@ -27026,9 +27222,7 @@
"make-error": { "make-error": {
"version": "1.3.6", "version": "1.3.6",
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
"optional": true,
"peer": true
}, },
"makeerror": { "makeerror": {
"version": "1.0.11", "version": "1.0.11",
@ -29093,20 +29287,20 @@
"version": "0.8.3" "version": "0.8.3"
}, },
"react-router": { "react-router": {
"version": "6.0.2", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==", "integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
"requires": { "requires": {
"history": "^5.1.0" "history": "^5.2.0"
} }
}, },
"react-router-dom": { "react-router-dom": {
"version": "6.0.2", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz", "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==", "integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
"requires": { "requires": {
"history": "^5.1.0", "history": "^5.2.0",
"react-router": "6.0.2" "react-router": "6.2.1"
} }
}, },
"react-scripts": { "react-scripts": {
@ -30884,6 +31078,12 @@
} }
} }
}, },
"swr": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/swr/-/swr-1.1.2.tgz",
"integrity": "sha512-UsM0eo5T+kRPyWFZtWRx2XR5qzohs/LS4lDC0GCyLpCYFmsfTk28UCVDbOE9+KtoXY4FnwHYiF+ZYEU3hnJ1lQ==",
"requires": {}
},
"symbol-tree": { "symbol-tree": {
"version": "3.2.4" "version": "3.2.4"
}, },
@ -31512,9 +31712,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.5.2", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==" "integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
}, },
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4" "version": "1.0.4"
@ -31664,9 +31864,9 @@
"version": "3.1.1" "version": "3.1.1"
}, },
"use-keyboard-shortcut": { "use-keyboard-shortcut": {
"version": "1.0.6", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.0.6.tgz", "resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.1.2.tgz",
"integrity": "sha512-xdH5+XZ6fpey4Cvuh/T0q0tBYu9RGlhg1Xf2W6THtGi5CnI/M90kVaCpfy/Ew8ZDyJnUEwSh4gYolVI2QtgciQ==", "integrity": "sha512-VrAu1avPLuFHShGo1RiPtCZ6htwsnLRlZc/w4+jmK99HCvIGG5WThz1KsoGgP8KCxP5c8a+pDjtnCgBg+3bXzA==",
"requires": {} "requires": {}
}, },
"util": { "util": {
@ -32513,9 +32713,7 @@
"yn": { "yn": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
"optional": true,
"peer": true
}, },
"yocto-queue": { "yocto-queue": {
"version": "0.1.0" "version": "0.1.0"

View File

@ -6,7 +6,7 @@
"dependencies": { "dependencies": {
"@blackbox-vision/react-qr-reader": "^5.0.0", "@blackbox-vision/react-qr-reader": "^5.0.0",
"@chainlink/contracts": "^0.2.2", "@chainlink/contracts": "^0.2.2",
"@craco/craco": "^6.4.2", "@craco/craco": "^6.4.3",
"@fontsource/fira-code": "^4.5.2", "@fontsource/fira-code": "^4.5.2",
"@fontsource/roboto": "^4.5.1", "@fontsource/roboto": "^4.5.1",
"@fontsource/roboto-mono": "^4.5.0", "@fontsource/roboto-mono": "^4.5.0",
@ -21,15 +21,15 @@
"@testing-library/react": "^11.1.0", "@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10", "@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",
"@types/node": "^14.17.5", "@types/node": "^16.11.14",
"@types/react": "^17.0.37", "@types/react": "^17.0.38",
"@types/react-blockies": "^1.4.1", "@types/react-blockies": "^1.4.1",
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"@types/react-highlight": "^0.12.5", "@types/react-highlight": "^0.12.5",
"@types/react-syntax-highlighter": "^13.5.2", "@types/react-syntax-highlighter": "^13.5.2",
"chart.js": "^3.6.2", "chart.js": "^3.7.0",
"ethers": "^5.5.2", "ethers": "^5.5.2",
"highlightjs-solidity": "^2.0.2", "highlightjs-solidity": "^2.0.3",
"react": "^17.0.2", "react": "^17.0.2",
"react-blockies": "^1.4.1", "react-blockies": "^1.4.1",
"react-chartjs-2": "^4.0.0", "react-chartjs-2": "^4.0.0",
@ -37,12 +37,13 @@
"react-error-boundary": "^3.1.4", "react-error-boundary": "^3.1.4",
"react-helmet-async": "^1.1.2", "react-helmet-async": "^1.1.2",
"react-image": "^4.0.3", "react-image": "^4.0.3",
"react-router-dom": "^6.0.2", "react-router-dom": "^6.2.1",
"react-scripts": "4.0.3", "react-scripts": "4.0.3",
"react-syntax-highlighter": "^15.4.5", "react-syntax-highlighter": "^15.4.5",
"serve": "^13.0.2", "serve": "^13.0.2",
"typescript": "^4.5.2", "swr": "^1.1.2",
"use-keyboard-shortcut": "^1.0.6", "typescript": "^4.5.4",
"use-keyboard-shortcut": "^1.1.2",
"web-vitals": "^1.0.1" "web-vitals": "^1.0.1"
}, },
"scripts": { "scripts": {

View File

@ -13,6 +13,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle"; import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
import StandardFrame from "./StandardFrame"; import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle"; import StandardSubtitle from "./StandardSubtitle";
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
import Copy from "./components/Copy"; import Copy from "./components/Copy";
import NavTab from "./components/NavTab"; import NavTab from "./components/NavTab";
import SourcifyLogo from "./sourcify/SourcifyLogo"; import SourcifyLogo from "./sourcify/SourcifyLogo";
@ -20,12 +21,19 @@ import AddressTransactionResults from "./address/AddressTransactionResults";
import Contracts from "./address/Contracts"; import Contracts from "./address/Contracts";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { useAppConfigContext } from "./useAppConfig"; import { useAppConfigContext } from "./useAppConfig";
import { useAddressOrENSFromURL } from "./useResolvedAddresses"; import { useAddressOrENS } from "./useResolvedAddresses";
import { useMultipleMetadata } from "./sourcify/useSourcify"; import { useMultipleMetadata } from "./sourcify/useSourcify";
import { ChecksummedAddress } from "./types"; import { ChecksummedAddress } from "./types";
import { useAddressesWithCode } from "./useErigonHooks"; import { useAddressesWithCode } from "./useErigonHooks";
const AddressTransactions: React.FC = () => { const AddressTransactionByNonce = React.lazy(
() =>
import(
/* webpackChunkName: "addresstxbynonce", webpackPrefetch: true */ "./AddressTransactionByNonce"
)
);
const Address: React.FC = () => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const { addressOrName, direction } = useParams(); const { addressOrName, direction } = useParams();
if (addressOrName === undefined) { if (addressOrName === undefined) {
@ -45,7 +53,7 @@ const AddressTransactions: React.FC = () => {
}, },
[navigate, direction, searchParams] [navigate, direction, searchParams]
); );
const [checksummedAddress, isENS, error] = useAddressOrENSFromURL( const [checksummedAddress, isENS, error] = useAddressOrENS(
addressOrName, addressOrName,
urlFixer urlFixer
); );
@ -78,12 +86,21 @@ const AddressTransactions: React.FC = () => {
? metadatas[checksummedAddress] ? metadatas[checksummedAddress]
: undefined; : undefined;
// Search address by nonce === transaction @ nonce
const rawNonce = searchParams.get("nonce");
if (rawNonce !== null) {
return (
<AddressTransactionByNonce
checksummedAddress={checksummedAddress}
rawNonce={rawNonce}
/>
);
}
return ( return (
<StandardFrame> <StandardFrame>
{error ? ( {error ? (
<span className="text-base"> <AddressOrENSNameNotFound addressOrENSName={addressOrName} />
"{addressOrName}" is not an ETH address or ENS name.
</span>
) : ( ) : (
checksummedAddress && ( checksummedAddress && (
<> <>
@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => {
); );
}; };
export default AddressTransactions; export default Address;

View File

@ -0,0 +1,106 @@
import React, { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import StandardFrame from "./StandardFrame";
import AddressOrENSNameInvalidNonce from "./components/AddressOrENSNameInvalidNonce";
import AddressOrENSNameNoTx from "./components/AddressOrENSNameNoTx";
import { ChecksummedAddress } from "./types";
import { transactionURL } from "./url";
import { useTransactionBySenderAndNonce } from "./useErigonHooks";
import { RuntimeContext } from "./useRuntime";
type AddressTransactionByNonceProps = {
checksummedAddress: ChecksummedAddress | undefined;
rawNonce: string;
};
const AddressTransactionByNonce: React.FC<AddressTransactionByNonceProps> = ({
checksummedAddress,
rawNonce,
}) => {
const { provider } = useContext(RuntimeContext);
// Calculate txCount ONLY when asked for latest nonce
const [txCount, setTxCount] = useState<number | undefined>();
useEffect(() => {
if (!provider || !checksummedAddress || rawNonce !== "latest") {
setTxCount(undefined);
return;
}
const readTxCount = async () => {
const count = await provider.getTransactionCount(checksummedAddress);
setTxCount(count);
};
readTxCount();
}, [provider, checksummedAddress, rawNonce]);
// Determine desired nonce from parse int query param or txCount - 1 nonce
// in case of latest
let nonce: number | undefined;
if (rawNonce === "latest") {
if (txCount !== undefined) {
nonce = txCount - 1;
}
} else {
nonce = parseInt(rawNonce, 10);
if (nonce < 0) {
nonce = NaN;
}
}
// Given all base params are determined, get the corresponding tx
const txHash = useTransactionBySenderAndNonce(
provider,
checksummedAddress,
nonce !== undefined && isNaN(nonce) ? undefined : nonce
);
const navigate = useNavigate();
// Loading...
if (
checksummedAddress === undefined ||
nonce === undefined ||
txHash === undefined
) {
return <StandardFrame />;
}
// Address hasn't made the first outbound tx yet
if (nonce < 0) {
return (
<StandardFrame>
<AddressOrENSNameNoTx addressOrENSName={checksummedAddress} />
</StandardFrame>
);
}
// Garbage nonce
if (isNaN(nonce)) {
return (
<StandardFrame>
<AddressOrENSNameInvalidNonce
addressOrENSName={checksummedAddress}
nonce={rawNonce}
/>
</StandardFrame>
);
}
// Valid nonce, but no tx found
if (txHash === null) {
return (
<StandardFrame>
<AddressOrENSNameInvalidNonce
addressOrENSName={checksummedAddress}
nonce={nonce.toString()}
/>
</StandardFrame>
);
}
// Success; replace and render filler
navigate(transactionURL(txHash), { replace: true });
return <StandardFrame />;
};
export default AddressTransactionByNonce;

View File

@ -17,11 +17,9 @@ const BlockTransactions = React.lazy(
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions" /* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
) )
); );
const AddressTransactions = React.lazy( const Address = React.lazy(
() => () =>
import( import(/* webpackChunkName: "address", webpackPrefetch: true */ "./Address")
/* webpackChunkName: "address", webpackPrefetch: true */ "./AddressTransactions"
)
); );
const Transaction = React.lazy( const Transaction = React.lazy(
() => () =>
@ -33,6 +31,12 @@ const London = React.lazy(
/* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London" /* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London"
) )
); );
const PageNotFound = React.lazy(
() =>
import(
/* webpackChunkName: "notfound", webpackPrefetch: true */ "./PageNotFound"
)
);
const App = () => { const App = () => {
const runtime = useRuntime(); const runtime = useRuntime();
@ -61,9 +65,9 @@ const App = () => {
<Route path="tx/:txhash/*" element={<Transaction />} /> <Route path="tx/:txhash/*" element={<Transaction />} />
<Route <Route
path="address/:addressOrName/*" path="address/:addressOrName/*"
element={<AddressTransactions />} element={<Address />}
/> />
<Route path="*" element={<Home />} /> <Route path="*" element={<PageNotFound />} />
</Route> </Route>
</Routes> </Routes>
</Router> </Router>

16
src/PageNotFound.tsx Normal file
View File

@ -0,0 +1,16 @@
import React from "react";
import { NavLink } from "react-router-dom";
import StandardFrame from "./StandardFrame";
const PageNotFound: React.FC = () => (
<StandardFrame>
<div className="border h-full m-auto flex flex-col justify-center items-center space-y-10">
<span className="text-4xl">Page not found!</span>
<NavLink className="text-link-blue hover:text-link-blue-hover" to="/">
Click here to go to home
</NavLink>
</div>
</StandardFrame>
);
export default PageNotFound;

View File

@ -10,13 +10,11 @@ import {
TokenMeta, TokenMeta,
TokenTransfer, TokenTransfer,
} from "./types"; } from "./types";
import { ResolvedAddresses } from "./api/address-resolver";
import { Metadata } from "./sourcify/useSourcify"; import { Metadata } from "./sourcify/useSourcify";
type TokenTransferItemProps = { type TokenTransferItemProps = {
t: TokenTransfer; t: TokenTransfer;
tokenMeta?: TokenMeta | null | undefined; tokenMeta?: TokenMeta | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>; metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
@ -24,7 +22,6 @@ type TokenTransferItemProps = {
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
t, t,
tokenMeta, tokenMeta,
resolvedAddresses,
metadatas, metadatas,
}) => ( }) => (
<div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100"> <div className="flex items-baseline space-x-2 px-2 py-1 truncate hover:bg-gray-100">
@ -37,7 +34,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress <TransactionAddress
address={t.from} address={t.from}
addressCtx={AddressContext.FROM} addressCtx={AddressContext.FROM}
resolvedAddresses={resolvedAddresses}
metadata={metadatas[t.from]} metadata={metadatas[t.from]}
/> />
</div> </div>
@ -46,7 +42,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
<TransactionAddress <TransactionAddress
address={t.to} address={t.to}
addressCtx={AddressContext.TO} addressCtx={AddressContext.TO}
resolvedAddresses={resolvedAddresses}
metadata={metadatas[t.to]} metadata={metadatas[t.to]}
/> />
</div> </div>
@ -60,11 +55,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
/> />
</ValueHighlighter> </ValueHighlighter>
</span> </span>
<TransactionAddress <TransactionAddress address={t.token} metadata={metadatas[t.token]} />
address={t.token}
resolvedAddresses={resolvedAddresses}
metadata={metadatas[t.token]}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,154 +1,13 @@
import React, { useMemo, useContext } from "react"; import React from "react";
import { useParams, Routes, Route } from "react-router-dom"; import { useParams } from "react-router-dom";
import { Tab } from "@headlessui/react"; import TransactionPageContent from "./TransactionPageContent";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { SelectionContext, useSelection } from "./useSelection";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { useETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
import {
transactionDataCollector,
useResolvedAddresses,
} from "./useResolvedAddresses";
import { SelectedTransactionContext } from "./useSelectedTransaction";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
const Transaction: React.FC = () => { const Transaction: React.FC = () => {
const { provider } = useContext(RuntimeContext);
const { txhash } = useParams(); const { txhash } = useParams();
if (txhash === undefined) { if (txhash === undefined) {
throw new Error("txhash couldn't be undefined here"); throw new Error("txhash couldn't be undefined here");
} }
return <TransactionPageContent txHash={txhash} />;
const txData = useTxData(provider, txhash);
const addrCollector = useMemo(
() => transactionDataCollector(txData),
[txData]
);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txhash}</span> not found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
resolvedAddresses={resolvedAddresses}
/>
}
/>
<Route
path="logs"
element={
<Logs
txData={txData}
metadata={metadata}
resolvedAddresses={resolvedAddresses}
/>
}
/>
<Route
path="trace"
element={
<Trace
txData={txData}
resolvedAddresses={resolvedAddresses}
/>
}
/>
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</SelectedTransactionContext.Provider>
);
}; };
export default Transaction; export default Transaction;

View File

@ -0,0 +1,131 @@
import React, { useContext, useMemo } from "react";
import { Route, Routes } from "react-router-dom";
import { Tab } from "@headlessui/react";
import StandardFrame from "./StandardFrame";
import StandardSubtitle from "./StandardSubtitle";
import ContentFrame from "./ContentFrame";
import NavTab from "./components/NavTab";
import { RuntimeContext } from "./useRuntime";
import { useInternalOperations, useTxData } from "./useErigonHooks";
import { SelectionContext, useSelection } from "./useSelection";
import { SelectedTransactionContext } from "./useSelectedTransaction";
import { useETHUSDOracle } from "./usePriceOracle";
import { useAppConfigContext } from "./useAppConfig";
import { useSourcify, useTransactionDescription } from "./sourcify/useSourcify";
const Details = React.lazy(
() =>
import(
/* webpackChunkName: "txdetails", webpackPrefetch: true */
"./transaction/Details"
)
);
const Logs = React.lazy(
() =>
import(
/* webpackChunkName: "txlogs", webpackPrefetch: true */ "./transaction/Logs"
)
);
const Trace = React.lazy(
() =>
import(
/* webpackChunkName: "txtrace", webpackPrefetch: true */ "./transaction/Trace"
)
);
type TransactionPageContentProps = {
txHash: string;
};
const TransactionPageContent: React.FC<TransactionPageContentProps> = ({
txHash,
}) => {
const { provider } = useContext(RuntimeContext);
const txData = useTxData(provider, txHash);
const internalOps = useInternalOperations(provider, txData);
const sendsEthToMiner = useMemo(() => {
if (!txData || !internalOps) {
return false;
}
for (const t of internalOps) {
if (t.to === txData.confirmedData?.miner) {
return true;
}
}
return false;
}, [txData, internalOps]);
const selectionCtx = useSelection();
const blockETHUSDPrice = useETHUSDOracle(
provider,
txData?.confirmedData?.blockNumber
);
const { sourcifySource } = useAppConfigContext();
const metadata = useSourcify(
txData?.to,
provider?.network.chainId,
sourcifySource
);
const txDesc = useTransactionDescription(metadata, txData);
return (
<SelectedTransactionContext.Provider value={txData}>
<StandardFrame>
<StandardSubtitle>Transaction Details</StandardSubtitle>
{txData === null && (
<ContentFrame>
<div className="py-4 text-sm">
Transaction <span className="font-hash">{txHash}</span> not found.
</div>
</ContentFrame>
)}
{txData && (
<SelectionContext.Provider value={selectionCtx}>
<Tab.Group>
<Tab.List className="flex space-x-2 border-l border-r border-t rounded-t-lg bg-white">
<NavTab href=".">Overview</NavTab>
{txData.confirmedData?.blockNumber !== undefined && (
<NavTab href="logs">
Logs
{txData && ` (${txData.confirmedData?.logs?.length ?? 0})`}
</NavTab>
)}
<NavTab href="trace">Trace</NavTab>
</Tab.List>
</Tab.Group>
<React.Suspense fallback={null}>
<Routes>
<Route
index
element={
<Details
txData={txData}
txDesc={txDesc}
toMetadata={metadata}
userDoc={metadata?.output.userdoc}
devDoc={metadata?.output.devdoc}
internalOps={internalOps}
sendsEthToMiner={sendsEthToMiner}
ethUSDPrice={blockETHUSDPrice}
/>
}
/>
<Route
path="logs"
element={<Logs txData={txData} metadata={metadata} />}
/>
<Route path="trace" element={<Trace txData={txData} />} />
</Routes>
</React.Suspense>
</SelectionContext.Provider>
)}
</StandardFrame>
</SelectedTransactionContext.Provider>
);
};
export default TransactionPageContent;

View File

@ -10,7 +10,6 @@ import { useFeeToggler } from "../search/useFeeToggler";
import { SelectionContext, useSelection } from "../useSelection"; import { SelectionContext, useSelection } from "../useSelection";
import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useMultipleETHUSDOracle } from "../usePriceOracle";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
import { useParams, useSearchParams } from "react-router-dom"; import { useParams, useSearchParams } from "react-router-dom";
import { ChecksummedAddress } from "../types"; import { ChecksummedAddress } from "../types";
import { useContractsMetadata } from "../hooks"; import { useContractsMetadata } from "../hooks";
@ -102,10 +101,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
}, [page]); }, [page]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags); const priceMap = useMultipleETHUSDOracle(provider, blockTags);
// Resolve all addresses that appear on this page results
const addrCollector = useMemo(() => pageCollector(page), [page]);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
// Calculate Sourcify metadata for all addresses that appear on this page results // Calculate Sourcify metadata for all addresses that appear on this page results
const addresses = useMemo(() => { const addresses = useMemo(() => {
const _addresses = [address]; const _addresses = [address];
@ -137,8 +132,8 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
address={address} address={address}
isFirst={controller?.isFirst} isFirst={controller?.isFirst}
isLast={controller?.isLast} isLast={controller?.isLast}
prevHash={page ? page[0].hash : ""} prevHash={page?.[0]?.hash ?? ""}
nextHash={page ? page[page.length - 1].hash : ""} nextHash={page?.[page.length - 1]?.hash ?? ""}
disabled={controller === undefined} disabled={controller === undefined}
/> />
</div> </div>
@ -152,7 +147,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
<TransactionItem <TransactionItem
key={tx.hash} key={tx.hash}
tx={tx} tx={tx}
resolvedAddresses={resolvedAddresses}
selectedAddress={address} selectedAddress={address}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}
@ -171,8 +165,8 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
address={address} address={address}
isFirst={controller?.isFirst} isFirst={controller?.isFirst}
isLast={controller?.isLast} isLast={controller?.isLast}
prevHash={page ? page[0].hash : ""} prevHash={page?.[0]?.hash ?? ""}
nextHash={page ? page[page.length - 1].hash : ""} nextHash={page?.[page.length - 1]?.hash ?? ""}
disabled={controller === undefined} disabled={controller === undefined}
/> />
</div> </div>

View File

@ -1,4 +1,3 @@
import { BaseProvider } from "@ethersproject/providers";
import { ensRenderer } from "../../components/ENSName"; import { ensRenderer } from "../../components/ENSName";
import { plainStringRenderer } from "../../components/PlainString"; import { plainStringRenderer } from "../../components/PlainString";
import { tokenRenderer } from "../../components/TokenName"; import { tokenRenderer } from "../../components/TokenName";
@ -48,30 +47,3 @@ resolverRendererRegistry.set(uniswapV2Resolver, uniswapV2PairRenderer);
resolverRendererRegistry.set(uniswapV3Resolver, uniswapV3PairRenderer); resolverRendererRegistry.set(uniswapV3Resolver, uniswapV3PairRenderer);
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer); resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
resolverRendererRegistry.set(hardcodedResolver, plainStringRenderer); resolverRendererRegistry.set(hardcodedResolver, plainStringRenderer);
// TODO: implement progressive resolving
export const batchPopulate = async (
provider: BaseProvider,
addresses: string[],
currentMap: ResolvedAddresses | undefined
): Promise<ResolvedAddresses> => {
const solvers: Promise<SelectedResolvedName<any> | undefined>[] = [];
const unresolvedAddresses = addresses.filter(
(a) => currentMap?.[a] === undefined
);
for (const a of unresolvedAddresses) {
solvers.push(mainResolver.resolveAddress(provider, a));
}
const resultMap: ResolvedAddresses = currentMap ? { ...currentMap } : {};
const results = await Promise.all(solvers);
for (let i = 0; i < results.length; i++) {
const r = results[i];
if (r === undefined) {
continue;
}
resultMap[unresolvedAddresses[i]] = r;
}
return resultMap;
};

View File

@ -8,7 +8,6 @@ import TransactionItem from "../search/TransactionItem";
import { useFeeToggler } from "../search/useFeeToggler"; import { useFeeToggler } from "../search/useFeeToggler";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { SelectionContext, useSelection } from "../useSelection"; import { SelectionContext, useSelection } from "../useSelection";
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
import { ChecksummedAddress, ProcessedTransaction } from "../types"; import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { PAGE_SIZE } from "../params"; import { PAGE_SIZE } from "../params";
import { useMultipleETHUSDOracle } from "../usePriceOracle"; import { useMultipleETHUSDOracle } from "../usePriceOracle";
@ -30,8 +29,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const selectionCtx = useSelection(); const selectionCtx = useSelection();
const [feeDisplay, feeDisplayToggler] = useFeeToggler(); const [feeDisplay, feeDisplayToggler] = useFeeToggler();
const addrCollector = useMemo(() => pageCollector(page), [page]);
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
const blockTags = useMemo(() => [blockTag], [blockTag]); const blockTags = useMemo(() => [blockTag], [blockTag]);
const priceMap = useMultipleETHUSDOracle(provider, blockTags); const priceMap = useMultipleETHUSDOracle(provider, blockTags);
@ -79,7 +76,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
<TransactionItem <TransactionItem
key={tx.hash} key={tx.hash}
tx={tx} tx={tx}
resolvedAddresses={resolvedAddresses}
feeDisplay={feeDisplay} feeDisplay={feeDisplay}
priceMap={priceMap} priceMap={priceMap}
metadatas={metadatas} metadatas={metadatas}

View File

@ -1,24 +1,23 @@
import React from "react"; import React, { useContext } from "react";
import {
ResolvedAddresses,
resolverRendererRegistry,
} from "../api/address-resolver";
import PlainAddress from "./PlainAddress"; import PlainAddress from "./PlainAddress";
import { resolverRendererRegistry } from "../api/address-resolver";
import { useResolvedAddress } from "../useResolvedAddresses";
import { RuntimeContext } from "../useRuntime";
import { ChecksummedAddress } from "../types";
type AddressOrENSNameProps = { type AddressOrENSNameProps = {
address: string; address: ChecksummedAddress;
selectedAddress?: string; selectedAddress?: string;
dontOverrideColors?: boolean; dontOverrideColors?: boolean;
resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({ const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
address, address,
selectedAddress, selectedAddress,
dontOverrideColors, dontOverrideColors,
resolvedAddresses,
}) => { }) => {
const resolvedAddress = resolvedAddresses?.[address]; const { provider } = useContext(RuntimeContext);
const resolvedAddress = useResolvedAddress(provider, address);
const linkable = address !== selectedAddress; const linkable = address !== selectedAddress;
if (!resolvedAddress) { if (!resolvedAddress) {

View File

@ -0,0 +1,25 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
import AddressOrENSName from "./AddressOrENSName";
type AddressOrENSNameInvalidNonceProps = {
addressOrENSName: string;
nonce: string;
};
const AddressOrENSNameInvalidNonce: React.FC<
AddressOrENSNameInvalidNonceProps
> = ({ addressOrENSName, nonce }) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="flex py-4 text-sm">
<AddressOrENSName address={addressOrENSName} />
<span>: no transaction found for nonce="{nonce}".</span>
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameInvalidNonce);

View File

@ -0,0 +1,24 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
import AddressOrENSName from "./AddressOrENSName";
type AddressOrENSNameNoTxProps = {
addressOrENSName: string;
};
const AddressOrENSNameNoTx: React.FC<AddressOrENSNameNoTxProps> = ({
addressOrENSName,
}) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="flex py-4 text-sm">
<AddressOrENSName address={addressOrENSName} />
<span>: no outbound transactions found.</span>
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameNoTx);

View File

@ -0,0 +1,22 @@
import React from "react";
import StandardSubtitle from "../StandardSubtitle";
import ContentFrame from "../ContentFrame";
type AddressOrENSNameNotFoundProps = {
addressOrENSName: string;
};
const AddressOrENSNameNotFound: React.FC<AddressOrENSNameNotFoundProps> = ({
addressOrENSName,
}) => (
<>
<StandardSubtitle>Transaction Details</StandardSubtitle>
<ContentFrame>
<div className="py-4 text-sm">
"{addressOrENSName}" is not an ETH address or ENS name.
</div>
</ContentFrame>
</>
);
export default React.memo(AddressOrENSNameNotFound);

View File

@ -9,7 +9,6 @@ import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
import AddressOrENSName from "./AddressOrENSName"; import AddressOrENSName from "./AddressOrENSName";
import SourcifyLogo from "../sourcify/SourcifyLogo"; import SourcifyLogo from "../sourcify/SourcifyLogo";
import { AddressContext, ZERO_ADDRESS } from "../types"; import { AddressContext, ZERO_ADDRESS } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
import { Metadata } from "../sourcify/useSourcify"; import { Metadata } from "../sourcify/useSourcify";
type DecoratedAddressLinkProps = { type DecoratedAddressLinkProps = {
@ -21,7 +20,6 @@ type DecoratedAddressLinkProps = {
selfDestruct?: boolean; selfDestruct?: boolean;
txFrom?: boolean; txFrom?: boolean;
txTo?: boolean; txTo?: boolean;
resolvedAddresses?: ResolvedAddresses | undefined;
metadata?: Metadata | null | undefined; metadata?: Metadata | null | undefined;
}; };
@ -34,7 +32,6 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
selfDestruct, selfDestruct,
txFrom, txFrom,
txTo, txTo,
resolvedAddresses,
metadata, metadata,
}) => { }) => {
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
@ -87,7 +84,6 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
address={address} address={address}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
dontOverrideColors={mint || burn} dontOverrideColors={mint || burn}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
); );

View File

@ -5,17 +5,12 @@ import TransactionAddress from "./TransactionAddress";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { InternalOperation } from "../types"; import { InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalCreateProps = { type InternalCreateProps = {
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalCreate: React.FC<InternalCreateProps> = ({ const InternalCreate: React.FC<InternalCreateProps> = ({ internalOp }) => (
internalOp,
resolvedAddresses,
}) => (
<div className="flex items-baseline space-x-1 whitespace-nowrap"> <div className="flex items-baseline space-x-1 whitespace-nowrap">
<span className="text-gray-500"> <span className="text-gray-500">
<FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE <FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE
@ -23,20 +18,11 @@ const InternalCreate: React.FC<InternalCreateProps> = ({
<span>Contract</span> <span>Contract</span>
<div className="flex items-baseline"> <div className="flex items-baseline">
<AddressHighlighter address={internalOp.to}> <AddressHighlighter address={internalOp.to}>
<DecoratedAddressLink <DecoratedAddressLink address={internalOp.to} creation />
address={internalOp.to}
creation
resolvedAddresses={resolvedAddresses}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
<span className="flex items-baseline text-gray-400"> <span className="flex items-baseline text-gray-400">
(Creator:{" "} (Creator: <TransactionAddress address={internalOp.from} />)
<TransactionAddress
address={internalOp.from}
resolvedAddresses={resolvedAddresses}
/>
)
</span> </span>
</div> </div>
); );

View File

@ -5,19 +5,16 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
import TransactionAddress from "./TransactionAddress"; import TransactionAddress from "./TransactionAddress";
type InternalSelfDestructProps = { type InternalSelfDestructProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
txData, txData,
internalOp, internalOp,
resolvedAddresses,
}) => { }) => {
const toMiner = const toMiner =
txData.confirmedData?.miner !== undefined && txData.confirmedData?.miner !== undefined &&
@ -32,21 +29,12 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
<span>Contract</span> <span>Contract</span>
<div className="flex items-baseline"> <div className="flex items-baseline">
<AddressHighlighter address={internalOp.from}> <AddressHighlighter address={internalOp.from}>
<DecoratedAddressLink <DecoratedAddressLink address={internalOp.from} selfDestruct />
address={internalOp.from}
selfDestruct
resolvedAddresses={resolvedAddresses}
/>
</AddressHighlighter> </AddressHighlighter>
</div> </div>
{internalOp.value.isZero() && ( {internalOp.value.isZero() && (
<div className="flex items-baseline text-gray-400"> <div className="flex items-baseline text-gray-400">
(To:{" "} (To: <TransactionAddress address={internalOp.to} />)
<TransactionAddress
address={internalOp.to}
resolvedAddresses={resolvedAddresses}
/>
)
</div> </div>
)} )}
</div> </div>
@ -64,11 +52,7 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
}`} }`}
> >
<DecoratedAddressLink <DecoratedAddressLink address={internalOp.to} miner={toMiner} />
address={internalOp.to}
miner={toMiner}
resolvedAddresses={resolvedAddresses}
/>
</div> </div>
</AddressHighlighter> </AddressHighlighter>
</div> </div>

View File

@ -3,37 +3,24 @@ import InternalTransfer from "./InternalTransfer";
import InternalSelfDestruct from "./InternalSelfDestruct"; import InternalSelfDestruct from "./InternalSelfDestruct";
import InternalCreate from "./InternalCreate"; import InternalCreate from "./InternalCreate";
import { TransactionData, InternalOperation, OperationType } from "../types"; import { TransactionData, InternalOperation, OperationType } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransactionOperationProps = { type InternalTransactionOperationProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> = const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
({ txData, internalOp, resolvedAddresses }) => ( ({ txData, internalOp }) => (
<> <>
{internalOp.type === OperationType.TRANSFER && ( {internalOp.type === OperationType.TRANSFER && (
<InternalTransfer <InternalTransfer txData={txData} internalOp={internalOp} />
txData={txData}
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
{internalOp.type === OperationType.SELF_DESTRUCT && ( {internalOp.type === OperationType.SELF_DESTRUCT && (
<InternalSelfDestruct <InternalSelfDestruct txData={txData} internalOp={internalOp} />
txData={txData}
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
{(internalOp.type === OperationType.CREATE || {(internalOp.type === OperationType.CREATE ||
internalOp.type === OperationType.CREATE2) && ( internalOp.type === OperationType.CREATE2) && (
<InternalCreate <InternalCreate internalOp={internalOp} />
internalOp={internalOp}
resolvedAddresses={resolvedAddresses}
/>
)} )}
</> </>
); );

View File

@ -5,18 +5,15 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { TransactionData, InternalOperation } from "../types"; import { TransactionData, InternalOperation } from "../types";
import { ResolvedAddresses } from "../api/address-resolver";
type InternalTransferProps = { type InternalTransferProps = {
txData: TransactionData; txData: TransactionData;
internalOp: InternalOperation; internalOp: InternalOperation;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InternalTransfer: React.FC<InternalTransferProps> = ({ const InternalTransfer: React.FC<InternalTransferProps> = ({
txData, txData,
internalOp, internalOp,
resolvedAddresses,
}) => { }) => {
const fromMiner = const fromMiner =
txData.confirmedData?.miner !== undefined && txData.confirmedData?.miner !== undefined &&
@ -44,7 +41,6 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
miner={fromMiner} miner={fromMiner}
txFrom={internalOp.from === txData.from} txFrom={internalOp.from === txData.from}
txTo={internalOp.from === txData.to} txTo={internalOp.from === txData.to}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>
@ -62,7 +58,6 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
miner={toMiner} miner={toMiner}
txFrom={internalOp.to === txData.from} txFrom={internalOp.to === txData.from}
txTo={internalOp.to === txData.to} txTo={internalOp.to === txData.to}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</AddressHighlighter> </AddressHighlighter>

View File

@ -1,20 +1,12 @@
import React from "react"; import React from "react";
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes"; import { useMethodSelector } from "../use4Bytes";
type MethodNameProps = { type MethodNameProps = {
data: string; data: string;
}; };
const MethodName: React.FC<MethodNameProps> = ({ data }) => { const MethodName: React.FC<MethodNameProps> = ({ data }) => {
const rawFourBytes = rawInputTo4Bytes(data); const [isSimpleTransfer, methodName, methodTitle] = useMethodSelector(data);
const fourBytesEntry = use4Bytes(rawFourBytes);
const methodName = fourBytesEntry?.name ?? rawFourBytes;
const isSimpleTransfer = rawFourBytes === "0x";
const methodTitle = isSimpleTransfer
? "ETH Transfer"
: methodName === rawFourBytes
? methodName
: `${methodName} [${rawFourBytes}]`;
return ( return (
<div <div

View File

@ -1,4 +1,5 @@
import React from "react"; import React from "react";
import { commify } from "@ethersproject/units";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp"; import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
@ -14,7 +15,7 @@ const Nonce: React.FC<NonceProps> = ({ value }) => (
<span className="text-green-400"> <span className="text-green-400">
<FontAwesomeIcon icon={faArrowUp} size="1x" /> <FontAwesomeIcon icon={faArrowUp} size="1x" />
</span> </span>
<span className="text-green-600">{value}</span> <span className="text-green-600">{commify(value)}</span>
</span> </span>
); );

View File

@ -1,7 +1,6 @@
import React from "react"; import React from "react";
import AddressHighlighter from "./AddressHighlighter"; import AddressHighlighter from "./AddressHighlighter";
import DecoratedAddressLink from "./DecoratedAddressLink"; import DecoratedAddressLink from "./DecoratedAddressLink";
import { ResolvedAddresses } from "../api/address-resolver";
import { useSelectedTransaction } from "../useSelectedTransaction"; import { useSelectedTransaction } from "../useSelectedTransaction";
import { AddressContext } from "../types"; import { AddressContext } from "../types";
import { Metadata } from "../sourcify/useSourcify"; import { Metadata } from "../sourcify/useSourcify";
@ -9,14 +8,12 @@ import { Metadata } from "../sourcify/useSourcify";
type TransactionAddressProps = { type TransactionAddressProps = {
address: string; address: string;
addressCtx?: AddressContext | undefined; addressCtx?: AddressContext | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
metadata?: Metadata | null | undefined; metadata?: Metadata | null | undefined;
}; };
const TransactionAddress: React.FC<TransactionAddressProps> = ({ const TransactionAddress: React.FC<TransactionAddressProps> = ({
address, address,
addressCtx, addressCtx,
resolvedAddresses,
metadata, metadata,
}) => { }) => {
const txData = useSelectedTransaction(); const txData = useSelectedTransaction();
@ -32,7 +29,6 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
txFrom={address === txData?.from} txFrom={address === txData?.from}
txTo={address === txData?.to || creation} txTo={address === txData?.to || creation}
creation={creation} creation={creation}
resolvedAddresses={resolvedAddresses}
metadata={metadata} metadata={metadata}
/> />
</AddressHighlighter> </AddressHighlighter>

View File

@ -1,5 +1,6 @@
import React from "react"; import React from "react";
import { NavLink } from "react-router-dom"; import { NavLink } from "react-router-dom";
import { transactionURL } from "../url";
type TransactionLinkProps = { type TransactionLinkProps = {
txHash: string; txHash: string;
@ -8,7 +9,7 @@ type TransactionLinkProps = {
const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => ( const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => (
<NavLink <NavLink
className="text-link-blue hover:text-link-blue-hover font-hash" className="text-link-blue hover:text-link-blue-hover font-hash"
to={`/tx/${txHash}`} to={transactionURL(txHash)}
> >
<p className="truncate">{txHash}</p> <p className="truncate">{txHash}</p>
</NavLink> </NavLink>

View File

@ -1,3 +1,3 @@
export const MIN_API_LEVEL = 5; export const MIN_API_LEVEL = 6;
export const PAGE_SIZE = 25; export const PAGE_SIZE = 25;

View File

@ -18,12 +18,10 @@ import { ChecksummedAddress, ProcessedTransaction } from "../types";
import { FeeDisplay } from "./useFeeToggler"; import { FeeDisplay } from "./useFeeToggler";
import { formatValue } from "../components/formatter"; import { formatValue } from "../components/formatter";
import ETH2USDValue from "../components/ETH2USDValue"; import ETH2USDValue from "../components/ETH2USDValue";
import { ResolvedAddresses } from "../api/address-resolver";
import { Metadata } from "../sourcify/useSourcify"; import { Metadata } from "../sourcify/useSourcify";
type TransactionItemProps = { type TransactionItemProps = {
tx: ProcessedTransaction; tx: ProcessedTransaction;
resolvedAddresses?: ResolvedAddresses;
selectedAddress?: string; selectedAddress?: string;
feeDisplay: FeeDisplay; feeDisplay: FeeDisplay;
priceMap: Record<BlockTag, BigNumber>; priceMap: Record<BlockTag, BigNumber>;
@ -32,7 +30,6 @@ type TransactionItemProps = {
const TransactionItem: React.FC<TransactionItemProps> = ({ const TransactionItem: React.FC<TransactionItemProps> = ({
tx, tx,
resolvedAddresses,
selectedAddress, selectedAddress,
feeDisplay, feeDisplay,
priceMap, priceMap,
@ -87,7 +84,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.from} address={tx.from}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.from} miner={tx.miner === tx.from}
resolvedAddresses={resolvedAddresses}
/> />
</AddressHighlighter> </AddressHighlighter>
)} )}
@ -110,7 +106,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.to} address={tx.to}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
miner={tx.miner === tx.to} miner={tx.miner === tx.to}
resolvedAddresses={resolvedAddresses}
metadata={metadatas[tx.to]} metadata={metadatas[tx.to]}
/> />
</AddressHighlighter> </AddressHighlighter>
@ -120,7 +115,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
address={tx.createdContractAddress!} address={tx.createdContractAddress!}
selectedAddress={selectedAddress} selectedAddress={selectedAddress}
creation creation
resolvedAddresses={resolvedAddresses}
metadata={metadatas[tx.createdContractAddress!]} metadata={metadatas[tx.createdContractAddress!]}
/> />
</AddressHighlighter> </AddressHighlighter>

View File

@ -206,17 +206,36 @@ export class SearchController {
} }
} }
const doSearch = (q: string, navigate: NavigateFunction) => { const doSearch = async (q: string, navigate: NavigateFunction) => {
if (isAddress(q)) { // Cleanup
navigate(`/address/${q}`, { replace: true }); q = q.trim();
let maybeAddress = q;
let maybeIndex = "";
const sepIndex = q.lastIndexOf(":");
if (sepIndex !== -1) {
maybeAddress = q.substring(0, sepIndex);
maybeIndex = q.substring(sepIndex + 1);
}
// Plain address?
if (isAddress(maybeAddress)) {
navigate(
`/address/${maybeAddress}${
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
}`,
{ replace: true }
);
return; return;
} }
// Tx hash?
if (isHexString(q, 32)) { if (isHexString(q, 32)) {
navigate(`/tx/${q}`, { replace: true }); navigate(`/tx/${q}`, { replace: true });
return; return;
} }
// Block number?
const blockNumber = parseInt(q); const blockNumber = parseInt(q);
if (!isNaN(blockNumber)) { if (!isNaN(blockNumber)) {
navigate(`/block/${blockNumber}`, { replace: true }); navigate(`/block/${blockNumber}`, { replace: true });
@ -224,7 +243,12 @@ const doSearch = (q: string, navigate: NavigateFunction) => {
} }
// Assume it is an ENS name // Assume it is an ENS name
navigate(`/address/${q}`); navigate(
`/address/${maybeAddress}${
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
}`,
{ replace: true }
);
}; };
export const useGenericSearch = (): [ export const useGenericSearch = (): [

View File

@ -15,6 +15,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
import TransactionAddress from "../components/TransactionAddress"; import TransactionAddress from "../components/TransactionAddress";
import Copy from "../components/Copy"; import Copy from "../components/Copy";
import Nonce from "../components/Nonce"; import Nonce from "../components/Nonce";
import NavNonce from "./NavNonce";
import Timestamp from "../components/Timestamp"; import Timestamp from "../components/Timestamp";
import InternalTransactionOperation from "../components/InternalTransactionOperation"; import InternalTransactionOperation from "../components/InternalTransactionOperation";
import MethodName from "../components/MethodName"; import MethodName from "../components/MethodName";
@ -37,12 +38,11 @@ import PercentagePosition from "../components/PercentagePosition";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedParamsTable from "./decoder/DecodedParamsTable";
import InputDecoder from "./decoder/InputDecoder"; import InputDecoder from "./decoder/InputDecoder";
import { import {
rawInputTo4Bytes, extract4Bytes,
use4Bytes, use4Bytes,
useTransactionDescription, useTransactionDescription,
} from "../use4Bytes"; } from "../use4Bytes";
import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify"; import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify";
import { ResolvedAddresses } from "../api/address-resolver";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks"; import { useContractsMetadata } from "../hooks";
import { useTransactionError } from "../useErigonHooks"; import { useTransactionError } from "../useErigonHooks";
@ -56,7 +56,6 @@ type DetailsProps = {
internalOps?: InternalOperation[]; internalOps?: InternalOperation[];
sendsEthToMiner: boolean; sendsEthToMiner: boolean;
ethUSDPrice: BigNumber | undefined; ethUSDPrice: BigNumber | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const Details: React.FC<DetailsProps> = ({ const Details: React.FC<DetailsProps> = ({
@ -68,13 +67,13 @@ const Details: React.FC<DetailsProps> = ({
internalOps, internalOps,
sendsEthToMiner, sendsEthToMiner,
ethUSDPrice, ethUSDPrice,
resolvedAddresses,
}) => { }) => {
const hasEIP1559 = const hasEIP1559 =
txData.confirmedData?.blockBaseFeePerGas !== undefined && txData.confirmedData?.blockBaseFeePerGas !== undefined &&
txData.confirmedData?.blockBaseFeePerGas !== null; txData.confirmedData?.blockBaseFeePerGas !== null;
const fourBytes = txData.to !== null ? rawInputTo4Bytes(txData.data) : "0x"; const fourBytes =
txData.to !== null ? extract4Bytes(txData.data) ?? "0x" : "0x";
const fourBytesEntry = use4Bytes(fourBytes); const fourBytesEntry = use4Bytes(fourBytes);
const fourBytesTxDesc = useTransactionDescription( const fourBytesTxDesc = useTransactionDescription(
fourBytesEntry, fourBytesEntry,
@ -199,7 +198,6 @@ const Details: React.FC<DetailsProps> = ({
hasParamNames hasParamNames
userMethod={userError} userMethod={userError}
devMethod={devError} devMethod={devError}
resolvedAddresses={resolvedAddresses}
/> />
)} )}
</Tab.Panel> </Tab.Panel>
@ -251,14 +249,12 @@ const Details: React.FC<DetailsProps> = ({
<InfoRow title="From / Nonce"> <InfoRow title="From / Nonce">
<div className="flex divide-x-2 divide-dotted divide-gray-300"> <div className="flex divide-x-2 divide-dotted divide-gray-300">
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress <TransactionAddress address={txData.from} />
address={txData.from}
resolvedAddresses={resolvedAddresses}
/>
<Copy value={txData.from} /> <Copy value={txData.from} />
</div> </div>
<div className="flex items-baseline pl-3"> <div className="flex items-baseline pl-3">
<Nonce value={txData.nonce} /> <Nonce value={txData.nonce} />
<NavNonce sender={txData.from} nonce={txData.nonce} />
</div> </div>
</div> </div>
</InfoRow> </InfoRow>
@ -267,7 +263,6 @@ const Details: React.FC<DetailsProps> = ({
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress <TransactionAddress
address={txData.to} address={txData.to}
resolvedAddresses={resolvedAddresses}
metadata={metadatas?.[txData.to]} metadata={metadatas?.[txData.to]}
/> />
<Copy value={txData.to} /> <Copy value={txData.to} />
@ -280,7 +275,6 @@ const Details: React.FC<DetailsProps> = ({
<div className="flex items-baseline space-x-2 -ml-1"> <div className="flex items-baseline space-x-2 -ml-1">
<TransactionAddress <TransactionAddress
address={txData.confirmedData?.createdContractAddress!} address={txData.confirmedData?.createdContractAddress!}
resolvedAddresses={resolvedAddresses}
metadata={ metadata={
metadatas?.[txData.confirmedData?.createdContractAddress!] metadatas?.[txData.confirmedData?.createdContractAddress!]
} }
@ -295,7 +289,6 @@ const Details: React.FC<DetailsProps> = ({
key={i} key={i}
txData={txData} txData={txData}
internalOp={op} internalOp={op}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</div> </div>
@ -313,7 +306,6 @@ const Details: React.FC<DetailsProps> = ({
key={i} key={i}
t={t} t={t}
tokenMeta={txData.tokenMetas[t.token]} tokenMeta={txData.tokenMetas[t.token]}
resolvedAddresses={resolvedAddresses}
metadatas={metadatas} metadatas={metadatas}
/> />
))} ))}
@ -437,7 +429,6 @@ const Details: React.FC<DetailsProps> = ({
data={txData.data} data={txData.data}
userMethod={userMethod} userMethod={userMethod}
devMethod={devMethod} devMethod={devMethod}
resolvedAddresses={resolvedAddresses}
/> />
</InfoRow> </InfoRow>
</ContentFrame> </ContentFrame>

View File

@ -8,23 +8,16 @@ import ModeTab from "../components/ModeTab";
import DecodedParamsTable from "./decoder/DecodedParamsTable"; import DecodedParamsTable from "./decoder/DecodedParamsTable";
import DecodedLogSignature from "./decoder/DecodedLogSignature"; import DecodedLogSignature from "./decoder/DecodedLogSignature";
import { useTopic0 } from "../useTopic0"; import { useTopic0 } from "../useTopic0";
import { ResolvedAddresses } from "../api/address-resolver";
import { ChecksummedAddress } from "../types"; import { ChecksummedAddress } from "../types";
import { Metadata } from "../sourcify/useSourcify"; import { Metadata } from "../sourcify/useSourcify";
type LogEntryProps = { type LogEntryProps = {
log: Log; log: Log;
logDesc: LogDescription | null | undefined; logDesc: LogDescription | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>; metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
}; };
const LogEntry: React.FC<LogEntryProps> = ({ const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
log,
logDesc,
resolvedAddresses,
metadatas,
}) => {
const rawTopic0 = log.topics[0]; const rawTopic0 = log.topics[0];
const topic0 = useTopic0(rawTopic0); const topic0 = useTopic0(rawTopic0);
@ -65,7 +58,6 @@ const LogEntry: React.FC<LogEntryProps> = ({
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress <TransactionAddress
address={log.address} address={log.address}
resolvedAddresses={resolvedAddresses}
metadata={metadatas[log.address]} metadata={metadatas[log.address]}
/> />
<Copy value={log.address} /> <Copy value={log.address} />
@ -109,7 +101,6 @@ const LogEntry: React.FC<LogEntryProps> = ({
args={resolvedLogDesc.args} args={resolvedLogDesc.args}
paramTypes={resolvedLogDesc.eventFragment.inputs} paramTypes={resolvedLogDesc.eventFragment.inputs}
hasParamNames={resolvedLogDesc === logDesc} hasParamNames={resolvedLogDesc === logDesc}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
</div> </div>

View File

@ -4,17 +4,15 @@ import ContentFrame from "../ContentFrame";
import LogEntry from "./LogEntry"; import LogEntry from "./LogEntry";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { Metadata } from "../sourcify/useSourcify"; import { Metadata } from "../sourcify/useSourcify";
import { ResolvedAddresses } from "../api/address-resolver";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { useContractsMetadata } from "../hooks"; import { useContractsMetadata } from "../hooks";
type LogsProps = { type LogsProps = {
txData: TransactionData; txData: TransactionData;
metadata: Metadata | null | undefined; metadata: Metadata | null | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => { const Logs: React.FC<LogsProps> = ({ txData, metadata }) => {
const baseMetadatas = useMemo((): Record<string, Metadata | null> => { const baseMetadatas = useMemo((): Record<string, Metadata | null> => {
if (!txData.to || metadata === undefined) { if (!txData.to || metadata === undefined) {
return {}; return {};
@ -68,7 +66,6 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
key={i} key={i}
log={l} log={l}
logDesc={logDescs?.[i]} logDesc={logDescs?.[i]}
resolvedAddresses={resolvedAddresses}
metadatas={metadatas} metadatas={metadatas}
/> />
))} ))}

View File

@ -0,0 +1,36 @@
import { NavLink } from "react-router-dom";
import { ChecksummedAddress } from "../types";
import { addressByNonceURL } from "../url";
// TODO: extract common component with block/NavButton
type NavButtonProps = {
sender: ChecksummedAddress;
nonce: number;
disabled?: boolean;
};
const NavButton: React.FC<NavButtonProps> = ({
sender,
nonce,
disabled,
children,
}) => {
if (disabled) {
return (
<span className="bg-link-blue bg-opacity-10 text-gray-300 rounded px-2 py-1 text-xs">
{children}
</span>
);
}
return (
<NavLink
className="bg-link-blue bg-opacity-10 text-link-blue hover:bg-opacity-100 hover:text-white rounded px-2 py-1 text-xs"
to={addressByNonceURL(sender, nonce)}
>
{children}
</NavLink>
);
};
export default NavButton;

View File

@ -0,0 +1,66 @@
import React, { useContext, useEffect } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft";
import { faChevronRight } from "@fortawesome/free-solid-svg-icons/faChevronRight";
import NavButton from "./NavButton";
import { ChecksummedAddress } from "../types";
import { RuntimeContext } from "../useRuntime";
import {
prefetchTransactionBySenderAndNonce,
useTransactionCount,
} from "../useErigonHooks";
import { useSWRConfig } from "swr";
type NavNonceProps = {
sender: ChecksummedAddress;
nonce: number;
};
const NavNonce: React.FC<NavNonceProps> = ({ sender, nonce }) => {
const { provider } = useContext(RuntimeContext);
const count = useTransactionCount(provider, sender);
// Prefetch
const swrConfig = useSWRConfig();
useEffect(() => {
if (!provider || !sender || nonce === undefined || count === undefined) {
return;
}
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce - 1);
prefetchTransactionBySenderAndNonce(swrConfig, provider, sender, nonce + 1);
if (count > 0) {
prefetchTransactionBySenderAndNonce(
swrConfig,
provider,
sender,
count - 1
);
}
}, [swrConfig, provider, sender, nonce, count]);
return (
<div className="pl-2 self-center flex space-x-1">
<NavButton sender={sender} nonce={nonce - 1} disabled={nonce === 0}>
<FontAwesomeIcon icon={faChevronLeft} />
</NavButton>
<NavButton
sender={sender}
nonce={nonce + 1}
disabled={count === undefined || nonce >= count - 1}
>
<FontAwesomeIcon icon={faChevronRight} />
</NavButton>
<NavButton
sender={sender}
nonce={count !== undefined ? count - 1 : -1}
disabled={count === undefined || nonce >= count - 1}
>
<FontAwesomeIcon icon={faChevronRight} />
<FontAwesomeIcon icon={faChevronRight} />
</NavButton>
</div>
);
};
export default React.memo(NavNonce);

View File

@ -1,37 +1,18 @@
import React, { useContext, useMemo } from "react"; import React, { useContext } from "react";
import ContentFrame from "../ContentFrame"; import ContentFrame from "../ContentFrame";
import TransactionAddress from "../components/TransactionAddress"; import TransactionAddress from "../components/TransactionAddress";
import TraceItem from "./TraceItem"; import TraceItem from "./TraceItem";
import { TransactionData } from "../types"; import { TransactionData } from "../types";
import { useBatch4Bytes } from "../use4Bytes"; import { useTraceTransaction } from "../useErigonHooks";
import { useTraceTransaction, useUniqueSignatures } from "../useErigonHooks";
import { RuntimeContext } from "../useRuntime"; import { RuntimeContext } from "../useRuntime";
import { ResolvedAddresses } from "../api/address-resolver";
import { tracesCollector, useResolvedAddresses } from "../useResolvedAddresses";
type TraceProps = { type TraceProps = {
txData: TransactionData; txData: TransactionData;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => { const Trace: React.FC<TraceProps> = ({ txData }) => {
const { provider } = useContext(RuntimeContext); const { provider } = useContext(RuntimeContext);
const traces = useTraceTransaction(provider, txData.transactionHash); const traces = useTraceTransaction(provider, txData.transactionHash);
const uniqueSignatures = useUniqueSignatures(traces);
const sigMap = useBatch4Bytes(uniqueSignatures);
const addrCollector = useMemo(() => tracesCollector(traces), [traces]);
const traceResolvedAddresses = useResolvedAddresses(provider, addrCollector);
const mergedResolvedAddresses = useMemo(() => {
const merge = {};
if (resolvedAddresses) {
Object.assign(merge, resolvedAddresses);
}
if (traceResolvedAddresses) {
Object.assign(merge, traceResolvedAddresses);
}
return merge;
}, [resolvedAddresses, traceResolvedAddresses]);
return ( return (
<ContentFrame tabs> <ContentFrame tabs>
@ -39,20 +20,11 @@ const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => {
{traces ? ( {traces ? (
<> <>
<div className="border hover:border-gray-500 rounded px-1 py-0.5"> <div className="border hover:border-gray-500 rounded px-1 py-0.5">
<TransactionAddress <TransactionAddress address={txData.from} />
address={txData.from}
resolvedAddresses={resolvedAddresses}
/>
</div> </div>
<div className="ml-5 space-y-3 self-stretch"> <div className="ml-5 space-y-3 self-stretch">
{traces.map((t, i, a) => ( {traces.map((t, i, a) => (
<TraceItem <TraceItem key={i} t={t} last={i === a.length - 1} />
key={i}
t={t}
last={i === a.length - 1}
fourBytesMap={sigMap}
resolvedAddresses={mergedResolvedAddresses}
/>
))} ))}
</div> </div>
</> </>

View File

@ -7,26 +7,19 @@ import FunctionSignature from "./FunctionSignature";
import InputDecoder from "./decoder/InputDecoder"; import InputDecoder from "./decoder/InputDecoder";
import ExpanderSwitch from "../components/ExpanderSwitch"; import ExpanderSwitch from "../components/ExpanderSwitch";
import { TraceEntry } from "../useErigonHooks"; import { TraceEntry } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import { import {
extract4Bytes, extract4Bytes,
FourBytesEntry, use4Bytes,
useTransactionDescription, useTransactionDescription,
} from "../use4Bytes"; } from "../use4Bytes";
type TraceInputProps = { type TraceInputProps = {
t: TraceEntry; t: TraceEntry;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const TraceInput: React.FC<TraceInputProps> = ({ const TraceInput: React.FC<TraceInputProps> = ({ t }) => {
t,
fourBytesMap,
resolvedAddresses,
}) => {
const raw4Bytes = extract4Bytes(t.input); const raw4Bytes = extract4Bytes(t.input);
const fourBytes = raw4Bytes !== null ? fourBytesMap[raw4Bytes] : null; const fourBytes = use4Bytes(raw4Bytes);
const sigText = const sigText =
raw4Bytes === null ? "<fallback>" : fourBytes?.name ?? raw4Bytes; raw4Bytes === null ? "<fallback>" : fourBytes?.name ?? raw4Bytes;
const hasParams = t.input.length > 10; const hasParams = t.input.length > 10;
@ -54,10 +47,7 @@ const TraceInput: React.FC<TraceInputProps> = ({
) : ( ) : (
<> <>
<span> <span>
<TransactionAddress <TransactionAddress address={t.to} />
address={t.to}
resolvedAddresses={resolvedAddresses}
/>
</span> </span>
{t.type !== "CREATE" && t.type !== "CREATE2" && ( {t.type !== "CREATE" && t.type !== "CREATE2" && (
<> <>
@ -93,7 +83,6 @@ const TraceInput: React.FC<TraceInputProps> = ({
data={t.input} data={t.input}
userMethod={undefined} userMethod={undefined}
devMethod={undefined} devMethod={undefined}
resolvedAddresses={resolvedAddresses}
/> />
</div> </div>
<div>)</div> <div>)</div>

View File

@ -3,24 +3,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faPlusSquare } from "@fortawesome/free-regular-svg-icons/faPlusSquare"; import { faPlusSquare } from "@fortawesome/free-regular-svg-icons/faPlusSquare";
import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare"; import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare";
import { Switch } from "@headlessui/react"; import { Switch } from "@headlessui/react";
import { FourBytesEntry } from "../use4Bytes";
import { TraceGroup } from "../useErigonHooks"; import { TraceGroup } from "../useErigonHooks";
import { ResolvedAddresses } from "../api/address-resolver";
import TraceInput from "./TraceInput"; import TraceInput from "./TraceInput";
type TraceItemProps = { type TraceItemProps = {
t: TraceGroup; t: TraceGroup;
last: boolean; last: boolean;
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const TraceItem: React.FC<TraceItemProps> = ({ const TraceItem: React.FC<TraceItemProps> = ({ t, last }) => {
t,
last,
fourBytesMap,
resolvedAddresses,
}) => {
const [expanded, setExpanded] = useState<boolean>(true); const [expanded, setExpanded] = useState<boolean>(true);
return ( return (
@ -42,11 +33,7 @@ const TraceItem: React.FC<TraceItemProps> = ({
/> />
</Switch> </Switch>
)} )}
<TraceInput <TraceInput t={t} />
t={t}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div> </div>
{t.children && ( {t.children && (
<div <div
@ -54,11 +41,7 @@ const TraceItem: React.FC<TraceItemProps> = ({
expanded ? "" : "hidden" expanded ? "" : "hidden"
}`} }`}
> >
<TraceChildren <TraceChildren c={t.children} />
c={t.children}
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
</div> </div>
)} )}
</> </>
@ -67,26 +50,16 @@ const TraceItem: React.FC<TraceItemProps> = ({
type TraceChildrenProps = { type TraceChildrenProps = {
c: TraceGroup[]; c: TraceGroup[];
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const TraceChildren: React.FC<TraceChildrenProps> = React.memo( const TraceChildren: React.FC<TraceChildrenProps> = React.memo(({ c }) => {
({ c, fourBytesMap, resolvedAddresses }) => { return (
return ( <>
<> {c.map((tc, i, a) => (
{c.map((tc, i, a) => ( <TraceItem key={i} t={tc} last={i === a.length - 1} />
<TraceItem ))}
key={i} </>
t={tc} );
last={i === a.length - 1} });
fourBytesMap={fourBytesMap}
resolvedAddresses={resolvedAddresses}
/>
))}
</>
);
}
);
export default TraceItem; export default TraceItem;

View File

@ -1,19 +1,14 @@
import React from "react"; import React from "react";
import TransactionAddress from "../../components/TransactionAddress"; import TransactionAddress from "../../components/TransactionAddress";
import Copy from "../../components/Copy"; import Copy from "../../components/Copy";
import { ResolvedAddresses } from "../../api/address-resolver";
type AddressDecoderProps = { type AddressDecoderProps = {
r: string; r: string;
resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const AddressDecoder: React.FC<AddressDecoderProps> = ({ const AddressDecoder: React.FC<AddressDecoderProps> = ({ r }) => (
r,
resolvedAddresses,
}) => (
<div className="flex items-baseline space-x-2 -ml-1 mr-3"> <div className="flex items-baseline space-x-2 -ml-1 mr-3">
<TransactionAddress address={r} resolvedAddresses={resolvedAddresses} /> <TransactionAddress address={r} />
<Copy value={r} /> <Copy value={r} />
</div> </div>
); );

View File

@ -8,7 +8,6 @@ import Uint256Decoder from "./Uint256Decoder";
import AddressDecoder from "./AddressDecoder"; import AddressDecoder from "./AddressDecoder";
import BooleanDecoder from "./BooleanDecoder"; import BooleanDecoder from "./BooleanDecoder";
import BytesDecoder from "./BytesDecoder"; import BytesDecoder from "./BytesDecoder";
import { ResolvedAddresses } from "../../api/address-resolver";
import SelectionHighlighter, { import SelectionHighlighter, {
valueSelector, valueSelector,
} from "../../components/SelectionHighlighter"; } from "../../components/SelectionHighlighter";
@ -20,7 +19,6 @@ type DecodedParamRowProps = {
paramType: ParamType; paramType: ParamType;
arrayElem?: number | undefined; arrayElem?: number | undefined;
help?: string | undefined; help?: string | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const DecodedParamRow: React.FC<DecodedParamRowProps> = ({ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
@ -30,7 +28,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
paramType, paramType,
arrayElem, arrayElem,
help, help,
resolvedAddresses,
}) => { }) => {
const [showHelp, setShowHelp] = useState<boolean>(false); const [showHelp, setShowHelp] = useState<boolean>(false);
@ -80,10 +77,7 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
{paramType.baseType === "uint256" ? ( {paramType.baseType === "uint256" ? (
<Uint256Decoder r={r} /> <Uint256Decoder r={r} />
) : paramType.baseType === "address" ? ( ) : paramType.baseType === "address" ? (
<AddressDecoder <AddressDecoder r={r.toString()} />
r={r.toString()}
resolvedAddresses={resolvedAddresses}
/>
) : paramType.baseType === "bool" ? ( ) : paramType.baseType === "bool" ? (
<BooleanDecoder r={r} /> <BooleanDecoder r={r} />
) : paramType.baseType === "bytes" ? ( ) : paramType.baseType === "bytes" ? (
@ -111,7 +105,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
i={idx} i={idx}
r={e} r={e}
paramType={paramType.components[idx]} paramType={paramType.components[idx]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
{paramType.baseType === "array" && {paramType.baseType === "array" &&
@ -122,7 +115,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
r={e} r={e}
paramType={paramType.arrayChildren} paramType={paramType.arrayChildren}
arrayElem={idx} arrayElem={idx}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</> </>

View File

@ -2,7 +2,6 @@ import React from "react";
import { ParamType, Result } from "@ethersproject/abi"; import { ParamType, Result } from "@ethersproject/abi";
import DecodedParamRow from "./DecodedParamRow"; import DecodedParamRow from "./DecodedParamRow";
import { DevMethod, UserMethod } from "../../sourcify/useSourcify"; import { DevMethod, UserMethod } from "../../sourcify/useSourcify";
import { ResolvedAddresses } from "../../api/address-resolver";
type DecodedParamsTableProps = { type DecodedParamsTableProps = {
args: Result; args: Result;
@ -10,7 +9,6 @@ type DecodedParamsTableProps = {
hasParamNames?: boolean; hasParamNames?: boolean;
userMethod?: UserMethod | undefined; userMethod?: UserMethod | undefined;
devMethod?: DevMethod | undefined; devMethod?: DevMethod | undefined;
resolvedAddresses?: ResolvedAddresses | undefined;
}; };
const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
@ -18,7 +16,6 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
paramTypes, paramTypes,
hasParamNames = true, hasParamNames = true,
devMethod, devMethod,
resolvedAddresses,
}) => ( }) => (
<table className="border w-full"> <table className="border w-full">
<thead> <thead>
@ -47,7 +44,6 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
r={r} r={r}
paramType={paramTypes[i]} paramType={paramTypes[i]}
help={devMethod?.params?.[paramTypes[i].name]} help={devMethod?.params?.[paramTypes[i].name]}
resolvedAddresses={resolvedAddresses}
/> />
))} ))}
</tbody> </tbody>

View File

@ -5,7 +5,6 @@ import { Tab } from "@headlessui/react";
import ModeTab from "../../components/ModeTab"; import ModeTab from "../../components/ModeTab";
import DecodedParamsTable from "./DecodedParamsTable"; import DecodedParamsTable from "./DecodedParamsTable";
import { DevMethod, UserMethod } from "../../sourcify/useSourcify"; import { DevMethod, UserMethod } from "../../sourcify/useSourcify";
import { ResolvedAddresses } from "../../api/address-resolver";
type InputDecoderProps = { type InputDecoderProps = {
fourBytes: string; fourBytes: string;
@ -14,7 +13,6 @@ type InputDecoderProps = {
data: string; data: string;
userMethod: UserMethod | undefined; userMethod: UserMethod | undefined;
devMethod: DevMethod | undefined; devMethod: DevMethod | undefined;
resolvedAddresses: ResolvedAddresses | undefined;
}; };
const InputDecoder: React.FC<InputDecoderProps> = ({ const InputDecoder: React.FC<InputDecoderProps> = ({
@ -24,7 +22,6 @@ const InputDecoder: React.FC<InputDecoderProps> = ({
data, data,
userMethod, userMethod,
devMethod, devMethod,
resolvedAddresses,
}) => { }) => {
const utfInput = useMemo(() => { const utfInput = useMemo(() => {
try { try {
@ -57,7 +54,6 @@ const InputDecoder: React.FC<InputDecoderProps> = ({
hasParamNames={hasParamNames} hasParamNames={hasParamNames}
userMethod={userMethod} userMethod={userMethod}
devMethod={devMethod} devMethod={devMethod}
resolvedAddresses={resolvedAddresses}
/> />
)} )}
</Tab.Panel> </Tab.Panel>

View File

@ -18,6 +18,11 @@ export const blockURL = (blockNum: BlockTag) => `/block/${blockNum}`;
export const blockTxsURL = (blockNum: BlockTag) => `/block/${blockNum}/txs`; export const blockTxsURL = (blockNum: BlockTag) => `/block/${blockNum}/txs`;
export const transactionURL = (txHash: string) => `/tx/${txHash}`;
export const addressByNonceURL = (address: ChecksummedAddress, nonce: number) =>
`/address/${address}?nonce=${nonce}`;
export enum SourcifySource { export enum SourcifySource {
// Resolve trusted IPNS for root IPFS // Resolve trusted IPNS for root IPFS
IPFS_IPNS, IPFS_IPNS,

View File

@ -1,27 +1,27 @@
import { useState, useEffect, useContext, useMemo } from "react"; import { useContext, useMemo } from "react";
import { import {
Fragment, Fragment,
Interface, Interface,
TransactionDescription, TransactionDescription,
} from "@ethersproject/abi"; } from "@ethersproject/abi";
import { BigNumberish } from "@ethersproject/bignumber";
import useSWRImmutable from "swr/immutable";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { fourBytesURL } from "./url"; import { fourBytesURL } from "./url";
import { BigNumberish } from "@ethersproject/bignumber";
export type FourBytesEntry = { export type FourBytesEntry = {
name: string; name: string;
signature: string | undefined; signature: string | undefined;
}; };
export type FourBytesMap = Record<string, FourBytesEntry | null | undefined>; /**
* Given a hex input data; extract the method selector
const simpleTransfer: FourBytesEntry = { *
name: "transfer", * @param rawInput Raw tx input including the "0x"
signature: undefined, * @returns the first 4 bytes, including the "0x" or null if the input
}; * contains an invalid selector, e.g., txs with 0x00 data; simple transfers (0x)
* return null as well as it is not a method selector
const fullCache = new Map<string, FourBytesEntry | null>(); */
export const extract4Bytes = (rawInput: string): string | null => { export const extract4Bytes = (rawInput: string): string | null => {
if (rawInput.length < 10) { if (rawInput.length < 10) {
return null; return null;
@ -29,8 +29,6 @@ export const extract4Bytes = (rawInput: string): string | null => {
return rawInput.slice(0, 10); return rawInput.slice(0, 10);
}; };
export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10);
const fetch4Bytes = async ( const fetch4Bytes = async (
assetsURLPrefix: string, assetsURLPrefix: string,
fourBytes: string fourBytes: string
@ -61,93 +59,59 @@ const fetch4Bytes = async (
} }
}; };
export const useBatch4Bytes = (
rawFourByteSigs: string[] | undefined
): FourBytesMap => {
const runtime = useContext(RuntimeContext);
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
const [fourBytesMap, setFourBytesMap] = useState<FourBytesMap>({});
useEffect(() => {
if (!rawFourByteSigs || assetsURLPrefix === undefined) {
setFourBytesMap({});
return;
}
const loadSigs = async () => {
const promises = rawFourByteSigs.map((s) =>
fetch4Bytes(assetsURLPrefix, s.slice(2))
);
const results = await Promise.all(promises);
const _fourBytesMap: Record<string, FourBytesEntry | null> = {};
for (let i = 0; i < rawFourByteSigs.length; i++) {
_fourBytesMap[rawFourByteSigs[i]] = results[i];
}
setFourBytesMap(_fourBytesMap);
};
loadSigs();
}, [assetsURLPrefix, rawFourByteSigs]);
return fourBytesMap;
};
/** /**
* Extract 4bytes DB info * Extract 4bytes DB info
* *
* @param rawFourBytes an hex string containing the 4bytes signature in the "0xXXXXXXXX" format. * @param rawFourBytes an hex string containing the 4bytes signature in the "0xXXXXXXXX" format.
*/ */
export const use4Bytes = ( export const use4Bytes = (
rawFourBytes: string rawFourBytes: string | null
): FourBytesEntry | null | undefined => { ): FourBytesEntry | null | undefined => {
if (rawFourBytes !== "0x") { if (rawFourBytes !== null && !rawFourBytes.startsWith("0x")) {
if (rawFourBytes.length !== 10 || !rawFourBytes.startsWith("0x")) { throw new Error(
throw new Error( `rawFourBytes must contain a bytes hex string starting with 0x; received value: "${rawFourBytes}"`
`rawFourBytes must contain a 4 bytes hex method signature starting with 0x; received value: "${rawFourBytes}"` );
);
}
} }
const runtime = useContext(RuntimeContext); const { config } = useContext(RuntimeContext);
const assetsURLPrefix = runtime.config?.assetsURLPrefix; const assetsURLPrefix = config?.assetsURLPrefix;
const fourBytes = rawFourBytes.slice(2); const fourBytesFetcher = (key: string | null) => {
const [entry, setEntry] = useState<FourBytesEntry | null | undefined>( if (key === null || key === "0x") {
fullCache.get(fourBytes) return undefined;
}
// Handle simple transfers with invalid selector like tx:
// 0x8bcbdcc1589b5c34c1e55909c8269a411f0267a4fed59a73dd4348cc71addbb9,
// which contains 0x00 as data
if (key.length !== 10) {
return undefined;
}
return fetch4Bytes(assetsURLPrefix!, key.slice(2));
};
const { data, error } = useSWRImmutable<FourBytesEntry | null | undefined>(
assetsURLPrefix !== undefined ? rawFourBytes : null,
fourBytesFetcher
); );
useEffect(() => { return error ? undefined : data;
if (assetsURLPrefix === undefined) { };
return;
}
if (fourBytes === "") {
return;
}
const loadSig = async () => { export const useMethodSelector = (data: string): [boolean, string, string] => {
const entry = await fetch4Bytes(assetsURLPrefix, fourBytes); const rawFourBytes = extract4Bytes(data);
fullCache.set(fourBytes, entry); const fourBytesEntry = use4Bytes(rawFourBytes);
setEntry(entry); const isSimpleTransfer = data === "0x";
}; const methodName = isSimpleTransfer
loadSig(); ? "transfer"
}, [fourBytes, assetsURLPrefix]); : fourBytesEntry?.name ?? rawFourBytes ?? "-";
const methodTitle = isSimpleTransfer
? "ETH Transfer"
: methodName === rawFourBytes
? methodName
: `${methodName} [${rawFourBytes}]`;
if (rawFourBytes === "0x") { return [isSimpleTransfer, methodName, methodTitle];
return simpleTransfer;
}
if (assetsURLPrefix === undefined) {
return undefined;
}
// Try to resolve 4bytes name
if (entry === null || entry === undefined) {
return entry;
}
// Simulates LRU
// TODO: implement LRU purging
fullCache.delete(fourBytes);
fullCache.set(fourBytes, entry);
return entry;
}; };
export const useTransactionDescription = ( export const useTransactionDescription = (

View File

@ -1,4 +1,4 @@
import { useState, useEffect, useMemo } from "react"; import { useState, useEffect } from "react";
import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider"; import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider";
import { JsonRpcProvider } from "@ethersproject/providers"; import { JsonRpcProvider } from "@ethersproject/providers";
import { getAddress } from "@ethersproject/address"; import { getAddress } from "@ethersproject/address";
@ -6,7 +6,7 @@ import { Contract } from "@ethersproject/contracts";
import { defaultAbiCoder } from "@ethersproject/abi"; import { defaultAbiCoder } from "@ethersproject/abi";
import { BigNumber } from "@ethersproject/bignumber"; import { BigNumber } from "@ethersproject/bignumber";
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes"; import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
import { extract4Bytes } from "./use4Bytes"; import useSWR, { useSWRConfig } from "swr";
import { getInternalOperations } from "./nodeFunctions"; import { getInternalOperations } from "./nodeFunctions";
import { import {
TokenMetas, TokenMetas,
@ -191,98 +191,107 @@ export const useTxData = (
} }
const readTxData = async () => { const readTxData = async () => {
const [_response, _receipt] = await Promise.all([ try {
provider.getTransaction(txhash), const [_response, _receipt] = await Promise.all([
provider.getTransactionReceipt(txhash), provider.getTransaction(txhash),
]); provider.getTransactionReceipt(txhash),
if (_response === null) { ]);
if (_response === null) {
setTxData(null);
return;
}
let _block: ExtendedBlock | undefined;
if (_response.blockNumber) {
_block = await readBlock(provider, _response.blockNumber.toString());
}
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
if (_receipt) {
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
}
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token] !== undefined) {
continue;
}
const erc20Contract = new Contract(t.token, erc20, provider);
try {
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
} catch (err) {
tokenMetas[t.token] = null;
console.warn(
`Couldn't get token ${t.token} metadata; ignoring`,
err
);
}
}
setTxData({
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
});
} catch (err) {
console.error(err);
setTxData(null); setTxData(null);
return;
} }
let _block: ExtendedBlock | undefined;
if (_response.blockNumber) {
_block = await readBlock(provider, _response.blockNumber.toString());
}
document.title = `Transaction ${_response.hash} | Otterscan`;
// Extract token transfers
const tokenTransfers: TokenTransfer[] = [];
if (_receipt) {
for (const l of _receipt.logs) {
if (l.topics.length !== 3) {
continue;
}
if (l.topics[0] !== TRANSFER_TOPIC) {
continue;
}
tokenTransfers.push({
token: l.address,
from: getAddress(hexDataSlice(arrayify(l.topics[1]), 12)),
to: getAddress(hexDataSlice(arrayify(l.topics[2]), 12)),
value: BigNumber.from(l.data),
});
}
}
// Extract token meta
const tokenMetas: TokenMetas = {};
for (const t of tokenTransfers) {
if (tokenMetas[t.token] !== undefined) {
continue;
}
const erc20Contract = new Contract(t.token, erc20, provider);
try {
const [name, symbol, decimals] = await Promise.all([
erc20Contract.name(),
erc20Contract.symbol(),
erc20Contract.decimals(),
]);
tokenMetas[t.token] = {
name,
symbol,
decimals,
};
} catch (err) {
tokenMetas[t.token] = null;
console.warn(`Couldn't get token ${t.token} metadata; ignoring`, err);
}
}
setTxData({
transactionHash: _response.hash,
from: _response.from,
to: _response.to,
value: _response.value,
tokenTransfers,
tokenMetas,
type: _response.type ?? 0,
maxFeePerGas: _response.maxFeePerGas,
maxPriorityFeePerGas: _response.maxPriorityFeePerGas,
gasPrice: _response.gasPrice!,
gasLimit: _response.gasLimit,
nonce: _response.nonce,
data: _response.data,
confirmedData:
_receipt === null
? undefined
: {
status: _receipt.status === 1,
blockNumber: _receipt.blockNumber,
transactionIndex: _receipt.transactionIndex,
blockBaseFeePerGas: _block!.baseFeePerGas,
blockTransactionCount: _block!.transactionCount,
confirmations: _receipt.confirmations,
timestamp: _block!.timestamp,
miner: _block!.miner,
createdContractAddress: _receipt.contractAddress,
fee: _response.gasPrice!.mul(_receipt.gasUsed),
gasUsed: _receipt.gasUsed,
logs: _receipt.logs,
},
});
}; };
readTxData(); readTxData();
}, [provider, txhash]); }, [provider, txhash]);
@ -408,46 +417,6 @@ export const useTraceTransaction = (
return traceGroups; return traceGroups;
}; };
/**
* Flatten a trace tree and extract and dedup 4byte function signatures
*/
export const useUniqueSignatures = (traces: TraceGroup[] | undefined) => {
const uniqueSignatures = useMemo(() => {
if (!traces) {
return undefined;
}
const sigs = new Set<string>();
let nextTraces: TraceGroup[] = [...traces];
while (nextTraces.length > 0) {
const traces = nextTraces;
nextTraces = [];
for (const t of traces) {
if (
t.type === "CALL" ||
t.type === "DELEGATECALL" ||
t.type === "STATICCALL" ||
t.type === "CALLCODE"
) {
const fourBytes = extract4Bytes(t.input);
if (fourBytes) {
sigs.add(fourBytes);
}
}
if (t.children) {
nextTraces.push(...t.children);
}
}
}
return [...sigs];
}, [traces]);
return uniqueSignatures;
};
const hasCode = async ( const hasCode = async (
provider: JsonRpcProvider, provider: JsonRpcProvider,
address: ChecksummedAddress address: ChecksummedAddress
@ -543,3 +512,91 @@ export const useTransactionError = (
return [errorMsg, data, isCustomError]; return [errorMsg, data, isCustomError];
}; };
export const useTransactionCount = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined
): number | undefined => {
const { data, error } = useSWR(
provider && sender ? { provider, sender } : null,
async ({ provider, sender }): Promise<number | undefined> =>
provider.getTransactionCount(sender)
);
if (error) {
return undefined;
}
return data;
};
type TransactionBySenderAndNonceKey = {
network: number;
sender: ChecksummedAddress;
nonce: number;
};
const getTransactionBySenderAndNonceFetcher =
(provider: JsonRpcProvider) =>
async ({
network,
sender,
nonce,
}: TransactionBySenderAndNonceKey): Promise<string | null | undefined> => {
if (nonce < 0) {
return undefined;
}
const result = (await provider.send("ots_getTransactionBySenderAndNonce", [
sender,
nonce,
])) as string;
// Empty or success
return result;
};
export const prefetchTransactionBySenderAndNonce = (
{ mutate }: ReturnType<typeof useSWRConfig>,
provider: JsonRpcProvider,
sender: ChecksummedAddress,
nonce: number
) => {
const key: TransactionBySenderAndNonceKey = {
network: provider.network.chainId,
sender,
nonce,
};
mutate(key, (curr: any) => {
if (curr) {
return curr;
}
return getTransactionBySenderAndNonceFetcher(provider)(key);
});
// }
};
export const useTransactionBySenderAndNonce = (
provider: JsonRpcProvider | undefined,
sender: ChecksummedAddress | undefined,
nonce: number | undefined
): string | null | undefined => {
const { data, error } = useSWR<
string | null | undefined,
any,
TransactionBySenderAndNonceKey | null
>(
provider && sender && nonce !== undefined
? {
network: provider.network.chainId,
sender,
nonce,
}
: null,
getTransactionBySenderAndNonceFetcher(provider!)
);
if (error) {
return undefined;
}
return data;
};

View File

@ -1,16 +1,13 @@
import { useState, useEffect, useRef, useContext } from "react"; import { useState, useEffect, useContext } from "react";
import { JsonRpcProvider } from "@ethersproject/providers"; import { BaseProvider } from "@ethersproject/providers";
import { getAddress, isAddress } from "@ethersproject/address"; import { getAddress, isAddress } from "@ethersproject/address";
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver"; import useSWRImmutable from "swr/immutable";
import { TraceGroup } from "./useErigonHooks"; import { mainResolver } from "./api/address-resolver";
import { SelectedResolvedName } from "./api/address-resolver/CompositeAddressResolver";
import { RuntimeContext } from "./useRuntime"; import { RuntimeContext } from "./useRuntime";
import { import { ChecksummedAddress } from "./types";
ChecksummedAddress,
ProcessedTransaction,
TransactionData,
} from "./types";
export const useAddressOrENSFromURL = ( export const useAddressOrENS = (
addressOrName: string, addressOrName: string,
urlFixer: (address: ChecksummedAddress) => void urlFixer: (address: ChecksummedAddress) => void
): [ ): [
@ -69,118 +66,22 @@ export const useAddressOrENSFromURL = (
return [checksummedAddress, isENS, error]; return [checksummedAddress, isENS, error];
}; };
export type AddressCollector = () => string[]; export const useResolvedAddress = (
provider: BaseProvider | undefined,
export const pageCollector = address: ChecksummedAddress
(page: ProcessedTransaction[] | undefined): AddressCollector => ): SelectedResolvedName<any> | undefined => {
() => { const fetcher = async (
if (!page) { key: string
return []; ): Promise<SelectedResolvedName<any> | undefined> => {
if (!provider) {
return undefined;
} }
return mainResolver.resolveAddress(provider, address);
const uniqueAddresses = new Set<string>();
for (const tx of page) {
if (tx.from) {
uniqueAddresses.add(tx.from);
}
if (tx.to) {
uniqueAddresses.add(tx.to);
}
if (tx.createdContractAddress) {
uniqueAddresses.add(tx.createdContractAddress);
}
}
return Array.from(uniqueAddresses);
}; };
export const transactionDataCollector = const { data, error } = useSWRImmutable(address, fetcher);
(txData: TransactionData | null | undefined): AddressCollector => if (error) {
() => { return undefined;
if (!txData) { }
return []; return data;
}
const uniqueAddresses = new Set<string>();
// Standard fields
uniqueAddresses.add(txData.from);
if (txData.to) {
uniqueAddresses.add(txData.to);
}
if (txData.confirmedData?.createdContractAddress) {
uniqueAddresses.add(txData.confirmedData?.createdContractAddress);
}
// Dig token transfers
for (const t of txData.tokenTransfers) {
uniqueAddresses.add(t.from);
uniqueAddresses.add(t.to);
uniqueAddresses.add(t.token);
}
// Dig log addresses
if (txData.confirmedData) {
for (const l of txData.confirmedData.logs) {
uniqueAddresses.add(l.address);
// TODO: find a way to dig over decoded address log attributes
}
}
return Array.from(uniqueAddresses);
};
export const tracesCollector =
(traces: TraceGroup[] | undefined): AddressCollector =>
() => {
if (traces === undefined) {
return [];
}
const uniqueAddresses = new Set<string>();
let searchTraces = [...traces];
while (searchTraces.length > 0) {
const nextSearch: TraceGroup[] = [];
for (const g of searchTraces) {
uniqueAddresses.add(g.from);
uniqueAddresses.add(g.to);
if (g.children) {
nextSearch.push(...g.children);
}
}
searchTraces = nextSearch;
}
return Array.from(uniqueAddresses);
};
export const useResolvedAddresses = (
provider: JsonRpcProvider | undefined,
addrCollector: AddressCollector
) => {
const [names, setNames] = useState<ResolvedAddresses>();
const ref = useRef<ResolvedAddresses | undefined>();
useEffect(() => {
ref.current = names;
});
useEffect(
() => {
if (!provider) {
return;
}
const populate = async () => {
const _addresses = addrCollector();
const _names = await batchPopulate(provider, _addresses, ref.current);
setNames(_names);
};
populate();
},
// DON'T put names variables in dependency array; this is intentional; useRef
[provider, addrCollector]
);
return names;
}; };

2
topic0

@ -1 +1 @@
Subproject commit 63794c46467dea47fd99ec47db745c482887367e Subproject commit cb6abe87055d2e2d54ba6a985903031420c4cbb1

@ -1 +1 @@
Subproject commit e779c7b400fc479f8442066f13565555be5bfcf3 Subproject commit 0e2488c4b4c366c0ed54d5d85b2feaa0f0940b05