Merge branch 'release/v2022.01.03-otterscan'
This commit is contained in:
commit
35fb50df92
|
@ -1,9 +1,12 @@
|
|||
[submodule "4bytes"]
|
||||
path = 4bytes
|
||||
url = https://github.com/ethereum-lists/4bytes.git
|
||||
ignore = dirty
|
||||
[submodule "trustwallet"]
|
||||
path = trustwallet
|
||||
url = https://github.com/trustwallet/assets.git
|
||||
ignore = dirty
|
||||
[submodule "topic0"]
|
||||
path = topic0
|
||||
url = https://github.com/wmitsuda/topic0.git
|
||||
ignore = dirty
|
||||
|
|
2
4bytes
2
4bytes
|
@ -1 +1 @@
|
|||
Subproject commit 20537524bfb01bee859c9cfa9a8784baacbcc7ae
|
||||
Subproject commit 0ee722e516c91dc6a3de01c26ea06955123eeddb
|
88
README.md
88
README.md
|
@ -56,93 +56,15 @@ However, you will see that we made many UI improvements.
|
|||
|
||||
## 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.
|
||||
|
||||
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).
|
||||
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).
|
||||
|
||||
## Kudos (in no particular order)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
```
|
|
@ -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.
|
|
@ -11,7 +11,7 @@
|
|||
"dependencies": {
|
||||
"@blackbox-vision/react-qr-reader": "^5.0.0",
|
||||
"@chainlink/contracts": "^0.2.2",
|
||||
"@craco/craco": "^6.4.2",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fontsource/fira-code": "^4.5.2",
|
||||
"@fontsource/roboto": "^4.5.1",
|
||||
"@fontsource/roboto-mono": "^4.5.0",
|
||||
|
@ -26,15 +26,15 @@
|
|||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^14.17.5",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/node": "^16.11.14",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-blockies": "^1.4.1",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-highlight": "^0.12.5",
|
||||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"chart.js": "^3.6.2",
|
||||
"chart.js": "^3.7.0",
|
||||
"ethers": "^5.5.2",
|
||||
"highlightjs-solidity": "^2.0.2",
|
||||
"highlightjs-solidity": "^2.0.3",
|
||||
"react": "^17.0.2",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-chartjs-2": "^4.0.0",
|
||||
|
@ -42,12 +42,13 @@
|
|||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.1.2",
|
||||
"react-image": "^4.0.3",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"serve": "^13.0.2",
|
||||
"typescript": "^4.5.2",
|
||||
"use-keyboard-shortcut": "^1.0.6",
|
||||
"swr": "^1.1.2",
|
||||
"typescript": "^4.5.4",
|
||||
"use-keyboard-shortcut": "^1.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1247,11 +1248,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@craco/craco": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz",
|
||||
"integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==",
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.3.tgz",
|
||||
"integrity": "sha512-RzkXYmNzRCGUyG7mM+IUMM+nvrpSfA34352sPSGQN76UivAmCAht3sI4v5JKgzO05oUK9Zwi6abCKD7iKXI8hQ==",
|
||||
"dependencies": {
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"cosmiconfig-typescript-loader": "^1.0.0",
|
||||
"cross-spawn": "^7.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"semver": "^7.3.2",
|
||||
|
@ -1267,6 +1269,30 @@
|
|||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
|
||||
|
@ -1282,6 +1308,24 @@
|
|||
"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": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
|
@ -1296,6 +1340,65 @@
|
|||
"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": {
|
||||
"version": "1.4.0",
|
||||
"license": "CC0-1.0",
|
||||
|
@ -2965,6 +3068,26 @@
|
|||
"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": {
|
||||
"version": "1.3.1",
|
||||
"license": "MIT"
|
||||
|
@ -3085,9 +3208,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "14.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz",
|
||||
"integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA=="
|
||||
"version": "16.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.14.tgz",
|
||||
"integrity": "sha512-mK6BKLpL0bG6v2CxHbm0ed6RcZrAtTHBTd/ZpnlVPVa3HkumsqLE4BC4u6TQ8D7pnrRbOU0am6epuALs+Ncnzw=="
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
"version": "2.4.0",
|
||||
|
@ -3110,9 +3233,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "17.0.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz",
|
||||
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==",
|
||||
"version": "17.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
|
||||
"integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
@ -5566,9 +5689,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
|
||||
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
|
||||
"integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
|
||||
},
|
||||
"node_modules/check-types": {
|
||||
"version": "11.1.2",
|
||||
|
@ -6171,9 +6294,7 @@
|
|||
"node_modules/create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
|
@ -6911,8 +7032,6 @@
|
|||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
|
@ -9243,14 +9362,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/highlightjs-solidity": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.2.tgz",
|
||||
"integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ=="
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.3.tgz",
|
||||
"integrity": "sha512-tjFm5dtIE61VQBzjlZmkCtY5fLs3CaEABbVuUNyXeW+UuOCsxMg3MsPFy0kCelHP74hPpkoqDejLrbnV1axAIw=="
|
||||
},
|
||||
"node_modules/history": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz",
|
||||
"integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
|
||||
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
|
@ -11430,9 +11549,7 @@
|
|||
"node_modules/make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
"version": "1.0.11",
|
||||
|
@ -14503,23 +14620,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz",
|
||||
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
|
||||
"integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
|
||||
"dependencies": {
|
||||
"history": "^5.1.0"
|
||||
"history": "^5.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz",
|
||||
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
|
||||
"integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
|
||||
"dependencies": {
|
||||
"history": "^5.1.0",
|
||||
"react-router": "6.0.2"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8",
|
||||
|
@ -17089,6 +17206,14 @@
|
|||
"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": {
|
||||
"version": "3.2.4",
|
||||
"license": "MIT"
|
||||
|
@ -18005,9 +18130,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
|
||||
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
@ -18248,9 +18373,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/use-keyboard-shortcut": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.0.6.tgz",
|
||||
"integrity": "sha512-xdH5+XZ6fpey4Cvuh/T0q0tBYu9RGlhg1Xf2W6THtGi5CnI/M90kVaCpfy/Ew8ZDyJnUEwSh4gYolVI2QtgciQ==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.1.2.tgz",
|
||||
"integrity": "sha512-VrAu1avPLuFHShGo1RiPtCZ6htwsnLRlZc/w4+jmK99HCvIGG5WThz1KsoGgP8KCxP5c8a+pDjtnCgBg+3bXzA==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0",
|
||||
"react-dom": "^16.8.0"
|
||||
|
@ -19449,8 +19574,6 @@
|
|||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
|
@ -20296,17 +20419,33 @@
|
|||
}
|
||||
},
|
||||
"@craco/craco": {
|
||||
"version": "6.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.2.tgz",
|
||||
"integrity": "sha512-egIooyvuzKM5dsvWe/U5ISyFpZwLnG9uuTF1fU4s/6b/hE8MvoxyaxKymQKgbtpfOZeH0ebtEP4cbH7xZ4XRbw==",
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@craco/craco/-/craco-6.4.3.tgz",
|
||||
"integrity": "sha512-RzkXYmNzRCGUyG7mM+IUMM+nvrpSfA34352sPSGQN76UivAmCAht3sI4v5JKgzO05oUK9Zwi6abCKD7iKXI8hQ==",
|
||||
"requires": {
|
||||
"cosmiconfig": "^7.0.1",
|
||||
"cosmiconfig-typescript-loader": "^1.0.0",
|
||||
"cross-spawn": "^7.0.0",
|
||||
"lodash": "^4.17.15",
|
||||
"semver": "^7.3.2",
|
||||
"webpack-merge": "^4.2.2"
|
||||
},
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz",
|
||||
|
@ -20319,6 +20458,15 @@
|
|||
"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": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
|
@ -20326,9 +20474,41 @@
|
|||
"requires": {
|
||||
"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": {
|
||||
"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": {
|
||||
"version": "1.3.1"
|
||||
},
|
||||
|
@ -21423,9 +21623,9 @@
|
|||
"version": "3.0.3"
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.17.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.17.5.tgz",
|
||||
"integrity": "sha512-bjqH2cX/O33jXT/UmReo2pM7DIJREPMnarixbQ57DOOzzFaI6D2+IcwaJQaJpv0M1E9TIhPCYVxrkcityLjlqA=="
|
||||
"version": "16.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.14.tgz",
|
||||
"integrity": "sha512-mK6BKLpL0bG6v2CxHbm0ed6RcZrAtTHBTd/ZpnlVPVa3HkumsqLE4BC4u6TQ8D7pnrRbOU0am6epuALs+Ncnzw=="
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.0"
|
||||
|
@ -21443,9 +21643,9 @@
|
|||
"version": "1.5.4"
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "17.0.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.37.tgz",
|
||||
"integrity": "sha512-2FS1oTqBGcH/s0E+CjrCCR9+JMpsu9b69RTFO+40ua43ZqP5MmQ4iUde/dMjWR909KxZwmOQIFq6AV6NjEG5xg==",
|
||||
"version": "17.0.38",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz",
|
||||
"integrity": "sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
@ -23148,9 +23348,9 @@
|
|||
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.6.2.tgz",
|
||||
"integrity": "sha512-Xz7f/fgtVltfQYWq0zL1Xbv7N2inpG+B54p3D5FSvpCdy3sM+oZhbqa42eNuYXltaVvajgX5UpKCU2GeeJIgxg=="
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
|
||||
"integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
|
||||
},
|
||||
"check-types": {
|
||||
"version": "11.1.2"
|
||||
|
@ -23580,9 +23780,7 @@
|
|||
"create-require": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ=="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
|
@ -24066,9 +24264,7 @@
|
|||
"diff": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
|
||||
},
|
||||
"diff-sequences": {
|
||||
"version": "26.6.2"
|
||||
|
@ -25601,14 +25797,14 @@
|
|||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
|
||||
},
|
||||
"highlightjs-solidity": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.2.tgz",
|
||||
"integrity": "sha512-q0aYUKiZ9MPQg41qx/KpXKaCpqql50qTvmwGYyLFfcjt9AE/+C9CwjVIdJZc7EYj6NGgJuFJ4im1gfgrzUU1fQ=="
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/highlightjs-solidity/-/highlightjs-solidity-2.0.3.tgz",
|
||||
"integrity": "sha512-tjFm5dtIE61VQBzjlZmkCtY5fLs3CaEABbVuUNyXeW+UuOCsxMg3MsPFy0kCelHP74hPpkoqDejLrbnV1axAIw=="
|
||||
},
|
||||
"history": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.1.0.tgz",
|
||||
"integrity": "sha512-zPuQgPacm2vH2xdORvGGz1wQMuHSIB56yNAy5FnLuwOwgSYyPKptJtcMm6Ev+hRGeS+GzhbmRacHzvlESbFwDg==",
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/history/-/history-5.2.0.tgz",
|
||||
"integrity": "sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.7.6"
|
||||
}
|
||||
|
@ -27026,9 +27222,7 @@
|
|||
"make-error": {
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw=="
|
||||
},
|
||||
"makeerror": {
|
||||
"version": "1.0.11",
|
||||
|
@ -29093,20 +29287,20 @@
|
|||
"version": "0.8.3"
|
||||
},
|
||||
"react-router": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.0.2.tgz",
|
||||
"integrity": "sha512-8/Wm3Ed8t7TuedXjAvV39+c8j0vwrI5qVsYqjFr5WkJjsJpEvNSoLRUbtqSEYzqaTUj1IV+sbPJxvO+accvU0Q==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz",
|
||||
"integrity": "sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg==",
|
||||
"requires": {
|
||||
"history": "^5.1.0"
|
||||
"history": "^5.2.0"
|
||||
}
|
||||
},
|
||||
"react-router-dom": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.0.2.tgz",
|
||||
"integrity": "sha512-cOpJ4B6raFutr0EG8O/M2fEoyQmwvZWomf1c6W2YXBZuFBx8oTk/zqjXghwScyhfrtnt0lANXV2182NQblRxFA==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz",
|
||||
"integrity": "sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA==",
|
||||
"requires": {
|
||||
"history": "^5.1.0",
|
||||
"react-router": "6.0.2"
|
||||
"history": "^5.2.0",
|
||||
"react-router": "6.2.1"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "3.2.4"
|
||||
},
|
||||
|
@ -31512,9 +31712,9 @@
|
|||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.5.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz",
|
||||
"integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw=="
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz",
|
||||
"integrity": "sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg=="
|
||||
},
|
||||
"unicode-canonical-property-names-ecmascript": {
|
||||
"version": "1.0.4"
|
||||
|
@ -31664,9 +31864,9 @@
|
|||
"version": "3.1.1"
|
||||
},
|
||||
"use-keyboard-shortcut": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.0.6.tgz",
|
||||
"integrity": "sha512-xdH5+XZ6fpey4Cvuh/T0q0tBYu9RGlhg1Xf2W6THtGi5CnI/M90kVaCpfy/Ew8ZDyJnUEwSh4gYolVI2QtgciQ==",
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/use-keyboard-shortcut/-/use-keyboard-shortcut-1.1.2.tgz",
|
||||
"integrity": "sha512-VrAu1avPLuFHShGo1RiPtCZ6htwsnLRlZc/w4+jmK99HCvIGG5WThz1KsoGgP8KCxP5c8a+pDjtnCgBg+3bXzA==",
|
||||
"requires": {}
|
||||
},
|
||||
"util": {
|
||||
|
@ -32513,9 +32713,7 @@
|
|||
"yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
|
||||
},
|
||||
"yocto-queue": {
|
||||
"version": "0.1.0"
|
||||
|
|
17
package.json
17
package.json
|
@ -6,7 +6,7 @@
|
|||
"dependencies": {
|
||||
"@blackbox-vision/react-qr-reader": "^5.0.0",
|
||||
"@chainlink/contracts": "^0.2.2",
|
||||
"@craco/craco": "^6.4.2",
|
||||
"@craco/craco": "^6.4.3",
|
||||
"@fontsource/fira-code": "^4.5.2",
|
||||
"@fontsource/roboto": "^4.5.1",
|
||||
"@fontsource/roboto-mono": "^4.5.0",
|
||||
|
@ -21,15 +21,15 @@
|
|||
"@testing-library/react": "^11.1.0",
|
||||
"@testing-library/user-event": "^12.1.10",
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/node": "^14.17.5",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/node": "^16.11.14",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/react-blockies": "^1.4.1",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-highlight": "^0.12.5",
|
||||
"@types/react-syntax-highlighter": "^13.5.2",
|
||||
"chart.js": "^3.6.2",
|
||||
"chart.js": "^3.7.0",
|
||||
"ethers": "^5.5.2",
|
||||
"highlightjs-solidity": "^2.0.2",
|
||||
"highlightjs-solidity": "^2.0.3",
|
||||
"react": "^17.0.2",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-chartjs-2": "^4.0.0",
|
||||
|
@ -37,12 +37,13 @@
|
|||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.1.2",
|
||||
"react-image": "^4.0.3",
|
||||
"react-router-dom": "^6.0.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-scripts": "4.0.3",
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"serve": "^13.0.2",
|
||||
"typescript": "^4.5.2",
|
||||
"use-keyboard-shortcut": "^1.0.6",
|
||||
"swr": "^1.1.2",
|
||||
"typescript": "^4.5.4",
|
||||
"use-keyboard-shortcut": "^1.1.2",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
|||
import { faQuestionCircle } from "@fortawesome/free-regular-svg-icons/faQuestionCircle";
|
||||
import StandardFrame from "./StandardFrame";
|
||||
import StandardSubtitle from "./StandardSubtitle";
|
||||
import AddressOrENSNameNotFound from "./components/AddressOrENSNameNotFound";
|
||||
import Copy from "./components/Copy";
|
||||
import NavTab from "./components/NavTab";
|
||||
import SourcifyLogo from "./sourcify/SourcifyLogo";
|
||||
|
@ -20,12 +21,19 @@ import AddressTransactionResults from "./address/AddressTransactionResults";
|
|||
import Contracts from "./address/Contracts";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { useAppConfigContext } from "./useAppConfig";
|
||||
import { useAddressOrENSFromURL } from "./useResolvedAddresses";
|
||||
import { useAddressOrENS } from "./useResolvedAddresses";
|
||||
import { useMultipleMetadata } from "./sourcify/useSourcify";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
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 { addressOrName, direction } = useParams();
|
||||
if (addressOrName === undefined) {
|
||||
|
@ -45,7 +53,7 @@ const AddressTransactions: React.FC = () => {
|
|||
},
|
||||
[navigate, direction, searchParams]
|
||||
);
|
||||
const [checksummedAddress, isENS, error] = useAddressOrENSFromURL(
|
||||
const [checksummedAddress, isENS, error] = useAddressOrENS(
|
||||
addressOrName,
|
||||
urlFixer
|
||||
);
|
||||
|
@ -78,12 +86,21 @@ const AddressTransactions: React.FC = () => {
|
|||
? metadatas[checksummedAddress]
|
||||
: undefined;
|
||||
|
||||
// Search address by nonce === transaction @ nonce
|
||||
const rawNonce = searchParams.get("nonce");
|
||||
if (rawNonce !== null) {
|
||||
return (
|
||||
<AddressTransactionByNonce
|
||||
checksummedAddress={checksummedAddress}
|
||||
rawNonce={rawNonce}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardFrame>
|
||||
{error ? (
|
||||
<span className="text-base">
|
||||
"{addressOrName}" is not an ETH address or ENS name.
|
||||
</span>
|
||||
<AddressOrENSNameNotFound addressOrENSName={addressOrName} />
|
||||
) : (
|
||||
checksummedAddress && (
|
||||
<>
|
||||
|
@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export default AddressTransactions;
|
||||
export default Address;
|
|
@ -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;
|
16
src/App.tsx
16
src/App.tsx
|
@ -17,11 +17,9 @@ const BlockTransactions = React.lazy(
|
|||
/* webpackChunkName: "blocktxs", webpackPrefetch: true */ "./BlockTransactions"
|
||||
)
|
||||
);
|
||||
const AddressTransactions = React.lazy(
|
||||
const Address = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "address", webpackPrefetch: true */ "./AddressTransactions"
|
||||
)
|
||||
import(/* webpackChunkName: "address", webpackPrefetch: true */ "./Address")
|
||||
);
|
||||
const Transaction = React.lazy(
|
||||
() =>
|
||||
|
@ -33,6 +31,12 @@ const London = React.lazy(
|
|||
/* webpackChunkName: "london", webpackPrefetch: true */ "./special/london/London"
|
||||
)
|
||||
);
|
||||
const PageNotFound = React.lazy(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "notfound", webpackPrefetch: true */ "./PageNotFound"
|
||||
)
|
||||
);
|
||||
|
||||
const App = () => {
|
||||
const runtime = useRuntime();
|
||||
|
@ -61,9 +65,9 @@ const App = () => {
|
|||
<Route path="tx/:txhash/*" element={<Transaction />} />
|
||||
<Route
|
||||
path="address/:addressOrName/*"
|
||||
element={<AddressTransactions />}
|
||||
element={<Address />}
|
||||
/>
|
||||
<Route path="*" element={<Home />} />
|
||||
<Route path="*" element={<PageNotFound />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</Router>
|
||||
|
|
|
@ -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;
|
|
@ -10,13 +10,11 @@ import {
|
|||
TokenMeta,
|
||||
TokenTransfer,
|
||||
} from "./types";
|
||||
import { ResolvedAddresses } from "./api/address-resolver";
|
||||
import { Metadata } from "./sourcify/useSourcify";
|
||||
|
||||
type TokenTransferItemProps = {
|
||||
t: TokenTransfer;
|
||||
tokenMeta?: TokenMeta | null | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
|
||||
};
|
||||
|
||||
|
@ -24,7 +22,6 @@ type TokenTransferItemProps = {
|
|||
const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
||||
t,
|
||||
tokenMeta,
|
||||
resolvedAddresses,
|
||||
metadatas,
|
||||
}) => (
|
||||
<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
|
||||
address={t.from}
|
||||
addressCtx={AddressContext.FROM}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[t.from]}
|
||||
/>
|
||||
</div>
|
||||
|
@ -46,7 +42,6 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
|||
<TransactionAddress
|
||||
address={t.to}
|
||||
addressCtx={AddressContext.TO}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[t.to]}
|
||||
/>
|
||||
</div>
|
||||
|
@ -60,11 +55,7 @@ const TokenTransferItem: React.FC<TokenTransferItemProps> = ({
|
|||
/>
|
||||
</ValueHighlighter>
|
||||
</span>
|
||||
<TransactionAddress
|
||||
address={t.token}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[t.token]}
|
||||
/>
|
||||
<TransactionAddress address={t.token} metadata={metadatas[t.token]} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,154 +1,13 @@
|
|||
import React, { useMemo, useContext } from "react";
|
||||
import { useParams, Routes, Route } 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 { 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"
|
||||
)
|
||||
);
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import TransactionPageContent from "./TransactionPageContent";
|
||||
|
||||
const Transaction: React.FC = () => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const { txhash } = useParams();
|
||||
if (txhash === undefined) {
|
||||
throw new Error("txhash couldn't be undefined here");
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
return <TransactionPageContent txHash={txhash} />;
|
||||
};
|
||||
|
||||
export default Transaction;
|
||||
|
|
|
@ -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;
|
|
@ -10,7 +10,6 @@ import { useFeeToggler } from "../search/useFeeToggler";
|
|||
import { SelectionContext, useSelection } from "../useSelection";
|
||||
import { useMultipleETHUSDOracle } from "../usePriceOracle";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
|
@ -102,10 +101,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
}, [page]);
|
||||
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
|
||||
const addresses = useMemo(() => {
|
||||
const _addresses = [address];
|
||||
|
@ -137,8 +132,8 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
address={address}
|
||||
isFirst={controller?.isFirst}
|
||||
isLast={controller?.isLast}
|
||||
prevHash={page ? page[0].hash : ""}
|
||||
nextHash={page ? page[page.length - 1].hash : ""}
|
||||
prevHash={page?.[0]?.hash ?? ""}
|
||||
nextHash={page?.[page.length - 1]?.hash ?? ""}
|
||||
disabled={controller === undefined}
|
||||
/>
|
||||
</div>
|
||||
|
@ -152,7 +147,6 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
<TransactionItem
|
||||
key={tx.hash}
|
||||
tx={tx}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
selectedAddress={address}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
|
@ -171,8 +165,8 @@ const AddressTransactionResults: React.FC<AddressTransactionResultsProps> = ({
|
|||
address={address}
|
||||
isFirst={controller?.isFirst}
|
||||
isLast={controller?.isLast}
|
||||
prevHash={page ? page[0].hash : ""}
|
||||
nextHash={page ? page[page.length - 1].hash : ""}
|
||||
prevHash={page?.[0]?.hash ?? ""}
|
||||
nextHash={page?.[page.length - 1]?.hash ?? ""}
|
||||
disabled={controller === undefined}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { ensRenderer } from "../../components/ENSName";
|
||||
import { plainStringRenderer } from "../../components/PlainString";
|
||||
import { tokenRenderer } from "../../components/TokenName";
|
||||
|
@ -48,30 +47,3 @@ resolverRendererRegistry.set(uniswapV2Resolver, uniswapV2PairRenderer);
|
|||
resolverRendererRegistry.set(uniswapV3Resolver, uniswapV3PairRenderer);
|
||||
resolverRendererRegistry.set(ercTokenResolver, tokenRenderer);
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ import TransactionItem from "../search/TransactionItem";
|
|||
import { useFeeToggler } from "../search/useFeeToggler";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { SelectionContext, useSelection } from "../useSelection";
|
||||
import { pageCollector, useResolvedAddresses } from "../useResolvedAddresses";
|
||||
import { ChecksummedAddress, ProcessedTransaction } from "../types";
|
||||
import { PAGE_SIZE } from "../params";
|
||||
import { useMultipleETHUSDOracle } from "../usePriceOracle";
|
||||
|
@ -30,8 +29,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
|||
const { provider } = useContext(RuntimeContext);
|
||||
const selectionCtx = useSelection();
|
||||
const [feeDisplay, feeDisplayToggler] = useFeeToggler();
|
||||
const addrCollector = useMemo(() => pageCollector(page), [page]);
|
||||
const resolvedAddresses = useResolvedAddresses(provider, addrCollector);
|
||||
const blockTags = useMemo(() => [blockTag], [blockTag]);
|
||||
const priceMap = useMultipleETHUSDOracle(provider, blockTags);
|
||||
|
||||
|
@ -79,7 +76,6 @@ const BlockTransactionResults: React.FC<BlockTransactionResultsProps> = ({
|
|||
<TransactionItem
|
||||
key={tx.hash}
|
||||
tx={tx}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
feeDisplay={feeDisplay}
|
||||
priceMap={priceMap}
|
||||
metadatas={metadatas}
|
||||
|
|
|
@ -1,24 +1,23 @@
|
|||
import React from "react";
|
||||
import {
|
||||
ResolvedAddresses,
|
||||
resolverRendererRegistry,
|
||||
} from "../api/address-resolver";
|
||||
import React, { useContext } from "react";
|
||||
import PlainAddress from "./PlainAddress";
|
||||
import { resolverRendererRegistry } from "../api/address-resolver";
|
||||
import { useResolvedAddress } from "../useResolvedAddresses";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
|
||||
type AddressOrENSNameProps = {
|
||||
address: string;
|
||||
address: ChecksummedAddress;
|
||||
selectedAddress?: string;
|
||||
dontOverrideColors?: boolean;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const AddressOrENSName: React.FC<AddressOrENSNameProps> = ({
|
||||
address,
|
||||
selectedAddress,
|
||||
dontOverrideColors,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const resolvedAddress = resolvedAddresses?.[address];
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
const resolvedAddress = useResolvedAddress(provider, address);
|
||||
const linkable = address !== selectedAddress;
|
||||
|
||||
if (!resolvedAddress) {
|
||||
|
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -9,7 +9,6 @@ import { faCoins } from "@fortawesome/free-solid-svg-icons/faCoins";
|
|||
import AddressOrENSName from "./AddressOrENSName";
|
||||
import SourcifyLogo from "../sourcify/SourcifyLogo";
|
||||
import { AddressContext, ZERO_ADDRESS } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
|
||||
type DecoratedAddressLinkProps = {
|
||||
|
@ -21,7 +20,6 @@ type DecoratedAddressLinkProps = {
|
|||
selfDestruct?: boolean;
|
||||
txFrom?: boolean;
|
||||
txTo?: boolean;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
metadata?: Metadata | null | undefined;
|
||||
};
|
||||
|
||||
|
@ -34,7 +32,6 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
|||
selfDestruct,
|
||||
txFrom,
|
||||
txTo,
|
||||
resolvedAddresses,
|
||||
metadata,
|
||||
}) => {
|
||||
const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS;
|
||||
|
@ -87,7 +84,6 @@ const DecoratedAddressLink: React.FC<DecoratedAddressLinkProps> = ({
|
|||
address={address}
|
||||
selectedAddress={selectedAddress}
|
||||
dontOverrideColors={mint || burn}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,17 +5,12 @@ import TransactionAddress from "./TransactionAddress";
|
|||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import { InternalOperation } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type InternalCreateProps = {
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalCreate: React.FC<InternalCreateProps> = ({
|
||||
internalOp,
|
||||
resolvedAddresses,
|
||||
}) => (
|
||||
const InternalCreate: React.FC<InternalCreateProps> = ({ internalOp }) => (
|
||||
<div className="flex items-baseline space-x-1 whitespace-nowrap">
|
||||
<span className="text-gray-500">
|
||||
<FontAwesomeIcon icon={faAngleRight} size="1x" /> CREATE
|
||||
|
@ -23,20 +18,11 @@ const InternalCreate: React.FC<InternalCreateProps> = ({
|
|||
<span>Contract</span>
|
||||
<div className="flex items-baseline">
|
||||
<AddressHighlighter address={internalOp.to}>
|
||||
<DecoratedAddressLink
|
||||
address={internalOp.to}
|
||||
creation
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<DecoratedAddressLink address={internalOp.to} creation />
|
||||
</AddressHighlighter>
|
||||
</div>
|
||||
<span className="flex items-baseline text-gray-400">
|
||||
(Creator:{" "}
|
||||
<TransactionAddress
|
||||
address={internalOp.from}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
)
|
||||
(Creator: <TransactionAddress address={internalOp.from} />)
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,19 +5,16 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
|
|||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import { TransactionData, InternalOperation } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import TransactionAddress from "./TransactionAddress";
|
||||
|
||||
type InternalSelfDestructProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
|
||||
txData,
|
||||
internalOp,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const toMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
|
@ -32,21 +29,12 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
|
|||
<span>Contract</span>
|
||||
<div className="flex items-baseline">
|
||||
<AddressHighlighter address={internalOp.from}>
|
||||
<DecoratedAddressLink
|
||||
address={internalOp.from}
|
||||
selfDestruct
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<DecoratedAddressLink address={internalOp.from} selfDestruct />
|
||||
</AddressHighlighter>
|
||||
</div>
|
||||
{internalOp.value.isZero() && (
|
||||
<div className="flex items-baseline text-gray-400">
|
||||
(To:{" "}
|
||||
<TransactionAddress
|
||||
address={internalOp.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
)
|
||||
(To: <TransactionAddress address={internalOp.to} />)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -64,11 +52,7 @@ const InternalSelfDestruct: React.FC<InternalSelfDestructProps> = ({
|
|||
toMiner ? "rounded px-2 py-1 bg-yellow-100" : ""
|
||||
}`}
|
||||
>
|
||||
<DecoratedAddressLink
|
||||
address={internalOp.to}
|
||||
miner={toMiner}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<DecoratedAddressLink address={internalOp.to} miner={toMiner} />
|
||||
</div>
|
||||
</AddressHighlighter>
|
||||
</div>
|
||||
|
|
|
@ -3,37 +3,24 @@ import InternalTransfer from "./InternalTransfer";
|
|||
import InternalSelfDestruct from "./InternalSelfDestruct";
|
||||
import InternalCreate from "./InternalCreate";
|
||||
import { TransactionData, InternalOperation, OperationType } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type InternalTransactionOperationProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalTransactionOperation: React.FC<InternalTransactionOperationProps> =
|
||||
({ txData, internalOp, resolvedAddresses }) => (
|
||||
({ txData, internalOp }) => (
|
||||
<>
|
||||
{internalOp.type === OperationType.TRANSFER && (
|
||||
<InternalTransfer
|
||||
txData={txData}
|
||||
internalOp={internalOp}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<InternalTransfer txData={txData} internalOp={internalOp} />
|
||||
)}
|
||||
{internalOp.type === OperationType.SELF_DESTRUCT && (
|
||||
<InternalSelfDestruct
|
||||
txData={txData}
|
||||
internalOp={internalOp}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<InternalSelfDestruct txData={txData} internalOp={internalOp} />
|
||||
)}
|
||||
{(internalOp.type === OperationType.CREATE ||
|
||||
internalOp.type === OperationType.CREATE2) && (
|
||||
<InternalCreate
|
||||
internalOp={internalOp}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<InternalCreate internalOp={internalOp} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,18 +5,15 @@ import { faAngleRight } from "@fortawesome/free-solid-svg-icons/faAngleRight";
|
|||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import { TransactionData, InternalOperation } from "../types";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
|
||||
type InternalTransferProps = {
|
||||
txData: TransactionData;
|
||||
internalOp: InternalOperation;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InternalTransfer: React.FC<InternalTransferProps> = ({
|
||||
txData,
|
||||
internalOp,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const fromMiner =
|
||||
txData.confirmedData?.miner !== undefined &&
|
||||
|
@ -44,7 +41,6 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
|
|||
miner={fromMiner}
|
||||
txFrom={internalOp.from === txData.from}
|
||||
txTo={internalOp.from === txData.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
</AddressHighlighter>
|
||||
|
@ -62,7 +58,6 @@ const InternalTransfer: React.FC<InternalTransferProps> = ({
|
|||
miner={toMiner}
|
||||
txFrom={internalOp.to === txData.from}
|
||||
txTo={internalOp.to === txData.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
</AddressHighlighter>
|
||||
|
|
|
@ -1,20 +1,12 @@
|
|||
import React from "react";
|
||||
import { rawInputTo4Bytes, use4Bytes } from "../use4Bytes";
|
||||
import { useMethodSelector } from "../use4Bytes";
|
||||
|
||||
type MethodNameProps = {
|
||||
data: string;
|
||||
};
|
||||
|
||||
const MethodName: React.FC<MethodNameProps> = ({ data }) => {
|
||||
const rawFourBytes = rawInputTo4Bytes(data);
|
||||
const fourBytesEntry = use4Bytes(rawFourBytes);
|
||||
const methodName = fourBytesEntry?.name ?? rawFourBytes;
|
||||
const isSimpleTransfer = rawFourBytes === "0x";
|
||||
const methodTitle = isSimpleTransfer
|
||||
? "ETH Transfer"
|
||||
: methodName === rawFourBytes
|
||||
? methodName
|
||||
: `${methodName} [${rawFourBytes}]`;
|
||||
const [isSimpleTransfer, methodName, methodTitle] = useMethodSelector(data);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from "react";
|
||||
import { commify } from "@ethersproject/units";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp";
|
||||
|
||||
|
@ -14,7 +15,7 @@ const Nonce: React.FC<NonceProps> = ({ value }) => (
|
|||
<span className="text-green-400">
|
||||
<FontAwesomeIcon icon={faArrowUp} size="1x" />
|
||||
</span>
|
||||
<span className="text-green-600">{value}</span>
|
||||
<span className="text-green-600">{commify(value)}</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import AddressHighlighter from "./AddressHighlighter";
|
||||
import DecoratedAddressLink from "./DecoratedAddressLink";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { useSelectedTransaction } from "../useSelectedTransaction";
|
||||
import { AddressContext } from "../types";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
|
@ -9,14 +8,12 @@ import { Metadata } from "../sourcify/useSourcify";
|
|||
type TransactionAddressProps = {
|
||||
address: string;
|
||||
addressCtx?: AddressContext | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
metadata?: Metadata | null | undefined;
|
||||
};
|
||||
|
||||
const TransactionAddress: React.FC<TransactionAddressProps> = ({
|
||||
address,
|
||||
addressCtx,
|
||||
resolvedAddresses,
|
||||
metadata,
|
||||
}) => {
|
||||
const txData = useSelectedTransaction();
|
||||
|
@ -32,7 +29,6 @@ const TransactionAddress: React.FC<TransactionAddressProps> = ({
|
|||
txFrom={address === txData?.from}
|
||||
txTo={address === txData?.to || creation}
|
||||
creation={creation}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadata}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { transactionURL } from "../url";
|
||||
|
||||
type TransactionLinkProps = {
|
||||
txHash: string;
|
||||
|
@ -8,7 +9,7 @@ type TransactionLinkProps = {
|
|||
const TransactionLink: React.FC<TransactionLinkProps> = ({ txHash }) => (
|
||||
<NavLink
|
||||
className="text-link-blue hover:text-link-blue-hover font-hash"
|
||||
to={`/tx/${txHash}`}
|
||||
to={transactionURL(txHash)}
|
||||
>
|
||||
<p className="truncate">{txHash}</p>
|
||||
</NavLink>
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
export const MIN_API_LEVEL = 5;
|
||||
export const MIN_API_LEVEL = 6;
|
||||
|
||||
export const PAGE_SIZE = 25;
|
||||
|
|
|
@ -18,12 +18,10 @@ import { ChecksummedAddress, ProcessedTransaction } from "../types";
|
|||
import { FeeDisplay } from "./useFeeToggler";
|
||||
import { formatValue } from "../components/formatter";
|
||||
import ETH2USDValue from "../components/ETH2USDValue";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
|
||||
type TransactionItemProps = {
|
||||
tx: ProcessedTransaction;
|
||||
resolvedAddresses?: ResolvedAddresses;
|
||||
selectedAddress?: string;
|
||||
feeDisplay: FeeDisplay;
|
||||
priceMap: Record<BlockTag, BigNumber>;
|
||||
|
@ -32,7 +30,6 @@ type TransactionItemProps = {
|
|||
|
||||
const TransactionItem: React.FC<TransactionItemProps> = ({
|
||||
tx,
|
||||
resolvedAddresses,
|
||||
selectedAddress,
|
||||
feeDisplay,
|
||||
priceMap,
|
||||
|
@ -87,7 +84,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
address={tx.from}
|
||||
selectedAddress={selectedAddress}
|
||||
miner={tx.miner === tx.from}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
)}
|
||||
|
@ -110,7 +106,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
address={tx.to}
|
||||
selectedAddress={selectedAddress}
|
||||
miner={tx.miner === tx.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[tx.to]}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
|
@ -120,7 +115,6 @@ const TransactionItem: React.FC<TransactionItemProps> = ({
|
|||
address={tx.createdContractAddress!}
|
||||
selectedAddress={selectedAddress}
|
||||
creation
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[tx.createdContractAddress!]}
|
||||
/>
|
||||
</AddressHighlighter>
|
||||
|
|
|
@ -206,17 +206,36 @@ export class SearchController {
|
|||
}
|
||||
}
|
||||
|
||||
const doSearch = (q: string, navigate: NavigateFunction) => {
|
||||
if (isAddress(q)) {
|
||||
navigate(`/address/${q}`, { replace: true });
|
||||
const doSearch = async (q: string, navigate: NavigateFunction) => {
|
||||
// Cleanup
|
||||
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;
|
||||
}
|
||||
|
||||
// Tx hash?
|
||||
if (isHexString(q, 32)) {
|
||||
navigate(`/tx/${q}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// Block number?
|
||||
const blockNumber = parseInt(q);
|
||||
if (!isNaN(blockNumber)) {
|
||||
navigate(`/block/${blockNumber}`, { replace: true });
|
||||
|
@ -224,7 +243,12 @@ const doSearch = (q: string, navigate: NavigateFunction) => {
|
|||
}
|
||||
|
||||
// Assume it is an ENS name
|
||||
navigate(`/address/${q}`);
|
||||
navigate(
|
||||
`/address/${maybeAddress}${
|
||||
maybeIndex !== "" ? `?nonce=${maybeIndex}` : ""
|
||||
}`,
|
||||
{ replace: true }
|
||||
);
|
||||
};
|
||||
|
||||
export const useGenericSearch = (): [
|
||||
|
|
|
@ -15,6 +15,7 @@ import BlockConfirmations from "../components/BlockConfirmations";
|
|||
import TransactionAddress from "../components/TransactionAddress";
|
||||
import Copy from "../components/Copy";
|
||||
import Nonce from "../components/Nonce";
|
||||
import NavNonce from "./NavNonce";
|
||||
import Timestamp from "../components/Timestamp";
|
||||
import InternalTransactionOperation from "../components/InternalTransactionOperation";
|
||||
import MethodName from "../components/MethodName";
|
||||
|
@ -37,12 +38,11 @@ import PercentagePosition from "../components/PercentagePosition";
|
|||
import DecodedParamsTable from "./decoder/DecodedParamsTable";
|
||||
import InputDecoder from "./decoder/InputDecoder";
|
||||
import {
|
||||
rawInputTo4Bytes,
|
||||
extract4Bytes,
|
||||
use4Bytes,
|
||||
useTransactionDescription,
|
||||
} from "../use4Bytes";
|
||||
import { DevDoc, Metadata, useError, UserDoc } from "../sourcify/useSourcify";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
import { useTransactionError } from "../useErigonHooks";
|
||||
|
@ -56,7 +56,6 @@ type DetailsProps = {
|
|||
internalOps?: InternalOperation[];
|
||||
sendsEthToMiner: boolean;
|
||||
ethUSDPrice: BigNumber | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const Details: React.FC<DetailsProps> = ({
|
||||
|
@ -68,13 +67,13 @@ const Details: React.FC<DetailsProps> = ({
|
|||
internalOps,
|
||||
sendsEthToMiner,
|
||||
ethUSDPrice,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const hasEIP1559 =
|
||||
txData.confirmedData?.blockBaseFeePerGas !== undefined &&
|
||||
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 fourBytesTxDesc = useTransactionDescription(
|
||||
fourBytesEntry,
|
||||
|
@ -199,7 +198,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
hasParamNames
|
||||
userMethod={userError}
|
||||
devMethod={devError}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
@ -251,14 +249,12 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<InfoRow title="From / Nonce">
|
||||
<div className="flex divide-x-2 divide-dotted divide-gray-300">
|
||||
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
|
||||
<TransactionAddress
|
||||
address={txData.from}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<TransactionAddress address={txData.from} />
|
||||
<Copy value={txData.from} />
|
||||
</div>
|
||||
<div className="flex items-baseline pl-3">
|
||||
<Nonce value={txData.nonce} />
|
||||
<NavNonce sender={txData.from} nonce={txData.nonce} />
|
||||
</div>
|
||||
</div>
|
||||
</InfoRow>
|
||||
|
@ -267,7 +263,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
<div className="flex items-baseline space-x-2 -ml-1">
|
||||
<TransactionAddress
|
||||
address={txData.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas?.[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">
|
||||
<TransactionAddress
|
||||
address={txData.confirmedData?.createdContractAddress!}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={
|
||||
metadatas?.[txData.confirmedData?.createdContractAddress!]
|
||||
}
|
||||
|
@ -295,7 +289,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
key={i}
|
||||
txData={txData}
|
||||
internalOp={op}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
@ -313,7 +306,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
key={i}
|
||||
t={t}
|
||||
tokenMeta={txData.tokenMetas[t.token]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
))}
|
||||
|
@ -437,7 +429,6 @@ const Details: React.FC<DetailsProps> = ({
|
|||
data={txData.data}
|
||||
userMethod={userMethod}
|
||||
devMethod={devMethod}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</InfoRow>
|
||||
</ContentFrame>
|
||||
|
|
|
@ -8,23 +8,16 @@ import ModeTab from "../components/ModeTab";
|
|||
import DecodedParamsTable from "./decoder/DecodedParamsTable";
|
||||
import DecodedLogSignature from "./decoder/DecodedLogSignature";
|
||||
import { useTopic0 } from "../useTopic0";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { ChecksummedAddress } from "../types";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
|
||||
type LogEntryProps = {
|
||||
log: Log;
|
||||
logDesc: LogDescription | null | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
metadatas: Record<ChecksummedAddress, Metadata | null | undefined>;
|
||||
};
|
||||
|
||||
const LogEntry: React.FC<LogEntryProps> = ({
|
||||
log,
|
||||
logDesc,
|
||||
resolvedAddresses,
|
||||
metadatas,
|
||||
}) => {
|
||||
const LogEntry: React.FC<LogEntryProps> = ({ log, logDesc, metadatas }) => {
|
||||
const rawTopic0 = log.topics[0];
|
||||
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">
|
||||
<TransactionAddress
|
||||
address={log.address}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadata={metadatas[log.address]}
|
||||
/>
|
||||
<Copy value={log.address} />
|
||||
|
@ -109,7 +101,6 @@ const LogEntry: React.FC<LogEntryProps> = ({
|
|||
args={resolvedLogDesc.args}
|
||||
paramTypes={resolvedLogDesc.eventFragment.inputs}
|
||||
hasParamNames={resolvedLogDesc === logDesc}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,17 +4,15 @@ import ContentFrame from "../ContentFrame";
|
|||
import LogEntry from "./LogEntry";
|
||||
import { TransactionData } from "../types";
|
||||
import { Metadata } from "../sourcify/useSourcify";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { useContractsMetadata } from "../hooks";
|
||||
|
||||
type LogsProps = {
|
||||
txData: TransactionData;
|
||||
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> => {
|
||||
if (!txData.to || metadata === undefined) {
|
||||
return {};
|
||||
|
@ -68,7 +66,6 @@ const Logs: React.FC<LogsProps> = ({ txData, metadata, resolvedAddresses }) => {
|
|||
key={i}
|
||||
log={l}
|
||||
logDesc={logDescs?.[i]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
metadatas={metadatas}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -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;
|
|
@ -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);
|
|
@ -1,37 +1,18 @@
|
|||
import React, { useContext, useMemo } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import ContentFrame from "../ContentFrame";
|
||||
import TransactionAddress from "../components/TransactionAddress";
|
||||
import TraceItem from "./TraceItem";
|
||||
import { TransactionData } from "../types";
|
||||
import { useBatch4Bytes } from "../use4Bytes";
|
||||
import { useTraceTransaction, useUniqueSignatures } from "../useErigonHooks";
|
||||
import { useTraceTransaction } from "../useErigonHooks";
|
||||
import { RuntimeContext } from "../useRuntime";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import { tracesCollector, useResolvedAddresses } from "../useResolvedAddresses";
|
||||
|
||||
type TraceProps = {
|
||||
txData: TransactionData;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => {
|
||||
const Trace: React.FC<TraceProps> = ({ txData }) => {
|
||||
const { provider } = useContext(RuntimeContext);
|
||||
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 (
|
||||
<ContentFrame tabs>
|
||||
|
@ -39,20 +20,11 @@ const Trace: React.FC<TraceProps> = ({ txData, resolvedAddresses }) => {
|
|||
{traces ? (
|
||||
<>
|
||||
<div className="border hover:border-gray-500 rounded px-1 py-0.5">
|
||||
<TransactionAddress
|
||||
address={txData.from}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<TransactionAddress address={txData.from} />
|
||||
</div>
|
||||
<div className="ml-5 space-y-3 self-stretch">
|
||||
{traces.map((t, i, a) => (
|
||||
<TraceItem
|
||||
key={i}
|
||||
t={t}
|
||||
last={i === a.length - 1}
|
||||
fourBytesMap={sigMap}
|
||||
resolvedAddresses={mergedResolvedAddresses}
|
||||
/>
|
||||
<TraceItem key={i} t={t} last={i === a.length - 1} />
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -7,26 +7,19 @@ import FunctionSignature from "./FunctionSignature";
|
|||
import InputDecoder from "./decoder/InputDecoder";
|
||||
import ExpanderSwitch from "../components/ExpanderSwitch";
|
||||
import { TraceEntry } from "../useErigonHooks";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import {
|
||||
extract4Bytes,
|
||||
FourBytesEntry,
|
||||
use4Bytes,
|
||||
useTransactionDescription,
|
||||
} from "../use4Bytes";
|
||||
|
||||
type TraceInputProps = {
|
||||
t: TraceEntry;
|
||||
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const TraceInput: React.FC<TraceInputProps> = ({
|
||||
t,
|
||||
fourBytesMap,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const TraceInput: React.FC<TraceInputProps> = ({ t }) => {
|
||||
const raw4Bytes = extract4Bytes(t.input);
|
||||
const fourBytes = raw4Bytes !== null ? fourBytesMap[raw4Bytes] : null;
|
||||
const fourBytes = use4Bytes(raw4Bytes);
|
||||
const sigText =
|
||||
raw4Bytes === null ? "<fallback>" : fourBytes?.name ?? raw4Bytes;
|
||||
const hasParams = t.input.length > 10;
|
||||
|
@ -54,10 +47,7 @@ const TraceInput: React.FC<TraceInputProps> = ({
|
|||
) : (
|
||||
<>
|
||||
<span>
|
||||
<TransactionAddress
|
||||
address={t.to}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<TransactionAddress address={t.to} />
|
||||
</span>
|
||||
{t.type !== "CREATE" && t.type !== "CREATE2" && (
|
||||
<>
|
||||
|
@ -93,7 +83,6 @@ const TraceInput: React.FC<TraceInputProps> = ({
|
|||
data={t.input}
|
||||
userMethod={undefined}
|
||||
devMethod={undefined}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
</div>
|
||||
<div>)</div>
|
||||
|
|
|
@ -3,24 +3,15 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|||
import { faPlusSquare } from "@fortawesome/free-regular-svg-icons/faPlusSquare";
|
||||
import { faMinusSquare } from "@fortawesome/free-regular-svg-icons/faMinusSquare";
|
||||
import { Switch } from "@headlessui/react";
|
||||
import { FourBytesEntry } from "../use4Bytes";
|
||||
import { TraceGroup } from "../useErigonHooks";
|
||||
import { ResolvedAddresses } from "../api/address-resolver";
|
||||
import TraceInput from "./TraceInput";
|
||||
|
||||
type TraceItemProps = {
|
||||
t: TraceGroup;
|
||||
last: boolean;
|
||||
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const TraceItem: React.FC<TraceItemProps> = ({
|
||||
t,
|
||||
last,
|
||||
fourBytesMap,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const TraceItem: React.FC<TraceItemProps> = ({ t, last }) => {
|
||||
const [expanded, setExpanded] = useState<boolean>(true);
|
||||
|
||||
return (
|
||||
|
@ -42,11 +33,7 @@ const TraceItem: React.FC<TraceItemProps> = ({
|
|||
/>
|
||||
</Switch>
|
||||
)}
|
||||
<TraceInput
|
||||
t={t}
|
||||
fourBytesMap={fourBytesMap}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<TraceInput t={t} />
|
||||
</div>
|
||||
{t.children && (
|
||||
<div
|
||||
|
@ -54,11 +41,7 @@ const TraceItem: React.FC<TraceItemProps> = ({
|
|||
expanded ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<TraceChildren
|
||||
c={t.children}
|
||||
fourBytesMap={fourBytesMap}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<TraceChildren c={t.children} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
@ -67,26 +50,16 @@ const TraceItem: React.FC<TraceItemProps> = ({
|
|||
|
||||
type TraceChildrenProps = {
|
||||
c: TraceGroup[];
|
||||
fourBytesMap: Record<string, FourBytesEntry | null | undefined>;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const TraceChildren: React.FC<TraceChildrenProps> = React.memo(
|
||||
({ c, fourBytesMap, resolvedAddresses }) => {
|
||||
return (
|
||||
<>
|
||||
{c.map((tc, i, a) => (
|
||||
<TraceItem
|
||||
key={i}
|
||||
t={tc}
|
||||
last={i === a.length - 1}
|
||||
fourBytesMap={fourBytesMap}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
const TraceChildren: React.FC<TraceChildrenProps> = React.memo(({ c }) => {
|
||||
return (
|
||||
<>
|
||||
{c.map((tc, i, a) => (
|
||||
<TraceItem key={i} t={tc} last={i === a.length - 1} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default TraceItem;
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
import React from "react";
|
||||
import TransactionAddress from "../../components/TransactionAddress";
|
||||
import Copy from "../../components/Copy";
|
||||
import { ResolvedAddresses } from "../../api/address-resolver";
|
||||
|
||||
type AddressDecoderProps = {
|
||||
r: string;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const AddressDecoder: React.FC<AddressDecoderProps> = ({
|
||||
r,
|
||||
resolvedAddresses,
|
||||
}) => (
|
||||
const AddressDecoder: React.FC<AddressDecoderProps> = ({ r }) => (
|
||||
<div className="flex items-baseline space-x-2 -ml-1 mr-3">
|
||||
<TransactionAddress address={r} resolvedAddresses={resolvedAddresses} />
|
||||
<TransactionAddress address={r} />
|
||||
<Copy value={r} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -8,7 +8,6 @@ import Uint256Decoder from "./Uint256Decoder";
|
|||
import AddressDecoder from "./AddressDecoder";
|
||||
import BooleanDecoder from "./BooleanDecoder";
|
||||
import BytesDecoder from "./BytesDecoder";
|
||||
import { ResolvedAddresses } from "../../api/address-resolver";
|
||||
import SelectionHighlighter, {
|
||||
valueSelector,
|
||||
} from "../../components/SelectionHighlighter";
|
||||
|
@ -20,7 +19,6 @@ type DecodedParamRowProps = {
|
|||
paramType: ParamType;
|
||||
arrayElem?: number | undefined;
|
||||
help?: string | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
|
||||
|
@ -30,7 +28,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
|
|||
paramType,
|
||||
arrayElem,
|
||||
help,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const [showHelp, setShowHelp] = useState<boolean>(false);
|
||||
|
||||
|
@ -80,10 +77,7 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
|
|||
{paramType.baseType === "uint256" ? (
|
||||
<Uint256Decoder r={r} />
|
||||
) : paramType.baseType === "address" ? (
|
||||
<AddressDecoder
|
||||
r={r.toString()}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
<AddressDecoder r={r.toString()} />
|
||||
) : paramType.baseType === "bool" ? (
|
||||
<BooleanDecoder r={r} />
|
||||
) : paramType.baseType === "bytes" ? (
|
||||
|
@ -111,7 +105,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
|
|||
i={idx}
|
||||
r={e}
|
||||
paramType={paramType.components[idx]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
{paramType.baseType === "array" &&
|
||||
|
@ -122,7 +115,6 @@ const DecodedParamRow: React.FC<DecodedParamRowProps> = ({
|
|||
r={e}
|
||||
paramType={paramType.arrayChildren}
|
||||
arrayElem={idx}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
|
|
@ -2,7 +2,6 @@ import React from "react";
|
|||
import { ParamType, Result } from "@ethersproject/abi";
|
||||
import DecodedParamRow from "./DecodedParamRow";
|
||||
import { DevMethod, UserMethod } from "../../sourcify/useSourcify";
|
||||
import { ResolvedAddresses } from "../../api/address-resolver";
|
||||
|
||||
type DecodedParamsTableProps = {
|
||||
args: Result;
|
||||
|
@ -10,7 +9,6 @@ type DecodedParamsTableProps = {
|
|||
hasParamNames?: boolean;
|
||||
userMethod?: UserMethod | undefined;
|
||||
devMethod?: DevMethod | undefined;
|
||||
resolvedAddresses?: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
|
||||
|
@ -18,7 +16,6 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
|
|||
paramTypes,
|
||||
hasParamNames = true,
|
||||
devMethod,
|
||||
resolvedAddresses,
|
||||
}) => (
|
||||
<table className="border w-full">
|
||||
<thead>
|
||||
|
@ -47,7 +44,6 @@ const DecodedParamsTable: React.FC<DecodedParamsTableProps> = ({
|
|||
r={r}
|
||||
paramType={paramTypes[i]}
|
||||
help={devMethod?.params?.[paramTypes[i].name]}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Tab } from "@headlessui/react";
|
|||
import ModeTab from "../../components/ModeTab";
|
||||
import DecodedParamsTable from "./DecodedParamsTable";
|
||||
import { DevMethod, UserMethod } from "../../sourcify/useSourcify";
|
||||
import { ResolvedAddresses } from "../../api/address-resolver";
|
||||
|
||||
type InputDecoderProps = {
|
||||
fourBytes: string;
|
||||
|
@ -14,7 +13,6 @@ type InputDecoderProps = {
|
|||
data: string;
|
||||
userMethod: UserMethod | undefined;
|
||||
devMethod: DevMethod | undefined;
|
||||
resolvedAddresses: ResolvedAddresses | undefined;
|
||||
};
|
||||
|
||||
const InputDecoder: React.FC<InputDecoderProps> = ({
|
||||
|
@ -24,7 +22,6 @@ const InputDecoder: React.FC<InputDecoderProps> = ({
|
|||
data,
|
||||
userMethod,
|
||||
devMethod,
|
||||
resolvedAddresses,
|
||||
}) => {
|
||||
const utfInput = useMemo(() => {
|
||||
try {
|
||||
|
@ -57,7 +54,6 @@ const InputDecoder: React.FC<InputDecoderProps> = ({
|
|||
hasParamNames={hasParamNames}
|
||||
userMethod={userMethod}
|
||||
devMethod={devMethod}
|
||||
resolvedAddresses={resolvedAddresses}
|
||||
/>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
|
|
|
@ -18,6 +18,11 @@ export const blockURL = (blockNum: BlockTag) => `/block/${blockNum}`;
|
|||
|
||||
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 {
|
||||
// Resolve trusted IPNS for root IPFS
|
||||
IPFS_IPNS,
|
||||
|
|
138
src/use4Bytes.ts
138
src/use4Bytes.ts
|
@ -1,27 +1,27 @@
|
|||
import { useState, useEffect, useContext, useMemo } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import {
|
||||
Fragment,
|
||||
Interface,
|
||||
TransactionDescription,
|
||||
} from "@ethersproject/abi";
|
||||
import { BigNumberish } from "@ethersproject/bignumber";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import { fourBytesURL } from "./url";
|
||||
import { BigNumberish } from "@ethersproject/bignumber";
|
||||
|
||||
export type FourBytesEntry = {
|
||||
name: string;
|
||||
signature: string | undefined;
|
||||
};
|
||||
|
||||
export type FourBytesMap = Record<string, FourBytesEntry | null | undefined>;
|
||||
|
||||
const simpleTransfer: FourBytesEntry = {
|
||||
name: "transfer",
|
||||
signature: undefined,
|
||||
};
|
||||
|
||||
const fullCache = new Map<string, FourBytesEntry | null>();
|
||||
|
||||
/**
|
||||
* Given a hex input data; extract the method selector
|
||||
*
|
||||
* @param rawInput Raw tx input including the "0x"
|
||||
* @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
|
||||
*/
|
||||
export const extract4Bytes = (rawInput: string): string | null => {
|
||||
if (rawInput.length < 10) {
|
||||
return null;
|
||||
|
@ -29,8 +29,6 @@ export const extract4Bytes = (rawInput: string): string | null => {
|
|||
return rawInput.slice(0, 10);
|
||||
};
|
||||
|
||||
export const rawInputTo4Bytes = (rawInput: string) => rawInput.slice(0, 10);
|
||||
|
||||
const fetch4Bytes = async (
|
||||
assetsURLPrefix: 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
|
||||
*
|
||||
* @param rawFourBytes an hex string containing the 4bytes signature in the "0xXXXXXXXX" format.
|
||||
*/
|
||||
export const use4Bytes = (
|
||||
rawFourBytes: string
|
||||
rawFourBytes: string | null
|
||||
): FourBytesEntry | null | undefined => {
|
||||
if (rawFourBytes !== "0x") {
|
||||
if (rawFourBytes.length !== 10 || !rawFourBytes.startsWith("0x")) {
|
||||
throw new Error(
|
||||
`rawFourBytes must contain a 4 bytes hex method signature starting with 0x; received value: "${rawFourBytes}"`
|
||||
);
|
||||
}
|
||||
if (rawFourBytes !== null && !rawFourBytes.startsWith("0x")) {
|
||||
throw new Error(
|
||||
`rawFourBytes must contain a bytes hex string starting with 0x; received value: "${rawFourBytes}"`
|
||||
);
|
||||
}
|
||||
|
||||
const runtime = useContext(RuntimeContext);
|
||||
const assetsURLPrefix = runtime.config?.assetsURLPrefix;
|
||||
const { config } = useContext(RuntimeContext);
|
||||
const assetsURLPrefix = config?.assetsURLPrefix;
|
||||
|
||||
const fourBytes = rawFourBytes.slice(2);
|
||||
const [entry, setEntry] = useState<FourBytesEntry | null | undefined>(
|
||||
fullCache.get(fourBytes)
|
||||
const fourBytesFetcher = (key: string | null) => {
|
||||
if (key === null || key === "0x") {
|
||||
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(() => {
|
||||
if (assetsURLPrefix === undefined) {
|
||||
return;
|
||||
}
|
||||
if (fourBytes === "") {
|
||||
return;
|
||||
}
|
||||
return error ? undefined : data;
|
||||
};
|
||||
|
||||
const loadSig = async () => {
|
||||
const entry = await fetch4Bytes(assetsURLPrefix, fourBytes);
|
||||
fullCache.set(fourBytes, entry);
|
||||
setEntry(entry);
|
||||
};
|
||||
loadSig();
|
||||
}, [fourBytes, assetsURLPrefix]);
|
||||
export const useMethodSelector = (data: string): [boolean, string, string] => {
|
||||
const rawFourBytes = extract4Bytes(data);
|
||||
const fourBytesEntry = use4Bytes(rawFourBytes);
|
||||
const isSimpleTransfer = data === "0x";
|
||||
const methodName = isSimpleTransfer
|
||||
? "transfer"
|
||||
: fourBytesEntry?.name ?? rawFourBytes ?? "-";
|
||||
const methodTitle = isSimpleTransfer
|
||||
? "ETH Transfer"
|
||||
: methodName === rawFourBytes
|
||||
? methodName
|
||||
: `${methodName} [${rawFourBytes}]`;
|
||||
|
||||
if (rawFourBytes === "0x") {
|
||||
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;
|
||||
return [isSimpleTransfer, methodName, methodTitle];
|
||||
};
|
||||
|
||||
export const useTransactionDescription = (
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Block, BlockWithTransactions } from "@ethersproject/abstract-provider";
|
||||
import { JsonRpcProvider } from "@ethersproject/providers";
|
||||
import { getAddress } from "@ethersproject/address";
|
||||
|
@ -6,7 +6,7 @@ import { Contract } from "@ethersproject/contracts";
|
|||
import { defaultAbiCoder } from "@ethersproject/abi";
|
||||
import { BigNumber } from "@ethersproject/bignumber";
|
||||
import { arrayify, hexDataSlice, isHexString } from "@ethersproject/bytes";
|
||||
import { extract4Bytes } from "./use4Bytes";
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { getInternalOperations } from "./nodeFunctions";
|
||||
import {
|
||||
TokenMetas,
|
||||
|
@ -191,98 +191,107 @@ export const useTxData = (
|
|||
}
|
||||
|
||||
const readTxData = async () => {
|
||||
const [_response, _receipt] = await Promise.all([
|
||||
provider.getTransaction(txhash),
|
||||
provider.getTransactionReceipt(txhash),
|
||||
]);
|
||||
if (_response === null) {
|
||||
try {
|
||||
const [_response, _receipt] = await Promise.all([
|
||||
provider.getTransaction(txhash),
|
||||
provider.getTransactionReceipt(txhash),
|
||||
]);
|
||||
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);
|
||||
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();
|
||||
}, [provider, txhash]);
|
||||
|
||||
|
@ -408,46 +417,6 @@ export const useTraceTransaction = (
|
|||
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 (
|
||||
provider: JsonRpcProvider,
|
||||
address: ChecksummedAddress
|
||||
|
@ -543,3 +512,91 @@ export const useTransactionError = (
|
|||
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
import { useState, useEffect, useRef, useContext } from "react";
|
||||
import { JsonRpcProvider } from "@ethersproject/providers";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import { BaseProvider } from "@ethersproject/providers";
|
||||
import { getAddress, isAddress } from "@ethersproject/address";
|
||||
import { batchPopulate, ResolvedAddresses } from "./api/address-resolver";
|
||||
import { TraceGroup } from "./useErigonHooks";
|
||||
import useSWRImmutable from "swr/immutable";
|
||||
import { mainResolver } from "./api/address-resolver";
|
||||
import { SelectedResolvedName } from "./api/address-resolver/CompositeAddressResolver";
|
||||
import { RuntimeContext } from "./useRuntime";
|
||||
import {
|
||||
ChecksummedAddress,
|
||||
ProcessedTransaction,
|
||||
TransactionData,
|
||||
} from "./types";
|
||||
import { ChecksummedAddress } from "./types";
|
||||
|
||||
export const useAddressOrENSFromURL = (
|
||||
export const useAddressOrENS = (
|
||||
addressOrName: string,
|
||||
urlFixer: (address: ChecksummedAddress) => void
|
||||
): [
|
||||
|
@ -69,118 +66,22 @@ export const useAddressOrENSFromURL = (
|
|||
return [checksummedAddress, isENS, error];
|
||||
};
|
||||
|
||||
export type AddressCollector = () => string[];
|
||||
|
||||
export const pageCollector =
|
||||
(page: ProcessedTransaction[] | undefined): AddressCollector =>
|
||||
() => {
|
||||
if (!page) {
|
||||
return [];
|
||||
export const useResolvedAddress = (
|
||||
provider: BaseProvider | undefined,
|
||||
address: ChecksummedAddress
|
||||
): SelectedResolvedName<any> | undefined => {
|
||||
const fetcher = async (
|
||||
key: string
|
||||
): Promise<SelectedResolvedName<any> | undefined> => {
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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);
|
||||
return mainResolver.resolveAddress(provider, address);
|
||||
};
|
||||
|
||||
export const transactionDataCollector =
|
||||
(txData: TransactionData | null | undefined): AddressCollector =>
|
||||
() => {
|
||||
if (!txData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
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;
|
||||
const { data, error } = useSWRImmutable(address, fetcher);
|
||||
if (error) {
|
||||
return undefined;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
|
2
topic0
2
topic0
|
@ -1 +1 @@
|
|||
Subproject commit 63794c46467dea47fd99ec47db745c482887367e
|
||||
Subproject commit cb6abe87055d2e2d54ba6a985903031420c4cbb1
|
|
@ -1 +1 @@
|
|||
Subproject commit e779c7b400fc479f8442066f13565555be5bfcf3
|
||||
Subproject commit 0e2488c4b4c366c0ed54d5d85b2feaa0f0940b05
|
Loading…
Reference in New Issue