diff --git a/.gitmodules b/.gitmodules index 3ee2444..e55ad32 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/4bytes b/4bytes index 2053752..0ee722e 160000 --- a/4bytes +++ b/4bytes @@ -1 +1 @@ -Subproject commit 20537524bfb01bee859c9cfa9a8784baacbcc7ae +Subproject commit 0ee722e516c91dc6a3de01c26ea06955123eeddb diff --git a/README.md b/README.md index d1aef5b..ceab17e 100644 --- a/README.md +++ b/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 -``` - -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," --private.api.addr 127.0.0.1:9090 --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: -``` - -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="" otterscan/otterscan: -``` - -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) diff --git a/docs/custom-jsonrpc.md b/docs/custom-jsonrpc.md new file mode 100644 index 0000000..497800b --- /dev/null +++ b/docs/custom-jsonrpc.md @@ -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: + +`` + +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" +} +``` diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 0000000..e092834 --- /dev/null +++ b/docs/install.md @@ -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. + +``` +/rpcdaemon --http.api "eth,erigon,ots," --private.api.addr 127.0.0.1:9090 --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: +``` + +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="" otterscan/otterscan: +``` + +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. diff --git a/package-lock.json b/package-lock.json index d24cbe1..1a17078 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" diff --git a/package.json b/package.json index e9e3b77..be1adae 100644 --- a/package.json +++ b/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": { diff --git a/src/AddressTransactions.tsx b/src/Address.tsx similarity index 88% rename from src/AddressTransactions.tsx rename to src/Address.tsx index d7222c0..3b9b62d 100644 --- a/src/AddressTransactions.tsx +++ b/src/Address.tsx @@ -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 ( + + ); + } + return ( {error ? ( - - "{addressOrName}" is not an ETH address or ENS name. - + ) : ( checksummedAddress && ( <> @@ -175,4 +192,4 @@ const AddressTransactions: React.FC = () => { ); }; -export default AddressTransactions; +export default Address; diff --git a/src/AddressTransactionByNonce.tsx b/src/AddressTransactionByNonce.tsx new file mode 100644 index 0000000..e804f84 --- /dev/null +++ b/src/AddressTransactionByNonce.tsx @@ -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 = ({ + checksummedAddress, + rawNonce, +}) => { + const { provider } = useContext(RuntimeContext); + + // Calculate txCount ONLY when asked for latest nonce + const [txCount, setTxCount] = useState(); + 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 ; + } + + // Address hasn't made the first outbound tx yet + if (nonce < 0) { + return ( + + + + ); + } + + // Garbage nonce + if (isNaN(nonce)) { + return ( + + + + ); + } + + // Valid nonce, but no tx found + if (txHash === null) { + return ( + + + + ); + } + + // Success; replace and render filler + navigate(transactionURL(txHash), { replace: true }); + return ; +}; + +export default AddressTransactionByNonce; diff --git a/src/App.tsx b/src/App.tsx index f31fff6..62d3b2c 100644 --- a/src/App.tsx +++ b/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 = () => { } /> } + element={
} /> - } /> + } /> diff --git a/src/PageNotFound.tsx b/src/PageNotFound.tsx new file mode 100644 index 0000000..690f0a0 --- /dev/null +++ b/src/PageNotFound.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { NavLink } from "react-router-dom"; +import StandardFrame from "./StandardFrame"; + +const PageNotFound: React.FC = () => ( + +
+ Page not found! + + Click here to go to home + +
+
+); + +export default PageNotFound; diff --git a/src/TokenTransferItem.tsx b/src/TokenTransferItem.tsx index 9e49ee4..2cf91a0 100644 --- a/src/TokenTransferItem.tsx +++ b/src/TokenTransferItem.tsx @@ -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; }; @@ -24,7 +22,6 @@ type TokenTransferItemProps = { const TokenTransferItem: React.FC = ({ t, tokenMeta, - resolvedAddresses, metadatas, }) => (
@@ -37,7 +34,6 @@ const TokenTransferItem: React.FC = ({
@@ -46,7 +42,6 @@ const TokenTransferItem: React.FC = ({ @@ -60,11 +55,7 @@ const TokenTransferItem: React.FC = ({ /> - + diff --git a/src/Transaction.tsx b/src/Transaction.tsx index 5101193..94b2e43 100644 --- a/src/Transaction.tsx +++ b/src/Transaction.tsx @@ -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 ( - - - Transaction Details - {txData === null && ( - -
- Transaction {txhash} not found. -
-
- )} - {txData && ( - - - - Overview - {txData.confirmedData?.blockNumber !== undefined && ( - - Logs - {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} - - )} - Trace - - - - - - } - /> - - } - /> - - } - /> - - - - )} -
-
- ); + return ; }; export default Transaction; diff --git a/src/TransactionPageContent.tsx b/src/TransactionPageContent.tsx new file mode 100644 index 0000000..377c345 --- /dev/null +++ b/src/TransactionPageContent.tsx @@ -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 = ({ + 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 ( + + + Transaction Details + {txData === null && ( + +
+ Transaction {txHash} not found. +
+
+ )} + {txData && ( + + + + Overview + {txData.confirmedData?.blockNumber !== undefined && ( + + Logs + {txData && ` (${txData.confirmedData?.logs?.length ?? 0})`} + + )} + Trace + + + + + + } + /> + } + /> + } /> + + + + )} +
+
+ ); +}; + +export default TransactionPageContent; diff --git a/src/address/AddressTransactionResults.tsx b/src/address/AddressTransactionResults.tsx index f1a373e..cc8665e 100644 --- a/src/address/AddressTransactionResults.tsx +++ b/src/address/AddressTransactionResults.tsx @@ -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 = ({ }, [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 = ({ 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} /> @@ -152,7 +147,6 @@ const AddressTransactionResults: React.FC = ({ = ({ 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} /> diff --git a/src/api/address-resolver/index.ts b/src/api/address-resolver/index.ts index c6a1687..57f1b37 100644 --- a/src/api/address-resolver/index.ts +++ b/src/api/address-resolver/index.ts @@ -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 => { - const solvers: Promise | 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; -}; diff --git a/src/block/BlockTransactionResults.tsx b/src/block/BlockTransactionResults.tsx index 01d867c..2d768cc 100644 --- a/src/block/BlockTransactionResults.tsx +++ b/src/block/BlockTransactionResults.tsx @@ -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 = ({ 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 = ({ = ({ address, selectedAddress, dontOverrideColors, - resolvedAddresses, }) => { - const resolvedAddress = resolvedAddresses?.[address]; + const { provider } = useContext(RuntimeContext); + const resolvedAddress = useResolvedAddress(provider, address); const linkable = address !== selectedAddress; if (!resolvedAddress) { diff --git a/src/components/AddressOrENSNameInvalidNonce.tsx b/src/components/AddressOrENSNameInvalidNonce.tsx new file mode 100644 index 0000000..a92496a --- /dev/null +++ b/src/components/AddressOrENSNameInvalidNonce.tsx @@ -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 }) => ( + <> + Transaction Details + +
+ + : no transaction found for nonce="{nonce}". +
+
+ +); + +export default React.memo(AddressOrENSNameInvalidNonce); diff --git a/src/components/AddressOrENSNameNoTx.tsx b/src/components/AddressOrENSNameNoTx.tsx new file mode 100644 index 0000000..c4f6178 --- /dev/null +++ b/src/components/AddressOrENSNameNoTx.tsx @@ -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 = ({ + addressOrENSName, +}) => ( + <> + Transaction Details + +
+ + : no outbound transactions found. +
+
+ +); + +export default React.memo(AddressOrENSNameNoTx); diff --git a/src/components/AddressOrENSNameNotFound.tsx b/src/components/AddressOrENSNameNotFound.tsx new file mode 100644 index 0000000..437aa6c --- /dev/null +++ b/src/components/AddressOrENSNameNotFound.tsx @@ -0,0 +1,22 @@ +import React from "react"; +import StandardSubtitle from "../StandardSubtitle"; +import ContentFrame from "../ContentFrame"; + +type AddressOrENSNameNotFoundProps = { + addressOrENSName: string; +}; + +const AddressOrENSNameNotFound: React.FC = ({ + addressOrENSName, +}) => ( + <> + Transaction Details + +
+ "{addressOrENSName}" is not an ETH address or ENS name. +
+
+ +); + +export default React.memo(AddressOrENSNameNotFound); diff --git a/src/components/DecoratedAddressLink.tsx b/src/components/DecoratedAddressLink.tsx index 0f13f2c..1b3870a 100644 --- a/src/components/DecoratedAddressLink.tsx +++ b/src/components/DecoratedAddressLink.tsx @@ -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 = ({ selfDestruct, txFrom, txTo, - resolvedAddresses, metadata, }) => { const mint = addressCtx === AddressContext.FROM && address === ZERO_ADDRESS; @@ -87,7 +84,6 @@ const DecoratedAddressLink: React.FC = ({ address={address} selectedAddress={selectedAddress} dontOverrideColors={mint || burn} - resolvedAddresses={resolvedAddresses} /> ); diff --git a/src/components/InternalCreate.tsx b/src/components/InternalCreate.tsx index e086e9d..cc13650 100644 --- a/src/components/InternalCreate.tsx +++ b/src/components/InternalCreate.tsx @@ -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 = ({ - internalOp, - resolvedAddresses, -}) => ( +const InternalCreate: React.FC = ({ internalOp }) => (
CREATE @@ -23,20 +18,11 @@ const InternalCreate: React.FC = ({ Contract
- +
- (Creator:{" "} - - ) + (Creator: )
); diff --git a/src/components/InternalSelfDestruct.tsx b/src/components/InternalSelfDestruct.tsx index 794b987..4df2119 100644 --- a/src/components/InternalSelfDestruct.tsx +++ b/src/components/InternalSelfDestruct.tsx @@ -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 = ({ txData, internalOp, - resolvedAddresses, }) => { const toMiner = txData.confirmedData?.miner !== undefined && @@ -32,21 +29,12 @@ const InternalSelfDestruct: React.FC = ({ Contract
- +
{internalOp.value.isZero() && (
- (To:{" "} - - ) + (To: )
)} @@ -64,11 +52,7 @@ const InternalSelfDestruct: React.FC = ({ toMiner ? "rounded px-2 py-1 bg-yellow-100" : "" }`} > - + diff --git a/src/components/InternalTransactionOperation.tsx b/src/components/InternalTransactionOperation.tsx index f167eb5..5870336 100644 --- a/src/components/InternalTransactionOperation.tsx +++ b/src/components/InternalTransactionOperation.tsx @@ -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 = - ({ txData, internalOp, resolvedAddresses }) => ( + ({ txData, internalOp }) => ( <> {internalOp.type === OperationType.TRANSFER && ( - + )} {internalOp.type === OperationType.SELF_DESTRUCT && ( - + )} {(internalOp.type === OperationType.CREATE || internalOp.type === OperationType.CREATE2) && ( - + )} ); diff --git a/src/components/InternalTransfer.tsx b/src/components/InternalTransfer.tsx index bf46778..0600e95 100644 --- a/src/components/InternalTransfer.tsx +++ b/src/components/InternalTransfer.tsx @@ -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 = ({ txData, internalOp, - resolvedAddresses, }) => { const fromMiner = txData.confirmedData?.miner !== undefined && @@ -44,7 +41,6 @@ const InternalTransfer: React.FC = ({ miner={fromMiner} txFrom={internalOp.from === txData.from} txTo={internalOp.from === txData.to} - resolvedAddresses={resolvedAddresses} /> @@ -62,7 +58,6 @@ const InternalTransfer: React.FC = ({ miner={toMiner} txFrom={internalOp.to === txData.from} txTo={internalOp.to === txData.to} - resolvedAddresses={resolvedAddresses} /> diff --git a/src/components/MethodName.tsx b/src/components/MethodName.tsx index c965a81..539db12 100644 --- a/src/components/MethodName.tsx +++ b/src/components/MethodName.tsx @@ -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 = ({ 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 (
= ({ value }) => ( - {value} + {commify(value)} ); diff --git a/src/components/TransactionAddress.tsx b/src/components/TransactionAddress.tsx index cb1edc9..a313b24 100644 --- a/src/components/TransactionAddress.tsx +++ b/src/components/TransactionAddress.tsx @@ -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 = ({ address, addressCtx, - resolvedAddresses, metadata, }) => { const txData = useSelectedTransaction(); @@ -32,7 +29,6 @@ const TransactionAddress: React.FC = ({ txFrom={address === txData?.from} txTo={address === txData?.to || creation} creation={creation} - resolvedAddresses={resolvedAddresses} metadata={metadata} /> diff --git a/src/components/TransactionLink.tsx b/src/components/TransactionLink.tsx index 8db23f3..06fde64 100644 --- a/src/components/TransactionLink.tsx +++ b/src/components/TransactionLink.tsx @@ -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 = ({ txHash }) => (

{txHash}

diff --git a/src/params.ts b/src/params.ts index d1464c6..b1ef13b 100644 --- a/src/params.ts +++ b/src/params.ts @@ -1,3 +1,3 @@ -export const MIN_API_LEVEL = 5; +export const MIN_API_LEVEL = 6; export const PAGE_SIZE = 25; diff --git a/src/search/TransactionItem.tsx b/src/search/TransactionItem.tsx index b6cfe1e..daa0088 100644 --- a/src/search/TransactionItem.tsx +++ b/src/search/TransactionItem.tsx @@ -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; @@ -32,7 +30,6 @@ type TransactionItemProps = { const TransactionItem: React.FC = ({ tx, - resolvedAddresses, selectedAddress, feeDisplay, priceMap, @@ -87,7 +84,6 @@ const TransactionItem: React.FC = ({ address={tx.from} selectedAddress={selectedAddress} miner={tx.miner === tx.from} - resolvedAddresses={resolvedAddresses} /> )} @@ -110,7 +106,6 @@ const TransactionItem: React.FC = ({ address={tx.to} selectedAddress={selectedAddress} miner={tx.miner === tx.to} - resolvedAddresses={resolvedAddresses} metadata={metadatas[tx.to]} /> @@ -120,7 +115,6 @@ const TransactionItem: React.FC = ({ address={tx.createdContractAddress!} selectedAddress={selectedAddress} creation - resolvedAddresses={resolvedAddresses} metadata={metadatas[tx.createdContractAddress!]} /> diff --git a/src/search/search.ts b/src/search/search.ts index 57a5958..95ebdce 100644 --- a/src/search/search.ts +++ b/src/search/search.ts @@ -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 = (): [ diff --git a/src/transaction/Details.tsx b/src/transaction/Details.tsx index 66c34e9..261e651 100644 --- a/src/transaction/Details.tsx +++ b/src/transaction/Details.tsx @@ -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 = ({ @@ -68,13 +67,13 @@ const Details: React.FC = ({ 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 = ({ hasParamNames userMethod={userError} devMethod={devError} - resolvedAddresses={resolvedAddresses} /> )} @@ -251,14 +249,12 @@ const Details: React.FC = ({
- +
+
@@ -267,7 +263,6 @@ const Details: React.FC = ({
@@ -280,7 +275,6 @@ const Details: React.FC = ({
= ({ key={i} txData={txData} internalOp={op} - resolvedAddresses={resolvedAddresses} /> ))}
@@ -313,7 +306,6 @@ const Details: React.FC = ({ key={i} t={t} tokenMeta={txData.tokenMetas[t.token]} - resolvedAddresses={resolvedAddresses} metadatas={metadatas} /> ))} @@ -437,7 +429,6 @@ const Details: React.FC = ({ data={txData.data} userMethod={userMethod} devMethod={devMethod} - resolvedAddresses={resolvedAddresses} /> diff --git a/src/transaction/LogEntry.tsx b/src/transaction/LogEntry.tsx index 07be33f..90f9bee 100644 --- a/src/transaction/LogEntry.tsx +++ b/src/transaction/LogEntry.tsx @@ -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; }; -const LogEntry: React.FC = ({ - log, - logDesc, - resolvedAddresses, - metadatas, -}) => { +const LogEntry: React.FC = ({ log, logDesc, metadatas }) => { const rawTopic0 = log.topics[0]; const topic0 = useTopic0(rawTopic0); @@ -65,7 +58,6 @@ const LogEntry: React.FC = ({
@@ -109,7 +101,6 @@ const LogEntry: React.FC = ({ args={resolvedLogDesc.args} paramTypes={resolvedLogDesc.eventFragment.inputs} hasParamNames={resolvedLogDesc === logDesc} - resolvedAddresses={resolvedAddresses} />
diff --git a/src/transaction/Logs.tsx b/src/transaction/Logs.tsx index eba82e1..9b903b7 100644 --- a/src/transaction/Logs.tsx +++ b/src/transaction/Logs.tsx @@ -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 = ({ txData, metadata, resolvedAddresses }) => { +const Logs: React.FC = ({ txData, metadata }) => { const baseMetadatas = useMemo((): Record => { if (!txData.to || metadata === undefined) { return {}; @@ -68,7 +66,6 @@ const Logs: React.FC = ({ txData, metadata, resolvedAddresses }) => { key={i} log={l} logDesc={logDescs?.[i]} - resolvedAddresses={resolvedAddresses} metadatas={metadatas} /> ))} diff --git a/src/transaction/NavButton.tsx b/src/transaction/NavButton.tsx new file mode 100644 index 0000000..cad49d2 --- /dev/null +++ b/src/transaction/NavButton.tsx @@ -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 = ({ + sender, + nonce, + disabled, + children, +}) => { + if (disabled) { + return ( + + {children} + + ); + } + + return ( + + {children} + + ); +}; + +export default NavButton; diff --git a/src/transaction/NavNonce.tsx b/src/transaction/NavNonce.tsx new file mode 100644 index 0000000..9efd47f --- /dev/null +++ b/src/transaction/NavNonce.tsx @@ -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 = ({ 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 ( +
+ + + + = count - 1} + > + + + = count - 1} + > + + + +
+ ); +}; + +export default React.memo(NavNonce); diff --git a/src/transaction/Trace.tsx b/src/transaction/Trace.tsx index 5002391..a04bcdb 100644 --- a/src/transaction/Trace.tsx +++ b/src/transaction/Trace.tsx @@ -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 = ({ txData, resolvedAddresses }) => { +const Trace: React.FC = ({ 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 ( @@ -39,20 +20,11 @@ const Trace: React.FC = ({ txData, resolvedAddresses }) => { {traces ? ( <>
- +
{traces.map((t, i, a) => ( - + ))}
diff --git a/src/transaction/TraceInput.tsx b/src/transaction/TraceInput.tsx index f7e4713..0524501 100644 --- a/src/transaction/TraceInput.tsx +++ b/src/transaction/TraceInput.tsx @@ -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; - resolvedAddresses: ResolvedAddresses | undefined; }; -const TraceInput: React.FC = ({ - t, - fourBytesMap, - resolvedAddresses, -}) => { +const TraceInput: React.FC = ({ t }) => { const raw4Bytes = extract4Bytes(t.input); - const fourBytes = raw4Bytes !== null ? fourBytesMap[raw4Bytes] : null; + const fourBytes = use4Bytes(raw4Bytes); const sigText = raw4Bytes === null ? "" : fourBytes?.name ?? raw4Bytes; const hasParams = t.input.length > 10; @@ -54,10 +47,7 @@ const TraceInput: React.FC = ({ ) : ( <> - + {t.type !== "CREATE" && t.type !== "CREATE2" && ( <> @@ -93,7 +83,6 @@ const TraceInput: React.FC = ({ data={t.input} userMethod={undefined} devMethod={undefined} - resolvedAddresses={resolvedAddresses} />
)
diff --git a/src/transaction/TraceItem.tsx b/src/transaction/TraceItem.tsx index 60e4d85..d6a4d7d 100644 --- a/src/transaction/TraceItem.tsx +++ b/src/transaction/TraceItem.tsx @@ -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; - resolvedAddresses: ResolvedAddresses | undefined; }; -const TraceItem: React.FC = ({ - t, - last, - fourBytesMap, - resolvedAddresses, -}) => { +const TraceItem: React.FC = ({ t, last }) => { const [expanded, setExpanded] = useState(true); return ( @@ -42,11 +33,7 @@ const TraceItem: React.FC = ({ /> )} - + {t.children && (
= ({ expanded ? "" : "hidden" }`} > - +
)} @@ -67,26 +50,16 @@ const TraceItem: React.FC = ({ type TraceChildrenProps = { c: TraceGroup[]; - fourBytesMap: Record; - resolvedAddresses: ResolvedAddresses | undefined; }; -const TraceChildren: React.FC = React.memo( - ({ c, fourBytesMap, resolvedAddresses }) => { - return ( - <> - {c.map((tc, i, a) => ( - - ))} - - ); - } -); +const TraceChildren: React.FC = React.memo(({ c }) => { + return ( + <> + {c.map((tc, i, a) => ( + + ))} + + ); +}); export default TraceItem; diff --git a/src/transaction/decoder/AddressDecoder.tsx b/src/transaction/decoder/AddressDecoder.tsx index 49726e6..7c5c3ce 100644 --- a/src/transaction/decoder/AddressDecoder.tsx +++ b/src/transaction/decoder/AddressDecoder.tsx @@ -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 = ({ - r, - resolvedAddresses, -}) => ( +const AddressDecoder: React.FC = ({ r }) => (
- +
); diff --git a/src/transaction/decoder/DecodedParamRow.tsx b/src/transaction/decoder/DecodedParamRow.tsx index 3ad47cc..f30381d 100644 --- a/src/transaction/decoder/DecodedParamRow.tsx +++ b/src/transaction/decoder/DecodedParamRow.tsx @@ -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 = ({ @@ -30,7 +28,6 @@ const DecodedParamRow: React.FC = ({ paramType, arrayElem, help, - resolvedAddresses, }) => { const [showHelp, setShowHelp] = useState(false); @@ -80,10 +77,7 @@ const DecodedParamRow: React.FC = ({ {paramType.baseType === "uint256" ? ( ) : paramType.baseType === "address" ? ( - + ) : paramType.baseType === "bool" ? ( ) : paramType.baseType === "bytes" ? ( @@ -111,7 +105,6 @@ const DecodedParamRow: React.FC = ({ i={idx} r={e} paramType={paramType.components[idx]} - resolvedAddresses={resolvedAddresses} /> ))} {paramType.baseType === "array" && @@ -122,7 +115,6 @@ const DecodedParamRow: React.FC = ({ r={e} paramType={paramType.arrayChildren} arrayElem={idx} - resolvedAddresses={resolvedAddresses} /> ))} diff --git a/src/transaction/decoder/DecodedParamsTable.tsx b/src/transaction/decoder/DecodedParamsTable.tsx index 78d94e6..40dd473 100644 --- a/src/transaction/decoder/DecodedParamsTable.tsx +++ b/src/transaction/decoder/DecodedParamsTable.tsx @@ -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 = ({ @@ -18,7 +16,6 @@ const DecodedParamsTable: React.FC = ({ paramTypes, hasParamNames = true, devMethod, - resolvedAddresses, }) => ( @@ -47,7 +44,6 @@ const DecodedParamsTable: React.FC = ({ r={r} paramType={paramTypes[i]} help={devMethod?.params?.[paramTypes[i].name]} - resolvedAddresses={resolvedAddresses} /> ))} diff --git a/src/transaction/decoder/InputDecoder.tsx b/src/transaction/decoder/InputDecoder.tsx index 0bd9835..07d6f80 100644 --- a/src/transaction/decoder/InputDecoder.tsx +++ b/src/transaction/decoder/InputDecoder.tsx @@ -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 = ({ @@ -24,7 +22,6 @@ const InputDecoder: React.FC = ({ data, userMethod, devMethod, - resolvedAddresses, }) => { const utfInput = useMemo(() => { try { @@ -57,7 +54,6 @@ const InputDecoder: React.FC = ({ hasParamNames={hasParamNames} userMethod={userMethod} devMethod={devMethod} - resolvedAddresses={resolvedAddresses} /> )} diff --git a/src/url.ts b/src/url.ts index 471c9d8..cf44ab6 100644 --- a/src/url.ts +++ b/src/url.ts @@ -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, diff --git a/src/use4Bytes.ts b/src/use4Bytes.ts index 690d5a3..85f4e98 100644 --- a/src/use4Bytes.ts +++ b/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; - -const simpleTransfer: FourBytesEntry = { - name: "transfer", - signature: undefined, -}; - -const fullCache = new Map(); - +/** + * 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({}); - 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 = {}; - 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( - 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( + 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 = ( diff --git a/src/useErigonHooks.ts b/src/useErigonHooks.ts index 66fe480..45378f5 100644 --- a/src/useErigonHooks.ts +++ b/src/useErigonHooks.ts @@ -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(); - 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 => + 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 => { + 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, + 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; +}; diff --git a/src/useResolvedAddresses.ts b/src/useResolvedAddresses.ts index 9250232..056fa98 100644 --- a/src/useResolvedAddresses.ts +++ b/src/useResolvedAddresses.ts @@ -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 | undefined => { + const fetcher = async ( + key: string + ): Promise | undefined> => { + if (!provider) { + return undefined; } - - const uniqueAddresses = new Set(); - 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(); - - // 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(); - 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(); - const ref = useRef(); - 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; }; diff --git a/topic0 b/topic0 index 63794c4..cb6abe8 160000 --- a/topic0 +++ b/topic0 @@ -1 +1 @@ -Subproject commit 63794c46467dea47fd99ec47db745c482887367e +Subproject commit cb6abe87055d2e2d54ba6a985903031420c4cbb1 diff --git a/trustwallet b/trustwallet index e779c7b..0e2488c 160000 --- a/trustwallet +++ b/trustwallet @@ -1 +1 @@ -Subproject commit e779c7b400fc479f8442066f13565555be5bfcf3 +Subproject commit 0e2488c4b4c366c0ed54d5d85b2feaa0f0940b05